@letta-ai/letta-code 0.34.24 → 0.34.26
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/cursor-worker.js +167 -0
- package/dist/agent-presets.js +1 -64
- package/dist/agent-presets.js.map +2 -2
- package/dist/channels-slack.js +30 -24
- package/dist/channels-slack.js.map +3 -3
- package/dist/mcp-client.js +233 -75
- package/dist/mcp-client.js.map +9 -9
- package/dist/mcp-oauth.js +233 -75
- package/dist/mcp-oauth.js.map +9 -9
- package/dist/types/agent/subagents/index.d.ts +4 -0
- package/dist/types/agent/subagents/index.d.ts.map +1 -1
- package/dist/types/agent/subagents/manager.d.ts.map +1 -1
- package/dist/types/agent/subagents/memory-worker.d.ts +2 -8
- package/dist/types/agent/subagents/memory-worker.d.ts.map +1 -1
- package/dist/types/backend/api/providers.d.ts +8 -2
- package/dist/types/backend/api/providers.d.ts.map +1 -1
- package/dist/types/backend/api/schedules.d.ts +2 -2
- package/dist/types/backend/api/schedules.d.ts.map +1 -1
- package/dist/types/backend/backend.d.ts +5 -2
- package/dist/types/backend/backend.d.ts.map +1 -1
- package/dist/types/backend/dev/fake-headless-backend.d.ts +2 -1
- package/dist/types/backend/dev/fake-headless-backend.d.ts.map +1 -1
- package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
- package/dist/types/backend/local/local-provider-auth-store.d.ts +2 -0
- package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
- package/dist/types/backend/local/local-transcript-history.d.ts +10 -0
- package/dist/types/backend/local/local-transcript-history.d.ts.map +1 -0
- package/dist/types/backend/local/local-transcript.d.ts +6 -0
- package/dist/types/backend/local/local-transcript.d.ts.map +1 -1
- package/dist/types/channels/slack/attachment-primitives.d.ts +2 -1
- package/dist/types/channels/slack/attachment-primitives.d.ts.map +1 -1
- package/dist/types/cli/subcommands/mcp.d.ts +2 -2
- package/dist/types/cli/subcommands/mcp.d.ts.map +1 -1
- package/dist/types/cron/process-identity.d.ts.map +1 -1
- package/dist/types/cron/run-log.d.ts +1 -0
- package/dist/types/cron/run-log.d.ts.map +1 -1
- package/dist/types/headless-environment-response.d.ts +27 -0
- package/dist/types/headless-environment-response.d.ts.map +1 -1
- package/dist/types/providers/byok-providers.d.ts +14 -0
- package/dist/types/providers/byok-providers.d.ts.map +1 -1
- package/dist/types/tools/impl/claude-stream-session.d.ts.map +1 -1
- package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
- package/dist/types/tools/impl/cursor-session.d.ts +26 -0
- package/dist/types/tools/impl/cursor-session.d.ts.map +1 -0
- package/dist/types/tools/impl/cursor-worker.d.ts +32 -0
- package/dist/types/tools/impl/cursor-worker.d.ts.map +1 -0
- package/dist/types/tools/impl/external-coding-agent-task.d.ts.map +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/memory-task-lifecycle.d.ts +2 -2
- package/dist/types/tools/impl/memory-task-lifecycle.d.ts.map +1 -1
- package/dist/types/tools/impl/remote-external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/tools/impl/wake.d.ts +4 -2
- package/dist/types/tools/impl/wake.d.ts.map +1 -1
- package/dist/types/tools/workflow/sdk-spawner.d.ts.map +1 -1
- package/dist/types/utils/native-cli-env.d.ts +3 -0
- package/dist/types/utils/native-cli-env.d.ts.map +1 -0
- package/dist/types/utils/process-liveness.d.ts +9 -0
- package/dist/types/utils/process-liveness.d.ts.map +1 -1
- package/letta.js +3553 -2911
- package/npm-shrinkwrap.json +291 -117
- package/package.json +5 -3
- package/scripts/agent-watch/verify-pr-identity.test.ts +148 -0
- package/scripts/agent-watch/verify-pr-identity.ts +72 -14
- package/scripts/claude-watch/agent-watch.ts +8 -2
- package/scripts/claude-watch/release-source.test.ts +120 -0
- package/scripts/claude-watch/release-source.ts +79 -9
- package/scripts/codex-watch/agent-watch.ts +2 -2
- package/scripts/codex-watch/release-analysis.test.ts +180 -4
- package/scripts/codex-watch/release-analysis.ts +65 -8
- package/scripts/source-file-size-baseline.json +5 -4
- package/skills/initializing-memory/SKILL.md +1 -1
- package/skills/maintaining-deferred-memory/SKILL.md +2 -0
- package/skills/managing-agent-webhooks/SKILL.md +23 -3
- package/skills/managing-agent-webhooks/scripts/manage-agent-webhooks.mjs +40 -11
- package/skills/self-configuration/SKILL.md +1 -1
- package/skills/syncing-memory-filesystem/SKILL.md +2 -10
- package/dist/types/agent/memory-worktree.d.ts +0 -83
- package/dist/types/agent/memory-worktree.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letta-ai/letta-code",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.26",
|
|
4
4
|
"lettaStartupLogProtocol": 1,
|
|
5
5
|
"description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"README.md",
|
|
14
14
|
"letta.js",
|
|
15
15
|
"image-resize-worker.js",
|
|
16
|
+
"cursor-worker.js",
|
|
16
17
|
"npm-shrinkwrap.json",
|
|
17
18
|
"assets/tutor-profile.png",
|
|
18
19
|
"scripts",
|
|
@@ -149,12 +150,13 @@
|
|
|
149
150
|
"access": "public"
|
|
150
151
|
},
|
|
151
152
|
"dependencies": {
|
|
153
|
+
"@cursor/sdk": "1.1.0",
|
|
152
154
|
"@earendil-works/pi-ai": "^0.99.1",
|
|
153
155
|
"@janhapke/sharp-electron": "0.35.3-electron.1",
|
|
154
156
|
"@letta-ai/letta-agent-sdk": "0.8.38",
|
|
155
157
|
"@letta-ai/letta-client": "^1.10.2",
|
|
156
158
|
"@letta-ai/trajectory": "0.2.0",
|
|
157
|
-
"@modelcontextprotocol/sdk": "1.
|
|
159
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
158
160
|
"@pierre/diffs": "1.2.2",
|
|
159
161
|
"@scarf/scarf": "^1.4.0",
|
|
160
162
|
"@shikijs/langs": "4.0.2",
|
|
@@ -275,7 +277,7 @@
|
|
|
275
277
|
},
|
|
276
278
|
"lettaLifecycleProtocol": 1,
|
|
277
279
|
"lettaRuntimeGraphProtocol": 1,
|
|
278
|
-
"lettaRuntimeGraphSha512": "sha512-
|
|
280
|
+
"lettaRuntimeGraphSha512": "sha512-m8L1ZKgKq+g8j+dssFvAVH9AngtP0IYXYXMI8Iz2IXr4YGlKmxyeN8yWu3NdpGkb8psnpwtPzRHRGJ/wVx1xhw==",
|
|
279
281
|
"patchedDependencies": {
|
|
280
282
|
"@earendil-works/pi-ai@0.99.1": "patches/@earendil-works%2Fpi-ai@0.99.1.patch"
|
|
281
283
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
enforceWatcherPrIdentity,
|
|
4
|
+
findWatcherPullRequests,
|
|
4
5
|
parseArgs,
|
|
5
6
|
type WatcherPullRequest,
|
|
6
7
|
} from "./verify-pr-identity.ts";
|
|
@@ -20,6 +21,25 @@ function pullRequest(
|
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function apiPullRequest(
|
|
25
|
+
overrides: Partial<{
|
|
26
|
+
body: string | null;
|
|
27
|
+
draft: boolean;
|
|
28
|
+
html_url: string;
|
|
29
|
+
number: number;
|
|
30
|
+
user: { login: string } | null;
|
|
31
|
+
}> = {},
|
|
32
|
+
) {
|
|
33
|
+
return {
|
|
34
|
+
body: marker,
|
|
35
|
+
draft: true,
|
|
36
|
+
html_url: "https://github.com/letta-ai/letta-code/pull/123",
|
|
37
|
+
number: 123,
|
|
38
|
+
user: { login: "amelia-letta" },
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
function enforce(
|
|
24
44
|
pullRequests: WatcherPullRequest[],
|
|
25
45
|
closed: string[] = [],
|
|
@@ -35,6 +55,134 @@ function enforce(
|
|
|
35
55
|
});
|
|
36
56
|
}
|
|
37
57
|
|
|
58
|
+
describe("findWatcherPullRequests", () => {
|
|
59
|
+
test("finds a marker PR omitted from every bounded search attempt", () => {
|
|
60
|
+
const calls: string[][] = [];
|
|
61
|
+
let waits = 0;
|
|
62
|
+
const result = findWatcherPullRequests(
|
|
63
|
+
{ repo: "letta-ai/letta-code", marker },
|
|
64
|
+
(args) => {
|
|
65
|
+
calls.push(args);
|
|
66
|
+
return args[0] === "pr" ? "[]" : JSON.stringify([[apiPullRequest()]]);
|
|
67
|
+
},
|
|
68
|
+
() => {
|
|
69
|
+
waits += 1;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(result.map((candidate) => candidate.number)).toEqual([123]);
|
|
74
|
+
expect(calls.filter(([command]) => command === "pr")).toHaveLength(3);
|
|
75
|
+
expect(calls.at(-1)?.slice(0, 6)).toEqual([
|
|
76
|
+
"api",
|
|
77
|
+
"--method",
|
|
78
|
+
"GET",
|
|
79
|
+
"--paginate",
|
|
80
|
+
"--slurp",
|
|
81
|
+
"repos/letta-ai/letta-code/pulls",
|
|
82
|
+
]);
|
|
83
|
+
expect(waits).toBe(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("does not miss a duplicate omitted from server-side search", () => {
|
|
87
|
+
const duplicate = apiPullRequest({
|
|
88
|
+
html_url: "https://github.com/letta-ai/letta-code/pull/124",
|
|
89
|
+
number: 124,
|
|
90
|
+
});
|
|
91
|
+
const result = findWatcherPullRequests(
|
|
92
|
+
{ repo: "letta-ai/letta-code", marker },
|
|
93
|
+
(args) =>
|
|
94
|
+
args[0] === "pr"
|
|
95
|
+
? JSON.stringify([pullRequest()])
|
|
96
|
+
: JSON.stringify([[apiPullRequest(), duplicate]]),
|
|
97
|
+
() => {},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(result.map((candidate) => candidate.number)).toEqual([123, 124]);
|
|
101
|
+
expect(() => enforce(result)).toThrow("Multiple open watcher PRs");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("proves persistent absence authoritatively and fails closed on API failure", () => {
|
|
105
|
+
const absent = findWatcherPullRequests(
|
|
106
|
+
{ repo: "letta-ai/letta-code", marker },
|
|
107
|
+
(args) => (args[0] === "pr" ? "[]" : "[[]]"),
|
|
108
|
+
() => {},
|
|
109
|
+
);
|
|
110
|
+
expect(absent).toEqual([]);
|
|
111
|
+
|
|
112
|
+
expect(() =>
|
|
113
|
+
findWatcherPullRequests(
|
|
114
|
+
{ repo: "letta-ai/letta-code", marker },
|
|
115
|
+
(args) => {
|
|
116
|
+
if (args[0] === "pr") throw new Error("search unavailable");
|
|
117
|
+
throw new Error("authoritative listing unavailable");
|
|
118
|
+
},
|
|
119
|
+
() => {},
|
|
120
|
+
),
|
|
121
|
+
).toThrow("authoritative listing unavailable");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("checks every authoritative page beyond 1,000 open pull requests", () => {
|
|
125
|
+
const pages = Array.from({ length: 10 }, (_, page) =>
|
|
126
|
+
Array.from({ length: 100 }, (_, index) =>
|
|
127
|
+
apiPullRequest({
|
|
128
|
+
body: "unrelated",
|
|
129
|
+
number: page * 100 + index + 1,
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
pages.push([apiPullRequest({ number: 1_001 })]);
|
|
134
|
+
const result = findWatcherPullRequests(
|
|
135
|
+
{ repo: "letta-ai/letta-code", marker },
|
|
136
|
+
(args) => (args[0] === "pr" ? "[]" : JSON.stringify(pages)),
|
|
137
|
+
() => {},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result.map((candidate) => candidate.number)).toEqual([1_001]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("rejects fuzzy authoritative candidates and keeps every exact match", () => {
|
|
144
|
+
const result = findWatcherPullRequests(
|
|
145
|
+
{ repo: "letta-ai/letta-code", marker },
|
|
146
|
+
(args) =>
|
|
147
|
+
args[0] === "pr"
|
|
148
|
+
? JSON.stringify([pullRequest()])
|
|
149
|
+
: JSON.stringify([
|
|
150
|
+
[
|
|
151
|
+
apiPullRequest({
|
|
152
|
+
body: "Pi ai watch 1.0.0 1.0.1",
|
|
153
|
+
number: 999,
|
|
154
|
+
}),
|
|
155
|
+
apiPullRequest(),
|
|
156
|
+
],
|
|
157
|
+
]),
|
|
158
|
+
() => {},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(result.map((candidate) => candidate.number)).toEqual([123]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("passes shell metacharacters as one search argument", () => {
|
|
165
|
+
const unsafeMarker = 'Watcher: $(touch /tmp/pwned); "quoted"';
|
|
166
|
+
const calls: string[][] = [];
|
|
167
|
+
findWatcherPullRequests(
|
|
168
|
+
{ repo: "letta-ai/letta-code", marker: unsafeMarker },
|
|
169
|
+
(args) => {
|
|
170
|
+
calls.push(args);
|
|
171
|
+
return args[0] === "pr" ? "[]" : "[[]]";
|
|
172
|
+
},
|
|
173
|
+
() => {},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (const command of calls.filter(([name]) => name === "pr")) {
|
|
177
|
+
expect(command[command.indexOf("--search") + 1]).toBe(
|
|
178
|
+
`${unsafeMarker} in:body`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
expect(calls.at(-1)).toContain("state=open");
|
|
182
|
+
expect(calls.at(-1)).toContain("per_page=100");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
38
186
|
describe("enforceWatcherPrIdentity", () => {
|
|
39
187
|
test("returns the matching Amelia draft PR", () => {
|
|
40
188
|
expect(enforce([pullRequest()])?.number).toBe(123);
|
|
@@ -95,22 +95,80 @@ function runGh(args: string[]): string {
|
|
|
95
95
|
return result.stdout;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
interface GitHubPullRequest {
|
|
99
|
+
body: string | null;
|
|
100
|
+
draft: boolean;
|
|
101
|
+
html_url: string;
|
|
102
|
+
number: number;
|
|
103
|
+
user: { login: string } | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const SEARCH_ATTEMPTS = 3;
|
|
107
|
+
|
|
108
|
+
export function findWatcherPullRequests(
|
|
109
|
+
args: Pick<Args, "repo" | "marker">,
|
|
110
|
+
executeGh: (command: string[]) => string = runGh,
|
|
111
|
+
waitForSearchIndex: () => void = () => Bun.sleepSync(1_000),
|
|
112
|
+
): WatcherPullRequest[] {
|
|
113
|
+
for (let attempt = 0; attempt < SEARCH_ATTEMPTS; attempt += 1) {
|
|
114
|
+
let foundExactHint = false;
|
|
115
|
+
try {
|
|
116
|
+
const candidates = JSON.parse(
|
|
117
|
+
executeGh([
|
|
118
|
+
"pr",
|
|
119
|
+
"list",
|
|
120
|
+
"--repo",
|
|
121
|
+
args.repo,
|
|
122
|
+
"--state",
|
|
123
|
+
"open",
|
|
124
|
+
"--search",
|
|
125
|
+
`${args.marker} in:body`,
|
|
126
|
+
"--limit",
|
|
127
|
+
"1000",
|
|
128
|
+
"--json",
|
|
129
|
+
"author,body,isDraft,number,url",
|
|
130
|
+
]),
|
|
131
|
+
) as WatcherPullRequest[];
|
|
132
|
+
foundExactHint = candidates.some((pullRequest) =>
|
|
133
|
+
pullRequest.body?.includes(args.marker),
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
// Search is only an acceleration hint. The authoritative list below must
|
|
137
|
+
// still prove presence, absence, and uniqueness.
|
|
138
|
+
}
|
|
139
|
+
if (foundExactHint) break;
|
|
140
|
+
if (attempt + 1 < SEARCH_ATTEMPTS) waitForSearchIndex();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pages = JSON.parse(
|
|
144
|
+
executeGh([
|
|
145
|
+
"api",
|
|
146
|
+
"--method",
|
|
147
|
+
"GET",
|
|
148
|
+
"--paginate",
|
|
149
|
+
"--slurp",
|
|
150
|
+
`repos/${args.repo}/pulls`,
|
|
151
|
+
"-f",
|
|
152
|
+
"state=open",
|
|
153
|
+
"-f",
|
|
154
|
+
"per_page=100",
|
|
155
|
+
]),
|
|
156
|
+
) as GitHubPullRequest[][];
|
|
157
|
+
return pages
|
|
158
|
+
.flatMap((page) => page)
|
|
159
|
+
.filter((pullRequest) => pullRequest.body?.includes(args.marker))
|
|
160
|
+
.map((pullRequest) => ({
|
|
161
|
+
author: pullRequest.user,
|
|
162
|
+
body: pullRequest.body,
|
|
163
|
+
isDraft: pullRequest.draft,
|
|
164
|
+
number: pullRequest.number,
|
|
165
|
+
url: pullRequest.html_url,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
98
169
|
function main(): void {
|
|
99
170
|
const args = parseArgs(process.argv.slice(2));
|
|
100
|
-
const pullRequests =
|
|
101
|
-
runGh([
|
|
102
|
-
"pr",
|
|
103
|
-
"list",
|
|
104
|
-
"--repo",
|
|
105
|
-
args.repo,
|
|
106
|
-
"--state",
|
|
107
|
-
"open",
|
|
108
|
-
"--limit",
|
|
109
|
-
"1000",
|
|
110
|
-
"--json",
|
|
111
|
-
"author,body,isDraft,number,url",
|
|
112
|
-
]),
|
|
113
|
-
) as WatcherPullRequest[];
|
|
171
|
+
const pullRequests = findWatcherPullRequests(args);
|
|
114
172
|
const verified = enforceWatcherPrIdentity({
|
|
115
173
|
...args,
|
|
116
174
|
pullRequests,
|
|
@@ -212,14 +212,20 @@ async function selectCandidate(
|
|
|
212
212
|
state: ClaudeWatchStateSnapshot | null,
|
|
213
213
|
args: Args,
|
|
214
214
|
): Promise<ClaudeReleaseCandidate> {
|
|
215
|
+
const previousVersion =
|
|
216
|
+
args.previousVersion ?? state?.package_version ?? null;
|
|
215
217
|
const [githubReleases, npmMetadata] = await Promise.all([
|
|
216
|
-
fetchClaudeGitHubReleases(
|
|
218
|
+
fetchClaudeGitHubReleases({
|
|
219
|
+
currentVersion: args.currentVersion,
|
|
220
|
+
previousVersion,
|
|
221
|
+
allowNpmOnlyExactVersions: args.validationOnly,
|
|
222
|
+
}),
|
|
217
223
|
fetchClaudeNpmMetadata(),
|
|
218
224
|
]);
|
|
219
225
|
const selected = selectClaudeReleaseCandidate({
|
|
220
226
|
githubReleases,
|
|
221
227
|
npmMetadata,
|
|
222
|
-
previousVersion
|
|
228
|
+
previousVersion,
|
|
223
229
|
currentVersion: args.currentVersion,
|
|
224
230
|
allowNpmOnlyExactVersions: args.validationOnly,
|
|
225
231
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ClaudeReleaseSourceDisagreementError,
|
|
3
|
+
fetchClaudeGitHubReleases,
|
|
3
4
|
parseClaudeGitHubReleases,
|
|
4
5
|
parseClaudeNpmMetadata,
|
|
5
6
|
releaseNotesForRange,
|
|
@@ -45,6 +46,125 @@ describe("parseClaudeGitHubReleases", () => {
|
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
describe("fetchClaudeGitHubReleases", () => {
|
|
50
|
+
test("resolves the latest release without fetching a list page", async () => {
|
|
51
|
+
const urls: string[] = [];
|
|
52
|
+
const fetched = await fetchClaudeGitHubReleases({
|
|
53
|
+
previousVersion: "1.2.0",
|
|
54
|
+
retries: 0,
|
|
55
|
+
fetch: (async (input) => {
|
|
56
|
+
urls.push(String(input));
|
|
57
|
+
return Response.json(github("1.2.0", 2));
|
|
58
|
+
}) as typeof fetch,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(fetched).toEqual([github("1.2.0", 2)]);
|
|
62
|
+
expect(urls).toEqual([
|
|
63
|
+
"https://api.github.com/repos/anthropics/claude-code/releases/latest",
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("resolves an explicit release without fetching a list page", async () => {
|
|
68
|
+
const urls: string[] = [];
|
|
69
|
+
const fetched = await fetchClaudeGitHubReleases({
|
|
70
|
+
currentVersion: "1.1.0",
|
|
71
|
+
previousVersion: "1.1.0",
|
|
72
|
+
retries: 0,
|
|
73
|
+
fetch: (async (input) => {
|
|
74
|
+
const url = String(input);
|
|
75
|
+
urls.push(url);
|
|
76
|
+
return Response.json(
|
|
77
|
+
url.endsWith("/latest") ? github("1.2.0", 2) : github("1.1.0", 1),
|
|
78
|
+
);
|
|
79
|
+
}) as typeof fetch,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(fetched).toEqual([github("1.1.0", 1), github("1.2.0", 2)]);
|
|
83
|
+
expect(urls).toEqual([
|
|
84
|
+
"https://api.github.com/repos/anthropics/claude-code/releases/latest",
|
|
85
|
+
"https://api.github.com/repos/anthropics/claude-code/releases/tags/v1.1.0",
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("pages only until the previous release needed for a range", async () => {
|
|
90
|
+
const urls: string[] = [];
|
|
91
|
+
const filler = Array.from({ length: 100 }, (_, index) => ({
|
|
92
|
+
...github(`2.0.${index}`, 3),
|
|
93
|
+
published_at: `2026-08-02T${String(Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}:00Z`,
|
|
94
|
+
}));
|
|
95
|
+
const fetched = await fetchClaudeGitHubReleases({
|
|
96
|
+
currentVersion: "1.2.0",
|
|
97
|
+
previousVersion: "1.0.0",
|
|
98
|
+
retries: 0,
|
|
99
|
+
fetch: (async (input) => {
|
|
100
|
+
const url = String(input);
|
|
101
|
+
urls.push(url);
|
|
102
|
+
if (url.endsWith("/latest")) return Response.json(github("1.2.0", 2));
|
|
103
|
+
if (url.endsWith("/tags/v1.2.0"))
|
|
104
|
+
return Response.json(github("1.2.0", 2));
|
|
105
|
+
if (url.endsWith("/tags/v1.0.0"))
|
|
106
|
+
return Response.json(github("1.0.0", 0));
|
|
107
|
+
if (url.endsWith("page=1")) return Response.json(filler);
|
|
108
|
+
if (url.endsWith("page=2"))
|
|
109
|
+
return Response.json([github("1.1.0", 1), github("1.0.0", 0)]);
|
|
110
|
+
throw new Error(`Unexpected request ${url}`);
|
|
111
|
+
}) as typeof fetch,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(fetched.map(({ tag_name }) => tag_name)).toContain("1.1.0");
|
|
115
|
+
expect(urls.filter((url) => url.includes("?per_page=100"))).toEqual([
|
|
116
|
+
"https://api.github.com/repos/anthropics/claude-code/releases?per_page=100&page=1",
|
|
117
|
+
"https://api.github.com/repos/anthropics/claude-code/releases?per_page=100&page=2",
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("keeps validation-only npm replays off list pages", async () => {
|
|
122
|
+
const urls: string[] = [];
|
|
123
|
+
const fetched = await fetchClaudeGitHubReleases({
|
|
124
|
+
currentVersion: "1.1.0",
|
|
125
|
+
previousVersion: "1.0.0",
|
|
126
|
+
allowNpmOnlyExactVersions: true,
|
|
127
|
+
retries: 0,
|
|
128
|
+
fetch: (async (input) => {
|
|
129
|
+
const url = String(input);
|
|
130
|
+
urls.push(url);
|
|
131
|
+
if (url.endsWith("/latest")) return Response.json(github("1.2.0", 2));
|
|
132
|
+
return new Response("Not Found", {
|
|
133
|
+
status: 404,
|
|
134
|
+
statusText: "Not Found",
|
|
135
|
+
});
|
|
136
|
+
}) as typeof fetch,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(fetched).toEqual([github("1.2.0", 2)]);
|
|
140
|
+
expect(urls.some((url) => url.includes("?per_page=100"))).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("preserves point-endpoint retry errors for a missing explicit tag", async () => {
|
|
144
|
+
let requests = 0;
|
|
145
|
+
const result = fetchClaudeGitHubReleases({
|
|
146
|
+
currentVersion: "9.9.9",
|
|
147
|
+
retries: 1,
|
|
148
|
+
backoffMs: 0,
|
|
149
|
+
sleep: async () => {},
|
|
150
|
+
fetch: (async (input) => {
|
|
151
|
+
requests++;
|
|
152
|
+
if (String(input).endsWith("/latest"))
|
|
153
|
+
return Response.json(github("1.2.0", 2));
|
|
154
|
+
return new Response("Not Found", {
|
|
155
|
+
status: 404,
|
|
156
|
+
statusText: "Not Found",
|
|
157
|
+
});
|
|
158
|
+
}) as typeof fetch,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await expect(result).rejects.toThrow(
|
|
162
|
+
"Failed to fetch https://api.github.com/repos/anthropics/claude-code/releases/tags/v9.9.9 after 2 attempts",
|
|
163
|
+
);
|
|
164
|
+
expect(requests).toBe(3);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
48
168
|
describe("parseClaudeNpmMetadata", () => {
|
|
49
169
|
test("includes channels, all publish times, integrity, and tarballs", () => {
|
|
50
170
|
const parsed = parseClaudeNpmMetadata({
|
|
@@ -18,6 +18,9 @@ export interface ReleaseSourceFetchOptions {
|
|
|
18
18
|
retries?: number;
|
|
19
19
|
backoffMs?: number;
|
|
20
20
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
21
|
+
currentVersion?: string | null;
|
|
22
|
+
previousVersion?: string | null;
|
|
23
|
+
allowNpmOnlyExactVersions?: boolean;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export interface ReleaseSelectionOptions {
|
|
@@ -373,27 +376,94 @@ async function fetchJson(
|
|
|
373
376
|
});
|
|
374
377
|
}
|
|
375
378
|
|
|
379
|
+
function isNotFoundFetchError(error: unknown): boolean {
|
|
380
|
+
return (
|
|
381
|
+
error instanceof Error &&
|
|
382
|
+
error.cause instanceof Error &&
|
|
383
|
+
error.cause.message.startsWith("404 ")
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
376
387
|
export async function fetchClaudeGitHubReleases(
|
|
377
388
|
options: ReleaseSourceFetchOptions = {},
|
|
378
389
|
): Promise<ClaudeGitHubRelease[]> {
|
|
379
|
-
const
|
|
390
|
+
const headers = {
|
|
391
|
+
Accept: "application/vnd.github+json",
|
|
392
|
+
"User-Agent": "letta-claude-watch",
|
|
393
|
+
...(process.env.GH_TOKEN
|
|
394
|
+
? { Authorization: `Bearer ${process.env.GH_TOKEN}` }
|
|
395
|
+
: {}),
|
|
396
|
+
};
|
|
397
|
+
const currentVersion = options.currentVersion
|
|
398
|
+
? normalizeGitHubReleaseTag(options.currentVersion)
|
|
399
|
+
: null;
|
|
400
|
+
const previousVersion = options.previousVersion
|
|
401
|
+
? normalizeGitHubReleaseTag(options.previousVersion)
|
|
402
|
+
: null;
|
|
403
|
+
const rawReleases: unknown[] = [
|
|
404
|
+
await fetchJson(`${GITHUB_RELEASES_URL}/latest`, options, headers),
|
|
405
|
+
];
|
|
406
|
+
let foundCurrent = true;
|
|
407
|
+
if (currentVersion) {
|
|
408
|
+
try {
|
|
409
|
+
rawReleases.push(
|
|
410
|
+
await fetchJson(
|
|
411
|
+
`${GITHUB_RELEASES_URL}/tags/${encodeURIComponent(`v${currentVersion}`)}`,
|
|
412
|
+
options,
|
|
413
|
+
headers,
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (!options.allowNpmOnlyExactVersions || !isNotFoundFetchError(error)) {
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
foundCurrent = false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const pointReleases = parseClaudeGitHubReleases(rawReleases);
|
|
425
|
+
const resolvedCurrentVersion =
|
|
426
|
+
currentVersion ?? pointReleases.at(-1)?.tag_name;
|
|
427
|
+
if (
|
|
428
|
+
!previousVersion ||
|
|
429
|
+
previousVersion === resolvedCurrentVersion ||
|
|
430
|
+
!foundCurrent
|
|
431
|
+
) {
|
|
432
|
+
return pointReleases;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
rawReleases.push(
|
|
437
|
+
await fetchJson(
|
|
438
|
+
`${GITHUB_RELEASES_URL}/tags/${encodeURIComponent(`v${previousVersion}`)}`,
|
|
439
|
+
options,
|
|
440
|
+
headers,
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (!options.allowNpmOnlyExactVersions || !isNotFoundFetchError(error)) {
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
return parseClaudeGitHubReleases(rawReleases);
|
|
448
|
+
}
|
|
449
|
+
|
|
380
450
|
for (let page = 1; ; page += 1) {
|
|
381
451
|
const payload = await fetchJson(
|
|
382
452
|
`${GITHUB_RELEASES_URL}?per_page=100&page=${page}`,
|
|
383
453
|
options,
|
|
384
|
-
|
|
385
|
-
Accept: "application/vnd.github+json",
|
|
386
|
-
"User-Agent": "letta-claude-watch",
|
|
387
|
-
...(process.env.GH_TOKEN
|
|
388
|
-
? { Authorization: `Bearer ${process.env.GH_TOKEN}` }
|
|
389
|
-
: {}),
|
|
390
|
-
},
|
|
454
|
+
headers,
|
|
391
455
|
);
|
|
392
456
|
if (!Array.isArray(payload)) {
|
|
393
457
|
throw new TypeError("GitHub releases response must be an array");
|
|
394
458
|
}
|
|
395
459
|
rawReleases.push(...payload);
|
|
396
|
-
|
|
460
|
+
const parsedPage = parseClaudeGitHubReleases(payload);
|
|
461
|
+
if (
|
|
462
|
+
parsedPage.some(({ tag_name }) => tag_name === previousVersion) ||
|
|
463
|
+
payload.length < 100
|
|
464
|
+
) {
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
397
467
|
}
|
|
398
468
|
return parseClaudeGitHubReleases(rawReleases);
|
|
399
469
|
}
|
|
@@ -90,7 +90,7 @@ async function main() {
|
|
|
90
90
|
let stableReleases: Release[] | undefined;
|
|
91
91
|
|
|
92
92
|
if (!state.audit_cursor_validated) {
|
|
93
|
-
stableReleases = await listStableReleases();
|
|
93
|
+
stableReleases = await listStableReleases({ fullHistory: true });
|
|
94
94
|
state = validateLegacyCodexAuditCursor(
|
|
95
95
|
state,
|
|
96
96
|
stableReleases.map((release) => release.tag_name),
|
|
@@ -103,7 +103,7 @@ async function main() {
|
|
|
103
103
|
if (!sinceTag && !currentTag) {
|
|
104
104
|
const cursorTag = getCodexAuditCursorTag(state);
|
|
105
105
|
if (cursorTag) {
|
|
106
|
-
stableReleases ??= await listStableReleases();
|
|
106
|
+
stableReleases ??= await listStableReleases({ sinceTag: cursorTag });
|
|
107
107
|
const latestRelease = findLatestStableReleaseAfter(
|
|
108
108
|
stableReleases,
|
|
109
109
|
cursorTag,
|