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 +29 -6
- package/README.md +3 -0
- package/docs/README.md +25 -0
- package/docs/config.md +110 -0
- package/docs/protocol.md +62 -0
- package/docs/provider-example.md +13 -0
- package/extensions/index.ts +14 -0
- package/lib/demo-provider.ts +3 -16
- package/lib/policy.ts +30 -17
- package/package.json +65 -65
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.
|
package/docs/provider-example.md
CHANGED
|
@@ -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.
|
package/extensions/index.ts
CHANGED
|
@@ -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 };
|
package/lib/demo-provider.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
73
|
+
function tagScore(knownTags: ReadonlySet<KnownHostTag>, preferredTags: readonly KnownHostTag[] | undefined): number {
|
|
67
74
|
if (!preferredTags || preferredTags.length === 0) return 0;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
84
|
+
function eventBoost(knownTags: ReadonlySet<KnownHostTag>): number {
|
|
75
85
|
let boost = 0;
|
|
76
|
-
if (knownTags.
|
|
77
|
-
if (knownTags.
|
|
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 (
|
|
94
|
+
if (KNOWN_HOST_TAG_SET.has(tag)) {
|
|
85
95
|
known.add(tag as KnownHostTag);
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
|
-
return
|
|
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
|
|
109
|
-
const isMuted =
|
|
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 =
|
|
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(
|
|
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.
|
|
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.
|
|
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": "^
|
|
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
|
+
}
|