@letta-ai/letta-code 0.30.27 → 0.30.29
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.
- package/dist/agent-presets.js +17 -17
- package/dist/agent-presets.js.map +1 -1
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/client-skills.d.ts +3 -0
- package/dist/types/agent/client-skills.d.ts.map +1 -1
- package/dist/types/agent/memory-git.d.ts +2 -0
- package/dist/types/agent/memory-git.d.ts.map +1 -1
- package/dist/types/agent/shared-memory-skills.d.ts +18 -0
- package/dist/types/agent/shared-memory-skills.d.ts.map +1 -0
- package/dist/types/backend/dev/pi-model-factory.d.ts +6 -0
- package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
- package/dist/types/backend/dev/pi-provider-registry.d.ts.map +1 -1
- package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
- package/dist/types/tools/impl/skill.d.ts +10 -5
- package/dist/types/tools/impl/skill.d.ts.map +1 -1
- package/dist/types/tools/secret-substitution.d.ts.map +1 -1
- package/dist/types/types/loop-status-protocol.d.ts +17 -0
- package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2 -19
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
- package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
- package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
- package/dist/types/websocket/listener/types.d.ts +4 -0
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/image-resize-worker.js +42 -20
- package/letta.js +85237 -138922
- package/package.json +4 -3
- package/scripts/claude-watch/agent-watch.ts +622 -0
- package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
- package/scripts/claude-watch/docs-snapshot.ts +672 -0
- package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
- package/scripts/claude-watch/github.ts +137 -0
- package/scripts/claude-watch/release-analysis.test.ts +235 -0
- package/scripts/claude-watch/release-analysis.ts +297 -0
- package/scripts/claude-watch/release-source.test.ts +179 -0
- package/scripts/claude-watch/release-source.ts +369 -0
- package/scripts/claude-watch/runtime-observations.ts +98 -0
- package/scripts/claude-watch/runtime-probe.test.ts +576 -0
- package/scripts/claude-watch/runtime-probe.ts +911 -0
- package/scripts/claude-watch/runtime-sandbox.ts +170 -0
- package/scripts/claude-watch/state-branch.test.ts +211 -0
- package/scripts/claude-watch/state-branch.ts +316 -0
- package/scripts/claude-watch/tracker.test.ts +148 -0
- package/scripts/claude-watch/tracker.ts +325 -0
- package/scripts/claude-watch/types.ts +186 -0
- package/scripts/claude-watch/update-tracker.ts +201 -0
- package/scripts/codex-watch/agent-watch.ts +65 -10
- package/scripts/codex-watch/release-analysis.test.ts +57 -0
- package/scripts/codex-watch/release-analysis.ts +45 -3
- package/scripts/codex-watch/tracker.test.ts +243 -6
- package/scripts/codex-watch/tracker.ts +222 -23
- package/scripts/pi-ai-watch/agent-watch.ts +253 -0
- package/scripts/pi-ai-watch/github.ts +145 -0
- package/scripts/pi-ai-watch/release-analysis.test.ts +137 -0
- package/scripts/pi-ai-watch/release-analysis.ts +371 -0
- package/scripts/pi-ai-watch/tracker.test.ts +138 -0
- package/scripts/pi-ai-watch/tracker.ts +303 -0
- package/scripts/pi-ai-watch/update-tracker.ts +215 -0
- package/scripts/run-unit-tests.cjs +2 -0
- package/scripts/source-file-size-baseline.json +3 -3
- package/skills/creating-mods/references/commands.md +1 -1
- package/skills/creating-mods/references/ui.md +1 -1
- package/skills/customizing-commands/SKILL.md +1 -1
- package/skills/initializing-memory/SKILL.md +7 -7
- package/skills/self-configuration/SKILL.md +4 -4
- package/scripts/codex-watch/check-release.ts +0 -128
- package/scripts/codex-watch/render-issue.ts +0 -273
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { appendFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
createIssueWithBody,
|
|
6
|
+
editIssueBody,
|
|
7
|
+
ensureLabels,
|
|
8
|
+
findIssueByExactTitle,
|
|
9
|
+
getIssueBody,
|
|
10
|
+
getPullRequestStatus,
|
|
11
|
+
} from "./github.ts";
|
|
12
|
+
import {
|
|
13
|
+
analyzePiAiRelease,
|
|
14
|
+
compareStableVersions,
|
|
15
|
+
DEFAULT_TARGET_REPO,
|
|
16
|
+
findNextStableRelease,
|
|
17
|
+
listStableReleases,
|
|
18
|
+
readInstalledVersion,
|
|
19
|
+
} from "./release-analysis.ts";
|
|
20
|
+
import {
|
|
21
|
+
advanceMergedPr,
|
|
22
|
+
getPendingPrForCursor,
|
|
23
|
+
hasCompletedRange,
|
|
24
|
+
initialTrackerState,
|
|
25
|
+
parseTrackerState,
|
|
26
|
+
renderTrackerBody,
|
|
27
|
+
} from "./tracker.ts";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TRACKER_TITLE = "pi-ai dependency upgrade tracker";
|
|
30
|
+
const DEFAULT_ANALYSIS_FILE = "pi-ai-watch-analysis.json";
|
|
31
|
+
|
|
32
|
+
interface Args {
|
|
33
|
+
dryRun: boolean;
|
|
34
|
+
previousVersion: string | null;
|
|
35
|
+
currentVersion: string | null;
|
|
36
|
+
repo: string;
|
|
37
|
+
trackerTitle: string;
|
|
38
|
+
analysisFile: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface TrackerIssue {
|
|
42
|
+
number: number;
|
|
43
|
+
url: string;
|
|
44
|
+
body: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv: string[]): Args {
|
|
48
|
+
const args: Args = {
|
|
49
|
+
dryRun: false,
|
|
50
|
+
previousVersion: null,
|
|
51
|
+
currentVersion: null,
|
|
52
|
+
repo: DEFAULT_TARGET_REPO,
|
|
53
|
+
trackerTitle: DEFAULT_TRACKER_TITLE,
|
|
54
|
+
analysisFile: DEFAULT_ANALYSIS_FILE,
|
|
55
|
+
};
|
|
56
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
57
|
+
const argument = argv[index];
|
|
58
|
+
if (argument === "--dry-run") args.dryRun = true;
|
|
59
|
+
else if (argument === "--previous-version") {
|
|
60
|
+
args.previousVersion = argv[++index] ?? null;
|
|
61
|
+
} else if (argument === "--current-version") {
|
|
62
|
+
args.currentVersion = argv[++index] ?? null;
|
|
63
|
+
} else if (argument === "--repo") {
|
|
64
|
+
args.repo = argv[++index] ?? args.repo;
|
|
65
|
+
} else if (argument === "--tracker-title") {
|
|
66
|
+
args.trackerTitle = argv[++index] ?? args.trackerTitle;
|
|
67
|
+
} else if (argument === "--analysis-file") {
|
|
68
|
+
args.analysisFile = argv[++index] ?? args.analysisFile;
|
|
69
|
+
} else if (argument === "--help" || argument === "-h") {
|
|
70
|
+
console.log(
|
|
71
|
+
"Usage: bun scripts/pi-ai-watch/agent-watch.ts [--dry-run] [--previous-version VERSION] [--current-version VERSION] [--repo OWNER/REPO] [--tracker-title TITLE] [--analysis-file FILE]",
|
|
72
|
+
);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return args;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main(): Promise<void> {
|
|
82
|
+
const args = parseArgs(process.argv.slice(2));
|
|
83
|
+
const installedVersion = readInstalledVersion();
|
|
84
|
+
const tracker = ensureTrackerIssue(args, installedVersion);
|
|
85
|
+
let state = parseTrackerState(tracker.body);
|
|
86
|
+
const releases = await listStableReleases();
|
|
87
|
+
let previousVersion = args.previousVersion;
|
|
88
|
+
let currentVersion = args.currentVersion;
|
|
89
|
+
const isScheduledSelection = !previousVersion && !currentVersion;
|
|
90
|
+
|
|
91
|
+
if (isScheduledSelection) {
|
|
92
|
+
const pending = getPendingPrForCursor(state);
|
|
93
|
+
if (pending?.pr_url) {
|
|
94
|
+
const status = getPullRequestStatus(pending.pr_url);
|
|
95
|
+
if (status.state === "OPEN") {
|
|
96
|
+
console.log(`Waiting for pending pi-ai upgrade PR: ${status.url}`);
|
|
97
|
+
writeCommonOutputs(tracker, {
|
|
98
|
+
installedVersion,
|
|
99
|
+
previousVersion: pending.previous_version,
|
|
100
|
+
currentVersion: pending.version,
|
|
101
|
+
});
|
|
102
|
+
writeOutput("pending_pr_url", status.url);
|
|
103
|
+
writeOutput("should_run_agent", "false");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (status.state === "MERGED") {
|
|
107
|
+
if (compareStableVersions(installedVersion, pending.version) < 0) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Merged pi-ai PR ${status.url} targets ${pending.version}, but main still declares ${installedVersion}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
state = advanceMergedPr(state, pending.version);
|
|
113
|
+
if (!args.dryRun) {
|
|
114
|
+
editIssueBody(args.repo, tracker.number, renderTrackerBody(state));
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.log(
|
|
118
|
+
`Retrying ${pending.version}; prior PR was closed unmerged.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const next = findNextStableRelease(releases, state.audit_cursor_version);
|
|
124
|
+
if (!next) {
|
|
125
|
+
console.log(
|
|
126
|
+
`No stable pi-ai release after ${state.audit_cursor_version}.`,
|
|
127
|
+
);
|
|
128
|
+
writeCommonOutputs(tracker, {
|
|
129
|
+
installedVersion,
|
|
130
|
+
previousVersion: state.audit_cursor_version,
|
|
131
|
+
currentVersion: state.audit_cursor_version,
|
|
132
|
+
});
|
|
133
|
+
writeOutput("should_run_agent", "false");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
previousVersion = state.audit_cursor_version;
|
|
137
|
+
currentVersion = next.version;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const analysis = await analyzePiAiRelease({
|
|
141
|
+
previousVersion,
|
|
142
|
+
currentVersion,
|
|
143
|
+
installedVersion,
|
|
144
|
+
stableReleases: releases,
|
|
145
|
+
});
|
|
146
|
+
writeFileSync(args.analysisFile, `${JSON.stringify(analysis, null, 2)}\n`);
|
|
147
|
+
writeCommonOutputs(tracker, {
|
|
148
|
+
installedVersion,
|
|
149
|
+
previousVersion: analysis.previous_version,
|
|
150
|
+
currentVersion: analysis.current_version,
|
|
151
|
+
});
|
|
152
|
+
writeOutput("analysis_file", args.analysisFile);
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
!args.dryRun &&
|
|
156
|
+
hasCompletedRange(
|
|
157
|
+
state,
|
|
158
|
+
analysis.previous_version,
|
|
159
|
+
analysis.current_version,
|
|
160
|
+
)
|
|
161
|
+
) {
|
|
162
|
+
console.log(
|
|
163
|
+
`Already completed pi-ai review ${analysis.previous_version}...${analysis.current_version}.`,
|
|
164
|
+
);
|
|
165
|
+
writeOutput("should_run_agent", "false");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (args.dryRun) {
|
|
170
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
171
|
+
writeOutput("should_run_agent", "false");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(
|
|
176
|
+
`pi-ai ${analysis.previous_version}...${analysis.current_version} needs Amelia review.`,
|
|
177
|
+
);
|
|
178
|
+
writeOutput("should_run_agent", "true");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ensureTrackerIssue(
|
|
182
|
+
args: Args,
|
|
183
|
+
installedVersion: string,
|
|
184
|
+
): TrackerIssue {
|
|
185
|
+
const existing = args.dryRun
|
|
186
|
+
? null
|
|
187
|
+
: findIssueByExactTitle(args.repo, args.trackerTitle);
|
|
188
|
+
if (existing) {
|
|
189
|
+
return {
|
|
190
|
+
number: existing.number,
|
|
191
|
+
url: `https://github.com/${args.repo}/issues/${existing.number}`,
|
|
192
|
+
body: getIssueBody(args.repo, existing.number),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const body = renderTrackerBody(initialTrackerState(installedVersion));
|
|
197
|
+
if (args.dryRun) {
|
|
198
|
+
return {
|
|
199
|
+
number: 0,
|
|
200
|
+
url: `https://github.com/${args.repo}/issues/0`,
|
|
201
|
+
body,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const labels = ["pi-ai-watch", "automation"];
|
|
206
|
+
ensureLabels(args.repo, labels);
|
|
207
|
+
const issueUrl = createIssueWithBody(
|
|
208
|
+
args.repo,
|
|
209
|
+
args.trackerTitle,
|
|
210
|
+
body,
|
|
211
|
+
labels,
|
|
212
|
+
);
|
|
213
|
+
return {
|
|
214
|
+
number: issueNumberFromUrl(issueUrl),
|
|
215
|
+
url: issueUrl,
|
|
216
|
+
body,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function issueNumberFromUrl(issueUrl: string): number {
|
|
221
|
+
const issueNumber = Number(issueUrl.trim().split("/").at(-1));
|
|
222
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
|
|
223
|
+
throw new Error(`Could not parse issue number from ${issueUrl}`);
|
|
224
|
+
}
|
|
225
|
+
return issueNumber;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function writeCommonOutputs(
|
|
229
|
+
tracker: TrackerIssue,
|
|
230
|
+
versions: {
|
|
231
|
+
installedVersion: string;
|
|
232
|
+
previousVersion: string;
|
|
233
|
+
currentVersion: string;
|
|
234
|
+
},
|
|
235
|
+
): void {
|
|
236
|
+
writeOutput("tracker_issue", String(tracker.number));
|
|
237
|
+
writeOutput("tracker_issue_url", tracker.url);
|
|
238
|
+
writeOutput("installed_version", versions.installedVersion);
|
|
239
|
+
writeOutput("previous_version", versions.previousVersion);
|
|
240
|
+
writeOutput("current_version", versions.currentVersion);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function writeOutput(name: string, value: string): void {
|
|
244
|
+
const outputPath = process.env.GITHUB_OUTPUT;
|
|
245
|
+
if (!outputPath) return;
|
|
246
|
+
appendFileSync(outputPath, `${name}=${value}\n`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main().catch((error) => {
|
|
250
|
+
console.error(error);
|
|
251
|
+
writeOutput("should_run_agent", "false");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|
|
@@ -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
|
+
}
|