pi-extension-utils 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.
Files changed (61) hide show
  1. package/README.md +91 -45
  2. package/dist/index.js +9 -140
  3. package/dist/src/client/index.d.ts +7 -0
  4. package/dist/src/client/index.js +25 -0
  5. package/dist/src/client/types.d.ts +17 -0
  6. package/dist/src/client/types.js +1 -0
  7. package/dist/src/config/index.d.ts +28 -0
  8. package/dist/src/config/index.js +222 -0
  9. package/dist/src/index.d.ts +10 -7
  10. package/dist/src/index.js +10 -7
  11. package/dist/src/logger/config.d.ts +11 -0
  12. package/dist/src/logger/config.js +27 -0
  13. package/dist/src/logger/index.d.ts +20 -0
  14. package/dist/src/logger/index.js +76 -0
  15. package/dist/src/{pane-overlay.d.ts → pane/overlay.d.ts} +1 -1
  16. package/dist/src/{pane-overlay.js → pane/overlay.js} +2 -2
  17. package/dist/src/reminders/client.d.ts +10 -0
  18. package/dist/src/reminders/client.js +49 -0
  19. package/dist/src/reminders/config.d.ts +5 -6
  20. package/dist/src/reminders/config.js +9 -23
  21. package/dist/src/reminders/debug.js +2 -2
  22. package/dist/src/reminders/host.d.ts +5 -0
  23. package/dist/src/reminders/host.js +298 -0
  24. package/dist/src/reminders/index.d.ts +3 -5
  25. package/dist/src/reminders/index.js +3 -308
  26. package/dist/src/reminders/types.d.ts +3 -0
  27. package/dist/src/reminders/types.js +3 -0
  28. package/dist/src/ui/client.d.ts +22 -0
  29. package/dist/src/ui/client.js +17 -0
  30. package/dist/src/utils-config.d.ts +24 -0
  31. package/dist/src/utils-config.js +38 -0
  32. package/dist/src/widgets/client.d.ts +27 -0
  33. package/dist/src/widgets/client.js +130 -0
  34. package/dist/src/widgets/host.d.ts +2 -0
  35. package/dist/src/widgets/host.js +143 -0
  36. package/dist/src/{protocol.d.ts → widgets/protocol.d.ts} +2 -6
  37. package/docs/README.md +11 -0
  38. package/docs/client.md +91 -0
  39. package/docs/config.md +86 -0
  40. package/docs/pane-overlay.md +88 -0
  41. package/docs/reference/reminders-spec.md +351 -0
  42. package/docs/reminders.md +68 -0
  43. package/docs/widgets.md +57 -0
  44. package/examples/README.md +23 -0
  45. package/examples/config.ts +31 -0
  46. package/examples/logger.ts +20 -0
  47. package/examples/pane-overlay.ts +47 -0
  48. package/examples/reminders.ts +25 -0
  49. package/examples/widget-coordinator.ts +18 -0
  50. package/package.json +12 -1
  51. package/dist/src/client.d.ts +0 -47
  52. package/dist/src/client.js +0 -194
  53. package/dist/src/logger.d.ts +0 -12
  54. package/dist/src/logger.js +0 -45
  55. /package/dist/src/{tui-chrome.d.ts → pane/chrome.d.ts} +0 -0
  56. /package/dist/src/{tui-chrome.js → pane/chrome.js} +0 -0
  57. /package/dist/src/{key-dispatch.d.ts → pane/key-dispatch.d.ts} +0 -0
  58. /package/dist/src/{key-dispatch.js → pane/key-dispatch.js} +0 -0
  59. /package/dist/src/{pane-state.d.ts → pane/state.d.ts} +0 -0
  60. /package/dist/src/{pane-state.js → pane/state.js} +0 -0
  61. /package/dist/src/{protocol.js → widgets/protocol.js} +0 -0
@@ -0,0 +1,351 @@
1
+ # SPEC: Pi Reminders
2
+
3
+ ## 1. Purpose
4
+
5
+ `pi-extension-utils` now contains the shared reminder host for Pi.
6
+
7
+ It centralizes dynamic guidance that producer extensions need to show the model. Packages such as `pi-dag-tasks`, `pi-dynamic-context-pruning`, and `pi-subagents` publish reminder intents over Pi's event bus. the reminders host aggregates those intents and writes compact `<system-reminder>` messages into conversation history when reminders are created, changed, forced, or due for a configured repeat interval.
8
+
9
+ The core goals are:
10
+
11
+ - producers publish intent instead of mutating transcript/tool-result content directly
12
+ - reminders are visible and durable like normal conversation context
13
+ - reminders are not repeated every provider request
14
+ - prompt-cache stability is preserved because old checkpointed messages are not rewritten
15
+
16
+ ## 2. Problem
17
+
18
+ Several Pi extensions need to remind the agent about transient state:
19
+
20
+ - task status and ready work from `pi-dag-tasks`
21
+ - compression nudges, protected-tail hints, and safe ranges from `pi-dynamic-context-pruning`
22
+ - async subagent attention/status hints from `pi-subagents`
23
+
24
+ The unsafe old pattern was to append reminder text directly into existing message content, tool results, or rendered compression blocks. That mutates prompt text that may already be inside a cached checkpoint. With Anthropic prompt caching, a later `cache_control: { type: "ephemeral" }` marker is a cache breakpoint/write marker, not an instruction to exclude volatile text from the cache key.
25
+
26
+ A provider-only volatile trailer avoids rewriting history, but repeating the same side-channel every request can confuse the model and is brittle for custom providers that bypass provider-payload hooks. The v2 model therefore writes reminders as durable history messages only when they are due.
27
+
28
+ ## 3. Product direction
29
+
30
+ Desired flow:
31
+
32
+ ```text
33
+ pi-dag-tasks / DCP / subagents / future extensions
34
+ -> pi.events.emit("reminder:upsert", reminderIntent)
35
+ -> src/reminders/reminders manager stores, groups, sorts, and tracks announcement state
36
+ -> before_agent_start returns or context queues one durable <system-reminder> custom message only when due
37
+ -> provider receives normal conversation history
38
+ ```
39
+
40
+ Only the reminders host should write model-facing reminder text. Producer packages should publish intent, not mutate prompt history.
41
+
42
+ ## 4. Goals
43
+
44
+ - Provide a neutral event-bus contract for extensions to publish reminder intents.
45
+ - Write reminders as durable conversation-history messages, not provider-only tails.
46
+ - Announce reminder changes, not reminder state every turn.
47
+ - Support one-shot, session, and persistent reminders with optional repeat intervals.
48
+ - Ensure equivalent reminder sets render byte-stably through deterministic ordering and formatting.
49
+ - Keep `source`, `id`, `priority`, `ttl`, `repeatEveryTurns`, and `metadata` internal to normal model-facing output.
50
+ - Avoid adding `cache_control` to reminders.
51
+ - Provide a migration path for existing task reminders, DCP nudges, and subagent attention messages.
52
+
53
+ ## 5. Non-goals
54
+
55
+ the reminders host must not become:
56
+
57
+ - a task manager or replacement for `pi-dag-tasks`
58
+ - a DCP compression policy engine
59
+ - a subagent scheduler
60
+ - a generic notification/UI system
61
+ - a prompt-cache router for all provider content
62
+ - a package that adds `cache_control` to reminders
63
+
64
+ ## 6. Cache and history invariants
65
+
66
+ These invariants are mandatory:
67
+
68
+ 1. **Do not mutate existing transcript messages.**
69
+ Reminder handling must not append text to prior user, assistant, tool result, bash execution, or compressed-block messages.
70
+
71
+ 2. **Do not mutate tool results.**
72
+ Tool outputs are often checkpointed. Reminder text must not be appended to `tool_result` content.
73
+
74
+ 3. **Do not mutate DCP compression blocks.**
75
+ Compression blocks represent stable summaries/checkpoints. Reminders and nudges must be rendered separately.
76
+
77
+ 4. **Write new history messages only when due.**
78
+ Do not repeat the entire active reminder set on every request. Write when created, changed, forced, or when a configured repeat interval elapses.
79
+
80
+ 5. **Do not put `cache_control` on reminders.**
81
+ Anthropic `cache_control: { type: "ephemeral" }` is a cache breakpoint/write marker. Reminder messages are normal history and should not create cache markers.
82
+
83
+ 6. **Render deterministically.**
84
+ For the same due reminder set, output order and formatting must be byte-stable.
85
+
86
+ ## 7. Event namespace
87
+
88
+ Event names:
89
+
90
+ - `reminder:upsert`
91
+ - `reminder:remove`
92
+ - `reminder:clear-source`
93
+ - `reminder:list`
94
+ - `reminder:announce-now`
95
+
96
+ Shared constants should be exported by the package so producer extensions do not duplicate string literals.
97
+
98
+ ## 8. Reminder intent contract
99
+
100
+ Initial TypeScript shape:
101
+
102
+ ```ts
103
+ export type ReminderTtl = "once" | "session" | "persistent";
104
+
105
+ export interface ReminderIntent {
106
+ /** Stable unique ID within the producer source. */
107
+ id: string;
108
+
109
+ /** Producer namespace, e.g. "pi-dag-tasks", "dcp", "pi-subagents". */
110
+ source: string;
111
+
112
+ /** Compact model-facing reminder text. Source/id are not rendered by default. */
113
+ text: string;
114
+
115
+ /** Optional compact display label, e.g. "Tasks", "DCP", "Subagents". */
116
+ label?: string;
117
+
118
+ /** Higher values render earlier. Default: 0. */
119
+ priority?: number;
120
+
121
+ /** Whether this reminder should be shown in chat. Default: true. */
122
+ display?: boolean;
123
+
124
+ /** Lifecycle policy. Default: "once". */
125
+ ttl?: ReminderTtl;
126
+
127
+ /** Optional repeat interval for persistent reminders. */
128
+ repeatEveryTurns?: number;
129
+
130
+ /** Optional structured metadata for debugging; not rendered. */
131
+ metadata?: Record<string, unknown>;
132
+ }
133
+ ```
134
+
135
+ Upsert semantics:
136
+
137
+ - key: `(source, id)`
138
+ - a later upsert with the same key replaces the previous reminder
139
+ - `source`, `id`, `priority`, `display`, `ttl`, `repeatEveryTurns`, and `metadata` are internal manager fields for replacement, sorting, lifecycle, and debugging; they are not rendered by default
140
+ - `text` must be short, already summarized, and safe to render as one compact line when possible
141
+ - `display: false` keeps the reminder model-visible in history but omits it from the chat renderer; mixed announcements display only the reminders whose `display` is not false
142
+ - empty or whitespace-only text removes the reminder
143
+ - create/change marks the reminder due for the next history announcement
144
+
145
+ TTL semantics:
146
+
147
+ - `once`: default; announce once into history, then remove
148
+ - `session`: announce on create/change; keep until explicit removal, `clear-source`, or session end
149
+ - `persistent`: announce on create/change; optionally repeat every `repeatEveryTurns`; keep until explicit removal, `clear-source`, or session end
150
+
151
+ Force semantics:
152
+
153
+ ```ts
154
+ export interface ReminderAnnounceNowRequest {
155
+ source?: string;
156
+ id?: string;
157
+ }
158
+ ```
159
+
160
+ - no payload: force all active reminders
161
+ - `source`: force reminders from one source
162
+ - `source` + `id`: force one reminder
163
+
164
+ Removal/list semantics:
165
+
166
+ ```ts
167
+ export interface ReminderRemoveRequest {
168
+ source: string;
169
+ id: string;
170
+ }
171
+
172
+ export interface ReminderClearSourceRequest {
173
+ source: string;
174
+ }
175
+
176
+ export interface ReminderListRequest {
177
+ source?: string;
178
+ resolve: (snapshot: ReminderSnapshot) => void;
179
+ reject?: (error: unknown) => void;
180
+ }
181
+ ```
182
+
183
+ ## 9. Rendering model
184
+
185
+ The manager renders due reminders into a single compact `<system-reminder>` message. It must not render one XML block per reminder, and it must not expose internal `source`, `id`, `priority`, `ttl`, `repeatEveryTurns`, or `metadata` unless an explicit debug surface asks for it.
186
+
187
+ Recommended model-facing shape:
188
+
189
+ ```text
190
+ <system-reminder>
191
+ Tasks: 3 open / 1 active / 2 ready. Active #7 Draft SPEC. Next: review injection boundary.
192
+ DCP: compress older closed ranges; do not end in protected tail >= m0184. Safe: m0041-m0097.
193
+ Subagents: 5de6cc9f needs attention; check status before interrupting.
194
+ </system-reminder>
195
+ ```
196
+
197
+ For token efficiency, render one line per source/group where possible. Multiple reminders from the same source should be merged into that source's line, ordered internally by priority and id.
198
+
199
+ Rendering order:
200
+
201
+ 1. groups by descending max `priority`
202
+ 2. ascending compact group label/source
203
+ 3. reminders within a group by descending `priority`, then ascending `id`
204
+
205
+ Formatting requirements:
206
+
207
+ - stable newline convention
208
+ - no timestamps unless supplied by the producer as stable text
209
+ - no random IDs
210
+ - no object key order leakage from metadata
211
+ - no `cache_control` on reminder messages
212
+ - no per-reminder XML attributes or wrappers in normal model-facing output
213
+ - escape or normalize wrapper-breaking text: producer text must not be able to close `</system-reminder>` early or create malformed reminder structure
214
+
215
+ Sanitization rules:
216
+
217
+ - trim leading/trailing whitespace from each reminder text
218
+ - collapse internal runs of whitespace/newlines to compact spaces unless a future API supports preformatted text
219
+ - replace literal `</system-reminder>` and other wrapper-breaking sequences in reminder text with safe escaped text
220
+ - derive labels from a small allowlist or sanitize to short alphanumeric labels plus `-`/`_`
221
+ - never render arbitrary metadata values in the reminder message
222
+
223
+ V2 intentionally does not support cross-source semantic deduplication. Replacement is by `(source, id)` only. Cross-source dedupe can hide important guidance and should require a separate design.
224
+
225
+ ## 10. Pi integration points
226
+
227
+ the reminders host is a normal Pi extension package.
228
+
229
+ Expected entrypoint:
230
+
231
+ ```ts
232
+ export default function (pi: ExtensionAPI): void {
233
+ // register event listeners
234
+ // write due reminder history messages
235
+ // clean up per-session state
236
+ }
237
+ ```
238
+
239
+ Initial implementation scope:
240
+
241
+ - event constants and shared TypeScript types
242
+ - in-memory reminder manager with announcement state
243
+ - compact deterministic renderer
244
+ - `before_agent_start` custom-message insertion for reminders due before a run
245
+ - `context` detection for reminders that become due during long-running agent loops, queued with `pi.sendMessage(..., { deliverAs: "steer" })`
246
+ - `/remind` debug command
247
+ - `reminder:list` debug event
248
+ - tests for lifecycle, repeat behavior, forced announcements, context insertion, rendering stability, and no provider-payload injection
249
+
250
+ Primary hooks:
251
+
252
+ - `pi.on("before_agent_start", ...)`: if reminders are already due before a run starts, return one custom message with `customType: "src/reminders/reminders"`, `display: true`, and `<system-reminder>` model-visible content.
253
+ - `pi.on("context", ...)`: if reminders become due during an agent loop, queue one persisted custom message with `pi.sendMessage(..., { deliverAs: "steer" })` so long-running agents can receive reminders before the next LLM call without transient context mutation.
254
+ - `pi.registerMessageRenderer("src/reminders/reminders", ...)`: render persisted reminder messages in chat without XML wrapper tags.
255
+
256
+ Lifecycle hooks:
257
+
258
+ - `session_start` / `session_shutdown`: clear in-memory reminders
259
+ - `turn_start`: advance the manager's turn counter for `repeatEveryTurns`
260
+
261
+ Provider payload hooks are not the primary injection path in v2. Provider-specific payload rewriting should not be used for reminders unless a separate design explicitly reintroduces it.
262
+
263
+ Deferred until after v2:
264
+
265
+ - custom renderers
266
+ - cross-source semantic dedupe
267
+ - subagent migration unless a specific volatile model-only path requires it
268
+
269
+ ## 11. Producer integration plan
270
+
271
+ ### `pi-dag-tasks`
272
+
273
+ Current task reminders should move from direct injection into last user/tool-result content to `reminder:upsert`.
274
+
275
+ Recommended intent:
276
+
277
+ ```ts
278
+ { source: "pi-dag-tasks", id: "state", label: "Tasks", text, ttl: "persistent", repeatEveryTurns: 10 }
279
+ ```
280
+
281
+ Task state should announce on create/change and, for the current v2 policy, repeat every 10 Pi turns. This can be tuned later.
282
+
283
+ ### `pi-dynamic-context-pruning`
284
+
285
+ DCP nudges should become one-shot reminder intents.
286
+
287
+ Recommended intent:
288
+
289
+ ```ts
290
+ { source: "dcp", id: "nudge", label: "DCP", text, ttl: "once", priority: 30 }
291
+ ```
292
+
293
+ DCP must stop appending nudge text to the latest visible user/assistant message. Compression block materialization remains separate from reminder rendering.
294
+
295
+ ### `pi-subagents`
296
+
297
+ Async attention/status hints should become reminder intents when they are guidance rather than durable transcript events.
298
+
299
+ Recommended internal IDs:
300
+
301
+ - `async-attention:<runId>`
302
+ - `async-complete:<runId>`
303
+
304
+ Subagent summaries that are intended as durable conversation content are not reminders and should not use this path.
305
+
306
+ ## 12. Failure behavior
307
+
308
+ Reminder handling is best-effort and must not block agent requests.
309
+
310
+ - Invalid reminder payloads should be logged/debuggable without breaking the session.
311
+ - Producer exceptions should not affect other reminders.
312
+ - If rendering fails, skip the reminder message and optionally emit a debug log.
313
+ - If no reminders are due, do not add an empty history message.
314
+
315
+ ## 13. Testing requirements
316
+
317
+ Initial tests should cover:
318
+
319
+ - upsert replaces by `(source, id)`
320
+ - remove and clear-source behavior
321
+ - `once`, `session`, and `persistent` lifecycle behavior
322
+ - `persistent` repeat behavior via `repeatEveryTurns`
323
+ - `reminder:announce-now` for one reminder and all reminders
324
+ - deterministic ordering by priority/source/id
325
+ - compact grouped rendering with one line per source/group where possible
326
+ - internal source/id/priority/ttl/repeatEveryTurns metadata is not rendered in normal output
327
+ - wrapper-breaking text such as `</system-reminder>`, `<`, and `&` cannot break the message
328
+ - no reminder output when store is empty or nothing is due
329
+ - rendered reminder contains no `cache_control`
330
+ - equivalent reminder sets render byte-identically
331
+ - provider/context hooks do not duplicate reminder injection
332
+
333
+ ## 14. Open questions
334
+
335
+ - Should `persistent` reminders ever survive session reload via persisted extension state, or remain in-memory only with producers responsible for republishing?
336
+ - Should `/remind` grow flags for persistent/debug reminders, or stay a simple one-shot debug command?
337
+ - What repeat interval should task-state reminders use, if any?
338
+ - Should compact labels be producer-supplied, manager-derived, or configurable?
339
+ - How should duplicate semantic reminders from multiple packages be resolved without hiding important guidance?
340
+
341
+ ## 15. Success criteria
342
+
343
+ The first useful v2 is complete when:
344
+
345
+ - the reminders host exposes stable event names and shared types.
346
+ - Producer packages can publish, replace, remove, clear, list, and force reminder intents.
347
+ - The manager writes deterministic compact `<system-reminder>` history messages only when reminders are due.
348
+ - No `cache_control` is attached to reminder messages.
349
+ - Provider/context hooks do not inject duplicate reminder tails.
350
+ - `/remind <text>` creates a one-shot debug reminder.
351
+ - `pi-dag-tasks` and DCP can migrate their reminders/nudges away from direct message mutation.
@@ -0,0 +1,68 @@
1
+ # Reminders
2
+
3
+ Reminders let producer extensions publish compact model-visible guidance.
4
+
5
+ Use reminders instead of appending text to prompts, transcript messages, or tool results.
6
+
7
+ ## Producer API
8
+
9
+ ```ts
10
+ client.reminders.upsert({
11
+ source: "my-extension",
12
+ id: "state",
13
+ label: "MyExt",
14
+ text: "One short reminder for the model.",
15
+ ttl: "session",
16
+ repeatEveryTurns: 10,
17
+ });
18
+ ```
19
+
20
+ ## Operations
21
+
22
+ | Call | Effect |
23
+ |---|---|
24
+ | `upsert(intent)` | Create or replace `(source, id)` |
25
+ | `remove(source, id)` | Remove one reminder |
26
+ | `clearSource(source)` | Remove all reminders for a source |
27
+ | `list(source?)` | Read current snapshot |
28
+ | `announceNow(payload?)` | Force due reminders to announce |
29
+
30
+ ## TTL
31
+
32
+ | TTL | Meaning |
33
+ |---|---|
34
+ | `once` | Announce once, then expire |
35
+ | `session` | Survives until session reset/shutdown |
36
+ | `persistent` | Survives across sessions |
37
+
38
+ ## Host config
39
+
40
+ The bundled reminder host stores package config in:
41
+
42
+ ```txt
43
+ getAgentDir()/config/utils.jsonc
44
+ ```
45
+
46
+ ```jsonc
47
+ {
48
+ "logging": {
49
+ "level": "info",
50
+ "maxFiles": 3,
51
+ "maxBytes": 1048576
52
+ },
53
+ "reminders": {
54
+ // Show reminders with display:false in the transcript UI for debugging.
55
+ "debugShowAllInTui": false
56
+ }
57
+ }
58
+ ```
59
+
60
+ Use `/reminders` to toggle the debug display setting from Pi.
61
+
62
+ ## Rules
63
+
64
+ - Keep `text` short.
65
+ - Use stable `(source, id)` keys.
66
+ - Prefer replacing one reminder over creating many.
67
+ - Do not put secrets in reminders.
68
+
@@ -0,0 +1,57 @@
1
+ # Widget Coordinator
2
+
3
+ Use the coordinator when more than one extension wants to render above or below the editor.
4
+
5
+ Raw `ctx.ui.setWidget()` calls from separate extensions do not give you a deterministic cross-extension order. Load timing can change which widget appears first. The coordinator gives every extension one shared ordered slot per placement, so widget order is stable.
6
+
7
+ ## Register
8
+
9
+ ```ts
10
+ client.widgets.set("belowEditor", "status", () => ({
11
+ render: () => ["my-extension: ready"],
12
+ invalidate: () => {},
13
+ }), { order: 10 });
14
+ ```
15
+
16
+ ## Remove
17
+
18
+ ```ts
19
+ client.widgets.remove("belowEditor", "status");
20
+ ```
21
+
22
+ ## Placements
23
+
24
+ | Placement | Slot |
25
+ |---|---|
26
+ | `aboveEditor` | Above the editor |
27
+ | `belowEditor` | Below the editor |
28
+
29
+ ## Ordering
30
+
31
+ Widgets in the same placement are sorted by:
32
+
33
+ 1. `order`
34
+ 2. insertion order
35
+
36
+ Use wide gaps so other extensions can fit between yours:
37
+
38
+ ```ts
39
+ { order: 10 } // primary status
40
+ { order: 50 } // secondary details
41
+ ```
42
+
43
+ ## Fullscreen behavior
44
+
45
+ When any client holds a fullscreen lease, coordinated widgets are hidden and restored afterwards.
46
+
47
+ ```ts
48
+ await client.ui.fullscreen((tui, theme, keybindings, done) => new MyComponent(tui, theme, done));
49
+ ```
50
+
51
+ ## Fallback
52
+
53
+ If the host is not ready yet, widget calls use the extension's own `ctx.ui.setWidget`. When the host announces readiness, the client clears the fallback widget and re-registers through the coordinator.
54
+
55
+ ## Example
56
+
57
+ See [examples/widget-coordinator.ts](../examples/widget-coordinator.ts).
@@ -0,0 +1,23 @@
1
+ # Examples
2
+
3
+ Copy an example into a Pi extension package and adjust names/state.
4
+
5
+ ## Examples
6
+
7
+ | File | Shows |
8
+ |---|---|
9
+ | [widget-coordinator.ts](widget-coordinator.ts) | Ordered widgets via `client.widgets` |
10
+ | [config.ts](config.ts) | Extension-owned JSON/JSONC config |
11
+ | [pane-overlay.ts](pane-overlay.ts) | Fullscreen master/detail UI |
12
+ | [reminders.ts](reminders.ts) | Reminder producer calls |
13
+ | [logger.ts](logger.ts) | Namespaced logger |
14
+
15
+ ## Local package testing
16
+
17
+ ```bash
18
+ cd ../pi-extension-utils
19
+ npm run build
20
+
21
+ cd ../your-extension
22
+ npm install ../pi-extension-utils
23
+ ```
@@ -0,0 +1,31 @@
1
+ import { Type, type Static } from "typebox";
2
+ import { defineConfig } from "pi-extension-utils";
3
+
4
+ const schema = Type.Object({
5
+ asyncByDefault: Type.Boolean({
6
+ default: false,
7
+ description: "Run jobs asynchronously unless the caller opts out.",
8
+ }),
9
+ maxDepth: Type.Number({
10
+ default: 1,
11
+ description: "Maximum nested job depth.",
12
+ }),
13
+ });
14
+
15
+ type ExampleConfig = Static<typeof schema>;
16
+
17
+ const config = defineConfig({
18
+ name: "example-extension",
19
+ // Uses getAgentDir()/config/example-extension.jsonc by default.
20
+ schema,
21
+ });
22
+
23
+ export function loadConfig(): ExampleConfig {
24
+ return config.get();
25
+ }
26
+
27
+ export function enableAsyncByDefault(): ExampleConfig {
28
+ return config.update((cfg) => {
29
+ cfg.asyncByDefault = true;
30
+ });
31
+ }
@@ -0,0 +1,20 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { createLogger } from "pi-extension-utils";
3
+
4
+ const log = createLogger("example-extension", {
5
+ level: "info",
6
+ maxFiles: 5,
7
+ });
8
+
9
+ export default function (pi: ExtensionAPI) {
10
+ pi.on("session_start", (_event, ctx) => {
11
+ log.info(`session started cwd=${ctx.cwd}`);
12
+ });
13
+
14
+ pi.registerCommand("example-log", {
15
+ description: "Write one example log line",
16
+ handler: async () => {
17
+ log.info("command ran");
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,47 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { connect, paneOverlay } from "pi-extension-utils";
3
+
4
+ interface RunRow {
5
+ id: string;
6
+ label: string;
7
+ lines: string[];
8
+ }
9
+
10
+ const runs: RunRow[] = [
11
+ { id: "a", label: "explorer", lines: ["status: running", "task: inspect files"] },
12
+ { id: "b", label: "qa", lines: ["status: done", "tests: passed"] },
13
+ ];
14
+
15
+ export default function (pi: ExtensionAPI) {
16
+ pi.registerCommand("example-dashboard", {
17
+ description: "Open an example pane overlay",
18
+ handler: async (_args, ctx) => {
19
+ const client = connect(pi, { ctx, clientId: "example-dashboard" });
20
+
21
+ await client.ui.fullscreen(
22
+ paneOverlay<void, RunRow>({
23
+ primary: {
24
+ title: "Runs",
25
+ mode: "cursor",
26
+ rows: runs,
27
+ selectionKey: (run) => run.id,
28
+ renderRow: (run) => run.label,
29
+ },
30
+ detail: {
31
+ title: (overlay) => overlay.selectedRow?.label ?? "Details",
32
+ rows: (overlay) => overlay.selectedRow?.lines ?? ["No selection"],
33
+ },
34
+ split: { initialFraction: 0.35, minPrimaryWidth: 20, minDetailWidth: 30 },
35
+ legendPlacement: "footer",
36
+ customActions: [
37
+ {
38
+ keys: "enter",
39
+ label: "select",
40
+ run: (overlay) => overlay.close(),
41
+ },
42
+ ],
43
+ }),
44
+ );
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,25 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { connect } from "pi-extension-utils";
3
+
4
+ export default function (pi: ExtensionAPI) {
5
+ pi.on("session_start", (_event, ctx) => {
6
+ const client = connect(pi, { ctx, clientId: "example-reminders" });
7
+
8
+ client.reminders.upsert({
9
+ source: "example-reminders",
10
+ id: "status",
11
+ label: "Example",
12
+ text: "Use the example project conventions.",
13
+ ttl: "session",
14
+ repeatEveryTurns: 10,
15
+ });
16
+ });
17
+
18
+ pi.registerCommand("example-clear-reminder", {
19
+ description: "Clear the example reminder",
20
+ handler: async (_args, ctx) => {
21
+ const client = connect(pi, { ctx, clientId: "example-reminders" });
22
+ client.reminders.remove("example-reminders", "status");
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,18 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { connect } from "pi-extension-utils";
3
+
4
+ export default function (pi: ExtensionAPI) {
5
+ pi.on("session_start", (_event, ctx) => {
6
+ const client = connect(pi, { ctx, clientId: "example-widget" });
7
+
8
+ client.widgets.set(
9
+ "belowEditor",
10
+ "example-status",
11
+ () => ({
12
+ render: () => ["example widget: ready"],
13
+ invalidate: () => {},
14
+ }),
15
+ { order: 10 },
16
+ );
17
+ });
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extension-utils",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Shared Pi extension utilities for coordinated widgets, fullscreen leases, and logging.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -13,6 +13,8 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "docs",
17
+ "examples",
16
18
  "README.md",
17
19
  "LICENSE"
18
20
  ],
@@ -30,6 +32,10 @@
30
32
  "keywords": [],
31
33
  "author": "",
32
34
  "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/barisgit/pi-extension-utils"
38
+ },
33
39
  "peerDependencies": {
34
40
  "@earendil-works/pi-agent-core": "*",
35
41
  "@earendil-works/pi-ai": "*",
@@ -40,7 +46,12 @@
40
46
  "@earendil-works/pi-agent-core": "^0.75.4",
41
47
  "@earendil-works/pi-ai": "^0.75.4",
42
48
  "@earendil-works/pi-coding-agent": "^0.75.4",
49
+ "@earendil-works/pi-tui": "^0.79.3",
43
50
  "@types/node": "^24.0.0",
44
51
  "typescript": "^6.0.3"
52
+ },
53
+ "dependencies": {
54
+ "jsonc-parser": "^3.3.1",
55
+ "typebox": "^1.2.9"
45
56
  }
46
57
  }