@gavin-hu/my-pi-agent 0.13.1 → 0.15.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 (237) hide show
  1. package/README.md +66 -41
  2. package/README.zh-CN.md +53 -36
  3. package/extensions/bytefly/README.md +321 -186
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/config.ts +4 -1
  7. package/extensions/bytefly/doctor/register.ts +24 -5
  8. package/extensions/bytefly/doctor/tools.ts +4 -0
  9. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  10. package/extensions/bytefly/footer/config.ts +10 -6
  11. package/extensions/bytefly/footer/price/config.ts +3 -2
  12. package/extensions/bytefly/footer/price/price.ts +5 -3
  13. package/extensions/bytefly/footer/price/rates.ts +63 -26
  14. package/extensions/bytefly/footer/render/status.ts +3 -3
  15. package/extensions/bytefly/header/component.ts +33 -10
  16. package/extensions/bytefly/header/config.ts +22 -0
  17. package/extensions/bytefly/header/register.ts +2 -1
  18. package/extensions/bytefly/index.ts +6 -6
  19. package/extensions/bytefly/notice/component.ts +8 -3
  20. package/extensions/bytefly/notify/config.ts +7 -12
  21. package/extensions/bytefly/notify/dispatch.ts +13 -5
  22. package/extensions/bytefly/panel/commands.ts +22 -2
  23. package/extensions/bytefly/panel/format.ts +17 -21
  24. package/extensions/bytefly/panel/settings-runtime.ts +86 -22
  25. package/extensions/bytefly/panel/settings.ts +279 -46
  26. package/extensions/bytefly/panel/themes.ts +32 -0
  27. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  28. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  29. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  30. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  31. package/extensions/bytefly/panel/tui/settings-view.ts +98 -13
  32. package/extensions/bytefly/panel/tui.ts +176 -166
  33. package/extensions/bytefly/picker/discovery.ts +1 -0
  34. package/extensions/bytefly/picker/types.ts +2 -0
  35. package/extensions/bytefly/separator/component.ts +17 -6
  36. package/extensions/bytefly/separator/config.ts +15 -0
  37. package/extensions/bytefly/separator/format.ts +60 -6
  38. package/extensions/bytefly/separator/register.ts +68 -9
  39. package/extensions/bytefly/separator/types.ts +8 -0
  40. package/extensions/bytefly/update/cache.ts +58 -19
  41. package/extensions/bytefly/update/check.ts +35 -0
  42. package/extensions/bytefly/update/format.ts +76 -35
  43. package/extensions/bytefly/update/register.ts +144 -55
  44. package/extensions/bytefly/update/state.ts +4 -2
  45. package/extensions/bytefly/update/suppress.ts +66 -0
  46. package/extensions/bytefly/update/types.ts +15 -0
  47. package/extensions/bytefly/working/config.ts +4 -5
  48. package/extensions/bytefly/working/format.ts +24 -7
  49. package/extensions/bytefly/working/register.ts +34 -3
  50. package/extensions/check/README.md +253 -0
  51. package/extensions/check/commands.ts +128 -0
  52. package/extensions/check/config.ts +39 -0
  53. package/extensions/check/detect.ts +269 -0
  54. package/extensions/check/failures.ts +230 -0
  55. package/extensions/check/format.ts +135 -0
  56. package/extensions/check/index.ts +26 -0
  57. package/extensions/check/render.ts +185 -0
  58. package/extensions/check/runner.ts +176 -0
  59. package/extensions/check/runtime.ts +244 -0
  60. package/extensions/check/source.ts +80 -0
  61. package/extensions/check/tools.ts +173 -0
  62. package/extensions/check/tui.ts +195 -0
  63. package/extensions/check/types.ts +99 -0
  64. package/extensions/document/README.md +1 -1
  65. package/extensions/document/extract/pdf.ts +8 -16
  66. package/extensions/document/formats.ts +2 -2
  67. package/extensions/file-browse/README.md +66 -32
  68. package/extensions/file-browse/client.ts +95 -32
  69. package/extensions/file-browse/config.ts +9 -9
  70. package/extensions/file-browse/git.ts +37 -47
  71. package/extensions/file-browse/html.ts +39 -58
  72. package/extensions/file-browse/markdown.ts +110 -92
  73. package/extensions/file-browse/raw-html.ts +478 -0
  74. package/extensions/file-browse/router.ts +32 -70
  75. package/extensions/file-browse/server.ts +18 -56
  76. package/extensions/file-browse/urls.ts +80 -0
  77. package/extensions/goal/README.md +103 -47
  78. package/extensions/goal/clarify-parse.ts +4 -9
  79. package/extensions/goal/clarify.ts +27 -4
  80. package/extensions/goal/commands.ts +24 -11
  81. package/extensions/goal/format.ts +41 -10
  82. package/extensions/goal/index.ts +39 -16
  83. package/extensions/goal/plan-route.ts +51 -0
  84. package/extensions/goal/schema.ts +6 -23
  85. package/extensions/goal/state.ts +39 -57
  86. package/extensions/goal/tools.ts +26 -20
  87. package/extensions/goal/types.ts +11 -1
  88. package/extensions/job/README.md +20 -6
  89. package/extensions/job/format.ts +18 -68
  90. package/extensions/job/index.ts +26 -41
  91. package/extensions/job/log-writer.ts +208 -0
  92. package/extensions/job/logs.ts +3 -2
  93. package/extensions/job/process.ts +6 -99
  94. package/extensions/job/runtime.ts +42 -181
  95. package/extensions/job/schema.ts +1 -4
  96. package/extensions/job/tools.ts +1 -15
  97. package/extensions/job/tui.ts +117 -307
  98. package/extensions/job/ui.ts +7 -33
  99. package/extensions/memory/README.md +3 -1
  100. package/extensions/memory/errors.ts +5 -6
  101. package/extensions/memory/tui.ts +14 -24
  102. package/extensions/plan/README.md +106 -18
  103. package/extensions/plan/commands.ts +9 -5
  104. package/extensions/plan/format.ts +87 -0
  105. package/extensions/plan/index.ts +31 -15
  106. package/extensions/plan/plans.ts +2 -3
  107. package/extensions/plan/policy.ts +0 -6
  108. package/extensions/plan/runtime.ts +4 -2
  109. package/extensions/plan/steps.ts +37 -8
  110. package/extensions/plan/todos.ts +44 -3
  111. package/extensions/plan/tools/enter.ts +94 -0
  112. package/extensions/plan/tools/exit.ts +296 -0
  113. package/extensions/plan/tools/nested.ts +21 -0
  114. package/extensions/plan/tools/steps.ts +136 -0
  115. package/extensions/plan/tools/todo-seed.ts +76 -0
  116. package/extensions/plan/tools/write.ts +127 -0
  117. package/extensions/plan/tools.ts +18 -703
  118. package/extensions/plan/web-client.ts +6 -22
  119. package/extensions/plan/web-page.ts +19 -30
  120. package/extensions/plan/web-router.ts +34 -71
  121. package/extensions/plan/web.ts +13 -40
  122. package/extensions/question/answers.ts +3 -8
  123. package/extensions/question/tui.ts +6 -6
  124. package/extensions/rewind/README.md +95 -22
  125. package/extensions/rewind/checklist.ts +185 -0
  126. package/extensions/rewind/commands.ts +8 -0
  127. package/extensions/rewind/config.ts +3 -11
  128. package/extensions/rewind/format.ts +220 -2
  129. package/extensions/rewind/git.ts +74 -7
  130. package/extensions/rewind/hunk-picker.ts +114 -0
  131. package/extensions/rewind/patch.ts +88 -0
  132. package/extensions/rewind/path-picker.ts +67 -100
  133. package/extensions/rewind/paths.ts +93 -19
  134. package/extensions/rewind/restore.ts +53 -10
  135. package/extensions/rewind/review-tui.ts +320 -0
  136. package/extensions/rewind/review.ts +397 -0
  137. package/extensions/rewind/rewind-tui.ts +4 -3
  138. package/extensions/rewind/runtime.ts +94 -17
  139. package/extensions/subagent/README.md +118 -8
  140. package/extensions/subagent/background-tui.ts +208 -0
  141. package/extensions/subagent/background.ts +470 -0
  142. package/extensions/subagent/commands.ts +80 -0
  143. package/extensions/subagent/config.ts +62 -0
  144. package/extensions/subagent/format.ts +24 -1
  145. package/extensions/subagent/index.ts +116 -1
  146. package/extensions/subagent/orchestrate.ts +16 -3
  147. package/extensions/subagent/render.ts +25 -0
  148. package/extensions/subagent/run.ts +113 -49
  149. package/extensions/subagent/schema.ts +12 -0
  150. package/extensions/subagent/transcripts.ts +202 -0
  151. package/extensions/subagent/types.ts +23 -0
  152. package/extensions/subagent/ui.ts +50 -0
  153. package/extensions/todo/README.md +18 -13
  154. package/extensions/todo/format.ts +3 -2
  155. package/extensions/todo/runtime.ts +3 -2
  156. package/extensions/todo/schema.ts +8 -84
  157. package/extensions/todo/state.ts +5 -46
  158. package/extensions/todo/tools.ts +2 -1
  159. package/extensions/todo/types.ts +6 -15
  160. package/extensions/watch/README.md +16 -6
  161. package/extensions/watch/child.ts +150 -0
  162. package/extensions/watch/format.ts +8 -48
  163. package/extensions/watch/index.ts +42 -58
  164. package/extensions/watch/runtime.ts +49 -128
  165. package/extensions/watch/schema.ts +1 -4
  166. package/extensions/watch/tools.ts +1 -11
  167. package/extensions/watch/tui.ts +103 -273
  168. package/extensions/watch/ui.ts +5 -32
  169. package/extensions/web-access/README.md +1 -1
  170. package/extensions/web-access/fetch/page.ts +1 -1
  171. package/extensions/web-access/fetch/tool.ts +6 -2
  172. package/extensions/web-access/search/search.ts +2 -5
  173. package/extensions/wechat/README.md +3 -1
  174. package/extensions/wechat/client.ts +23 -0
  175. package/extensions/wechat/commands.ts +57 -42
  176. package/extensions/wechat/deliver.ts +5 -9
  177. package/extensions/wechat/errors.ts +3 -6
  178. package/extensions/wechat/inbound.ts +2 -5
  179. package/extensions/wechat/index.ts +2 -8
  180. package/extensions/wechat/outbound.ts +2 -4
  181. package/extensions/wechat/peer-worker.ts +3 -8
  182. package/extensions/wechat/poll.ts +10 -9
  183. package/extensions/wechat/presence.ts +35 -62
  184. package/extensions/wechat/reply-pipeline.ts +3 -1
  185. package/extensions/wechat/status-screen.ts +1 -1
  186. package/extensions/wechat/timer.ts +57 -0
  187. package/extensions/worktree/config.ts +8 -20
  188. package/extensions/worktree/enter.ts +7 -12
  189. package/extensions/worktree/git.ts +38 -69
  190. package/extensions/worktree/index.ts +5 -4
  191. package/extensions/worktree/registry.ts +7 -7
  192. package/extensions/worktree/root-tools.ts +2 -2
  193. package/extensions/worktree/status.ts +3 -2
  194. package/lib/README.md +22 -8
  195. package/lib/config.ts +83 -12
  196. package/lib/errors.ts +12 -0
  197. package/lib/format.ts +56 -0
  198. package/lib/git/read.ts +8 -0
  199. package/lib/goal-state.ts +23 -8
  200. package/lib/http/headers.ts +54 -0
  201. package/lib/http/index.ts +13 -0
  202. package/lib/http/response.ts +46 -0
  203. package/lib/http/router.ts +52 -0
  204. package/lib/http/server.ts +91 -0
  205. package/lib/lines.ts +77 -0
  206. package/lib/pdf.ts +82 -0
  207. package/lib/plan-entry.ts +71 -0
  208. package/lib/plan-state.ts +34 -0
  209. package/lib/report.ts +99 -0
  210. package/lib/requirements.ts +25 -19
  211. package/lib/spawn.ts +170 -0
  212. package/lib/todo-state.ts +110 -23
  213. package/lib/tool-names.ts +7 -0
  214. package/lib/tool-result.ts +33 -0
  215. package/lib/tui/README.md +68 -3
  216. package/lib/tui/chrome.ts +28 -4
  217. package/lib/tui/columns.ts +6 -1
  218. package/lib/tui/confirm.ts +65 -0
  219. package/lib/tui/dock-screen.ts +410 -0
  220. package/lib/tui/glyphs.ts +2 -1
  221. package/lib/tui/indent.ts +1 -1
  222. package/lib/tui/index.ts +2 -0
  223. package/lib/tui/rows.ts +21 -0
  224. package/lib/tui/status-controller.ts +35 -1
  225. package/lib/tui/tool.ts +59 -1
  226. package/lib/web/components.ts +20 -1
  227. package/lib/web/markup.ts +32 -0
  228. package/lib/web/runtime.ts +50 -0
  229. package/package.json +23 -5
  230. package/skills/init/SKILL.md +165 -0
  231. package/skills/init/references/agents-md-quality.md +103 -0
  232. package/skills/init/references/interview-questions.md +81 -0
  233. package/skills/refactor/SKILL.md +141 -0
  234. package/themes/bytefly-dark.json +2 -3
  235. package/themes/bytefly-light.json +2 -3
  236. package/extensions/watch/process.ts +0 -63
  237. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -4,11 +4,15 @@
4
4
  * Pi has no built-in turn separator. On every fully settled agent run
5
5
  * (`agent_settled`, one per user turn) this appends an inert custom entry that
6
6
  * is excluded from LLM context, and a registered entry renderer draws it as a
7
- * centered `╌╌╌ turn N ╌╌╌` rule in the interactive transcript.
7
+ * centered `╌╌╌ turn N ╌╌╌` rule in the interactive transcript. The entry also
8
+ * carries the run's receipt — duration, top-level tool count, failed tools, and
9
+ * whether the user aborted it — so the rule can read as a flight log.
8
10
  *
9
11
  * `agent_settled` is deliberate: `turn_end` fires once per assistant message,
10
12
  * so tool-calling rounds would each get a line, while a settled run is exactly
11
- * one user turn.
13
+ * one user turn. The run's first `agent_start` opens the receipt and later
14
+ * starts (a retry or a continuation) stay in it, so the duration covers the
15
+ * whole settled run rather than one attempt.
12
16
  */
13
17
 
14
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -17,6 +21,12 @@ import { SeparatorComponent } from "./component.ts";
17
21
  import { CONFIG } from "./config.ts";
18
22
  import type { TurnSeparatorData } from "./types.ts";
19
23
 
24
+ /** Host seams, injected by tests so the duration is deterministic. */
25
+ export interface SeparatorDeps {
26
+ /** The clock the duration is read from. */
27
+ now?: () => number;
28
+ }
29
+
20
30
  /** Count separators already on the branch, so numbering survives resume and fork. */
21
31
  function countSeparators(entries: readonly { type: string; customType?: string }[]): number {
22
32
  let count = 0;
@@ -27,17 +37,66 @@ function countSeparators(entries: readonly { type: string; customType?: string }
27
37
  }
28
38
 
29
39
  /** Register the separator renderer and append one on every settled turn. */
30
- export function registerTurnSeparator(pi: ExtensionAPI, pad: PadSource = createPadStore()): void {
40
+ export function registerTurnSeparator(
41
+ pi: ExtensionAPI,
42
+ pad: PadSource = createPadStore(),
43
+ deps: SeparatorDeps = {},
44
+ ): void {
45
+ const now = deps.now ?? (() => Date.now());
46
+ // The current run's receipt. `startedAt` is the first `agent_start`; the
47
+ // counters span every top-level tool in the settled run.
48
+ let startedAt: number | undefined;
49
+ let tools = 0;
50
+ let failures = 0;
51
+
52
+ const reset = (): void => {
53
+ startedAt = undefined;
54
+ tools = 0;
55
+ failures = 0;
56
+ };
57
+
31
58
  pi.registerEntryRenderer<TurnSeparatorData>(CONFIG.customType, (entry, _options, theme) => {
32
- const turn = entry.data?.turn;
33
- if (typeof turn !== "number" || !Number.isFinite(turn)) return undefined;
34
- return new SeparatorComponent(turn, theme, pad.resolve());
59
+ const data = entry.data;
60
+ if (!data || typeof data.turn !== "number" || !Number.isFinite(data.turn)) return undefined;
61
+ return new SeparatorComponent(data, theme, pad.resolve());
62
+ });
63
+
64
+ pi.on("agent_start", (_event, ctx) => {
65
+ if (ctx.mode !== "tui" || startedAt !== undefined) return;
66
+ startedAt = now();
67
+ tools = 0;
68
+ failures = 0;
35
69
  });
36
70
 
37
- pi.on("agent_settled", (_event, ctx) => {
71
+ // A nested tool is an implementation detail of its parent, not a transcript
72
+ // block, so it neither counts nor contributes a failure.
73
+ pi.on("tool_execution_start", (event) => {
74
+ if (startedAt === undefined || event.parentToolCallId) return;
75
+ tools += 1;
76
+ });
77
+
78
+ pi.on("tool_execution_end", (event) => {
79
+ if (startedAt === undefined || event.parentToolCallId || !event.isError) return;
80
+ failures += 1;
81
+ });
82
+
83
+ pi.on("agent_settled", (event, ctx) => {
38
84
  // The transcript is interactive-only; keep RPC/JSON/print sessions clean.
39
- if (ctx.mode !== "tui") return;
85
+ if (ctx.mode !== "tui") {
86
+ reset();
87
+ return;
88
+ }
40
89
  const turn = countSeparators(ctx.sessionManager.getBranch()) + 1;
41
- pi.appendEntry<TurnSeparatorData>(CONFIG.customType, { turn });
90
+ const data: TurnSeparatorData = { turn };
91
+ if (startedAt !== undefined) {
92
+ data.durationMs = Math.max(0, now() - startedAt);
93
+ if (tools > 0) data.tools = tools;
94
+ if (failures > 0) data.failures = failures;
95
+ }
96
+ if (event.aborted) data.aborted = true;
97
+ pi.appendEntry<TurnSeparatorData>(CONFIG.customType, data);
98
+ reset();
42
99
  });
100
+
101
+ pi.on("session_shutdown", () => reset());
43
102
  }
@@ -9,4 +9,12 @@
9
9
  export interface TurnSeparatorData {
10
10
  /** 1-based index of the completed user turn this separator follows. */
11
11
  turn: number;
12
+ /** From the run's first `agent_start` to `agent_settled`, in milliseconds. */
13
+ durationMs?: number;
14
+ /** Top-level tool calls the run started. */
15
+ tools?: number;
16
+ /** Top-level tool calls that ended in error. */
17
+ failures?: number;
18
+ /** Whether the run ended because the user aborted it. */
19
+ aborted?: boolean;
12
20
  }
@@ -1,34 +1,42 @@
1
1
  /**
2
- * The published-version answer, cached on disk with the time it was read.
2
+ * The package-update snapshot, cached on disk with the time it was read.
3
3
  *
4
- * The check already fires once per session, which leaves two gaps: a session open
5
- * for days never re-reads npm, and every new session pays for a request it made
6
- * minutes earlier. A timestamp is what fixes the second, so the cache lives in the
7
- * agent directory rather than in memory — one entry, one window, shared by every
8
- * session that starts inside it.
4
+ * The 24-hour window is what makes a long-lived session different from a fresh
5
+ * one: a session that starts inside the window reuses the last read instead of
6
+ * repeating Pi's npm/git checks, and `/bytefly update` re-runs them on demand.
7
+ * The entry is keyed by the working directory and the effective package sources,
8
+ * so a project switch or an install/removal invalidates it instead of showing
9
+ * another project's list.
9
10
  *
10
- * A cache can only ever be a miss. An absent, unreadable, or wrong-typed file
11
- * reads as `undefined` and the caller fetches; nothing here throws, because a
12
- * stale file must never become a session error.
11
+ * A cache can only ever be a miss. An absent, unreadable, old-schema, or
12
+ * mistyped file reads as `undefined` and the caller fetches; nothing here
13
+ * throws, because a stale file must never become a session error.
13
14
  */
14
15
 
16
+ import { createHash } from "node:crypto";
15
17
  import { readFileSync } from "node:fs";
16
18
  import { join } from "node:path";
19
+ import type { PackageSource } from "@earendil-works/pi-coding-agent";
17
20
  import { writeConfigFile } from "../../../lib/config.ts";
21
+ import type { PackageUpdate } from "./types.ts";
18
22
  import { parseVersion } from "./version.ts";
19
23
 
20
24
  /** The file name under `<agentDir>/bytefly/`. */
21
25
  export const UPDATE_CACHE_FILE = "update.json";
22
26
 
23
- /** How long a cached answer is reused before the next session re-reads npm. */
27
+ /** How long a cached snapshot is reused before the next session re-reads. */
24
28
  export const UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
25
29
 
26
- /** One cached answer: the newest published version and when it was read. */
30
+ /** One cached snapshot: Pi's list, this package's version, and when it was read. */
27
31
  export interface UpdateCacheEntry {
28
- /** The version npm's `latest` tag pointed at. */
29
- latest: string;
30
- /** Epoch milliseconds of the read that produced `latest`. */
32
+ /** Identity of the cwd plus the effective package sources. */
33
+ key: string;
34
+ /** Epoch milliseconds of the read that produced this snapshot. */
31
35
  checkedAt: number;
36
+ /** Every package Pi reported as updateable. */
37
+ updates: PackageUpdate[];
38
+ /** This package's new version, when it is one of the updates. */
39
+ latest?: string;
32
40
  }
33
41
 
34
42
  /** Where the cache lives for an agent directory. */
@@ -36,15 +44,41 @@ export function updateCachePath(agentDir: string): string {
36
44
  return join(agentDir, "bytefly", UPDATE_CACHE_FILE);
37
45
  }
38
46
 
39
- /** Read the cache; a missing, corrupt, or mistyped entry is `undefined`. */
47
+ /** Stable identity for the settings a snapshot was read under. */
48
+ export function updateCacheKey(cwd: string, packages: readonly PackageSource[] | undefined): string {
49
+ return createHash("sha256")
50
+ .update(JSON.stringify({ cwd, packages: packages ?? [] }))
51
+ .digest("hex");
52
+ }
53
+
54
+ /** Whether a value has the fields Pi's package update carries. */
55
+ function isPackageUpdate(value: unknown): value is PackageUpdate {
56
+ if (!value || typeof value !== "object") return false;
57
+ const update = value as Partial<PackageUpdate>;
58
+ return (
59
+ typeof update.source === "string" &&
60
+ typeof update.displayName === "string" &&
61
+ (update.type === "npm" || update.type === "git") &&
62
+ (update.scope === "user" || update.scope === "project")
63
+ );
64
+ }
65
+
66
+ /** Read the cache; a missing, corrupt, old-schema, or mistyped entry is `undefined`. */
40
67
  export function readUpdateCache(path: string): UpdateCacheEntry | undefined {
41
68
  try {
42
69
  const raw = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown> | undefined;
43
70
  if (!raw || typeof raw !== "object") return undefined;
44
- const latest = typeof raw.latest === "string" ? raw.latest.trim() : "";
71
+ const key = typeof raw.key === "string" ? raw.key.trim() : "";
45
72
  const checkedAt = typeof raw.checkedAt === "number" && Number.isFinite(raw.checkedAt) ? raw.checkedAt : 0;
46
- if (!parseVersion(latest) || checkedAt <= 0) return undefined;
47
- return { latest, checkedAt };
73
+ const updates = Array.isArray(raw.updates) ? raw.updates : undefined;
74
+ if (!key || checkedAt <= 0 || !updates || !updates.every(isPackageUpdate)) return undefined;
75
+ const latest = typeof raw.latest === "string" ? raw.latest.trim() : "";
76
+ return {
77
+ key,
78
+ checkedAt,
79
+ updates,
80
+ ...(parseVersion(latest) ? { latest } : {}),
81
+ };
48
82
  } catch {
49
83
  return undefined;
50
84
  }
@@ -55,7 +89,12 @@ export function readUpdateCache(path: string): UpdateCacheEntry | undefined {
55
89
  * treats a throw as a miss: a read-only agent directory must not break a session.
56
90
  */
57
91
  export function writeUpdateCache(path: string, entry: UpdateCacheEntry): void {
58
- writeConfigFile(path, { latest: entry.latest, checkedAt: entry.checkedAt });
92
+ writeConfigFile(path, {
93
+ key: entry.key,
94
+ checkedAt: entry.checkedAt,
95
+ updates: entry.updates,
96
+ ...(entry.latest ? { latest: entry.latest } : {}),
97
+ });
59
98
  }
60
99
 
61
100
  /**
@@ -10,6 +10,8 @@
10
10
  * never sees an error from a check nobody asked for.
11
11
  */
12
12
 
13
+ import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
14
+ import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
13
15
  import { parseVersion } from "./version.ts";
14
16
 
15
17
  /** The published document holding the version npm's `latest` tag points at. */
@@ -37,6 +39,39 @@ export interface UpdateCheckDeps {
37
39
  timeoutMs?: number;
38
40
  }
39
41
 
42
+ /** What the package-manager read needs. */
43
+ export interface AvailableUpdatesDeps {
44
+ /** Session working directory. */
45
+ cwd: string;
46
+ /** Pi agent directory; defaults to `getAgentDir()`. */
47
+ agentDir?: string;
48
+ /** Whether project-local packages may be read. */
49
+ projectTrusted: boolean;
50
+ /** The real check captured before suppression; defaults to the manager method. */
51
+ checkForAvailableUpdates?: PackageUpdateCheck;
52
+ }
53
+
54
+ /**
55
+ * Every package Pi would update, or `undefined` when the check cannot run.
56
+ *
57
+ * The distinction is load-bearing: an empty list is a real answer ("everything is
58
+ * current") that the caller caches for a day, while a failed read must leave the
59
+ * previous snapshot and the header marker alone.
60
+ */
61
+ export async function fetchAvailableUpdates(deps: AvailableUpdatesDeps): Promise<PackageUpdate[] | undefined> {
62
+ try {
63
+ const agentDir = deps.agentDir ?? getAgentDir();
64
+ const settingsManager = SettingsManager.create(deps.cwd, agentDir, {
65
+ projectTrusted: deps.projectTrusted,
66
+ });
67
+ const manager = new DefaultPackageManager({ cwd: deps.cwd, agentDir, settingsManager });
68
+ const check = deps.checkForAvailableUpdates ?? ((current) => current.checkForAvailableUpdates());
69
+ return await check(manager);
70
+ } catch {
71
+ return undefined;
72
+ }
73
+ }
74
+
40
75
  /** The latest published version, or `undefined` when it cannot be read. */
41
76
  export async function fetchLatestVersion(deps: UpdateCheckDeps = {}): Promise<string | undefined> {
42
77
  const request: FetchLike = deps.fetch ?? ((url, init) => globalThis.fetch(url, init));
@@ -1,25 +1,27 @@
1
1
  /**
2
- * Wording for the update check: the header marker, the one transcript notice, and
3
- * the answer an explicit re-check prints.
2
+ * Wording for the update surface: the header marker, the one transcript notice,
3
+ * and the answer an explicit re-check prints.
4
4
  *
5
- * All three read the same two facts — the version this session loaded and the
6
- * version the registry publishes — and none adds a third. The marker is the
7
- * shortest honest form (`↑0.12.0`); the notice names the command and the release
8
- * URL as text, because the notice renderer has no link support; and the re-check
9
- * says what it just found, including the two cases it cannot answer (offline, and
10
- * a registry it could not read).
5
+ * Pi's package manager decides which configured packages are behind. The marker
6
+ * is still only this package's newest version (`↑0.12.0`); the notice names every
7
+ * package Pi reported and, for this package, the version arrow and release URL
8
+ * the notice renderer can print as text.
11
9
  */
12
10
 
13
11
  import pkg from "../../../package.json";
14
12
  import { noticeMessage, type NoticeMessage } from "../../../lib/notice.ts";
15
13
  import { GLYPHS } from "../../../lib/tui/glyphs.ts";
16
14
  import type { UpdateOutcome } from "./state.ts";
15
+ import type { PackageUpdate } from "./types.ts";
17
16
 
18
- /** The header marker: an up arrow and the newest version, e.g. `↑0.12.0`. */
17
+ /** The header marker: an up arrow and this package's newest version. */
19
18
  export function updateMarker(latest: string): string {
20
19
  return `${GLYPHS.update}${latest}`;
21
20
  }
22
21
 
22
+ /** The command that resolves every row in the notice. */
23
+ export const UPDATE_COMMAND = "pi update --extensions";
24
+
23
25
  /**
24
26
  * The release page for a version, or `undefined` when this checkout does not name
25
27
  * a GitHub repository. Derived from `package.json` so a rename cannot leave a dead
@@ -32,35 +34,72 @@ export function releaseUrl(version: string): string | undefined {
32
34
  return `${base}/releases/tag/v${version}`;
33
35
  }
34
36
 
35
- /** The single notice a session posts when a newer version is published. */
36
- export function updateNotice(current: string, latest: string): NoticeMessage {
37
- const release = releaseUrl(latest);
38
- const command = "pi update --extensions";
37
+ /** Whether a row is this package, whose version the header and notice show. */
38
+ export function isOwnPackage(update: PackageUpdate): boolean {
39
+ return update.type === "npm" && update.displayName === pkg.name;
40
+ }
41
+
42
+ /** The scope word Pi's package update carries. */
43
+ function scopeLabel(update: PackageUpdate): string {
44
+ return update.scope === "project" ? "project" : "global";
45
+ }
46
+
47
+ /** One package row: the version arrow for this package, the scope for the rest. */
48
+ function packageRow(
49
+ update: PackageUpdate,
50
+ current: string,
51
+ ownLatest?: string,
52
+ ): {
53
+ label: string;
54
+ detail: string;
55
+ severity: "info";
56
+ } {
57
+ const detail = isOwnPackage(update) && ownLatest ? `v${current} → v${ownLatest}` : scopeLabel(update);
58
+ return { label: update.displayName, detail, severity: "info" };
59
+ }
60
+
61
+ /** The headline shared by the notice and the re-check answer. */
62
+ function headline(count: number): string {
63
+ return count === 1 ? "1 package update available" : `${count} package updates available`;
64
+ }
65
+
66
+ /** The headline and the package names, the sentence both answers open with. */
67
+ function namesLine(updates: readonly PackageUpdate[]): string {
68
+ return `${headline(updates.length)}: ${updates.map((update) => update.displayName).join(", ")}.`;
69
+ }
70
+
71
+ /** The release URL for this package's new version, when it is known. */
72
+ function ownRelease(ownLatest?: string): string | undefined {
73
+ return ownLatest ? releaseUrl(ownLatest) : undefined;
74
+ }
75
+
76
+ /** The single notice a session posts when Pi reports package updates. */
77
+ export function updateNotice(updates: readonly PackageUpdate[], current: string, ownLatest?: string): NoticeMessage {
78
+ const own = updates.find(isOwnPackage);
79
+ const release = own ? ownRelease(ownLatest) : undefined;
80
+ const parts = [namesLine(updates)];
81
+ if (own && ownLatest) parts.push(`${pkg.name} v${current} → v${ownLatest}.`);
82
+ parts.push(`Run \`${UPDATE_COMMAND}\` to update.`);
83
+ if (release) parts.push(`Release notes: ${release}`);
39
84
  return noticeMessage({
40
- content:
41
- `bytefly ${latest} is available (current ${current}). Run \`${command}\` to update.` +
42
- (release ? ` Release notes: ${release}` : ""),
85
+ content: parts.join(" "),
43
86
  details: {
44
87
  source: "bytefly",
45
- title: `${latest} is available`,
88
+ title: headline(updates.length),
46
89
  severity: "info",
47
- items: release
48
- ? [
49
- { label: "current", detail: `v${current}` },
50
- { label: "update", detail: command },
51
- { label: "release", detail: release },
52
- ]
53
- : [
54
- { label: "current", detail: `v${current}` },
55
- { label: "update", detail: command },
56
- ],
90
+ items: [
91
+ ...updates.map((update) => packageRow(update, current, ownLatest)),
92
+ { label: "update", detail: UPDATE_COMMAND },
93
+ ...(release ? [{ label: "release", detail: release }] : []),
94
+ ],
57
95
  },
58
96
  });
59
97
  }
60
98
 
61
99
  /**
62
100
  * What `/bytefly update` prints. The background check is silent because nobody
63
- * asked; here someone did, so a skip and a failed read are answers too.
101
+ * asked; here someone did, so a skip, a failure, and an up-to-date answer are
102
+ * answers too.
64
103
  */
65
104
  export function updateOutcomeText(
66
105
  outcome: UpdateOutcome,
@@ -71,17 +110,19 @@ export function updateOutcomeText(
71
110
  return { text: "bytefly: the update check was skipped (PI_OFFLINE is set).", severity: "info" };
72
111
  case "failed":
73
112
  return {
74
- text: "bytefly: could not read the published version — check the network and try again.",
113
+ text: "bytefly: could not check package updates — check the network and try again.",
75
114
  severity: "warning",
76
115
  };
77
116
  case "current":
78
- return { text: `bytefly: up to date — v${outcome.latest} is the newest published version.`, severity: "info" };
117
+ return { text: "bytefly: up to date — no configured package has an update.", severity: "info" };
79
118
  case "available": {
80
- const release = releaseUrl(outcome.latest);
81
- return {
82
- text: `bytefly: v${outcome.latest} is available (current ${current}).${release ? ` ${release}` : ""}`,
83
- severity: "info",
84
- };
119
+ const own = outcome.updates.some(isOwnPackage);
120
+ const release = own ? ownRelease(outcome.ownLatest) : undefined;
121
+ const parts = [`bytefly: ${namesLine(outcome.updates)}`];
122
+ if (own && outcome.ownLatest) parts.push(`This package is v${current} → v${outcome.ownLatest}.`);
123
+ parts.push(`Run ${UPDATE_COMMAND}.`);
124
+ if (release) parts.push(`Release notes: ${release}`);
125
+ return { text: parts.join(" "), severity: "info" };
85
126
  }
86
127
  }
87
128
  }