@letta-ai/letta-code 0.27.20 → 0.27.22

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 (31) hide show
  1. package/dist/agent-presets.js +3768 -0
  2. package/dist/agent-presets.js.map +17 -0
  3. package/dist/types/agent/agent-tags.d.ts +19 -0
  4. package/dist/types/agent/agent-tags.d.ts.map +1 -0
  5. package/dist/types/agent/create-agent-request.d.ts +55 -0
  6. package/dist/types/agent/create-agent-request.d.ts.map +1 -0
  7. package/dist/types/agent/memory-constants.d.ts +2 -0
  8. package/dist/types/agent/memory-constants.d.ts.map +1 -0
  9. package/dist/types/agent/memory.d.ts +29 -0
  10. package/dist/types/agent/memory.d.ts.map +1 -0
  11. package/dist/types/agent/model-catalog.d.ts +362 -0
  12. package/dist/types/agent/model-catalog.d.ts.map +1 -0
  13. package/dist/types/agent/personality-presets.d.ts +64 -0
  14. package/dist/types/agent/personality-presets.d.ts.map +1 -0
  15. package/dist/types/agent/prompt-assets.d.ts +32 -0
  16. package/dist/types/agent/prompt-assets.d.ts.map +1 -0
  17. package/dist/types/agent-presets.d.ts +16 -0
  18. package/dist/types/agent-presets.d.ts.map +1 -0
  19. package/dist/types/constants.d.ts +53 -0
  20. package/dist/types/constants.d.ts.map +1 -0
  21. package/dist/types/types/protocol_v2.d.ts +3 -1
  22. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  23. package/letta.js +2029 -3395
  24. package/package.json +23 -1
  25. package/scripts/codex-watch/agent-watch.ts +186 -0
  26. package/scripts/codex-watch/check-release.ts +18 -316
  27. package/scripts/codex-watch/github.ts +121 -0
  28. package/scripts/codex-watch/release-analysis.ts +288 -0
  29. package/scripts/codex-watch/tracker.test.ts +127 -0
  30. package/scripts/codex-watch/tracker.ts +238 -0
  31. package/scripts/codex-watch/update-tracker.ts +131 -0
@@ -0,0 +1,121 @@
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 function runGh(args: string[], input?: string): string {
7
+ const res = spawnSync("gh", args, {
8
+ encoding: "utf8",
9
+ input,
10
+ maxBuffer: 50 * 1024 * 1024,
11
+ stdio: input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
12
+ });
13
+ if (res.status !== 0) {
14
+ throw new Error(`gh ${args.join(" ")} failed:\n${res.stderr}`);
15
+ }
16
+ return res.stdout;
17
+ }
18
+
19
+ export function ghJson<T>(args: string[], input?: string): T {
20
+ return JSON.parse(runGh(args, input)) as T;
21
+ }
22
+
23
+ export function ensureLabels(repo: string, labels: string[]): void {
24
+ for (const label of labels) {
25
+ const res = spawnSync("gh", ["label", "create", label, "--repo", repo], {
26
+ encoding: "utf8",
27
+ stdio: ["ignore", "pipe", "pipe"],
28
+ });
29
+ if (res.status !== 0 && !res.stderr.includes("already exists")) {
30
+ throw new Error(`gh label create ${label} failed:\n${res.stderr}`);
31
+ }
32
+ }
33
+ }
34
+
35
+ export function createIssueWithBody(
36
+ repo: string,
37
+ title: string,
38
+ body: string,
39
+ labels: string[] = [],
40
+ ): string {
41
+ const bodyFile = writeTempMarkdown(body, "codex-watch-issue");
42
+ try {
43
+ const args = [
44
+ "issue",
45
+ "create",
46
+ "--repo",
47
+ repo,
48
+ "--title",
49
+ title,
50
+ "--body-file",
51
+ bodyFile,
52
+ ];
53
+ for (const label of labels) args.push("--label", label);
54
+ return runGh(args).trim();
55
+ } finally {
56
+ rmSync(bodyFile, { force: true });
57
+ }
58
+ }
59
+
60
+ export function editIssueBody(
61
+ repo: string,
62
+ issueNumber: number,
63
+ body: string,
64
+ ): void {
65
+ const bodyFile = writeTempMarkdown(body, "codex-watch-tracker");
66
+ try {
67
+ runGh([
68
+ "issue",
69
+ "edit",
70
+ String(issueNumber),
71
+ "--repo",
72
+ repo,
73
+ "--body-file",
74
+ bodyFile,
75
+ ]);
76
+ } finally {
77
+ rmSync(bodyFile, { force: true });
78
+ }
79
+ }
80
+
81
+ export function getIssueBody(repo: string, issueNumber: number): string {
82
+ const issue = ghJson<{ body: string | null }>([
83
+ "issue",
84
+ "view",
85
+ String(issueNumber),
86
+ "--repo",
87
+ repo,
88
+ "--json",
89
+ "body",
90
+ ]);
91
+ return issue.body ?? "";
92
+ }
93
+
94
+ export function findIssueByExactTitle(
95
+ repo: string,
96
+ title: string,
97
+ ): { number: number; title: string; state: string } | null {
98
+ const issues = ghJson<
99
+ Array<{ number: number; title: string; state: string }>
100
+ >([
101
+ "issue",
102
+ "list",
103
+ "--repo",
104
+ repo,
105
+ "--state",
106
+ "all",
107
+ "--search",
108
+ `${title} in:title`,
109
+ "--limit",
110
+ "20",
111
+ "--json",
112
+ "number,title,state",
113
+ ]);
114
+ return issues.find((issue) => issue.title === title) ?? null;
115
+ }
116
+
117
+ function writeTempMarkdown(body: string, prefix: string): string {
118
+ const bodyFile = join(tmpdir(), `${prefix}-${Date.now()}.md`);
119
+ writeFileSync(bodyFile, body);
120
+ return bodyFile;
121
+ }
@@ -0,0 +1,288 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import {
6
+ decideVerdict,
7
+ diffModelsJson,
8
+ type ModelsDiff,
9
+ type ModelsJson,
10
+ type Verdict,
11
+ } from "./diff-models-json.ts";
12
+ import type { PathChangeSummary, RenderInput } from "./render-issue.ts";
13
+
14
+ export const CODEX_REPO = "openai/codex";
15
+ export const DEFAULT_TARGET_REPO =
16
+ process.env.GITHUB_REPOSITORY || "letta-ai/letta-code";
17
+ export const WATCHED_PATHS = [
18
+ "codex-rs/models-manager/models.json",
19
+ "codex-rs/models-manager/prompt.md",
20
+ "codex-rs/core/src/tools",
21
+ "codex-rs/apply-patch",
22
+ ];
23
+
24
+ const MAX_COMMITS_PER_PATH = 8;
25
+
26
+ export interface Release {
27
+ tag_name: string;
28
+ draft: boolean;
29
+ prerelease: boolean;
30
+ html_url: string;
31
+ body: string | null;
32
+ published_at: string | null;
33
+ }
34
+
35
+ export interface AnalyzeCodexReleaseOptions {
36
+ sinceTag: string | null;
37
+ currentTag: string | null;
38
+ }
39
+
40
+ export interface CodexWatchAnalysis extends RenderInput {
41
+ verdict: Verdict;
42
+ models_diff: ModelsDiff | null;
43
+ compare_url: string;
44
+ changed_files: string[];
45
+ }
46
+
47
+ export async function analyzeCodexRelease(
48
+ options: AnalyzeCodexReleaseOptions,
49
+ ): Promise<CodexWatchAnalysis> {
50
+ const stables = await listStableReleases();
51
+ if (stables.length === 0) throw new Error("No stable Codex releases found");
52
+
53
+ const current = options.currentTag
54
+ ? stables.find((r) => r.tag_name === options.currentTag)
55
+ : stables.at(-1);
56
+ if (!current)
57
+ throw new Error(`Could not find current release ${options.currentTag}`);
58
+
59
+ const previous = options.sinceTag
60
+ ? (stables.find((r) => r.tag_name === options.sinceTag) ??
61
+ ({ tag_name: options.sinceTag } as Release))
62
+ : findPreviousStable(stables, current.tag_name);
63
+ if (!previous)
64
+ throw new Error(
65
+ `Could not find previous stable before ${current.tag_name}`,
66
+ );
67
+
68
+ const tmp = mkdtempSync(join(tmpdir(), "codex-watch-"));
69
+ try {
70
+ const repoDir = cloneCodex(tmp);
71
+ fetchTag(repoDir, previous.tag_name);
72
+ fetchTag(repoDir, current.tag_name);
73
+
74
+ let modelsDiff: ModelsDiff | null = null;
75
+ let parseError = false;
76
+ try {
77
+ const prevRaw = showFile(
78
+ repoDir,
79
+ previous.tag_name,
80
+ "codex-rs/models-manager/models.json",
81
+ );
82
+ const currRaw = showFile(
83
+ repoDir,
84
+ current.tag_name,
85
+ "codex-rs/models-manager/models.json",
86
+ );
87
+ if (!prevRaw || !currRaw)
88
+ throw new Error("missing models.json at one tag");
89
+ modelsDiff = diffModelsJson(
90
+ JSON.parse(prevRaw) as ModelsJson,
91
+ JSON.parse(currRaw) as ModelsJson,
92
+ );
93
+ } catch (err) {
94
+ parseError = true;
95
+ console.error(`Failed to parse/diff models.json: ${String(err)}`);
96
+ }
97
+
98
+ const changed = changedFiles(repoDir, previous.tag_name, current.tag_name);
99
+ const changedSet = new Set(changed);
100
+ const promptMdChanged = changedSet.has("codex-rs/models-manager/prompt.md");
101
+ const toolsDirChanged = changed.some((f) =>
102
+ f.startsWith("codex-rs/core/src/tools/"),
103
+ );
104
+ const applyPatchDirChanged = changed.some((f) =>
105
+ f.startsWith("codex-rs/apply-patch/"),
106
+ );
107
+ const verdict = decideVerdict({
108
+ models_diff: modelsDiff,
109
+ prompt_md_changed: promptMdChanged,
110
+ tools_dir_changed: toolsDirChanged,
111
+ apply_patch_dir_changed: applyPatchDirChanged,
112
+ parse_error: parseError,
113
+ });
114
+
115
+ const pathChanges: PathChangeSummary[] = WATCHED_PATHS.map((path) => ({
116
+ path,
117
+ commits: commitsForPath(
118
+ repoDir,
119
+ previous.tag_name,
120
+ current.tag_name,
121
+ path,
122
+ ),
123
+ })).filter((p) => p.commits.length > 0);
124
+
125
+ return {
126
+ previous_tag: previous.tag_name,
127
+ current_tag: current.tag_name,
128
+ release_url: current.html_url,
129
+ release_notes_md: current.body ?? "",
130
+ verdict,
131
+ models_diff: modelsDiff,
132
+ prompt_md_changed: promptMdChanged,
133
+ prompt_md_diff_preview: promptMdChanged
134
+ ? diffPreview(
135
+ repoDir,
136
+ previous.tag_name,
137
+ current.tag_name,
138
+ "codex-rs/models-manager/prompt.md",
139
+ )
140
+ : null,
141
+ path_changes: pathChanges,
142
+ workflow_run_url: workflowRunUrl(),
143
+ compare_url: `https://github.com/${CODEX_REPO}/compare/${previous.tag_name}...${current.tag_name}`,
144
+ changed_files: changed,
145
+ };
146
+ } finally {
147
+ rmSync(tmp, { recursive: true, force: true });
148
+ }
149
+ }
150
+
151
+ export async function listStableReleases(): Promise<Release[]> {
152
+ const releases: Release[] = [];
153
+ const headers: Record<string, string> = {
154
+ Accept: "application/vnd.github+json",
155
+ "X-GitHub-Api-Version": "2022-11-28",
156
+ };
157
+ const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
158
+ if (token) headers.Authorization = `Bearer ${token}`;
159
+
160
+ for (let page = 1; page <= 10; page++) {
161
+ const url = `https://api.github.com/repos/${CODEX_REPO}/releases?per_page=100&page=${page}`;
162
+ const res = await fetch(url, { headers });
163
+ if (!res.ok) {
164
+ throw new Error(
165
+ `GitHub releases API failed (${res.status}): ${await res.text()}`,
166
+ );
167
+ }
168
+ const batch = (await res.json()) as Release[];
169
+ releases.push(...batch);
170
+ if (batch.length < 100) break;
171
+ }
172
+ return releases
173
+ .filter(isStableRelease)
174
+ .sort((a, b) => (a.published_at ?? "").localeCompare(b.published_at ?? ""));
175
+ }
176
+
177
+ function isStableRelease(release: Release): boolean {
178
+ if (release.draft || release.prerelease) return false;
179
+ return /^(rust-v|v)?\d+\.\d+\.\d+$/.test(release.tag_name);
180
+ }
181
+
182
+ function findPreviousStable(
183
+ stables: Release[],
184
+ currentTag: string,
185
+ ): Release | null {
186
+ const idx = stables.findIndex((r) => r.tag_name === currentTag);
187
+ if (idx <= 0) return null;
188
+ return stables[idx - 1] ?? null;
189
+ }
190
+
191
+ function cloneCodex(tmp: string): string {
192
+ const dir = join(tmp, "codex");
193
+ git([
194
+ "clone",
195
+ "--filter=blob:none",
196
+ "--no-checkout",
197
+ `https://github.com/${CODEX_REPO}.git`,
198
+ dir,
199
+ ]);
200
+ return dir;
201
+ }
202
+
203
+ function fetchTag(repoDir: string, tag: string): void {
204
+ git(
205
+ [
206
+ "fetch",
207
+ "--filter=blob:none",
208
+ "origin",
209
+ `refs/tags/${tag}:refs/tags/${tag}`,
210
+ ],
211
+ repoDir,
212
+ );
213
+ }
214
+
215
+ function showFile(repoDir: string, tag: string, path: string): string | null {
216
+ const res = spawnSync("git", ["show", `${tag}:${path}`], {
217
+ cwd: repoDir,
218
+ encoding: "utf8",
219
+ stdio: ["ignore", "pipe", "pipe"],
220
+ });
221
+ if (res.status !== 0) return null;
222
+ return res.stdout;
223
+ }
224
+
225
+ function changedFiles(
226
+ repoDir: string,
227
+ prevTag: string,
228
+ currTag: string,
229
+ ): string[] {
230
+ return git(["diff", "--name-only", `${prevTag}..${currTag}`], repoDir)
231
+ .split("\n")
232
+ .filter(Boolean);
233
+ }
234
+
235
+ function commitsForPath(
236
+ repoDir: string,
237
+ prevTag: string,
238
+ currTag: string,
239
+ path: string,
240
+ ): string[] {
241
+ const out = git(
242
+ ["log", "--format=%h %s", `${prevTag}..${currTag}`, "--", path],
243
+ repoDir,
244
+ );
245
+ const commits = out.split("\n").filter(Boolean);
246
+ if (commits.length <= MAX_COMMITS_PER_PATH) return commits;
247
+ return [
248
+ ...commits.slice(0, MAX_COMMITS_PER_PATH),
249
+ `…and ${commits.length - MAX_COMMITS_PER_PATH} more commits`,
250
+ ];
251
+ }
252
+
253
+ function diffPreview(
254
+ repoDir: string,
255
+ prevTag: string,
256
+ currTag: string,
257
+ path: string,
258
+ ): string | null {
259
+ const out = git(
260
+ ["diff", "--unified=2", `${prevTag}..${currTag}`, "--", path],
261
+ repoDir,
262
+ );
263
+ if (!out.trim()) return null;
264
+ return out.split("\n").slice(0, 120).join("\n");
265
+ }
266
+
267
+ function git(args: string[], cwd?: string): string {
268
+ const res = spawnSync("git", args, {
269
+ cwd,
270
+ encoding: "utf8",
271
+ stdio: ["ignore", "pipe", "pipe"],
272
+ });
273
+ if (res.status !== 0) {
274
+ throw new Error(`git ${args.join(" ")} failed:\n${res.stderr}`);
275
+ }
276
+ return res.stdout;
277
+ }
278
+
279
+ function workflowRunUrl(): string {
280
+ if (
281
+ process.env.GITHUB_SERVER_URL &&
282
+ process.env.GITHUB_REPOSITORY &&
283
+ process.env.GITHUB_RUN_ID
284
+ ) {
285
+ return `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
286
+ }
287
+ return "local dry-run";
288
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CodexWatchAnalysis } from "./release-analysis.ts";
3
+ import {
4
+ emptyTrackerState,
5
+ hasProcessedTag,
6
+ parseTrackerState,
7
+ recordAnalysis,
8
+ renderTrackerBody,
9
+ serializeTrackerState,
10
+ type TrackerEntry,
11
+ upsertTrackerEntry,
12
+ } from "./tracker.ts";
13
+
14
+ function analysis(
15
+ tag: string,
16
+ verdict: CodexWatchAnalysis["verdict"] = "no-op",
17
+ ): CodexWatchAnalysis {
18
+ return {
19
+ previous_tag: "rust-v0.1.0",
20
+ current_tag: tag,
21
+ release_url: `https://github.com/openai/codex/releases/tag/${tag}`,
22
+ release_notes_md: "",
23
+ verdict,
24
+ models_diff: null,
25
+ prompt_md_changed: false,
26
+ prompt_md_diff_preview: null,
27
+ path_changes: [],
28
+ workflow_run_url: "https://github.com/letta-ai/letta-code/actions/runs/1",
29
+ compare_url: `https://github.com/openai/codex/compare/rust-v0.1.0...${tag}`,
30
+ changed_files: [],
31
+ };
32
+ }
33
+
34
+ function entry(index: number, outcome: TrackerEntry["outcome"]): TrackerEntry {
35
+ const verdict =
36
+ outcome === "recorded_noop" ? "no-op" : "tool-surface review needed";
37
+ return {
38
+ tag: `rust-v0.${index}.0`,
39
+ previous_tag: `rust-v0.${index - 1}.0`,
40
+ verdict,
41
+ outcome,
42
+ pr_url:
43
+ outcome === "pr_created"
44
+ ? `https://github.com/letta-ai/letta-code/pull/${index}`
45
+ : null,
46
+ notes: `notes ${index}`,
47
+ processed_at: `2026-07-02T00:${String(index).padStart(2, "0")}:00.000Z`,
48
+ compare_url: `https://github.com/openai/codex/compare/rust-v0.${index - 1}.0...rust-v0.${index}.0`,
49
+ workflow_run_url: "https://github.com/letta-ai/letta-code/actions/runs/1",
50
+ };
51
+ }
52
+
53
+ describe("tracker state", () => {
54
+ test("returns empty state when hidden state is absent or malformed", () => {
55
+ expect(parseTrackerState("plain body")).toEqual(emptyTrackerState());
56
+ expect(
57
+ parseTrackerState("<!-- codex-agent-watch-state\nnot json\n-->"),
58
+ ).toEqual(emptyTrackerState());
59
+ });
60
+
61
+ test("round-trips hidden state through rendered body", () => {
62
+ const state = recordAnalysis(emptyTrackerState(), {
63
+ analysis: analysis("rust-v0.2.0", "tool-surface review needed"),
64
+ outcome: "no_local_impact",
65
+ notes: "upstream-only router change",
66
+ processedAt: "2026-07-02T00:00:00.000Z",
67
+ });
68
+
69
+ const body = renderTrackerBody(state);
70
+ expect(parseTrackerState(body)).toEqual(state);
71
+ expect(body).toContain("rust-v0.2.0");
72
+ expect(body).toContain("upstream-only router change");
73
+ });
74
+
75
+ test("records noops in hidden state without adding visible table rows", () => {
76
+ const state = recordAnalysis(emptyTrackerState(), {
77
+ analysis: analysis("rust-v0.2.0"),
78
+ outcome: "recorded_noop",
79
+ notes: "no watched tool-surface changes detected",
80
+ processedAt: "2026-07-02T00:00:00.000Z",
81
+ });
82
+
83
+ const body = renderTrackerBody(state);
84
+ expect(hasProcessedTag(parseTrackerState(body), "rust-v0.2.0")).toBe(true);
85
+ expect(body).toContain("_No actionable releases recorded yet._");
86
+ expect(body).toContain("no watched changes");
87
+ });
88
+
89
+ test("keeps the last 50 processed releases in hidden state", () => {
90
+ let state = emptyTrackerState();
91
+ for (let i = 1; i <= 60; i++) {
92
+ state = upsertTrackerEntry(state, entry(i, "recorded_noop"));
93
+ }
94
+
95
+ expect(state.processed).toHaveLength(50);
96
+ expect(state.processed[0]?.tag).toBe("rust-v0.60.0");
97
+ expect(state.processed.at(-1)?.tag).toBe("rust-v0.11.0");
98
+ expect(parseTrackerState(serializeTrackerState(state))).toEqual(state);
99
+ });
100
+
101
+ test("renders at most 20 non-noop rows", () => {
102
+ let state = emptyTrackerState();
103
+ for (let i = 1; i <= 25; i++) {
104
+ state = upsertTrackerEntry(state, entry(i, "no_local_impact"));
105
+ }
106
+
107
+ const body = renderTrackerBody(state);
108
+ expect(body).toContain("| [rust-v0.25.0]");
109
+ expect(body).toContain("| [rust-v0.6.0]");
110
+ expect(body).not.toContain("| [rust-v0.5.0]");
111
+ });
112
+
113
+ test("replaces an existing tag instead of duplicating it", () => {
114
+ let state = upsertTrackerEntry(
115
+ emptyTrackerState(),
116
+ entry(2, "recorded_noop"),
117
+ );
118
+ state = upsertTrackerEntry(state, {
119
+ ...entry(2, "pr_created"),
120
+ notes: "opened a fix",
121
+ });
122
+
123
+ expect(state.processed).toHaveLength(1);
124
+ expect(state.processed[0]?.outcome).toBe("pr_created");
125
+ expect(renderTrackerBody(state)).toContain("opened a fix");
126
+ });
127
+ });