portable-agent-layer 0.90.0 → 0.91.1

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.
@@ -0,0 +1,108 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { palPkg } from "../hooks/lib/paths";
4
+
5
+ export interface Release {
6
+ tag_name: string;
7
+ body?: string | null;
8
+ }
9
+
10
+ export interface WhatsNew {
11
+ items: string[];
12
+ more: number;
13
+ url: string;
14
+ }
15
+
16
+ const SHOWN = 3;
17
+
18
+ function parts(version: string): number[] {
19
+ return version.replace(/^v/, "").split(".").map(Number);
20
+ }
21
+
22
+ export function newer(a: string, b: string): boolean {
23
+ const [x, y] = [parts(a), parts(b)];
24
+ for (let i = 0; i < 3; i++) {
25
+ if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /** "* **rating:** confirm corrections ([abc](url))" reads "Confirm corrections". */
31
+ export function noteText(bullet: string): string {
32
+ const text = bullet
33
+ .replace(/^\s*[*-]\s*/, "")
34
+ .replace(/^\*\*[^*]+:\*\*\s*/, "")
35
+ .replace(/\s*\(\[[^\]]*\]\([^)]*\)\)/g, "")
36
+ .trim();
37
+ return text.charAt(0).toUpperCase() + text.slice(1);
38
+ }
39
+
40
+ function section(body: string, heading: string): string[] {
41
+ const start = body.indexOf(`### ${heading}`);
42
+ if (start < 0) return [];
43
+ const rest = body.slice(start).split("\n").slice(1);
44
+ const end = rest.findIndex((line) => line.startsWith("#"));
45
+ return (end < 0 ? rest : rest.slice(0, end))
46
+ .filter((line) => /^\s*[*-]\s/.test(line))
47
+ .map(noteText);
48
+ }
49
+
50
+ export function releaseNotes(body: string): string[] {
51
+ return [...section(body, "Features"), ...section(body, "Bug Fixes")];
52
+ }
53
+
54
+ function repoSlug(pkg = palPkg()): string | null {
55
+ try {
56
+ const manifest = JSON.parse(readFileSync(resolve(pkg, "package.json"), "utf-8")) as {
57
+ repository?: { url?: string };
58
+ };
59
+ return (
60
+ /github\.com\/([^/]+\/[^/.]+)/.exec(manifest.repository?.url ?? "")?.[1] ?? null
61
+ );
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ export function summarizeReleases(
68
+ releases: Release[],
69
+ from: string,
70
+ to: string,
71
+ slug: string
72
+ ): WhatsNew | null {
73
+ const items = releases
74
+ .filter((r) => newer(r.tag_name, from) && !newer(r.tag_name, to))
75
+ .flatMap((r) => releaseNotes(r.body ?? ""));
76
+ if (items.length === 0) return null;
77
+ return {
78
+ items: items.slice(0, SHOWN),
79
+ more: Math.max(0, items.length - SHOWN),
80
+ url: `github.com/${slug}/releases/tag/v${to}`,
81
+ };
82
+ }
83
+
84
+ async function fetchReleases(slug: string): Promise<Release[]> {
85
+ const response = await fetch(
86
+ `https://api.github.com/repos/${slug}/releases?per_page=30`,
87
+ {
88
+ headers: { accept: "application/vnd.github+json" },
89
+ signal: AbortSignal.timeout(3000),
90
+ }
91
+ );
92
+ return response.ok ? ((await response.json()) as Release[]) : [];
93
+ }
94
+
95
+ /** Null when the notes cannot be fetched in time: an update never waits on GitHub. */
96
+ export async function whatsNew(
97
+ from: string,
98
+ to: string,
99
+ fetchAll: (slug: string) => Promise<Release[]> = fetchReleases
100
+ ): Promise<WhatsNew | null> {
101
+ const slug = repoSlug();
102
+ if (!slug) return null;
103
+ try {
104
+ return summarizeReleases(await fetchAll(slug), from, to, slug);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
@@ -18,6 +18,7 @@ import {
18
18
  } from "node:fs";
19
19
  import { homedir } from "node:os";
20
20
  import { dirname, isAbsolute, resolve, sep } from "node:path";
21
+ import { createStyle, PALETTE, type Rgb } from "../cli/ui/style";
21
22
  import {
22
23
  AGENT_NAMES,
23
24
  AGENT_REGISTRY,
@@ -45,12 +46,47 @@ function narrate(line: string): void {
45
46
  if (narrating) console.log(line);
46
47
  }
47
48
 
49
+ export type LogLevel = "info" | "success" | "warn" | "error" | "detail";
50
+ type LogSink = (level: LogLevel, message: string) => void;
51
+
52
+ let sink: LogSink | null = null;
53
+
54
+ /** Hands every log line to `collect` while `work` runs, so a caller can place it under its own step. */
55
+ export async function capturingLog<T>(
56
+ collect: LogSink,
57
+ work: () => Promise<T>
58
+ ): Promise<T> {
59
+ const previous = sink;
60
+ sink = collect;
61
+ try {
62
+ return await work();
63
+ } finally {
64
+ sink = previous;
65
+ }
66
+ }
67
+
68
+ const LEVEL_COLOR: Record<LogLevel, Rgb> = {
69
+ info: PALETTE.indigo,
70
+ success: PALETTE.green,
71
+ warn: PALETTE.amber,
72
+ error: PALETTE.rose,
73
+ detail: PALETTE.indigo,
74
+ };
75
+
76
+ function tagged(level: LogLevel, message: string): string {
77
+ return `${createStyle().paint(LEVEL_COLOR[level], "[pal]")} ${message}`;
78
+ }
79
+
80
+ function emit(level: LogLevel, message: string, write: (line: string) => void): void {
81
+ if (sink) sink(level, message);
82
+ else write(tagged(level, message));
83
+ }
84
+
48
85
  export const log = {
49
- info: (msg: string) => narrate(`\x1b[34m[pal]\x1b[0m ${msg}`),
50
- success: (msg: string) => narrate(`\x1b[32m[pal]\x1b[0m ${msg}`),
51
- heading: (label: string) => narrate(`\n━━━ ${label} ━━━`),
52
- warn: (msg: string) => console.log(`\x1b[33m[pal]\x1b[0m ${msg}`),
53
- error: (msg: string) => console.error(`\x1b[31m[pal]\x1b[0m ${msg}`),
86
+ info: (msg: string) => emit("info", msg, narrate),
87
+ success: (msg: string) => emit("success", msg, narrate),
88
+ warn: (msg: string) => emit("warn", msg, (line) => console.log(line)),
89
+ error: (msg: string) => emit("error", msg, (line) => console.error(line)),
54
90
 
55
91
  /**
56
92
  * Per-item narration from inside a loop, where the caller already reports the
@@ -61,7 +97,7 @@ export const log = {
61
97
  */
62
98
  detail: (msg: string) => {
63
99
  if (runningUnderTest()) return;
64
- narrate(`\x1b[34m[pal]\x1b[0m ${msg}`);
100
+ emit("detail", msg, narrate);
65
101
  },
66
102
  };
67
103
 
@@ -1152,6 +1188,13 @@ function shippedAgentNames(): Set<string> {
1152
1188
  );
1153
1189
  }
1154
1190
 
1191
+ export function installedShippedAgentCount(agent: AgentName): number {
1192
+ const dir = nativeAgentsDir(agent);
1193
+ return [...shippedAgentNames()].filter((name) =>
1194
+ existsSync(resolve(dir, agentFileName(name, agent)))
1195
+ ).length;
1196
+ }
1197
+
1155
1198
  /** List the user-authored subagents in ~/.pal/agents/. */
1156
1199
  export function listPersonalSubagents(): string[] {
1157
1200
  if (!existsSync(palAgentsStore())) return [];