@letta-ai/letta-code 0.30.28 → 0.30.30

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 (82) hide show
  1. package/dist/agent-presets.js +55 -2490
  2. package/dist/agent-presets.js.map +4 -4
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/acting-user.d.ts +21 -0
  6. package/dist/types/agent/acting-user.d.ts.map +1 -0
  7. package/dist/types/agent/available-models.d.ts +2 -0
  8. package/dist/types/agent/available-models.d.ts.map +1 -1
  9. package/dist/types/agent/client-skills.d.ts +3 -0
  10. package/dist/types/agent/client-skills.d.ts.map +1 -1
  11. package/dist/types/agent/memory-git.d.ts +3 -11
  12. package/dist/types/agent/memory-git.d.ts.map +1 -1
  13. package/dist/types/agent/message.d.ts +1 -0
  14. package/dist/types/agent/message.d.ts.map +1 -1
  15. package/dist/types/agent/model-catalog.d.ts +14 -52
  16. package/dist/types/agent/model-catalog.d.ts.map +1 -1
  17. package/dist/types/agent/model.d.ts.map +1 -1
  18. package/dist/types/agent/personality-presets.d.ts +1 -1
  19. package/dist/types/agent/personality-presets.d.ts.map +1 -1
  20. package/dist/types/agent/shared-memory-skills.d.ts +18 -0
  21. package/dist/types/agent/shared-memory-skills.d.ts.map +1 -0
  22. package/dist/types/agent/subagents/index.d.ts +1 -1
  23. package/dist/types/agent/subagents/index.d.ts.map +1 -1
  24. package/dist/types/agent/turn-recovery-policy.d.ts +10 -7
  25. package/dist/types/agent/turn-recovery-policy.d.ts.map +1 -1
  26. package/dist/types/agent-presets.d.ts +0 -1
  27. package/dist/types/agent-presets.d.ts.map +1 -1
  28. package/dist/types/backend/api/conversations.d.ts +1 -0
  29. package/dist/types/backend/api/conversations.d.ts.map +1 -1
  30. package/dist/types/backend/backend.d.ts +4 -2
  31. package/dist/types/backend/backend.d.ts.map +1 -1
  32. package/dist/types/backend/dev/pi-model-factory.d.ts +6 -0
  33. package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
  34. package/dist/types/backend/dev/pi-provider-registry.d.ts.map +1 -1
  35. package/dist/types/backend/local/local-model-config.d.ts +3 -0
  36. package/dist/types/backend/local/local-model-config.d.ts.map +1 -1
  37. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  38. package/dist/types/backend/local/system-prompt-compilation.d.ts.map +1 -1
  39. package/dist/types/cli/helpers/stream-processor.d.ts +1 -0
  40. package/dist/types/cli/helpers/stream-processor.d.ts.map +1 -1
  41. package/dist/types/cli/helpers/stream-stall-reconciler.d.ts +2 -1
  42. package/dist/types/cli/helpers/stream-stall-reconciler.d.ts.map +1 -1
  43. package/dist/types/cli/helpers/stream.d.ts +1 -1
  44. package/dist/types/cli/helpers/stream.d.ts.map +1 -1
  45. package/dist/types/reminders/state.d.ts +6 -1
  46. package/dist/types/reminders/state.d.ts.map +1 -1
  47. package/dist/types/tools/impl/bash-output.d.ts +1 -0
  48. package/dist/types/tools/impl/bash-output.d.ts.map +1 -1
  49. package/dist/types/tools/impl/github-pull-request-tracker.d.ts +7 -3
  50. package/dist/types/tools/impl/github-pull-request-tracker.d.ts.map +1 -1
  51. package/dist/types/tools/impl/glob.d.ts +1 -0
  52. package/dist/types/tools/impl/glob.d.ts.map +1 -1
  53. package/dist/types/tools/impl/grep.d.ts +1 -0
  54. package/dist/types/tools/impl/grep.d.ts.map +1 -1
  55. package/dist/types/tools/impl/skill.d.ts +10 -5
  56. package/dist/types/tools/impl/skill.d.ts.map +1 -1
  57. package/dist/types/tools/impl/task-output.d.ts +1 -0
  58. package/dist/types/tools/impl/task-output.d.ts.map +1 -1
  59. package/dist/types/tools/impl/task.d.ts.map +1 -1
  60. package/dist/types/tools/manager.d.ts.map +1 -1
  61. package/dist/types/tools/toolset.d.ts.map +1 -1
  62. package/dist/types/types/approval-classification-protocol.d.ts +13 -0
  63. package/dist/types/types/approval-classification-protocol.d.ts.map +1 -0
  64. package/dist/types/types/protocol_v2.d.ts +4 -8
  65. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  66. package/image-resize-worker.js +42 -20
  67. package/letta.js +94957 -151018
  68. package/package.json +4 -3
  69. package/scripts/codex-watch/agent-watch.ts +63 -8
  70. package/scripts/codex-watch/release-analysis.test.ts +57 -0
  71. package/scripts/codex-watch/release-analysis.ts +31 -1
  72. package/scripts/codex-watch/tracker.test.ts +243 -6
  73. package/scripts/codex-watch/tracker.ts +221 -20
  74. package/scripts/isolated-unit-tests.json +15 -0
  75. package/scripts/pi-ai-watch/agent-watch.ts +253 -0
  76. package/scripts/pi-ai-watch/github.ts +145 -0
  77. package/scripts/pi-ai-watch/release-analysis.test.ts +137 -0
  78. package/scripts/pi-ai-watch/release-analysis.ts +371 -0
  79. package/scripts/pi-ai-watch/tracker.test.ts +138 -0
  80. package/scripts/pi-ai-watch/tracker.ts +303 -0
  81. package/scripts/pi-ai-watch/update-tracker.ts +215 -0
  82. package/scripts/source-file-size-baseline.json +6 -6
@@ -0,0 +1,145 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export interface PullRequestStatus {
7
+ state: "OPEN" | "CLOSED" | "MERGED";
8
+ url: string;
9
+ }
10
+
11
+ export function runGh(args: string[], input?: string): string {
12
+ const result = spawnSync("gh", args, {
13
+ encoding: "utf8",
14
+ input,
15
+ maxBuffer: 50 * 1024 * 1024,
16
+ stdio: input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
17
+ });
18
+ if (result.status !== 0) {
19
+ throw new Error(`gh ${args.join(" ")} failed:\n${result.stderr}`);
20
+ }
21
+ return result.stdout;
22
+ }
23
+
24
+ export function ghJson<T>(args: string[], input?: string): T {
25
+ return JSON.parse(runGh(args, input)) as T;
26
+ }
27
+
28
+ export function ensureLabels(repo: string, labels: string[]): void {
29
+ for (const label of labels) {
30
+ const result = spawnSync("gh", ["label", "create", label, "--repo", repo], {
31
+ encoding: "utf8",
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ });
34
+ if (result.status !== 0 && !result.stderr.includes("already exists")) {
35
+ throw new Error(`gh label create ${label} failed:\n${result.stderr}`);
36
+ }
37
+ }
38
+ }
39
+
40
+ export function createIssueWithBody(
41
+ repo: string,
42
+ title: string,
43
+ body: string,
44
+ labels: string[],
45
+ ): string {
46
+ const bodyFile = writeTempMarkdown(body, "pi-ai-watch-issue");
47
+ try {
48
+ const args = [
49
+ "issue",
50
+ "create",
51
+ "--repo",
52
+ repo,
53
+ "--title",
54
+ title,
55
+ "--body-file",
56
+ bodyFile,
57
+ ];
58
+ for (const label of labels) args.push("--label", label);
59
+ return runGh(args).trim();
60
+ } finally {
61
+ rmSync(bodyFile, { force: true });
62
+ }
63
+ }
64
+
65
+ export function editIssueBody(
66
+ repo: string,
67
+ issueNumber: number,
68
+ body: string,
69
+ ): void {
70
+ const bodyFile = writeTempMarkdown(body, "pi-ai-watch-tracker");
71
+ try {
72
+ runGh([
73
+ "issue",
74
+ "edit",
75
+ String(issueNumber),
76
+ "--repo",
77
+ repo,
78
+ "--body-file",
79
+ bodyFile,
80
+ ]);
81
+ } finally {
82
+ rmSync(bodyFile, { force: true });
83
+ }
84
+ }
85
+
86
+ export function getIssueBody(repo: string, issueNumber: number): string {
87
+ const issue = ghJson<{ body: string | null }>([
88
+ "issue",
89
+ "view",
90
+ String(issueNumber),
91
+ "--repo",
92
+ repo,
93
+ "--json",
94
+ "body",
95
+ ]);
96
+ return issue.body ?? "";
97
+ }
98
+
99
+ export function findIssueByExactTitle(
100
+ repo: string,
101
+ title: string,
102
+ ): { number: number; title: string } | null {
103
+ const issues = ghJson<Array<{ number: number; title: string }>>([
104
+ "issue",
105
+ "list",
106
+ "--repo",
107
+ repo,
108
+ "--state",
109
+ "all",
110
+ "--search",
111
+ `${title} in:title`,
112
+ "--limit",
113
+ "20",
114
+ "--json",
115
+ "number,title",
116
+ ]);
117
+ return issues.find((issue) => issue.title === title) ?? null;
118
+ }
119
+
120
+ export function getPullRequestStatus(prUrl: string): PullRequestStatus {
121
+ const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)$/.exec(
122
+ prUrl,
123
+ );
124
+ if (!match) throw new Error(`Invalid GitHub pull request URL ${prUrl}`);
125
+ const [, owner, repo, number] = match;
126
+ const pull = ghJson<{ state: "OPEN" | "CLOSED"; mergedAt: string | null }>([
127
+ "pr",
128
+ "view",
129
+ number as string,
130
+ "--repo",
131
+ `${owner}/${repo}`,
132
+ "--json",
133
+ "state,mergedAt",
134
+ ]);
135
+ return {
136
+ state: pull.mergedAt ? "MERGED" : pull.state,
137
+ url: prUrl,
138
+ };
139
+ }
140
+
141
+ function writeTempMarkdown(body: string, prefix: string): string {
142
+ const path = join(tmpdir(), `${prefix}-${Date.now()}.md`);
143
+ writeFileSync(path, body);
144
+ return path;
145
+ }
@@ -0,0 +1,137 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ areAdjacentStableReleases,
7
+ compareStableVersions,
8
+ extractChangelogSection,
9
+ findNextStableRelease,
10
+ type PackageRelease,
11
+ parseRegistryMetadata,
12
+ readInstalledVersion,
13
+ } from "./release-analysis.ts";
14
+
15
+ const temporaryDirectories: string[] = [];
16
+
17
+ afterEach(() => {
18
+ for (const directory of temporaryDirectories.splice(0)) {
19
+ rmSync(directory, { recursive: true, force: true });
20
+ }
21
+ });
22
+
23
+ describe("pi-ai npm releases", () => {
24
+ test("filters prereleases and orders stable versions semantically", () => {
25
+ const releases = parseRegistryMetadata({
26
+ versions: {
27
+ "0.10.0": {
28
+ version: "0.10.0",
29
+ gitHead: "new",
30
+ dist: { integrity: "sha-new", tarball: "https://example/new.tgz" },
31
+ },
32
+ "0.9.1-beta.0": { version: "0.9.1-beta.0" },
33
+ "0.9.2": { version: "0.9.2" },
34
+ },
35
+ time: {
36
+ "0.10.0": "2026-01-03T00:00:00Z",
37
+ "0.9.1-beta.0": "2026-01-01T00:00:00Z",
38
+ "0.9.2": "2026-01-02T00:00:00Z",
39
+ },
40
+ });
41
+
42
+ expect(releases.map((release) => release.version)).toEqual([
43
+ "0.9.2",
44
+ "0.10.0",
45
+ ]);
46
+ expect(releases[1]).toMatchObject({
47
+ integrity: "sha-new",
48
+ tarball_url: "https://example/new.tgz",
49
+ git_head: "new",
50
+ });
51
+ });
52
+
53
+ test("rejects stable releases without publication times", () => {
54
+ expect(() =>
55
+ parseRegistryMetadata({
56
+ versions: { "0.82.1": { version: "0.82.1" } },
57
+ time: {},
58
+ }),
59
+ ).toThrow("missing publication time for 0.82.1");
60
+ });
61
+
62
+ test("selects exactly the next stable version after the cursor", () => {
63
+ const releases = releaseList("0.82.1", "0.83.0", "0.84.0");
64
+ expect(findNextStableRelease(releases, "0.82.1")?.version).toBe("0.83.0");
65
+ expect(findNextStableRelease(releases, "0.84.0")).toBeNull();
66
+ expect(() => findNextStableRelease(releases, "0.80.0")).toThrow(
67
+ "Could not find pi-ai cursor release 0.80.0",
68
+ );
69
+ });
70
+
71
+ test("recognizes only adjacent release pairs", () => {
72
+ const releases = releaseList("0.82.1", "0.83.0", "0.84.0");
73
+ expect(areAdjacentStableReleases(releases, "0.82.1", "0.83.0")).toBe(true);
74
+ expect(areAdjacentStableReleases(releases, "0.82.1", "0.84.0")).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe("pi-ai release evidence", () => {
79
+ test("extracts only the requested changelog section", () => {
80
+ const changelog = `# Changelog
81
+
82
+ ## [0.84.0] - 2026-08-06
83
+
84
+ ### Added
85
+
86
+ - Current feature.
87
+
88
+ ## [0.83.0] - 2026-07-29
89
+
90
+ - Previous feature.
91
+ `;
92
+ expect(
93
+ extractChangelogSection(changelog, "0.84.0"),
94
+ ).toBe(`## [0.84.0] - 2026-08-06
95
+
96
+ ### Added
97
+
98
+ - Current feature.`);
99
+ expect(() => extractChangelogSection(changelog, "0.82.1")).toThrow(
100
+ "Could not find pi-ai changelog section 0.82.1",
101
+ );
102
+ });
103
+
104
+ test("reads the exact resolved version from bun.lock", () => {
105
+ const directory = mkdtempSync(join(tmpdir(), "pi-ai-watch-test-"));
106
+ temporaryDirectories.push(directory);
107
+ const lockfile = join(directory, "bun.lock");
108
+ writeFileSync(
109
+ lockfile,
110
+ `{
111
+ "workspaces": {
112
+ "": { "dependencies": { "@earendil-works/pi-ai": "^0.82.0" } }
113
+ },
114
+ "packages": {
115
+ "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.82.1", ""]
116
+ }
117
+ }`,
118
+ );
119
+ expect(readInstalledVersion(lockfile)).toBe("0.82.1");
120
+ });
121
+
122
+ test("compares multi-digit semantic version components", () => {
123
+ expect(compareStableVersions("0.9.9", "0.10.0")).toBeLessThan(0);
124
+ expect(compareStableVersions("1.0.0", "0.99.99")).toBeGreaterThan(0);
125
+ expect(compareStableVersions("0.84.2", "0.84.2")).toBe(0);
126
+ });
127
+ });
128
+
129
+ function releaseList(...versions: string[]): PackageRelease[] {
130
+ return versions.map((version, index) => ({
131
+ version,
132
+ published_at: `2026-08-${String(index + 1).padStart(2, "0")}T00:00:00Z`,
133
+ integrity: null,
134
+ tarball_url: null,
135
+ git_head: null,
136
+ }));
137
+ }
@@ -0,0 +1,371 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export const PI_AI_PACKAGE = "@earendil-works/pi-ai";
7
+ export const PI_AI_REPO = "earendil-works/pi";
8
+ export const DEFAULT_TARGET_REPO =
9
+ process.env.GITHUB_REPOSITORY || "letta-ai/letta-code";
10
+
11
+ interface RegistryVersion {
12
+ version: string;
13
+ gitHead?: string;
14
+ dist?: {
15
+ integrity?: string;
16
+ tarball?: string;
17
+ };
18
+ }
19
+
20
+ interface RegistryMetadata {
21
+ versions?: Record<string, RegistryVersion>;
22
+ time?: Record<string, string>;
23
+ }
24
+
25
+ export interface PackageRelease {
26
+ version: string;
27
+ published_at: string;
28
+ integrity: string | null;
29
+ tarball_url: string | null;
30
+ git_head: string | null;
31
+ }
32
+
33
+ export interface AnalyzePiAiReleaseOptions {
34
+ previousVersion: string | null;
35
+ currentVersion: string | null;
36
+ installedVersion?: string;
37
+ stableReleases?: PackageRelease[];
38
+ }
39
+
40
+ export interface PiAiWatchAnalysis {
41
+ package: typeof PI_AI_PACKAGE;
42
+ installed_version: string;
43
+ previous_version: string;
44
+ current_version: string;
45
+ is_adjacent_release: boolean;
46
+ published_at: string;
47
+ integrity: string | null;
48
+ tarball_url: string | null;
49
+ git_head: string | null;
50
+ release_url: string;
51
+ compare_url: string;
52
+ changelog_md: string;
53
+ changed_files: string[];
54
+ diff_stat: string;
55
+ package_json_diff: string | null;
56
+ workflow_run_url: string;
57
+ }
58
+
59
+ export async function analyzePiAiRelease(
60
+ options: AnalyzePiAiReleaseOptions,
61
+ ): Promise<PiAiWatchAnalysis> {
62
+ const releases = options.stableReleases ?? (await listStableReleases());
63
+ if (releases.length === 0) throw new Error("No stable pi-ai releases found");
64
+
65
+ const current = options.currentVersion
66
+ ? releases.find((release) => release.version === options.currentVersion)
67
+ : releases.at(-1);
68
+ if (!current) {
69
+ throw new Error(
70
+ `Could not find current pi-ai release ${options.currentVersion}`,
71
+ );
72
+ }
73
+
74
+ const previous = options.previousVersion
75
+ ? releases.find((release) => release.version === options.previousVersion)
76
+ : findPreviousStableRelease(releases, current.version);
77
+ if (!previous) {
78
+ throw new Error(
79
+ `Could not find previous pi-ai release before ${current.version}`,
80
+ );
81
+ }
82
+
83
+ const installedVersion = options.installedVersion ?? readInstalledVersion();
84
+ const previousTag = `v${previous.version}`;
85
+ const currentTag = `v${current.version}`;
86
+ const temp = mkdtempSync(join(tmpdir(), "pi-ai-watch-"));
87
+ try {
88
+ const repoDir = clonePi(temp);
89
+ fetchTag(repoDir, previousTag);
90
+ fetchTag(repoDir, currentTag);
91
+ verifyTagMatchesPackage(repoDir, previousTag, previous);
92
+ verifyTagMatchesPackage(repoDir, currentTag, current);
93
+ const changelog = showFile(repoDir, currentTag, "packages/ai/CHANGELOG.md");
94
+ if (changelog === null) {
95
+ throw new Error(`packages/ai/CHANGELOG.md is missing at ${currentTag}`);
96
+ }
97
+
98
+ return {
99
+ package: PI_AI_PACKAGE,
100
+ installed_version: installedVersion,
101
+ previous_version: previous.version,
102
+ current_version: current.version,
103
+ is_adjacent_release: areAdjacentStableReleases(
104
+ releases,
105
+ previous.version,
106
+ current.version,
107
+ ),
108
+ published_at: current.published_at,
109
+ integrity: current.integrity,
110
+ tarball_url: current.tarball_url,
111
+ git_head: current.git_head,
112
+ release_url: `https://github.com/${PI_AI_REPO}/releases/tag/${currentTag}`,
113
+ compare_url: `https://github.com/${PI_AI_REPO}/compare/${previousTag}...${currentTag}`,
114
+ changelog_md: extractChangelogSection(changelog, current.version),
115
+ changed_files: changedFiles(repoDir, previousTag, currentTag),
116
+ diff_stat: diffStat(repoDir, previousTag, currentTag),
117
+ package_json_diff: diffPreview(
118
+ repoDir,
119
+ previousTag,
120
+ currentTag,
121
+ "packages/ai/package.json",
122
+ ),
123
+ workflow_run_url: workflowRunUrl(),
124
+ };
125
+ } finally {
126
+ rmSync(temp, { recursive: true, force: true });
127
+ }
128
+ }
129
+
130
+ export async function listStableReleases(): Promise<PackageRelease[]> {
131
+ const response = await fetch(
132
+ "https://registry.npmjs.org/@earendil-works%2Fpi-ai",
133
+ );
134
+ if (!response.ok) {
135
+ throw new Error(
136
+ `npm registry request failed (${response.status}): ${await response.text()}`,
137
+ );
138
+ }
139
+ return parseRegistryMetadata((await response.json()) as RegistryMetadata);
140
+ }
141
+
142
+ export function parseRegistryMetadata(
143
+ metadata: RegistryMetadata,
144
+ ): PackageRelease[] {
145
+ if (!metadata.versions || !metadata.time) {
146
+ throw new Error(
147
+ "pi-ai npm metadata is missing versions or publication times",
148
+ );
149
+ }
150
+
151
+ return Object.entries(metadata.versions)
152
+ .filter(([version]) => isStableVersion(version))
153
+ .map(([version, details]) => {
154
+ const publishedAt = metadata.time?.[version];
155
+ if (!publishedAt) {
156
+ throw new Error(
157
+ `pi-ai npm metadata is missing publication time for ${version}`,
158
+ );
159
+ }
160
+ return {
161
+ version,
162
+ published_at: publishedAt,
163
+ integrity: details.dist?.integrity ?? null,
164
+ tarball_url: details.dist?.tarball ?? null,
165
+ git_head: details.gitHead ?? null,
166
+ };
167
+ })
168
+ .sort((left, right) => compareStableVersions(left.version, right.version));
169
+ }
170
+
171
+ export function readInstalledVersion(lockfilePath = "bun.lock"): string {
172
+ const lockfile = readFileSync(lockfilePath, "utf8");
173
+ const escapedPackage = escapeRegExp(PI_AI_PACKAGE);
174
+ const match = new RegExp(
175
+ `"${escapedPackage}": \\["${escapedPackage}@(\\d+\\.\\d+\\.\\d+)"`,
176
+ ).exec(lockfile);
177
+ if (!match?.[1]) {
178
+ throw new Error(
179
+ `Could not parse resolved ${PI_AI_PACKAGE} version from ${lockfilePath}`,
180
+ );
181
+ }
182
+ return match[1];
183
+ }
184
+
185
+ export function findNextStableRelease(
186
+ releases: PackageRelease[],
187
+ cursorVersion: string,
188
+ ): PackageRelease | null {
189
+ const index = releases.findIndex(
190
+ (release) => release.version === cursorVersion,
191
+ );
192
+ if (index < 0) {
193
+ throw new Error(`Could not find pi-ai cursor release ${cursorVersion}`);
194
+ }
195
+ return releases[index + 1] ?? null;
196
+ }
197
+
198
+ export function areAdjacentStableReleases(
199
+ releases: PackageRelease[],
200
+ previousVersion: string,
201
+ currentVersion: string,
202
+ ): boolean {
203
+ return (
204
+ findPreviousStableRelease(releases, currentVersion)?.version ===
205
+ previousVersion
206
+ );
207
+ }
208
+
209
+ export function extractChangelogSection(
210
+ changelog: string,
211
+ version: string,
212
+ ): string {
213
+ const heading = new RegExp(`^## \\[${escapeRegExp(version)}\\].*$`, "m");
214
+ const match = heading.exec(changelog);
215
+ if (!match)
216
+ throw new Error(`Could not find pi-ai changelog section ${version}`);
217
+ const start = match.index;
218
+ const remaining = changelog.slice(start + match[0].length);
219
+ const next = /^## \[/m.exec(remaining);
220
+ const end = next ? start + match[0].length + next.index : changelog.length;
221
+ return changelog.slice(start, end).trim();
222
+ }
223
+
224
+ function findPreviousStableRelease(
225
+ releases: PackageRelease[],
226
+ currentVersion: string,
227
+ ): PackageRelease | null {
228
+ const index = releases.findIndex(
229
+ (release) => release.version === currentVersion,
230
+ );
231
+ if (index <= 0) return null;
232
+ return releases[index - 1] ?? null;
233
+ }
234
+
235
+ export function compareStableVersions(left: string, right: string): number {
236
+ const leftParts = parseVersion(left);
237
+ const rightParts = parseVersion(right);
238
+ for (let index = 0; index < leftParts.length; index += 1) {
239
+ const difference = leftParts[index]! - rightParts[index]!;
240
+ if (difference !== 0) return difference;
241
+ }
242
+ return 0;
243
+ }
244
+
245
+ function parseVersion(version: string): [number, number, number] {
246
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
247
+ if (!match) throw new Error(`Invalid stable pi-ai version ${version}`);
248
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
249
+ }
250
+
251
+ function isStableVersion(version: string): boolean {
252
+ return /^\d+\.\d+\.\d+$/.test(version);
253
+ }
254
+
255
+ function clonePi(temp: string): string {
256
+ const directory = join(temp, "pi");
257
+ git([
258
+ "clone",
259
+ "--filter=blob:none",
260
+ "--no-checkout",
261
+ `https://github.com/${PI_AI_REPO}.git`,
262
+ directory,
263
+ ]);
264
+ return directory;
265
+ }
266
+
267
+ function fetchTag(repoDir: string, tag: string): void {
268
+ git(
269
+ [
270
+ "fetch",
271
+ "--filter=blob:none",
272
+ "origin",
273
+ `refs/tags/${tag}:refs/tags/${tag}`,
274
+ ],
275
+ repoDir,
276
+ );
277
+ }
278
+
279
+ function verifyTagMatchesPackage(
280
+ repoDir: string,
281
+ tag: string,
282
+ release: PackageRelease,
283
+ ): void {
284
+ if (!release.git_head) return;
285
+ const tagCommit = git(["rev-list", "-n", "1", tag], repoDir).trim();
286
+ if (tagCommit !== release.git_head) {
287
+ throw new Error(
288
+ `pi-ai npm ${release.version} gitHead ${release.git_head} does not match ${tag} commit ${tagCommit}`,
289
+ );
290
+ }
291
+ }
292
+
293
+ function showFile(repoDir: string, tag: string, path: string): string | null {
294
+ const result = spawnSync("git", ["show", `${tag}:${path}`], {
295
+ cwd: repoDir,
296
+ encoding: "utf8",
297
+ stdio: ["ignore", "pipe", "pipe"],
298
+ });
299
+ return result.status === 0 ? result.stdout : null;
300
+ }
301
+
302
+ function changedFiles(
303
+ repoDir: string,
304
+ previousTag: string,
305
+ currentTag: string,
306
+ ): string[] {
307
+ return git(
308
+ [
309
+ "diff",
310
+ "--name-only",
311
+ `${previousTag}..${currentTag}`,
312
+ "--",
313
+ "packages/ai",
314
+ ],
315
+ repoDir,
316
+ )
317
+ .split("\n")
318
+ .filter(Boolean);
319
+ }
320
+
321
+ function diffStat(
322
+ repoDir: string,
323
+ previousTag: string,
324
+ currentTag: string,
325
+ ): string {
326
+ return git(
327
+ ["diff", "--stat", `${previousTag}..${currentTag}`, "--", "packages/ai"],
328
+ repoDir,
329
+ ).trim();
330
+ }
331
+
332
+ function diffPreview(
333
+ repoDir: string,
334
+ previousTag: string,
335
+ currentTag: string,
336
+ path: string,
337
+ ): string | null {
338
+ const output = git(
339
+ ["diff", "--unified=3", `${previousTag}..${currentTag}`, "--", path],
340
+ repoDir,
341
+ );
342
+ return output.trim() ? output.split("\n").slice(0, 160).join("\n") : null;
343
+ }
344
+
345
+ function git(args: string[], cwd?: string): string {
346
+ const result = spawnSync("git", args, {
347
+ cwd,
348
+ encoding: "utf8",
349
+ maxBuffer: 50 * 1024 * 1024,
350
+ stdio: ["ignore", "pipe", "pipe"],
351
+ });
352
+ if (result.status !== 0) {
353
+ throw new Error(`git ${args.join(" ")} failed:\n${result.stderr}`);
354
+ }
355
+ return result.stdout;
356
+ }
357
+
358
+ function workflowRunUrl(): string {
359
+ if (
360
+ process.env.GITHUB_SERVER_URL &&
361
+ process.env.GITHUB_REPOSITORY &&
362
+ process.env.GITHUB_RUN_ID
363
+ ) {
364
+ return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
365
+ }
366
+ return "local dry-run";
367
+ }
368
+
369
+ function escapeRegExp(value: string): string {
370
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
371
+ }