pi-widget-host 0.3.5 → 0.3.11

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,12 +4,39 @@ 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.11] - 2026-09-27
8
+
9
+ - chore: periodic patch bump after 7+ days without npm publish
10
+
11
+
12
+
13
+
14
+
15
+ All notable changes to this project will be documented in this file.
16
+
17
+ This project follows semantic versioning.
18
+ ## [0.3.10] - 2026-09-28
19
+
20
+ - chore: periodic patch bump after 7+ days without npm publish
21
+ ## [0.3.9] - 2026-09-28
22
+
23
+ - chore: periodic patch bump after 7+ days without npm publish
24
+ ## [0.3.8] - 2026-09-28
25
+
26
+ - chore: periodic patch bump after 7+ days without npm publish
27
+ ## [0.3.7] - 2026-09-28
28
+
29
+ - chore: periodic patch bump after 7+ days without npm publish
30
+ ## [0.3.6] - 2026-08-22
31
+
32
+ ### Changed
33
+
34
+ - Merge the 2026-08-22 managed OSS dependency and maintenance PR batch.
7
35
  ## [0.3.5] - 2026-08-04
8
36
 
9
37
  ### Changed
10
38
 
11
39
  - Bump package version for the Discord release webhook verification.
12
-
13
40
  ## [0.3.4] - 2026-07-21
14
41
 
15
42
  ### Added
@@ -20,26 +47,22 @@ This project follows semantic versioning.
20
47
 
21
48
  - CONTRIBUTING release instructions now match the auto-release and publish workflow (no `follow-tags`).
22
49
  - Dependency updates for `pi-widget-core` and development tooling.
23
-
24
50
  ## [0.3.3] - 2026-07-04
25
51
 
26
52
  ### Added
27
53
 
28
54
  - Buy Me a Coffee sponsor button to README and native GitHub funding link via `.github/FUNDING.yml`.
29
-
30
55
  ## [0.3.2] - 2026-06-26
31
56
 
32
57
  ### Fixed
33
58
 
34
59
  - npm Trusted Publishing publish retry after registry configuration fix (no functional changes from 0.3.1).
35
-
36
60
  ## [0.3.1] - 2026-06-25
37
61
 
38
62
  ### Changed
39
63
 
40
64
  - 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.
41
65
  - `Package contents` now lists the actual shipped paths instead of unused template resource directories.
42
-
43
66
  ## [0.3.0] - 2026-06-24
44
67
 
45
68
  ### Added
@@ -50,7 +73,6 @@ This project follows semantic versioning.
50
73
  ### Changed
51
74
 
52
75
  - Minor semver bump marking the completed Host-only MVP: event boost, known tags, registry protocol, and demo-provider dogfooding path.
53
-
54
76
  ## [0.2.0] - 2026-06-15
55
77
 
56
78
  ### Added
@@ -65,4 +87,5 @@ This project follows semantic versioning.
65
87
  ### Changed
66
88
 
67
89
  - Replaced template placeholders and removed template-only skill, prompt, and theme resources.
90
+ ## Unreleased
68
91
 
package/README.md CHANGED
@@ -99,6 +99,8 @@ Future provider packages can publish to the host without importing this package
99
99
 
100
100
  See [`docs/protocol.md`](docs/protocol.md) and the copy-paste [`minimal provider example`](docs/provider-example.md).
101
101
 
102
+ Preset time blocks (`morning`, `day`, `evening`, `night`) follow the host machine's local hour with no timezone override — see [Limitations](docs/protocol.md#limitations) for how this affects presets such as `focus-day` and `night-owl`.
103
+
102
104
  ## Built-in demo provider
103
105
 
104
106
  The built-in demo provider exists to prove the host loop first:
@@ -114,6 +116,7 @@ The built-in demo provider exists to prove the host loop first:
114
116
  |---|---|
115
117
  | `extensions/index.ts` | Pi extension entrypoint and `/widget-host:*` command registration |
116
118
  | `lib/` | config store, registry protocol, policy evaluation, and demo provider |
119
+ | `docs/README.md` | documentation index for protocol, release, and maintenance docs |
117
120
  | `docs/protocol.md` | registry protocol reference for future provider packages |
118
121
  | `docs/provider-example.md` | minimal copy-paste provider publishing through the registry |
119
122
  | `docs/release.md` | Trusted Publishing release notes |
package/docs/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Documentation index
2
+
3
+ Entry point for `pi-widget-host` maintainer and provider-author docs.
4
+
5
+ ## Guides
6
+
7
+ | Doc | Purpose |
8
+ |---|---|
9
+ | [`config.md`](config.md) | `HostConfig` field contract, defaults, and `normalizeConfig` coercion rules. |
10
+ | [`protocol.md`](protocol.md) | Registry protocol reference (`globalThis`, required fields, tags, TTL). Includes [time-block timezone limitations](protocol.md#limitations) and [scheduling examples](protocol.md#scheduling-examples-local-hour-assumptions). |
11
+ | [`provider-example.md`](provider-example.md) | Minimal copy-paste provider that publishes through the registry. Notes [timezone assumptions](provider-example.md#timezone-assumptions) for `updatedAt` vs preset time blocks. |
12
+ | [`release.md`](release.md) | npm Trusted Publishing workflow and CI release automation. |
13
+
14
+ ## Maintenance context
15
+
16
+ | Doc | Purpose |
17
+ |---|---|
18
+ | [ROADMAP.md](https://github.com/eiei114/pi-widget-host/blob/main/ROADMAP.md) | Living release status, priorities, and bounded maintenance seeds. |
19
+ | [`npm-publish-run-2026-07-04.md`](npm-publish-run-2026-07-04.md) | Notes from the 2026-07-04 publish run (historical reference). |
20
+
21
+ ## Quick links
22
+
23
+ - Package README: [`../README.md`](../README.md)
24
+ - Contributing: [CONTRIBUTING.md](https://github.com/eiei114/pi-widget-host/blob/main/CONTRIBUTING.md)
25
+ - Changelog: [`../CHANGELOG.md`](../CHANGELOG.md)
package/docs/config.md ADDED
@@ -0,0 +1,110 @@
1
+ # Host config schema and normalization
2
+
3
+ `pi-widget-host` persists host settings in a JSON file under the Pi agent directory. Every read and write path runs the payload through `normalizeConfig` in `lib/config.ts` so callers always receive a stable `HostConfig` shape.
4
+
5
+ ## Storage location
6
+
7
+ | Item | Value |
8
+ |---|---|
9
+ | Default path | `~/.pi/agent/pi-widget-host-config.json` |
10
+ | Override | Set `PI_WIDGET_HOST_AGENT_DIR` to use `<dir>/pi-widget-host-config.json` instead |
11
+
12
+ If the file is missing, unreadable, or contains invalid JSON, `readHostConfig()` returns `createDefaultConfig()` without throwing.
13
+
14
+ ## `HostConfig` fields
15
+
16
+ All fields are required on the normalized object. Unknown keys in stored JSON are ignored.
17
+
18
+ | Field | Type | Default | `normalizeConfig` coercion |
19
+ |---|---|---|---|
20
+ | `schemaVersion` | `1` (literal) | `1` | Always rewritten to `1`. Any stored value is replaced. |
21
+ | `setupComplete` | `boolean` | `false` | `true` only when the input value is strictly `true`; every other value becomes `false`. |
22
+ | `demoProviderEnabled` | `boolean` | `false` | `true` only when the input value is strictly `true`; every other value becomes `false`. |
23
+ | `presetId` | `string` | `"always-demo"` | Valid built-in preset ids are preserved. Invalid ids, non-string values, and strings that do not exactly match a built-in id (including whitespace-wrapped values such as `" focus-day "`) normalize to `"always-demo"`. |
24
+ | `mutedProviderIds` | `string[]` | `[]` | When the input is an array, string elements with non-blank content (checked via `trim()`) are kept as written, including surrounding whitespace; duplicates removed, order preserved. Any non-array input becomes `[]`. |
25
+
26
+ Built-in preset ids (from `lib/policy.ts`):
27
+
28
+ - `always-demo` (default)
29
+ - `focus-day`
30
+ - `night-owl`
31
+
32
+ ## Normalization entry points
33
+
34
+ | Function | Behavior |
35
+ |---|---|
36
+ | `createDefaultConfig()` | Returns a fresh default object without touching disk. |
37
+ | `readHostConfig()` | Reads JSON from disk, parses it, then normalizes. On any I/O or parse failure, returns `createDefaultConfig()`. |
38
+ | `writeHostConfig(config)` | Normalizes the input, writes pretty-printed JSON, returns the normalized value. |
39
+ | `updateHostConfig(mutator)` | Reads current config, applies `mutator`, then writes through `writeHostConfig`. |
40
+
41
+ ## Non-object and partial payloads
42
+
43
+ When the parsed JSON root is not a plain object (`null`, array, string, number, boolean), normalization treats the payload as `{}` and produces the full default config shape.
44
+
45
+ Examples:
46
+
47
+ ```json
48
+ "hello"
49
+ ```
50
+
51
+ ```json
52
+ 42
53
+ ```
54
+
55
+ Both normalize to:
56
+
57
+ ```json
58
+ {
59
+ "schemaVersion": 1,
60
+ "setupComplete": false,
61
+ "demoProviderEnabled": false,
62
+ "presetId": "always-demo",
63
+ "mutedProviderIds": []
64
+ }
65
+ ```
66
+
67
+ Partial objects keep valid fields and coerce the rest:
68
+
69
+ ```json
70
+ {
71
+ "setupComplete": true,
72
+ "presetId": "focus-day",
73
+ "mutedProviderIds": ["alpha", "alpha", " "]
74
+ }
75
+ ```
76
+
77
+ Normalizes to:
78
+
79
+ ```json
80
+ {
81
+ "schemaVersion": 1,
82
+ "setupComplete": true,
83
+ "demoProviderEnabled": false,
84
+ "presetId": "focus-day",
85
+ "mutedProviderIds": ["alpha"]
86
+ }
87
+ ```
88
+
89
+ ## Malformed field examples
90
+
91
+ | Stored value | Normalized result |
92
+ |---|---|
93
+ | `"presetId": "not-a-real-preset"` | `"presetId": "always-demo"` |
94
+ | `"presetId": " focus-day "` | `"presetId": "always-demo"` |
95
+ | `"mutedProviderIds": "alpha"` | `"mutedProviderIds": []` |
96
+ | `"mutedProviderIds": [" alpha "]` | `"mutedProviderIds": [" alpha "]` |
97
+ | `"mutedProviderIds": ["ok", 42, "", " ", "ok"]` | `"mutedProviderIds": ["ok"]` |
98
+ | `"setupComplete": "yes"` | `"setupComplete": false` |
99
+ | `"demoProviderEnabled": 1` | `"demoProviderEnabled": false` |
100
+
101
+ ## Schema versioning
102
+
103
+ `schemaVersion` is pinned to `1`. There is no forward migration path yet; future versions would extend `normalizeConfig` to upgrade older shapes before returning `HostConfig`.
104
+
105
+ ## Related code
106
+
107
+ - Type definition: `lib/types.ts` (`HostConfig`)
108
+ - Normalization and persistence: `lib/config.ts`
109
+ - Preset catalog: `lib/policy.ts` (`PRESET_OPTIONS`, `getPreset`, `DEFAULT_PRESET_ID`)
110
+ - Tests: `tests/config.test.ts`
package/docs/protocol.md CHANGED
@@ -51,3 +51,65 @@ type WidgetHostRegistry = {
51
51
  - `ttlMs` + `updatedAt` drive stale exclusion
52
52
  - providers render final lines themselves; the host only picks a winner
53
53
  - the host can mute providers from config without uninstalling the package
54
+
55
+ ## Limitations
56
+
57
+ ### Time blocks use the host's local timezone
58
+
59
+ Policy evaluation calls `detectTimeBlock(now)` in `lib/policy.ts`, which reads `now.getHours()` — the **host machine's local hour** in the process timezone. There is no `HostConfig` field or environment variable to override the timezone or shift block boundaries.
60
+
61
+ Block boundaries (local hour, inclusive start / exclusive end unless noted):
62
+
63
+ | Block | Local hour range |
64
+ |---|---|
65
+ | `morning` | 05:00–10:59 |
66
+ | `day` | 11:00–16:59 |
67
+ | `evening` | 17:00–21:59 |
68
+ | `night` | 22:00–04:59 |
69
+
70
+ Because presets are keyed by these blocks, the same `presetId` can behave differently on machines in different timezones or when the OS timezone changes. For example:
71
+
72
+ - **`focus-day`** keeps the shared slot active during `morning`, `day`, and `evening`, but stays **silent at `night`**. A provider that looks "blocked" at 23:00 UTC may still be eligible at 23:00 JST on a Tokyo-local host.
73
+ - **`night-owl`** stays **silent during `morning` and `day`**, then allows the slot in `evening` and `night`. Travel or remote machines can flip eligibility without any config change.
74
+
75
+ When debugging provider priority or preset behavior, check the host's local time and current block before assuming a config bug.
76
+
77
+ ### Scheduling examples (local hour assumptions)
78
+
79
+ The examples below mirror `tests/time-block-policy.test.ts`. Each `Date` is constructed with the host's **local** calendar fields (`new Date(year, month, day, hour, …)`), so the resulting time block follows the machine timezone, not UTC.
80
+
81
+ **Example 1 — `night-owl` stays silent during work hours**
82
+
83
+ With `presetId: "night-owl"` and one eligible demo provider:
84
+
85
+ | Local time on host | `detectTimeBlock` | Widget slot |
86
+ |---|---|---|
87
+ | 07:00 | `morning` | silent |
88
+ | 13:00 | `day` | silent |
89
+ | 19:00 | `evening` | active |
90
+ | 23:00 | `night` | active |
91
+
92
+ The same saved preset can look "broken" when you compare against UTC. At **23:00 UTC** on a UTC host the block is `night` and the slot is active; on a **Tokyo-local host** that instant is **08:00 JST** (`morning`), so `night-owl` correctly stays silent.
93
+
94
+ **Example 2 — `focus-day` vs `always-demo` at local night**
95
+
96
+ At **23:30 local** with one eligible provider:
97
+
98
+ - `always-demo` — slot stays active (`night` block is allowed).
99
+ - `focus-day` — slot is silent (`night` block sets `allowedProviderIds: []`).
100
+
101
+ **Example 3 — TTL refresh scheduling is UTC-based**
102
+
103
+ Stale exclusion and the host's stale-TTL `setTimeout` use `updatedAt` (ISO-8601 UTC) plus `ttlMs`. That path does **not** read local hour boundaries:
104
+
105
+ ```ts
106
+ registry.set({
107
+ providerId: "example.stale-check",
108
+ available: true,
109
+ lines: ["hello"],
110
+ updatedAt: new Date().toISOString(), // UTC instant
111
+ ttlMs: 90_000,
112
+ });
113
+ ```
114
+
115
+ A provider published at `2026-06-15T12:00:00.000Z` with `ttlMs: 60_000` goes stale at `12:01:00Z` on every host, regardless of preset or local time block. Do not mix this TTL clock with preset time-block scheduling when debugging.
@@ -33,3 +33,16 @@ registry?.set({
33
33
  ```
34
34
 
35
35
  Required fields are `providerId`, `available`, `lines`, and `updatedAt`. Optional fields such as `priority`, `tags`, `mode`, and `ttlMs` help the host choose between eligible providers. See [`protocol.md`](protocol.md) for the full registry shape and host selection notes.
36
+
37
+ ## Timezone assumptions
38
+
39
+ Two different clocks appear in this example:
40
+
41
+ | Field / mechanism | Time basis | Used for |
42
+ |---|---|---|
43
+ | `updatedAt` (`toISOString()`) | UTC instant | stale exclusion and stale-TTL refresh scheduling |
44
+ | Preset time blocks (`morning`, `day`, …) | Host machine local hour | when a saved preset allows or silences the shared slot |
45
+
46
+ `new Date().toISOString()` in the snippet above is correct for TTL bookkeeping. It does **not** pin the provider to a local time block — the host evaluates blocks separately via `detectTimeBlock(now)` when applying preset policy.
47
+
48
+ Tags cannot gate availability: the host scores `tags` only against the current block's `preferredTags` (plus the `playing-now` / `matchday` event boost) to adjust `effectivePriority`. Eligibility still depends on registry state (`available`, `lines`, stale TTL, host mutes) and the preset's `allowedProviderIds`. If your provider must appear only during certain local hours, enforce that in your own publish loop; the host will still classify the current block from the **process local timezone**. See [Limitations — scheduling examples](protocol.md#scheduling-examples-local-hour-assumptions) for preset walkthroughs (`focus-day`, `night-owl`) and a UTC-vs-local debugging scenario.
@@ -258,3 +258,17 @@ export default function widgetHostExtension(pi: ExtensionAPI) {
258
258
  },
259
259
  });
260
260
  }
261
+
262
+ export function resetHostExtensionStateForTests(): void {
263
+ clearStaleTimer();
264
+ registryDispose?.();
265
+ registryDispose = undefined;
266
+ stopDemoProviderHeartbeat();
267
+ uiBridge = undefined;
268
+ }
269
+
270
+ export function isRegistryWatcherActiveForTests(): boolean {
271
+ return registryDispose !== undefined;
272
+ }
273
+
274
+ export { ensureRegistryWatcher, refreshHostWidget };
@@ -1,6 +1,6 @@
1
1
  import { createProviderRuntime, type ProviderRuntime } from "pi-widget-core/provider";
2
2
  import { readHostConfig } from "./config.ts";
3
- import { detectTimeBlock } from "./policy.ts";
3
+ import { DEFAULT_PRESET_ID, detectTimeBlock, getBlockPreferredTags, getPreset } from "./policy.ts";
4
4
  import { DEMO_PROVIDER_ID, type HostConfig, type ProviderEntry } from "./types.ts";
5
5
 
6
6
  const HEARTBEAT_MS = 30_000;
@@ -14,20 +14,6 @@ function ensureRuntime(): ProviderRuntime {
14
14
  return runtime;
15
15
  }
16
16
 
17
- function baseTagsForBlock(block: ReturnType<typeof detectTimeBlock>): string[] {
18
- switch (block) {
19
- case "morning":
20
- return ["music", "idle"];
21
- case "day":
22
- return ["sports", "idle"];
23
- case "evening":
24
- return ["music", "sports"];
25
- case "night":
26
- default:
27
- return ["idle", "music"];
28
- }
29
- }
30
-
31
17
  function readEventTags(): string[] {
32
18
  const tags: string[] = [];
33
19
  if (["1", "true", "yes"].includes(String(process.env.PI_WIDGET_HOST_DEMO_PLAYING_NOW ?? "").toLowerCase())) {
@@ -41,7 +27,8 @@ function readEventTags(): string[] {
41
27
 
42
28
  export function buildDemoProviderEntry(config: HostConfig, now = new Date()): ProviderEntry {
43
29
  const block = detectTimeBlock(now);
44
- const tags = [...new Set([...baseTagsForBlock(block), ...readEventTags()])];
30
+ const preferredTags = getBlockPreferredTags(getPreset(DEFAULT_PRESET_ID), block);
31
+ const tags = [...new Set([...preferredTags, ...readEventTags()])];
45
32
 
46
33
  return {
47
34
  providerId: DEMO_PROVIDER_ID,
package/lib/policy.ts CHANGED
@@ -2,6 +2,7 @@ import { type HostConfig, type HostTimeBlock, type KnownHostTag, type PolicyEval
2
2
 
3
3
  export const DEFAULT_PRESET_ID = "always-demo";
4
4
  export const KNOWN_HOST_TAGS: readonly KnownHostTag[] = ["music", "sports", "playing-now", "matchday", "idle"] as const;
5
+ const KNOWN_HOST_TAG_SET = new Set<string>(KNOWN_HOST_TAGS);
5
6
 
6
7
  export const PRESET_OPTIONS: readonly PolicyPreset[] = [
7
8
  {
@@ -39,8 +40,14 @@ export const PRESET_OPTIONS: readonly PolicyPreset[] = [
39
40
  },
40
41
  ];
41
42
 
43
+ const PRESET_BY_ID = new Map(PRESET_OPTIONS.map((preset) => [preset.id, preset]));
44
+
42
45
  export function getPreset(id: string | undefined): PolicyPreset {
43
- return PRESET_OPTIONS.find((preset) => preset.id === id) ?? PRESET_OPTIONS[0]!;
46
+ return PRESET_BY_ID.get(id ?? DEFAULT_PRESET_ID) ?? PRESET_OPTIONS[0]!;
47
+ }
48
+
49
+ export function getBlockPreferredTags(preset: PolicyPreset, block: HostTimeBlock): readonly KnownHostTag[] {
50
+ return preset.blocks[block].preferredTags ?? [];
44
51
  }
45
52
 
46
53
  export function detectTimeBlock(now = new Date()): HostTimeBlock {
@@ -63,29 +70,32 @@ export function isEntryStale(entry: Pick<ProviderEntry, "updatedAt" | "ttlMs">,
63
70
  return typeof remaining === "number" ? remaining <= 0 : false;
64
71
  }
65
72
 
66
- function tagScore(knownTags: KnownHostTag[], preferredTags: readonly KnownHostTag[] | undefined): number {
73
+ function tagScore(knownTags: ReadonlySet<KnownHostTag>, preferredTags: readonly KnownHostTag[] | undefined): number {
67
74
  if (!preferredTags || preferredTags.length === 0) return 0;
68
- const scores = preferredTags
69
- .map((tag, index) => (knownTags.includes(tag) ? Math.max(40 - index * 10, 10) : 0))
70
- .filter((value) => value > 0);
71
- return scores.length > 0 ? Math.max(...scores) : 0;
75
+ let best = 0;
76
+ for (let index = 0; index < preferredTags.length; index++) {
77
+ const tag = preferredTags[index]!;
78
+ if (!knownTags.has(tag)) continue;
79
+ best = Math.max(best, Math.max(40 - index * 10, 10));
80
+ }
81
+ return best;
72
82
  }
73
83
 
74
- function eventBoost(knownTags: KnownHostTag[]): number {
84
+ function eventBoost(knownTags: ReadonlySet<KnownHostTag>): number {
75
85
  let boost = 0;
76
- if (knownTags.includes("playing-now")) boost += 100;
77
- if (knownTags.includes("matchday")) boost += 80;
86
+ if (knownTags.has("playing-now")) boost += 100;
87
+ if (knownTags.has("matchday")) boost += 80;
78
88
  return boost;
79
89
  }
80
90
 
81
- function normalizeKnownTags(tags: string[] | undefined): KnownHostTag[] {
91
+ function normalizeKnownTags(tags: string[] | undefined): Set<KnownHostTag> {
82
92
  const known = new Set<KnownHostTag>();
83
93
  for (const tag of tags ?? []) {
84
- if ((KNOWN_HOST_TAGS as readonly string[]).includes(tag)) {
94
+ if (KNOWN_HOST_TAG_SET.has(tag)) {
85
95
  known.add(tag as KnownHostTag);
86
96
  }
87
97
  }
88
- return [...known];
98
+ return known;
89
99
  }
90
100
 
91
101
  export function describePreset(preset: PolicyPreset): string {
@@ -103,13 +113,16 @@ export function evaluateProviderEntries(entries: readonly ProviderEntry[], confi
103
113
  const preset = getPreset(config.presetId);
104
114
  const timeBlock = detectTimeBlock(now);
105
115
  const blockPolicy = preset.blocks[timeBlock];
116
+ const mutedProviderIds = new Set(config.mutedProviderIds);
117
+ const allowedProviderIds =
118
+ blockPolicy.allowedProviderIds === undefined ? undefined : new Set(blockPolicy.allowedProviderIds);
106
119
 
107
120
  const providerStates = entries.map<ProviderState>((entry) => {
108
- const knownTags = normalizeKnownTags(entry.tags);
109
- const isMuted = config.mutedProviderIds.includes(entry.providerId);
121
+ const knownTagSet = normalizeKnownTags(entry.tags);
122
+ const isMuted = mutedProviderIds.has(entry.providerId);
110
123
  const isStale = isEntryStale(entry, now);
111
124
  const hasLines = Array.isArray(entry.lines) && entry.lines.length > 0;
112
- const isAllowed = blockPolicy.allowedProviderIds === undefined || blockPolicy.allowedProviderIds.includes(entry.providerId);
125
+ const isAllowed = allowedProviderIds === undefined || allowedProviderIds.has(entry.providerId);
113
126
  const reasons: string[] = [];
114
127
 
115
128
  if (!entry.available) reasons.push("provider reported unavailable");
@@ -118,7 +131,7 @@ export function evaluateProviderEntries(entries: readonly ProviderEntry[], confi
118
131
  if (isMuted) reasons.push("muted in host config");
119
132
  if (isStale) reasons.push("stale ttl");
120
133
 
121
- const effectivePriority = (entry.priority ?? 0) + tagScore(knownTags, blockPolicy.preferredTags) + eventBoost(knownTags);
134
+ const effectivePriority = (entry.priority ?? 0) + tagScore(knownTagSet, blockPolicy.preferredTags) + eventBoost(knownTagSet);
122
135
 
123
136
  let effectiveStatus: ProviderState["effectiveStatus"] = "eligible";
124
137
  if (isMuted) {
@@ -133,7 +146,7 @@ export function evaluateProviderEntries(entries: readonly ProviderEntry[], confi
133
146
  ...entry,
134
147
  effectivePriority,
135
148
  effectiveStatus,
136
- knownTags,
149
+ knownTags: [...knownTagSet],
137
150
  reasons,
138
151
  isMuted,
139
152
  isStale,
package/package.json CHANGED
@@ -1,65 +1,65 @@
1
- {
2
- "name": "pi-widget-host",
3
- "version": "0.3.5",
4
- "description": "Host package for managing one shared Pi widget slot across multiple providers with preset policies and a built-in demo provider.",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "Keisu",
8
- "keywords": [
9
- "pi-package",
10
- "pi-extension",
11
- "widget",
12
- "host",
13
- "display-policy"
14
- ],
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/eiei114/pi-widget-host.git"
18
- },
19
- "bugs": {
20
- "url": "https://github.com/eiei114/pi-widget-host/issues"
21
- },
22
- "homepage": "https://github.com/eiei114/pi-widget-host#readme",
23
- "files": [
24
- "extensions/",
25
- "lib/",
26
- "docs/",
27
- "README.md",
28
- "LICENSE",
29
- "CHANGELOG.md"
30
- ],
31
- "scripts": {
32
- "typecheck": "tsc --noEmit",
33
- "test": "node --import tsx --test tests/*.test.ts",
34
- "ci": "npm run typecheck && npm test && npm run pack:check",
35
- "pack:check": "npm pack --dry-run"
36
- },
37
- "engines": {
38
- "node": ">=20"
39
- },
40
- "pi": {
41
- "extensions": [
42
- "./extensions/index.ts"
43
- ]
44
- },
45
- "publishConfig": {
46
- "access": "public"
47
- },
48
- "dependencies": {
49
- "pi-widget-core": "^0.1.0"
50
- },
51
- "peerDependencies": {
52
- "@earendil-works/pi-coding-agent": "*"
53
- },
54
- "peerDependenciesMeta": {
55
- "@earendil-works/pi-coding-agent": {
56
- "optional": true
57
- }
58
- },
59
- "devDependencies": {
60
- "@earendil-works/pi-coding-agent": "latest",
61
- "@types/node": "^26.0.0",
62
- "tsx": "^4.20.6",
63
- "typescript": "^6.0.3"
64
- }
65
- }
1
+ {
2
+ "name": "pi-widget-host",
3
+ "version": "0.3.11",
4
+ "description": "Host package for managing one shared Pi widget slot across multiple providers with preset policies and a built-in demo provider.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Keisu",
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "widget",
12
+ "host",
13
+ "display-policy"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/eiei114/pi-widget-host.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/eiei114/pi-widget-host/issues"
21
+ },
22
+ "homepage": "https://github.com/eiei114/pi-widget-host#readme",
23
+ "files": [
24
+ "extensions/",
25
+ "lib/",
26
+ "docs/",
27
+ "README.md",
28
+ "LICENSE",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "node --import tsx --test tests/*.test.ts",
34
+ "ci": "npm run typecheck && npm test && npm run pack:check",
35
+ "pack:check": "npm pack --dry-run"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "pi": {
41
+ "extensions": [
42
+ "./extensions/index.ts"
43
+ ]
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "pi-widget-core": "^0.1.4"
50
+ },
51
+ "peerDependencies": {
52
+ "@earendil-works/pi-coding-agent": "*"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@earendil-works/pi-coding-agent": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@earendil-works/pi-coding-agent": "latest",
61
+ "@types/node": "^26.0.0",
62
+ "tsx": "^4.20.6",
63
+ "typescript": "^7.0.2"
64
+ }
65
+ }