@gavin-hu/my-pi-agent 0.11.4 → 0.13.0

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 (197) hide show
  1. package/README.md +38 -26
  2. package/README.zh-CN.md +33 -23
  3. package/extensions/bytefly/README.md +381 -225
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +52 -6
  7. package/extensions/bytefly/doctor/tools.ts +1 -3
  8. package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
  9. package/extensions/bytefly/footer/config.ts +7 -1
  10. package/extensions/bytefly/footer/delegation.ts +100 -0
  11. package/extensions/bytefly/footer/price/config.ts +54 -15
  12. package/extensions/bytefly/footer/price/price.ts +20 -7
  13. package/extensions/bytefly/footer/price/types.ts +13 -7
  14. package/extensions/bytefly/footer/register.ts +13 -2
  15. package/extensions/bytefly/footer/render/format.ts +3 -2
  16. package/extensions/bytefly/footer/render/lines.ts +2 -0
  17. package/extensions/bytefly/footer/render/segments.ts +30 -2
  18. package/extensions/bytefly/footer/types.ts +12 -0
  19. package/extensions/bytefly/header/component.ts +21 -6
  20. package/extensions/bytefly/header/config.ts +3 -0
  21. package/extensions/bytefly/header/register.ts +34 -3
  22. package/extensions/bytefly/index.ts +15 -4
  23. package/extensions/bytefly/notify/config.ts +2 -0
  24. package/extensions/bytefly/notify/format.ts +19 -1
  25. package/extensions/bytefly/notify/register.ts +37 -13
  26. package/extensions/bytefly/notify/types.ts +2 -0
  27. package/extensions/bytefly/panel/commands.ts +34 -13
  28. package/extensions/bytefly/panel/settings-runtime.ts +71 -28
  29. package/extensions/bytefly/panel/settings.ts +293 -48
  30. package/extensions/bytefly/panel/tui/entries.ts +58 -0
  31. package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
  32. package/extensions/bytefly/panel/tui/frame.ts +71 -0
  33. package/extensions/bytefly/panel/tui/nav.ts +75 -0
  34. package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
  35. package/extensions/bytefly/panel/tui.ts +205 -319
  36. package/extensions/bytefly/picker/runtime.ts +61 -2
  37. package/extensions/bytefly/picker/toggle.ts +56 -0
  38. package/extensions/bytefly/update/cache.ts +73 -0
  39. package/extensions/bytefly/update/check.ts +59 -0
  40. package/extensions/bytefly/update/format.ts +87 -0
  41. package/extensions/bytefly/update/register.ts +158 -0
  42. package/extensions/bytefly/update/state.ts +40 -0
  43. package/extensions/bytefly/update/version.ts +83 -0
  44. package/extensions/document/README.md +24 -14
  45. package/extensions/document/config.ts +4 -0
  46. package/extensions/document/deadline.ts +61 -0
  47. package/extensions/document/tool.ts +14 -5
  48. package/extensions/file-browse/README.md +12 -8
  49. package/extensions/file-browse/client.ts +6 -7
  50. package/extensions/file-browse/files.ts +0 -4
  51. package/extensions/file-browse/git.ts +9 -10
  52. package/extensions/file-browse/html.ts +17 -7
  53. package/extensions/file-browse/router.ts +24 -15
  54. package/extensions/file-open/README.md +30 -15
  55. package/extensions/file-open/index.ts +4 -3
  56. package/extensions/file-open/open.ts +52 -21
  57. package/extensions/goal/README.md +45 -13
  58. package/extensions/goal/clarify-prompt.ts +5 -36
  59. package/extensions/goal/clarify.ts +55 -86
  60. package/extensions/goal/commands.ts +41 -35
  61. package/extensions/goal/format.ts +12 -4
  62. package/extensions/goal/schema.ts +6 -2
  63. package/extensions/goal/tools.ts +28 -8
  64. package/extensions/goal/tui.ts +1 -1
  65. package/extensions/goal/types.ts +6 -2
  66. package/extensions/job/README.md +16 -12
  67. package/extensions/job/runtime.ts +23 -6
  68. package/extensions/job/status.ts +4 -2
  69. package/extensions/memory/README.md +56 -14
  70. package/extensions/memory/commands.ts +20 -2
  71. package/extensions/memory/doctor.ts +137 -0
  72. package/extensions/memory/format.ts +24 -7
  73. package/extensions/memory/runtime.ts +49 -0
  74. package/extensions/memory/store.ts +31 -0
  75. package/extensions/memory/tui.ts +136 -12
  76. package/extensions/plan/README.md +83 -36
  77. package/extensions/plan/blocks.ts +1 -35
  78. package/extensions/plan/commands.ts +84 -19
  79. package/extensions/plan/index.ts +35 -11
  80. package/extensions/plan/plan-index.ts +234 -0
  81. package/extensions/plan/plan-text.ts +95 -0
  82. package/extensions/plan/plans.ts +91 -2
  83. package/extensions/plan/policy.ts +4 -3
  84. package/extensions/plan/review.ts +246 -0
  85. package/extensions/plan/runtime.ts +73 -24
  86. package/extensions/plan/steps.ts +60 -39
  87. package/extensions/plan/todos.ts +29 -60
  88. package/extensions/plan/tools.ts +188 -244
  89. package/extensions/plan/tui.ts +18 -31
  90. package/extensions/plan/types.ts +12 -0
  91. package/extensions/plan/web-client.ts +51 -18
  92. package/extensions/plan/web-page.ts +7 -2
  93. package/extensions/plan/web-router.ts +11 -3
  94. package/extensions/plan/web.ts +106 -41
  95. package/extensions/question/README.md +1 -1
  96. package/extensions/question/tools.ts +1 -1
  97. package/extensions/rewind/README.md +28 -8
  98. package/extensions/rewind/commands.ts +1 -1
  99. package/extensions/rewind/format.ts +4 -4
  100. package/extensions/rewind/git.ts +56 -18
  101. package/extensions/rewind/index.ts +3 -5
  102. package/extensions/rewind/path-picker.ts +198 -0
  103. package/extensions/rewind/paths.ts +128 -0
  104. package/extensions/rewind/restore.ts +69 -28
  105. package/extensions/rewind/rewind.ts +80 -3
  106. package/extensions/rewind/runtime.ts +13 -5
  107. package/extensions/rewind/snapshot-run.ts +5 -2
  108. package/extensions/rewind/types.ts +2 -0
  109. package/extensions/subagent/README.md +7 -1
  110. package/extensions/subagent/accounting.ts +34 -0
  111. package/extensions/subagent/index.ts +23 -3
  112. package/extensions/subagent/stream.ts +2 -0
  113. package/extensions/subagent/types.ts +2 -0
  114. package/extensions/todo/README.md +19 -11
  115. package/extensions/todo/index.ts +5 -3
  116. package/extensions/todo/nudge.ts +58 -9
  117. package/extensions/todo/runtime.ts +28 -17
  118. package/extensions/watch/README.md +8 -3
  119. package/extensions/watch/index.ts +19 -0
  120. package/extensions/web-access/README.md +46 -16
  121. package/extensions/web-access/charset.ts +58 -0
  122. package/extensions/web-access/fetch/find.ts +11 -7
  123. package/extensions/web-access/fetch/format.ts +64 -18
  124. package/extensions/web-access/fetch/page.ts +5 -1
  125. package/extensions/web-access/fetch/tool.ts +20 -21
  126. package/extensions/web-access/http.ts +49 -8
  127. package/extensions/web-access/search/facts.ts +38 -0
  128. package/extensions/web-access/search/tool.ts +14 -3
  129. package/extensions/web-access/text.ts +50 -11
  130. package/extensions/wechat/README.md +5 -4
  131. package/extensions/wechat/tools.ts +0 -2
  132. package/extensions/worktree/README.md +74 -15
  133. package/extensions/worktree/config.ts +11 -0
  134. package/extensions/worktree/exit.ts +35 -3
  135. package/extensions/worktree/gaps.ts +74 -0
  136. package/extensions/worktree/guard.ts +50 -4
  137. package/extensions/worktree/handoff.ts +55 -0
  138. package/extensions/worktree/index.ts +12 -2
  139. package/extensions/worktree/runtime.ts +20 -0
  140. package/extensions/worktree/status.ts +15 -2
  141. package/extensions/worktree/tools.ts +3 -2
  142. package/lib/README.md +12 -9
  143. package/lib/config.ts +43 -1
  144. package/lib/env.ts +12 -0
  145. package/lib/events.ts +86 -1
  146. package/lib/goal-state.ts +95 -0
  147. package/lib/requirements.ts +340 -46
  148. package/lib/todo-state.ts +62 -0
  149. package/lib/tui/README.md +91 -0
  150. package/lib/tui/glyphs.ts +2 -0
  151. package/lib/web/layout.ts +12 -2
  152. package/package.json +5 -3
  153. package/themes/bytefly-dark.json +1 -1
  154. package/assets/bytefly.schema.json +0 -785
  155. package/assets/chrome.png +0 -0
  156. package/assets/file-browse.png +0 -0
  157. package/assets/job.png +0 -0
  158. package/assets/plan-review.png +0 -0
  159. package/assets/question.png +0 -0
  160. package/assets/rewind.png +0 -0
  161. package/assets/todo.png +0 -0
  162. package/assets/tool-renderings.png +0 -0
  163. package/assets/tools/ask_user_question-cancelled.png +0 -0
  164. package/assets/tools/ask_user_question.png +0 -0
  165. package/assets/tools/bash.png +0 -0
  166. package/assets/tools/edit.png +0 -0
  167. package/assets/tools/enter_plan_mode.png +0 -0
  168. package/assets/tools/enter_worktree.png +0 -0
  169. package/assets/tools/exit_plan_mode.png +0 -0
  170. package/assets/tools/exit_worktree.png +0 -0
  171. package/assets/tools/find.png +0 -0
  172. package/assets/tools/goal.png +0 -0
  173. package/assets/tools/grep.png +0 -0
  174. package/assets/tools/job.png +0 -0
  175. package/assets/tools/list_worktrees.png +0 -0
  176. package/assets/tools/ls.png +0 -0
  177. package/assets/tools/memory-long-subject.png +0 -0
  178. package/assets/tools/memory.png +0 -0
  179. package/assets/tools/notice.png +0 -0
  180. package/assets/tools/open_path.png +0 -0
  181. package/assets/tools/prune_worktrees.png +0 -0
  182. package/assets/tools/read.png +0 -0
  183. package/assets/tools/read_doc-running.png +0 -0
  184. package/assets/tools/read_doc.png +0 -0
  185. package/assets/tools/run_doctor.png +0 -0
  186. package/assets/tools/send_wechat.png +0 -0
  187. package/assets/tools/set_plan_steps.png +0 -0
  188. package/assets/tools/subagent.png +0 -0
  189. package/assets/tools/todo.png +0 -0
  190. package/assets/tools/turn-rule.png +0 -0
  191. package/assets/tools/user-message.png +0 -0
  192. package/assets/tools/watch.png +0 -0
  193. package/assets/tools/web_fetch-rejected.png +0 -0
  194. package/assets/tools/web_fetch.png +0 -0
  195. package/assets/tools/web_search.png +0 -0
  196. package/assets/tools/write.png +0 -0
  197. package/assets/tools/write_plan.png +0 -0
package/lib/README.md CHANGED
@@ -16,22 +16,24 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
16
16
  | Module | Responsibility |
17
17
  |---|---|
18
18
  | [`browser.ts`](./browser.ts) | Cross-platform default-browser opener shared by `file-browse` and `plan`: pure `browserCommand(url, platform)` plus best-effort `openInBrowser(url, { platform, spawn })` with an injectable spawn. |
19
- | [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection`. Includes `clampInteger` / `clampNumber` / `cleanString` / `asRecord` / `readJson` / `readJsonResult`. |
19
+ | [`config.ts`](./config.ts) | Config loading, two ways: `loadConfigFile` for a whole file, and the shared `bytefly.json` (`SHARED_CONFIG_FILE`) via `configSection` / `configValueAt` / `loadConfigSection` (global, then project), plus the atomic `writeConfigFile` / `writeConfigSection` / `writeConfigValue` / `writeConfigValueAt` (the last two edit one leaf, flat or nested, preserving siblings and metadata). Includes `clampInteger` / `clampNumber` / `cleanString` / `asRecord` / `readJson` / `readJsonResult`. |
20
20
  | [`env.ts`](./env.ts) | Env-var contracts shared across extensions: the worktree environment (`ENV_ROOT`, `worktreeRoot`, `resolveEffectiveCwd`) used by worktree, file-browse, job, and rewind, plus `isExtensionEnabled` / `ENV_DISABLED_EXTENSIONS` for the package-wide disable list. |
21
21
  | [`escape.ts`](./escape.ts) | `escapeHtml`: the single HTML text/attribute escape used by the browse page renderers and the Markdown and syntax highlighters. |
22
- | [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` and `JobFinishedEvent`, published by `job` and consumed by bytefly's notify capability. |
22
+ | [`events.ts`](./events.ts) | Inter-extension `pi.events` channels and payloads: `JOB_FINISHED_CHANNEL` / `JobFinishedEvent` (published by `job`, consumed by bytefly's notify capability), `SUBAGENT_FINISHED_CHANNEL` / `SubagentFinishedEvent` (published by `subagent`, consumed by bytefly's footer roll-up), and `WEB_SEARCH_PROVIDER_CHANNEL` / `WebSearchProviderEvent` (published by `web-access` at session start, consumed by bytefly's doctor so it can report the resolved provider without importing the extension — the key itself is never in the payload). |
23
23
  | [`notice.ts`](./notice.ts) | The shared transcript notice: `NOTICE_TYPE`, the `NoticeDetails` / `NoticeItem` shape, `noticeMessage` (the `display: true` message both send paths accept), `isNotice`, and `pruneStaleNotices` — posted by `job` and `watch`, rendered by bytefly. |
24
24
  | [`format.ts`](./format.ts) | Pure text/number formatting shared by renderers: `formatTokens`, `formatCount`, `formatDuration` (`450ms`, `3.2s`, `1m05s`, `2h03m`), `stripControlChars`, `sanitize`, `sanitizeLine` — the boundary for untrusted, single-line text — and `shortenPath`. |
25
25
  | [`interaction.ts`](./interaction.ts) | Turn-affinity prompt contract: the channel a remote transport marks on the UI context it owns (`INTERACTION_TURN_KEY`), the `InteractionChannel` interface (`request` / `post` / `postFile`), and `remoteChannel` / `isRemoteTurn` / `supportsCustomUI` / `askHuman` (with an optional file, fallback text, and digest preface) plus the shared “Other” label. |
26
26
  | [`git/`](./git/) | Shared git plumbing. [`git/runner.ts`](./git/runner.ts) is the `RunGit` seam (`createExecRunner` with timeout/env handling, `runGitOrThrow`, `GitError`); [`git/read.ts`](./git/read.ts) holds the read helpers (`repoRoot`, `repoRootFor`, `gitDir`, `revParse`, `hasCommits`, `currentBranch`); [`git/index.ts`](./git/index.ts) is the barrel. |
27
27
  | [`highlight.ts`](./highlight.ts) | First-party, escape-first syntax tokenizer (`detectLanguage`, `fenceLanguage`, `highlightLines`, `HighlightLanguage`, `TokenClass`) for js/ts, json, css, html, shell, python, java, yaml, and sql; shared by `file-browse` and plan's code blocks. |
28
- | [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (`screenHint`, `dockListFrame` (the docked-list frame), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`), [`tui/columns.ts`](./tui/columns.ts) (`splitRow`, `fitCells`, `flowCells`, `padTo`, `clip`, `clipMiddle`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job` and `watch` share), [`tui/tool.ts`](./tui/tool.ts) (`toolCallTitle`, the width-safe `ToolCallText` / `reuseCallText`, sanitized `errorLine` / `rejectionLine`, `moreHint`, `syncElapsedTimer`, `elapsedSuffix` — the tool-transcript kit), [`tui/render-cache.ts`](./tui/render-cache.ts) (`RenderCache`: the per-instance line memo a component that derives its lines from another component's output renders through, so a repeat frame is one comparison), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
28
+ | [`tui/`](./tui/) | The Bytefly terminal design system shared by every interactive surface, whose conventions are its own doc, [`tui/README.md`](./tui/README.md). Modules: [`tui/tokens.ts`](./tui/tokens.ts) (semantic role -> theme color, `paint`), [`tui/glyphs.ts`](./tui/glyphs.ts) (`GLYPHS`, `SEPARATORS`, `STATUS_KEYS`, `EXPAND_KEYBINDING` with `expandKey` / `expandHint`, `GLYPH_GAP` — the vocabulary both sides of a status must agree on), [`tui/chrome.ts`](./tui/chrome.ts) (`screenHint`, `dockListFrame` (the docked-list frame), `viewportRows`, `FULL_SCREEN_OVERLAY`, `reuseText`, `editorTheme`), [`tui/rules.ts`](./tui/rules.ts) (the rule glyph `RULE_GLYPH`, and `plainRule` / `titleRule` / `centeredRule`), [`tui/indent.ts`](./tui/indent.ts) (the transcript block indent: `BODY_INDENT`, an optional rail prefix that a wrapped call line is kept out of, and `bodyLines` / `indentBlockLines` / `indentedLines`), [`tui/rows.ts`](./tui/rows.ts) (`listRow`, `checkbox`, `chip`, `keyValueRow`, `emptyState`), [`tui/columns.ts`](./tui/columns.ts) (`splitRow`, `fitCells`, `flowCells`, `padTo`, `clip`, `clipMiddle`), [`tui/gauge.ts`](./tui/gauge.ts) (`computeGauge`, `gaugeRole`, `gaugeBar`), [`tui/line-pane.ts`](./tui/line-pane.ts) (`LinePane`, the scrollable log/match pane), [`tui/status-controller.ts`](./tui/status-controller.ts) (`createStatusController`, the footer chips `job` and `watch` share), [`tui/tool.ts`](./tui/tool.ts) (`toolCallTitle`, the width-safe `ToolCallText` / `reuseCallText`, sanitized `errorLine` / `rejectionLine`, `moreHint`, `syncElapsedTimer`, `elapsedSuffix` — the tool-transcript kit), [`tui/render-cache.ts`](./tui/render-cache.ts) (`RenderCache`: the per-instance line memo a component that derives its lines from another component's output renders through, so a repeat frame is one comparison), and [`tui/cursor.ts`](./tui/cursor.ts) (scroll and navigation: `fitRows`, `keepVisible`, `clampScroll`, `navIntent`, `navigateList`, `navigateScroll`, `wheelDelta`, `scrollList`, `isCloseKey`, `selectionMarker`, `formatRange`, and the `ListCursor` (by index) and `IdCursor` (by stable id) state). |
29
29
  | [`path.ts`](./path.ts) | `isInside` (string containment) plus `realPathOfNearest` / `isInsideReal` (symlink-aware): the containment predicates the file-browse, worktree, and doc guards share. |
30
30
  | [`pi-invocation.ts`](./pi-invocation.ts) | `getPiInvocation`: how to launch a nested `pi` process, shared by `subagent` and bytefly's doctor surface. |
31
31
  | [`policy.ts`](./policy.ts) | `createReadOnlyPolicy`: default-deny tool classification from `readOnlyHint` + explicit allow/deny. |
32
32
  | [`rails.ts`](./rails.ts) | Above-editor widget ordering (`goal` → `todo`) and dock-screen suppression, carried on `pi.events`. |
33
- | [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. |
33
+ | [`requirements.ts`](./requirements.ts) | Per-extension runtime prerequisite table plus a pure `probeExtension(seams)` over injected seams (exec, config reads, optional imports, the published search provider, env, and a refresh-only reachability probe): bytefly's doctor surface reports on it and the extensions that own a dependency format their errors from it. A check kind is either a prerequisite (`binary`, `module`, `state`, `invocation`, `notify`, `provider`) or a `safety` switch whose off value trades a protection away, read from `bytefly.json` global-then-project. |
34
34
  | [`shell.ts`](./shell.ts) | Shell escape rule shared by the worktree isolation guard and plan mode's read-only git guard: `SHELL_ESCAPABLE`, `isShellEscapable`, `hasShellEscape`. |
35
+ | [`goal-state.ts`](./goal-state.ts) | The published `goal` state replayed from the session branch: `PublishedGoal`, `GOAL_ENTRY_TYPE`, `MAX_GOAL_OBJECTIVE`, and `lastGoal(entries)` (the last valid goal, skipping a rejected call and honouring an explicit clear), so `plan` can restate an active goal while planning and offer to record one on approval — with no extension importing another's modules. |
36
+ | [`todo-state.ts`](./todo-state.ts) | The published `todo` state replayed from the session branch: `ExistingTodo` and `lastTodos(entries)` (the last valid list, skipping a rejected call), so `plan` can merge an approved plan's steps into it and `goal` can name the items still open when a goal is marked achieved — with no extension importing another's modules. |
35
37
  | [`tool-names.ts`](./tool-names.ts) | Tool names and one parameter name that more than one extension must agree on, so an orchestrating `ctx.executeTool()` call or schema probe breaks the build on a rename. |
36
38
  | [`web/`](./web/) | The Bytefly web brand system shared by the browser surfaces: [`web/tokens.ts`](./web/tokens.ts) (palette derived from the Bytefly terminal themes, scales, and the `tok-*` map), [`web/base.ts`](./web/base.ts), [`web/components.ts`](./web/components.ts), [`web/layout.ts`](./web/layout.ts), [`web/runtime.ts`](./web/runtime.ts) (`WEB_THEME_STORAGE_KEY`, `THEME_JS`, `THEME_TOGGLE_JS`), [`web/markup.ts`](./web/markup.ts), and the [`web/index.ts`](./web/index.ts) barrel. |
37
39
 
@@ -46,8 +48,9 @@ belongs in that extension instead — see `extensions/web-access/http.ts` and
46
48
  glyph and the `setStatus` key are one contract, not two literals. Tool names
47
49
  shared by an orchestrating tool and the tool it calls — and parameter names
48
50
  one extension probes on another — live in `tool-names.ts`.
49
- - **A color is a role, not a token.** Draw through [`tui/tokens.ts`](./tui/tokens.ts)
50
- (`paint(theme, role, text)`) so a theme swap re-skins every surface; the
51
- primitives in `tui/` already do, and a new one should too. The one exception is
52
- Pi's tool-rendering vocabulary (`toolOutput`, `toolTitle`), which stays direct:
53
- those tokens style a tool block, and no role maps to them.
51
+ - **A color is a role, not a token.** The rules and the primitives are in
52
+ [`tui/README.md`](./tui/README.md#color-is-a-role): draw through
53
+ [`tui/tokens.ts`](./tui/tokens.ts) (`paint(theme, role, text)`) so a theme swap
54
+ re-skins every surface. The one exception is Pi's tool-rendering vocabulary
55
+ (`toolOutput`, `toolTitle`), which stays direct: those tokens style a tool
56
+ block, and no role maps to them.
package/lib/config.ts CHANGED
@@ -139,6 +139,22 @@ export function configSection(
139
139
  return asRecord(raw?.[key]);
140
140
  }
141
141
 
142
+ /**
143
+ * The value at `path` in a document or section, or `undefined` when a step is
144
+ * absent or not an object. A one-element path reads a top-level key, so this
145
+ * also answers "is this setting stated here" for the flat case. `raw` may be an
146
+ * untrusted JSON value or an already-normalized config object.
147
+ */
148
+ export function configValueAt(raw: unknown, path: readonly string[]): unknown {
149
+ let current: unknown = raw;
150
+ for (const key of path) {
151
+ const record = asRecord(current);
152
+ if (!record) return undefined;
153
+ current = record[key];
154
+ }
155
+ return current;
156
+ }
157
+
142
158
  /** Options for {@link loadConfigSection}. */
143
159
  export interface SectionOptions {
144
160
  /** Agent directory override; `worktree` resolves it explicitly. */
@@ -197,12 +213,38 @@ function readEditableDocument(path: string): Record<string, unknown> {
197
213
  * malformed document.
198
214
  */
199
215
  export function writeConfigValue(path: string, section: string, key: string, value: unknown): void {
216
+ writeConfigValueAt(path, section, [key], value);
217
+ }
218
+
219
+ /**
220
+ * Set the leaf at `keyPath` in one top-level section, with the same guarantees
221
+ * as {@link writeConfigValue}: siblings, other sections, and metadata survive,
222
+ * and the file and the section are created when absent. Intermediate objects
223
+ * are created as needed; a non-object in the way is replaced, because a refused
224
+ * edit would leave a hand-written typo unfixable from the screen. Refuses a
225
+ * malformed document and an empty path.
226
+ */
227
+ export function writeConfigValueAt(path: string, section: string, keyPath: readonly string[], value: unknown): void {
228
+ if (keyPath.length === 0) throw new Error(`Cannot update ${path}: empty key path`);
200
229
  const document = readEditableDocument(path);
201
230
  const current = configSection(document, section) ?? {};
202
- document[section] = { ...current, [key]: value };
231
+ document[section] = setPath(current, keyPath, value);
203
232
  writeConfigFile(path, document);
204
233
  }
205
234
 
235
+ /** Copy `source` with the leaf at `path` replaced; every sibling key is kept. */
236
+ function setPath(source: Record<string, unknown>, path: readonly string[], value: unknown): Record<string, unknown> {
237
+ const [head] = path;
238
+ const next = { ...source };
239
+ if (head === undefined) return next;
240
+ if (path.length === 1) {
241
+ next[head] = value;
242
+ return next;
243
+ }
244
+ next[head] = setPath(asRecord(source[head]) ?? {}, path.slice(1), value);
245
+ return next;
246
+ }
247
+
206
248
  /**
207
249
  * Delete one key from one top-level section, preserving its siblings and the
208
250
  * rest of the document. An emptied section is removed rather than left as `{}`.
package/lib/env.ts CHANGED
@@ -16,6 +16,9 @@ export const ENV_ROOT = "PI_WORKTREE_ROOT";
16
16
  */
17
17
  export const ENV_DISABLED_EXTENSIONS = "PI_DISABLED_EXTENSIONS";
18
18
 
19
+ /** Env var that turns off Pi's automatic network activity, including our own. */
20
+ export const ENV_OFFLINE = "PI_OFFLINE";
21
+
19
22
  /** False when `name` appears in `PI_DISABLED_EXTENSIONS`, true otherwise. */
20
23
  export function isExtensionEnabled(name: string): boolean {
21
24
  const raw = process.env[ENV_DISABLED_EXTENSIONS];
@@ -29,6 +32,15 @@ export function isExtensionEnabled(name: string): boolean {
29
32
  return !disabled.has(name.toLowerCase());
30
33
  }
31
34
 
35
+ /**
36
+ * Whether `PI_OFFLINE` is on, read the way Pi reads it: `1`, `true`, or `yes`
37
+ * (case-insensitive). Any other value, including empty, is off.
38
+ */
39
+ export function isOffline(): boolean {
40
+ const raw = process.env[ENV_OFFLINE]?.trim().toLowerCase();
41
+ return raw === "1" || raw === "true" || raw === "yes";
42
+ }
43
+
32
44
  /** The active worktree root from the environment, when set and still on disk. */
33
45
  export function worktreeRoot(): string | undefined {
34
46
  const root = process.env[ENV_ROOT];
package/lib/events.ts CHANGED
@@ -10,7 +10,9 @@
10
10
  *
11
11
  * The bus carries no state. `rails.ts` and `worktree` publish their own channels
12
12
  * from the modules that own them; a channel crosses an extension boundary only
13
- * when the payload is the whole contract, which is the case here.
13
+ * when the payload is the whole contract, which is the case here: `job` publishes
14
+ * a finished job for bytefly's notify capability, and `subagent` publishes a
15
+ * finished child run for bytefly's footer roll-up.
14
16
  */
15
17
 
16
18
  /** EventBus channel: a background job reached a terminal state. */
@@ -25,6 +27,89 @@ export const JOB_FINISHED_CHANNEL = "my-pi-agent/job-finished";
25
27
  */
26
28
  export type FinishedJobStatus = "exited" | "failed" | "killed" | "unknown";
27
29
 
30
+ /** EventBus channel: a `watch` wake match fired while the agent was idle. */
31
+ export const WATCH_MATCH_CHANNEL = "my-pi-agent/watch-match";
32
+
33
+ /**
34
+ * Payload on {@link WATCH_MATCH_CHANNEL}: what a wake match saw.
35
+ *
36
+ * Emitted by `watch` and consumed by bytefly's notify capability, so the two can
37
+ * agree on an alert without `watch` learning about delivery methods. The line is
38
+ * **raw** — the matched text is arbitrary file or process output — and the
39
+ * consumer sanitizes it before it reaches a notification, exactly as it does for a
40
+ * job label. `count` covers the whole wake cycle, so one notification can say how
41
+ * many lines matched while the body describes the newest one.
42
+ */
43
+ export interface WatchMatchEvent {
44
+ /** How many matches were pending when the wake fired. */
45
+ count: number;
46
+ /** The watcher that matched, e.g. `w3`. */
47
+ watcherId: string;
48
+ /** Its label, e.g. `build`. */
49
+ label: string;
50
+ /** The newest matched line, unsanitized. */
51
+ line: string;
52
+ }
53
+
54
+ /** EventBus channel: one subagent run finished (a child `pi` process exited). */
55
+ export const SUBAGENT_FINISHED_CHANNEL = "my-pi-agent/subagent-finished";
56
+
57
+ /**
58
+ * Payload on {@link SUBAGENT_FINISHED_CHANNEL}: what one finished child run spent.
59
+ *
60
+ * It carries the *pricing inputs* rather than a total, because the consumer prices
61
+ * them with the same rate table and currency as the session's own cost, and a
62
+ * currency change must re-price them rather than leave a figure in the old unit.
63
+ * `recordedCost` is Pi's own dollar figure for the child's assistant calls.
64
+ *
65
+ * A run that produced no assistant message (an unknown agent, a spawn failure)
66
+ * publishes nothing: there is no spend to account for.
67
+ */
68
+ export interface SubagentFinishedEvent {
69
+ /** Agent that ran, e.g. `explorer`. */
70
+ agent: string;
71
+ /** Provider and model the child reported, when it reported one. */
72
+ provider: string | null;
73
+ model: string | null;
74
+ input: number;
75
+ output: number;
76
+ cacheRead: number;
77
+ cacheWrite: number;
78
+ /** Pi's recorded cost for the child's calls, in USD. */
79
+ recordedCost: number;
80
+ /** Epoch ms when the run finished, or `null` when unknown. */
81
+ at: number | null;
82
+ /** Child process exit code; a model-level failure still exits 0. */
83
+ exitCode: number;
84
+ }
85
+
86
+ /** EventBus channel: `web-access` resolved its search provider for this session. */
87
+ export const WEB_SEARCH_PROVIDER_CHANNEL = "my-pi-agent/web-search-provider";
88
+
89
+ /**
90
+ * Payload on {@link WEB_SEARCH_PROVIDER_CHANNEL}: what `web_search` will use.
91
+ *
92
+ * `web-access` owns the provider registry, each provider's `isConfigured`, and the
93
+ * `auto` resolution rule, so it is the only place those facts can come from: this
94
+ * event is how `doctor` learns them without importing an extension. The key
95
+ * itself is never published — only the variable name and whether a literal key is
96
+ * configured — so a diagnostic can never leak a secret into a log or a notice.
97
+ */
98
+ export interface WebSearchProviderEvent {
99
+ /** Resolved backend id, e.g. `searxng`. */
100
+ provider: string;
101
+ /** Why this provider won: an explicit choice, one that is configured, or the keyless default. */
102
+ reason: "explicit" | "configured" | "default";
103
+ /** Whether the resolved provider's configuration is enough to call it. */
104
+ configured: boolean;
105
+ /** URL a refresh may probe, when the provider has a configurable endpoint. */
106
+ endpoint: string | null;
107
+ /** Environment variable a key would come from, when the provider takes one. */
108
+ apiKeyEnv: string | null;
109
+ /** Whether a literal key is configured, so a missing variable is not the whole story. */
110
+ keyConfigured: boolean;
111
+ }
112
+
28
113
  /** Payload on {@link JOB_FINISHED_CHANNEL}: one job that just stopped. */
29
114
  export interface JobFinishedEvent {
30
115
  /** Job id, e.g. `j3`. */
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The published goal state, read back from the session branch (pure).
3
+ *
4
+ * `goal` publishes its objective two ways: every `goal` tool result carries the
5
+ * goal in `details` — a rejected call carries the unchanged goal *and* an
6
+ * `error`, which is not a write — and the `/goal` command appends a `goal` custom
7
+ * entry whose `data.goal` is `null` when the goal is cleared. Replaying the branch
8
+ * in order is how another extension reads the goal without importing goal's
9
+ * modules: `plan` restates an active goal in its planning context and offers to
10
+ * record one when a plan is approved with none.
11
+ *
12
+ * Stored values are re-validated and re-sanitized here. Branch data can be
13
+ * tampered with, migrated, or written by an older version, and a reader must not
14
+ * pass escapes or unbounded text onward; anything that fails validation is
15
+ * ignored rather than clearing a goal that is still valid.
16
+ *
17
+ * The entry type and the objective limit live here and are imported by `goal`, so
18
+ * the publisher and the reader cannot drift. `goal` keeps its own reconstruction
19
+ * (`extensions/goal/state.ts`) because it also drives the widget; this module is
20
+ * the minimal contract other extensions may depend on.
21
+ */
22
+
23
+ import { sanitizeLine } from "./format.ts";
24
+ import { GOAL_TOOL } from "./tool-names.ts";
25
+
26
+ /** Custom-entry type `goal` uses to persist a goal set outside the tool. */
27
+ export const GOAL_ENTRY_TYPE = "goal";
28
+
29
+ /** Longest objective the goal tool accepts, and the most a reader will replay. */
30
+ export const MAX_GOAL_OBJECTIVE = 2000;
31
+
32
+ /** Whether the goal is still being pursued, or already done. */
33
+ export type PublishedGoalStatus = "active" | "achieved";
34
+
35
+ /** The goal as another extension may read it. */
36
+ export interface PublishedGoal {
37
+ /** The objective, sanitized to a single line. */
38
+ objective: string;
39
+ status: PublishedGoalStatus;
40
+ }
41
+
42
+ /** Minimal shape of a session entry, used for runtime narrowing. */
43
+ interface BranchEntryLike {
44
+ type?: string;
45
+ customType?: string;
46
+ data?: unknown;
47
+ message?: {
48
+ role?: string;
49
+ toolName?: string;
50
+ details?: unknown;
51
+ };
52
+ }
53
+
54
+ /**
55
+ * A stored goal value: `undefined` when it cannot be read, `null` for an explicit
56
+ * clear, otherwise the validated goal.
57
+ */
58
+ function readGoal(value: unknown): PublishedGoal | null | undefined {
59
+ if (value === null) return null;
60
+ if (!value || typeof value !== "object") return undefined;
61
+ const goal = value as { objective?: unknown; status?: unknown };
62
+ if (typeof goal.objective !== "string") return undefined;
63
+ if (goal.status !== "active" && goal.status !== "achieved") return undefined;
64
+ const objective = sanitizeLine(goal.objective);
65
+ if (!objective || objective.length > MAX_GOAL_OBJECTIVE) return undefined;
66
+ return { objective, status: goal.status };
67
+ }
68
+
69
+ /** The last goal written on the branch, or `null` when the session has none. */
70
+ export function lastGoal(entries: Iterable<unknown>): PublishedGoal | null {
71
+ let goal: PublishedGoal | null = null;
72
+ for (const raw of entries) {
73
+ const entry = raw as BranchEntryLike;
74
+
75
+ // The `/goal` command records a custom entry; the tool records a result below.
76
+ // Both replay in branch order, so the last write wins.
77
+ if (entry?.type === "custom" && entry.customType === GOAL_ENTRY_TYPE) {
78
+ const data = entry.data as { goal?: unknown } | null | undefined;
79
+ if (!data || typeof data !== "object" || !("goal" in data)) continue;
80
+ const next = readGoal(data.goal);
81
+ if (next !== undefined) goal = next;
82
+ continue;
83
+ }
84
+
85
+ if (entry?.type !== "message") continue;
86
+ const message = entry.message;
87
+ if (message?.role !== "toolResult" || message.toolName !== GOAL_TOOL) continue;
88
+ const details = message.details as { goal?: unknown; error?: unknown } | undefined;
89
+ if (!details || details.error) continue;
90
+ if (!("goal" in details)) continue;
91
+ const next = readGoal(details.goal);
92
+ if (next !== undefined) goal = next;
93
+ }
94
+ return goal;
95
+ }