pi-widget-host 0.2.0 → 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/CHANGELOG.md CHANGED
@@ -1,9 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Add Buy Me a Coffee sponsor button to README and native GitHub funding link via `.github/FUNDING.yml`.
6
+
3
7
  All notable changes to this project will be documented in this file.
4
8
 
5
9
  This project follows semantic versioning.
6
10
 
11
+ ## [0.3.2] - 2026-06-26
12
+
13
+ ### Fixed
14
+
15
+ - npm Trusted Publishing publish retry after registry configuration fix (no functional changes from 0.3.1).
16
+
17
+ ## [0.3.1] - 2026-06-25
18
+
19
+ ### Changed
20
+
21
+ - 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.
22
+ - `Package contents` now lists the actual shipped paths instead of unused template resource directories.
23
+
24
+ ## [0.3.0] - 2026-06-24
25
+
26
+ ### Added
27
+
28
+ - Package-readiness hardening for the first publishable Host-only MVP release boundary.
29
+ - Explicit acceptance coverage for `matchday` event boost, known host tags, and silent empty slot behavior.
30
+
31
+ ### Changed
32
+
33
+ - Minor semver bump marking the completed Host-only MVP: event boost, known tags, registry protocol, and demo-provider dogfooding path.
34
+
7
35
  ## [0.2.0] - 2026-06-15
8
36
 
9
37
  ### Added
@@ -18,3 +46,4 @@ This project follows semantic versioning.
18
46
  ### Changed
19
47
 
20
48
  - Replaced template placeholders and removed template-only skill, prompt, and theme resources.
49
+
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
8
  [![Pi package](https://img.shields.io/badge/pi-package-purple.svg)](https://pi.dev/packages)
9
9
  [![Trusted Publishing](https://img.shields.io/badge/npm-Trusted%20Publishing-blue.svg)](docs/release.md)
10
+ <a href="https://buymeacoffee.com/ekawano114m"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" height="60"></a>
10
11
 
11
12
  > Shared-slot host for managing one prompt-top Pi widget across multiple providers.
12
13
 
@@ -32,19 +33,31 @@ v1 is a **Host-only MVP**:
32
33
 
33
34
  ## Install
34
35
 
35
- Published package:
36
+ Install the published npm package with Pi:
36
37
 
37
38
  ```bash
38
39
  pi install npm:pi-widget-host
39
40
  ```
40
41
 
41
- GitHub install:
42
+ Install into the current project instead of your user Pi settings:
43
+
44
+ ```bash
45
+ pi install npm:pi-widget-host -l
46
+ ```
47
+
48
+ Or install from GitHub:
42
49
 
43
50
  ```bash
44
51
  pi install git:github.com/eiei114/pi-widget-host
45
52
  ```
46
53
 
47
- Local dev:
54
+ Try it without permanently installing:
55
+
56
+ ```bash
57
+ pi -e npm:pi-widget-host
58
+ ```
59
+
60
+ Local development from this repository:
48
61
 
49
62
  ```bash
50
63
  pi -e .
@@ -97,17 +110,21 @@ The built-in demo provider exists to prove the host loop first:
97
110
 
98
111
  | Path | Purpose |
99
112
  |---|---|
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 |
113
+ | `extensions/index.ts` | Pi extension entrypoint and `/widget-host:*` command registration |
114
+ | `lib/` | config store, registry protocol, policy evaluation, and demo provider |
115
+ | `docs/protocol.md` | registry protocol reference for future provider packages |
116
+ | `docs/release.md` | Trusted Publishing release notes |
103
117
 
104
118
  ## Development
105
119
 
106
120
  ```bash
107
121
  npm install
108
122
  npm run ci
123
+ npm run pack:check
109
124
  ```
110
125
 
126
+ `npm run ci` runs typecheck, tests, and pack validation. `npm run pack:check` runs `npm pack --dry-run` to confirm the shipped tarball contents.
127
+
111
128
  ## Release
112
129
 
113
130
  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.3",
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
  }