@letta-ai/letta-code 0.27.23 → 0.27.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.27.23",
3
+ "version": "0.27.25",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.0",
@@ -97,6 +97,7 @@
97
97
  "check:filename-casing": "node scripts/check-filename-casing.js",
98
98
  "check:test-mock-isolation": "bun run scripts/check-test-mock-isolation.js",
99
99
  "check:test-coverage": "node scripts/check-test-coverage.cjs",
100
+ "check:skill-frontmatter": "node scripts/check-skill-frontmatter.js",
100
101
  "check:bundled-skill-scripts": "node scripts/check-bundled-skill-scripts.js",
101
102
  "check": "bun run scripts/check.js",
102
103
  "dev": "node scripts/dev.cjs",
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+
6
+ const isStagedOnly = process.argv.includes("--staged");
7
+
8
+ function git(args) {
9
+ const result = spawnSync("git", args, {
10
+ cwd: process.cwd(),
11
+ encoding: "utf8",
12
+ maxBuffer: 20 * 1024 * 1024,
13
+ });
14
+
15
+ if (result.status !== 0) {
16
+ throw new Error(
17
+ (result.stderr || result.stdout || "git command failed").trim(),
18
+ );
19
+ }
20
+
21
+ return result.stdout;
22
+ }
23
+
24
+ function splitNul(output) {
25
+ return output.split("\0").filter(Boolean);
26
+ }
27
+
28
+ function isSkillMarkdownFile(file) {
29
+ return file.split(/[\\/]/).pop() === "SKILL.md";
30
+ }
31
+
32
+ function getCandidateFiles() {
33
+ if (isStagedOnly) {
34
+ return splitNul(
35
+ git(["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"]),
36
+ ).filter(isSkillMarkdownFile);
37
+ }
38
+
39
+ return splitNul(git(["ls-files", "-z"])).filter(isSkillMarkdownFile);
40
+ }
41
+
42
+ function readStagedFile(file) {
43
+ return git(["show", `:${file}`]);
44
+ }
45
+
46
+ function readCurrentFile(file) {
47
+ if (!existsSync(file)) {
48
+ return null;
49
+ }
50
+ return readFileSync(file, "utf8");
51
+ }
52
+
53
+ function checkSkillFrontmatterName(content) {
54
+ const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
55
+ const match = normalized.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
56
+ if (!match) {
57
+ return "missing YAML frontmatter";
58
+ }
59
+
60
+ const frontmatter = match[1] ?? "";
61
+ const nameLine = frontmatter
62
+ .split("\n")
63
+ .find((line) => /^\s*name\s*:/.test(line));
64
+
65
+ if (!nameLine) {
66
+ return "missing name frontmatter";
67
+ }
68
+
69
+ const nameValue = nameLine.replace(/^\s*name\s*:\s*/, "").trim();
70
+ if (!nameValue) {
71
+ return "empty name frontmatter";
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ let violations = [];
78
+ let files = [];
79
+ try {
80
+ files = getCandidateFiles();
81
+ violations = files.flatMap((file) => {
82
+ const content = isStagedOnly ? readStagedFile(file) : readCurrentFile(file);
83
+ if (content === null) {
84
+ return [];
85
+ }
86
+
87
+ const reason = checkSkillFrontmatterName(content);
88
+ return reason ? [{ file, reason }] : [];
89
+ });
90
+ } catch (error) {
91
+ console.error(
92
+ `Failed to check skill frontmatter: ${error instanceof Error ? error.message : String(error)}`,
93
+ );
94
+ process.exit(1);
95
+ }
96
+
97
+ if (violations.length > 0) {
98
+ console.error("\n❌ Skill frontmatter violations found:\n");
99
+ for (const violation of violations) {
100
+ console.error(`${violation.file}`);
101
+ console.error(` ${violation.reason}`);
102
+ console.error(
103
+ " ↳ Add a non-empty `name:` field to SKILL.md frontmatter.\n",
104
+ );
105
+ }
106
+ console.error(
107
+ `Found ${violations.length} skill frontmatter violation${violations.length === 1 ? "" : "s"}.`,
108
+ );
109
+ process.exit(1);
110
+ }
111
+
112
+ if (!isStagedOnly) {
113
+ console.log(
114
+ `✅ Skill frontmatter name headers present (${files.length} SKILL.md file${files.length === 1 ? "" : "s"}).`,
115
+ );
116
+ }
package/scripts/check.js CHANGED
@@ -19,6 +19,7 @@ const checks = [
19
19
  { name: "filename casing", script: ["check:filename-casing"] },
20
20
  { name: "test mock isolation", script: ["check:test-mock-isolation"] },
21
21
  { name: "test coverage", script: ["check:test-coverage"] },
22
+ { name: "skill frontmatter", script: ["check:skill-frontmatter"] },
22
23
  { name: "bundled skill scripts", script: ["check:bundled-skill-scripts"] },
23
24
  { name: "biome", script: ["lint"] },
24
25
  { name: "typescript", script: ["typecheck"] },
@@ -116,7 +116,8 @@ Review changes, then commit with a descriptive message:
116
116
  cd $MEMORY_DIR
117
117
  git status # Review what changed before staging
118
118
  git add <specific files> # Stage targeted paths — avoid blind `git add -A`
119
- git commit --author="<AGENT_NAME> <<ACTUAL_AGENT_ID>@letta.com>" -m "fix(doctor): <summary> 🏥
119
+ author_name="${AGENT_NAME:-$AGENT_ID}"
120
+ git commit --author="$author_name <$AGENT_ID@letta.com>" -m "fix(doctor): <summary> 🏥
120
121
 
121
122
  <identified issues and implemented solutions>"
122
123
 
@@ -155,7 +155,7 @@ Before finishing, verify:
155
155
  | `references/providers.md` | Adding a custom model/API provider for local agents |
156
156
  | `references/events.md` | Reacting to lifecycle/tool/turn events or transforming turns/tools |
157
157
  | `references/permissions.md` | Enforcing dynamic tool allow/ask/deny policy before approval/execution |
158
- | `references/ui.md` | Panels (including order-0 statusline) or `ui.panels` capability guards are involved |
158
+ | `references/ui.md` | Panels (including order-0 statusline and order-1 dreaming indicator) or `ui.panels` capability guards are involved |
159
159
  | `references/plan-mode.md` | Recreating plan mode with commands, tools, events, permissions, and local state |
160
160
  | `references/analysis-mode.md` | Phrase-triggered diagnostic mode with turn reminders (simpler than plan-mode) |
161
161
  | `references/architecture.md` | Multiple capabilities, local state, cleanup, background model work, or non-trivial composition |
@@ -38,7 +38,7 @@ if (letta.capabilities.ui.panels) {
38
38
  `order` is a signed coordinate around the input:
39
39
 
40
40
  - `order > 1` — additive panels above the input, higher nearer the top (default `100`).
41
- - `order === 1` — replaces the default product-status row (currently dreaming/reflection status). Use this only when intentionally overriding that row; otherwise use `order > 1`.
41
+ - `order === 1` — replaces the default dreaming/reflection indicator above the input. Use this only when intentionally overriding that row; otherwise use `order > 1`.
42
42
  - `order === 0` — the primary line just below the input, overriding the built-in `agent · model`. This is the statusline slot; use `customizing-statusline` for that work.
43
43
  - `order < 0` — stacks below the primary line, `-1` closest.
44
44
 
@@ -51,6 +51,12 @@ render(ctx: {
51
51
  width: number;
52
52
  agent: { id, name };
53
53
  model: { id, displayName, provider, reasoningEffort };
54
+ backgroundAgents: Array<{
55
+ type: string;
56
+ status: string;
57
+ durationMs: number;
58
+ agentId: string | null;
59
+ }>;
54
60
  subagents: { list(): SubagentLifecycleItem[] };
55
61
  row(left, right, width): string;
56
62
  columns(parts: string[], width): string;
@@ -63,6 +69,39 @@ render(ctx: {
63
69
 
64
70
  Close panels when they are transient, and close/replace long-lived panels from the activation disposer if reload should remove them.
65
71
 
72
+ ### Panel use case: dreaming indicator overrides
73
+
74
+ When a user asks to change the "dreaming" UI/indicator, the reflection status above the input, or to add the full background-agent URL, use an `order: 1` panel replacement. The user should not need to know any internal row/component name.
75
+
76
+ Checklist:
77
+
78
+ - Open a panel with `order: 1`, not an additive panel.
79
+ - Read active hidden background agents from `ctx.backgroundAgents`; filter by `status` (`pending`/`running`).
80
+ - Use `agent.agentId` to build `https://app.letta.com/chat/${agent.agentId}`.
81
+ - If the user asks for the full URL, render visible text; do not use `ctx.link()` because it hides the URL behind OSC-8.
82
+ - If preserving animation, own the timer and call `panel.update()`; clean up timer and panel in the disposer.
83
+ - Keep `render()` pure: do not call diagnostics or mutate external state from render.
84
+
85
+ Critical shape:
86
+
87
+ ```ts
88
+ const panel = letta.ui.openPanel({
89
+ id: "dreaming-url",
90
+ order: 1,
91
+ render(ctx) {
92
+ const agent = ctx.backgroundAgents.find(
93
+ (a) => a.status === "pending" || a.status === "running",
94
+ );
95
+ if (!agent) return "";
96
+
97
+ const url = agent.agentId
98
+ ? `https://app.letta.com/chat/${agent.agentId}`
99
+ : null;
100
+ // Render spinner/label/elapsed, plus visible URL if requested.
101
+ },
102
+ });
103
+ ```
104
+
66
105
  ### Commands that open panels
67
106
 
68
107
  If a command's `run()` opens a panel, guard the command **registration** on `letta.capabilities.ui.panels` — not just the `openPanel` call:
@@ -706,7 +706,8 @@ Check if they're satisfied or want further refinement. Then commit and push memo
706
706
  cd $MEMORY_DIR
707
707
  git status # Review what changed before staging
708
708
  git add <specific files> # Stage targeted paths — avoid blind `git add -A`
709
- git commit --author="<AGENT_NAME> <<ACTUAL_AGENT_ID>@letta.com>" -m "feat(init): <summary> ✨
709
+ author_name="${AGENT_NAME:-$AGENT_ID}"
710
+ git commit --author="$author_name <$AGENT_ID@letta.com>" -m "feat(init): <summary> ✨
710
711
 
711
712
  <what was initialized and key decisions made>"
712
713