@letta-ai/letta-code 0.28.15 → 0.28.16

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.28.15",
3
+ "version": "0.28.16",
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",
@@ -11,6 +11,7 @@
11
11
  "LICENSE",
12
12
  "README.md",
13
13
  "letta.js",
14
+ "image-resize-worker.js",
14
15
  "scripts",
15
16
  "skills",
16
17
  "vendor",
@@ -9,12 +9,12 @@
9
9
  "src/cli/app/AppView.tsx": 1750,
10
10
  "src/cli/app/use-approval-flow.ts": 1163,
11
11
  "src/cli/app/use-configuration-handlers.ts": 1392,
12
- "src/cli/app/use-conversation-loop.ts": 2935,
12
+ "src/cli/app/use-conversation-loop.ts": 2936,
13
13
  "src/cli/app/use-submit-handler.ts": 4099,
14
- "src/cli/components/AgentSelector.tsx": 1151,
14
+ "src/cli/components/AgentSelector.tsx": 1111,
15
15
  "src/cli/components/InputRich.tsx": 2219,
16
16
  "src/cli/components/ModelSelector.tsx": 1204,
17
- "src/cli/components/ProviderSelector.tsx": 1676,
17
+ "src/cli/components/ProviderSelector.tsx": 1672,
18
18
  "src/cli/components/ToolCallMessageRich.tsx": 1109,
19
19
  "src/cli/helpers/accumulator.ts": 1596,
20
20
  "src/cli/helpers/reflection-transcript.ts": 2080,
@@ -26,14 +26,14 @@
26
26
  "src/hooks/integration.test.ts": 1147,
27
27
  "src/index.ts": 2803,
28
28
  "src/mods/learning-harness.ts": 2434,
29
- "src/mods/mod-engine.test.ts": 2178,
30
- "src/mods/mod-engine.ts": 1879,
29
+ "src/mods/mod-engine.test.ts": 2132,
30
+ "src/mods/mod-engine.ts": 1838,
31
31
  "src/mods/package-installer.test.ts": 1248,
32
32
  "src/mods/package-installer.ts": 1201,
33
33
  "src/mods/package-registry.ts": 1033,
34
34
  "src/permissions/read-only-shell.test.ts": 1303,
35
35
  "src/permissions/read-only-shell.ts": 2009,
36
- "src/providers/chatgpt-usage-service.ts": 1115,
36
+ "src/providers/chatgpt-usage-service.ts": 1112,
37
37
  "src/settings-manager.test.ts": 1669,
38
38
  "src/settings-manager.ts": 2113,
39
39
  "src/tools/impl/enter-worktree.ts": 1260,
@@ -41,7 +41,7 @@
41
41
  "src/tools/manager.ts": 3282,
42
42
  "src/tools/tool-execution-context.test.ts": 1422,
43
43
  "src/types/protocol_v2.ts": 2943,
44
- "src/websocket/listen-client-concurrency.test.ts": 3016,
44
+ "src/websocket/listen-client-concurrency.test.ts": 3017,
45
45
  "src/websocket/listen-client-protocol.test.ts": 6723,
46
46
  "src/websocket/listener/commands/channels.ts": 1390,
47
47
  "src/websocket/listener/commands/memory.ts": 1114,
@@ -118,7 +118,7 @@ Agents can inspect local mod diagnostics at:
118
118
  ## Rules
119
119
 
120
120
  - Do not create project mods.
121
- - Custom provider mods are local-backend/local-agent only. They do not add providers for Constellation/cloud agents.
121
+ - Custom provider mods are local-backend/local-agent only. They do not add providers for agents managed through the Letta API.
122
122
  - Provider mods may run in a provider-only listener context; keep provider registration independent from commands/tools/UI and guard everything else.
123
123
  - Direct mod files should not assume third-party npm packages are available. Use Node/Bun built-ins unless packaging is explicitly requested.
124
124
  - Do not do surprising side effects on startup; mods activate on app start and `/reload`.
@@ -139,7 +139,7 @@ ctx.conversation.updateLlmConfig(opts) // change model / reasoning effort / cont
139
139
 
140
140
  A forked handle keeps the same agent/backend defaults and targets the forked conversation. Use forked handles for background model work. Use `getHistory({ limit, order, includeErrors })` when local logic needs conversation context.
141
141
 
142
- `updateLlmConfig({ model?, reasoningEffort?, contextWindow?, scope? })` changes the model, reasoning effort, and/or context window, and works across local and Constellation backends. Only the fields you pass change; the rest are preserved, so `updateLlmConfig({ contextWindow })` adjusts just the context window without touching the model or reasoning effort. `scope` defaults to `"conversation"` (a conversation-scoped override that leaves the agent's default untouched); pass `scope: "agent"` to change the agent default. Changing reasoning effort without a model resolves the current model to rebuild provider-specific settings. The change takes effect on the next turn (the model is resolved per provider request).
142
+ `updateLlmConfig({ model?, reasoningEffort?, contextWindow?, scope? })` changes the model, reasoning effort, and/or context window, and works across local and Letta Cloud backends. Only the fields you pass change; the rest are preserved, so `updateLlmConfig({ contextWindow })` adjusts just the context window without touching the model or reasoning effort. `scope` defaults to `"conversation"` (a conversation-scoped override that leaves the agent's default untouched); pass `scope: "agent"` to change the agent default. Changing reasoning effort without a model resolves the current model to rebuild provider-specific settings. The change takes effect on the next turn (the model is resolved per provider request).
143
143
 
144
144
  Tools currently receive `ctx.conversation.getHistory()` but not fork/send helpers. If a tool needs model-side follow-up, return information for the model to act on instead of starting a hidden run from the tool.
145
145
 
@@ -112,7 +112,7 @@ export default function activate(letta) {
112
112
 
113
113
  For commands with `runWhenBusy: true`, do not return `prompt` while the agent is running. Use the scoped conversation handle directly, update a panel/status if available, and return `{ type: "handled" }` quickly.
114
114
 
115
- Use `ctx.conversation` for conversation operations that should work across local and Constellation backends. The handle is bound to the active conversation and backend for that command invocation, so composed flows like fork-then-send stay on the same backend. Use `letta.client` only for server-specific API calls.
115
+ Use `ctx.conversation` for conversation operations that should work across local and Letta Cloud backends. The handle is bound to the active conversation and backend for that command invocation, so composed flows like fork-then-send stay on the same backend. Use `letta.client` only for server-specific API calls.
116
116
 
117
117
  Common pattern:
118
118
 
@@ -61,7 +61,7 @@ Lifecycle, turn, tool, compaction, and llm events are wired today.
61
61
 
62
62
  Lifecycle handlers are notification-only and should not return values. `turn_start` handlers can transform or cancel outbound user-message turns. `tool_start` handlers can transform the tool arguments before execution. Compaction and llm handlers are notification-only.
63
63
 
64
- `compact_start`/`compact_end` and `llm_start`/`llm_end` only fire on the **local backend**, where compaction and provider requests run client-side. On the constellation backend that work happens server-side and these events do not fire, so guard with `letta.capabilities.events.compact` / `letta.capabilities.events.llm` for portable mods.
64
+ `compact_start`/`compact_end` and `llm_start`/`llm_end` only fire on the **local backend**, where compaction and provider requests run client-side. On the Letta Cloud backend that work happens server-side and these events do not fire, so guard with `letta.capabilities.events.compact` / `letta.capabilities.events.llm` for portable mods.
65
65
 
66
66
  ## Supported events
67
67
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use provider mods when the user wants a **local agent** to use a model provider that is not built into `/connect` and `/model`.
4
4
 
5
- Important: provider mods are local-backend/local-agent only. They register local provider metadata for the TUI, headless local runtime, and desktop listener. They do not add providers for Constellation/cloud agents.
5
+ Important: provider mods are local-backend/local-agent only. They register local provider metadata for the TUI, headless local runtime, and desktop listener. They do not add providers for agents managed through the Letta API.
6
6
 
7
7
  For multi-capability mods that combine a provider with commands, tools, UI, or state, also read `architecture.md`.
8
8
 
@@ -14,19 +14,38 @@ how to answer questions about Letta correctly.
14
14
 
15
15
  1. **Self-inspection first for questions about THIS agent.** "What model are
16
16
  you using?", "what tools do you have?", "what's in your memory?" are
17
- questions about the running session, not the docs. Answer them from the
18
- live environment: your system prompt and agent info, `/status`-style
19
- command surfaces, settings files, and MemFS. Do not fetch docs for these.
20
- 2. **Fetch the docs index.** For product questions, fetch
21
- `https://docs.letta.com/llms.txt` a curated index of every current
22
- documentation page with descriptions. Pick the most relevant page URLs.
23
- 3. **Fetch the specific pages.** Append `/index.md` to any docs URL for the
24
- canonical LLM-friendly markdown version (e.g.
25
- `https://docs.letta.com/configuration/models/index.md`). Read the page,
26
- then answer. Cite the doc URL(s) you used so the user can go deeper.
27
- 4. **If the docs are unreachable**, say so explicitly, give your best answer,
28
- and clearly mark it as possibly out of date with a link to
29
- https://docs.letta.com. Never silently fall back to memory.
17
+ questions about the running session, not the docs. Load the
18
+ `self-configuration` skill for model or settings questions and use its
19
+ backend-aware active configuration report. Use the system prompt, agent
20
+ info, tool schemas, and MemFS for the other live facts. Do not infer active
21
+ state from recent/default preference lists, and do not fetch docs for these.
22
+ 2. **Fetch the docs index directly.** For product questions, run:
23
+
24
+ ```bash
25
+ node <SKILL_DIR>/scripts/fetch-letta-docs.mjs
26
+ ```
27
+
28
+ The helper retrieves `https://docs.letta.com/llms.txt` from the docs host,
29
+ verifies its ETag against the body, and prints the paths to a current local
30
+ copy and heading outline. Read the outline, then read the relevant index
31
+ lines to pick the best page URL.
32
+ 3. **Fetch the specific page directly.** Pass the exact canonical URL from the
33
+ index back to the same helper, for example:
34
+
35
+ ```bash
36
+ node <SKILL_DIR>/scripts/fetch-letta-docs.mjs \
37
+ --docs-url "https://docs.letta.com/configuration/models/index.md"
38
+ ```
39
+
40
+ Read the returned docs path before running the helper for another URL. The
41
+ helper uses native HTTPS with a curl fallback; do not use `fetch_webpage`
42
+ for the normal docs route because its upstream content cache may be stale.
43
+ Cite the public doc URL so the user can go deeper.
44
+ 4. **If direct retrieval fails**, use `fetch_webpage` only as a fallback with a
45
+ fresh query parameter on the same official docs URL. Disclose that the
46
+ fallback may be stale. If that also fails, say the docs are unreachable,
47
+ give your best answer, and clearly mark it as possibly out of date with a
48
+ link to https://docs.letta.com. Never silently fall back to memory.
30
49
 
31
50
  ## Hard rules
32
51
 
@@ -66,9 +85,9 @@ rather than leaving the user stuck.
66
85
 
67
86
  ## Caching
68
87
 
69
- Cache fetched pages under the Letta home directory: `~/.letta/docs-cache/`
70
- (Windows: `%USERPROFILE%\.letta\docs-cache\`). Resolve `~` to an absolute
71
- path before passing paths to file tools they do not expand it. Reuse cached
72
- pages within a session; refetch `llms.txt` when the cached copy is older than
73
- about a day.
88
+ The helper owns the cache. It uses the first writable temporary directory from
89
+ `TMPDIR`, `TEMP`, `TMP`, `/private/tmp`, or `/tmp`, and accepts `--cache-dir`
90
+ when an explicit location is needed. Every invocation checks the live ETag and
91
+ reuses the local document only when its body hash still matches. Do not create
92
+ or manage a second cache yourself.
74
93
 
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { createHash, randomBytes } from "node:crypto";
5
+ import { constants as fsConstants } from "node:fs";
6
+ import {
7
+ access,
8
+ mkdir,
9
+ readFile,
10
+ rename,
11
+ rm,
12
+ stat,
13
+ writeFile,
14
+ } from "node:fs/promises";
15
+ import path from "node:path";
16
+ import process from "node:process";
17
+ import { pathToFileURL } from "node:url";
18
+ import { promisify } from "node:util";
19
+
20
+ const DEFAULT_DOCS_URL = "https://docs.letta.com/llms.txt";
21
+ const CACHE_DIRECTORY_NAME = "letta-docs-cache";
22
+ const DOCUMENT_NAME = "letta-docs.md";
23
+ const OUTLINE_NAME = "letta-docs.outline.md";
24
+ const USER_AGENT = "letta-guide";
25
+ const runFile = promisify(execFile);
26
+
27
+ class DocsFetchError extends Error {
28
+ constructor(message, cause) {
29
+ super(message, cause ? { cause } : undefined);
30
+ this.name = "DocsFetchError";
31
+ }
32
+ }
33
+
34
+ function bodyDigest(body) {
35
+ return createHash("md5").update(body).digest("hex");
36
+ }
37
+
38
+ function hasProxyEnvironment() {
39
+ return ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"].some(
40
+ (key) => Boolean(process.env[key]),
41
+ );
42
+ }
43
+
44
+ function temporaryFile(directory, extension) {
45
+ return path.join(
46
+ directory,
47
+ `.letta-docs-${process.pid}-${Date.now()}-${randomBytes(5).toString("hex")}${extension}`,
48
+ );
49
+ }
50
+
51
+ function parseHeaderDump(raw) {
52
+ const responseBlocks = raw
53
+ .replace(/\r\n/g, "\n")
54
+ .trim()
55
+ .split(/\n\n+/)
56
+ .filter((block) => block.startsWith("HTTP/"));
57
+ const finalBlock = responseBlocks.at(-1);
58
+ if (!finalBlock) {
59
+ throw new DocsFetchError("curl returned no HTTP response headers.");
60
+ }
61
+
62
+ const [statusLine, ...lines] = finalBlock.split("\n");
63
+ const status = Number(/^HTTP\/\S+\s+(\d{3})/.exec(statusLine)?.[1]);
64
+ if (!Number.isInteger(status)) {
65
+ throw new DocsFetchError(
66
+ `curl returned an invalid status line: ${statusLine}`,
67
+ );
68
+ }
69
+
70
+ const headers = new Map();
71
+ for (const line of lines) {
72
+ const separator = line.indexOf(":");
73
+ if (separator < 1) continue;
74
+ headers.set(
75
+ line.slice(0, separator).trim().toLowerCase(),
76
+ line.slice(separator + 1).trim(),
77
+ );
78
+ }
79
+ return { headers, status };
80
+ }
81
+
82
+ async function curlRequest(url, method, cacheDirectory, timeoutMs) {
83
+ const headersFile = temporaryFile(cacheDirectory, ".headers");
84
+ const bodyFile = temporaryFile(cacheDirectory, ".body");
85
+ const executables =
86
+ process.platform === "win32" ? ["curl.exe", "curl"] : ["curl"];
87
+ const argumentsList = [
88
+ "--silent",
89
+ "--show-error",
90
+ "--location",
91
+ "--dump-header",
92
+ headersFile,
93
+ "--output",
94
+ bodyFile,
95
+ "--user-agent",
96
+ USER_AGENT,
97
+ "--max-time",
98
+ String(Math.max(1, Math.ceil(timeoutMs / 1000))),
99
+ ...(method === "HEAD" ? ["--head"] : ["--request", method]),
100
+ url,
101
+ ];
102
+
103
+ let failure;
104
+ for (const executable of executables) {
105
+ try {
106
+ await runFile(executable, argumentsList, { windowsHide: true });
107
+ const [rawHeaders, body] = await Promise.all([
108
+ readFile(headersFile, "utf8"),
109
+ readFile(bodyFile, "utf8"),
110
+ ]);
111
+ return { ...parseHeaderDump(rawHeaders), body };
112
+ } catch (error) {
113
+ failure = error;
114
+ if (error?.code !== "ENOENT") break;
115
+ } finally {
116
+ await Promise.all([
117
+ rm(headersFile, { force: true }),
118
+ rm(bodyFile, { force: true }),
119
+ ]);
120
+ }
121
+ }
122
+ throw new DocsFetchError(
123
+ failure?.code === "ENOENT"
124
+ ? "curl is unavailable in this environment."
125
+ : `curl could not ${method} ${url}.`,
126
+ failure,
127
+ );
128
+ }
129
+
130
+ async function nodeRequest(url, method, _cacheDirectory, timeoutMs) {
131
+ if (typeof fetch !== "function") {
132
+ throw new DocsFetchError("Native fetch requires Node.js 18 or newer.");
133
+ }
134
+ const controller = new AbortController();
135
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
136
+ try {
137
+ const response = await fetch(url, {
138
+ method,
139
+ headers: { "User-Agent": USER_AGENT },
140
+ redirect: "follow",
141
+ signal: controller.signal,
142
+ });
143
+ const headers = new Map();
144
+ response.headers.forEach((value, key) => {
145
+ headers.set(key.toLowerCase(), value);
146
+ });
147
+ return {
148
+ body: method === "HEAD" ? "" : await response.text(),
149
+ headers,
150
+ status: response.status,
151
+ };
152
+ } finally {
153
+ clearTimeout(timeout);
154
+ }
155
+ }
156
+
157
+ async function requestDocument(url, method, cacheDirectory, timeoutMs) {
158
+ const transports = hasProxyEnvironment()
159
+ ? [curlRequest, nodeRequest]
160
+ : [nodeRequest, curlRequest];
161
+ let failure;
162
+ for (const transport of transports) {
163
+ try {
164
+ const result = await transport(url, method, cacheDirectory, timeoutMs);
165
+ if (result.status < 200 || result.status >= 300) {
166
+ throw new DocsFetchError(
167
+ `${method} ${url} failed with HTTP ${result.status}.`,
168
+ );
169
+ }
170
+ return result;
171
+ } catch (error) {
172
+ failure = error;
173
+ }
174
+ }
175
+ throw new DocsFetchError(`${method} ${url} could not be fetched.`, failure);
176
+ }
177
+
178
+ function digestFromEtag(headers) {
179
+ const etag = headers.get("etag") ?? "";
180
+ const digest = /^(?:W\/)?"([a-f0-9]{32})"$/i.exec(etag)?.[1];
181
+ if (!digest) {
182
+ throw new DocsFetchError(
183
+ "Letta docs response is missing a content-MD5 ETag.",
184
+ );
185
+ }
186
+ return digest.toLowerCase();
187
+ }
188
+
189
+ async function nearestExistingDirectory(candidate) {
190
+ let current = path.resolve(candidate);
191
+ while (true) {
192
+ try {
193
+ return (await stat(current)).isDirectory() ? current : null;
194
+ } catch (error) {
195
+ if (error?.code !== "ENOENT") return null;
196
+ }
197
+ const parent = path.dirname(current);
198
+ if (parent === current) return null;
199
+ current = parent;
200
+ }
201
+ }
202
+
203
+ async function writableCacheDirectory(candidate) {
204
+ if (!candidate) return null;
205
+ const resolved = path.resolve(candidate);
206
+ try {
207
+ const existing = await stat(resolved);
208
+ if (!existing.isDirectory()) return null;
209
+ } catch (error) {
210
+ if (error?.code !== "ENOENT") return null;
211
+ }
212
+ const existingParent = await nearestExistingDirectory(resolved);
213
+ if (!existingParent) return null;
214
+ try {
215
+ await access(existingParent, fsConstants.W_OK | fsConstants.X_OK);
216
+ return resolved;
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ async function selectCacheDirectory(override) {
223
+ if (override) return writableCacheDirectory(override);
224
+ const candidates = [process.env.TMPDIR, process.env.TEMP, process.env.TMP]
225
+ .filter(Boolean)
226
+ .map((directory) => path.join(directory, CACHE_DIRECTORY_NAME));
227
+ if (process.platform !== "win32") {
228
+ candidates.push(
229
+ path.join("/private/tmp", CACHE_DIRECTORY_NAME),
230
+ path.join("/tmp", CACHE_DIRECTORY_NAME),
231
+ );
232
+ }
233
+ for (const candidate of new Set(candidates)) {
234
+ const usable = await writableCacheDirectory(candidate);
235
+ if (usable) return usable;
236
+ }
237
+ return null;
238
+ }
239
+
240
+ async function atomicWrite(destination, contents) {
241
+ const temporary = temporaryFile(
242
+ path.dirname(destination),
243
+ `.${path.basename(destination)}.tmp`,
244
+ );
245
+ await writeFile(temporary, contents, "utf8");
246
+ await rename(temporary, destination);
247
+ }
248
+
249
+ function createOutline(markdown) {
250
+ const lines = markdown.replace(/\r\n/g, "\n").split("\n");
251
+ if (lines.at(-1) === "") lines.pop();
252
+ const entries = [];
253
+ let fenced = false;
254
+ lines.forEach((line, index) => {
255
+ if (/^\s*(?:```|~~~)/.test(line)) {
256
+ fenced = !fenced;
257
+ return;
258
+ }
259
+ if (fenced) return;
260
+ const heading = /^(#{2,3})\s+(.+?)\s*$/.exec(line);
261
+ if (!heading) return;
262
+ entries.push({
263
+ level: heading[1].length,
264
+ title: heading[2]
265
+ .replace(/\s+#+\s*$/, "")
266
+ .replace(/\s+/g, " ")
267
+ .trim(),
268
+ start: index + 1,
269
+ end: lines.length,
270
+ });
271
+ });
272
+ entries.forEach((entry, index) => {
273
+ const nextPeer = entries
274
+ .slice(index + 1)
275
+ .find((candidate) => candidate.level <= entry.level);
276
+ if (nextPeer) entry.end = nextPeer.start - 1;
277
+ });
278
+
279
+ const lowestLevel = entries.length
280
+ ? Math.min(...entries.map((entry) => entry.level))
281
+ : 2;
282
+ const text = entries.length
283
+ ? entries
284
+ .map(
285
+ (entry) =>
286
+ `${" ".repeat(entry.level - lowestLevel)}- ${entry.title} (lines ${entry.start}-${entry.end})`,
287
+ )
288
+ .join("\n")
289
+ : "No markdown headings found.";
290
+ return {
291
+ headingCount: entries.length,
292
+ lineCount: lines.length,
293
+ markdown: `# Letta Docs Outline\n\n${text}\n`,
294
+ };
295
+ }
296
+
297
+ async function cachedBody(documentPath, expectedDigest) {
298
+ try {
299
+ const body = await readFile(documentPath, "utf8");
300
+ return bodyDigest(body) === expectedDigest ? body : null;
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+
306
+ async function fetchLettaDocs({
307
+ docsUrl = DEFAULT_DOCS_URL,
308
+ cacheDir,
309
+ timeoutMs = 30000,
310
+ } = {}) {
311
+ const selectedCache = await selectCacheDirectory(cacheDir);
312
+ if (!selectedCache) {
313
+ throw new DocsFetchError(
314
+ "No writable docs cache is available; pass --cache-dir to override.",
315
+ );
316
+ }
317
+ await mkdir(selectedCache, { recursive: true });
318
+
319
+ const documentPath = path.join(selectedCache, DOCUMENT_NAME);
320
+ const outlinePath = path.join(selectedCache, OUTLINE_NAME);
321
+ const head = await requestDocument(docsUrl, "HEAD", selectedCache, timeoutMs);
322
+ const expectedDigest = digestFromEtag(head.headers);
323
+ let body = await cachedBody(documentPath, expectedDigest);
324
+ const cacheStatus = body === null ? "updated" : "hit";
325
+
326
+ if (body === null) {
327
+ const get = await requestDocument(docsUrl, "GET", selectedCache, timeoutMs);
328
+ const getDigest = digestFromEtag(get.headers);
329
+ if (getDigest !== expectedDigest) {
330
+ throw new DocsFetchError(
331
+ `ETag changed between HEAD and GET for ${docsUrl}.`,
332
+ );
333
+ }
334
+ if (bodyDigest(get.body) !== expectedDigest) {
335
+ throw new DocsFetchError(
336
+ `ETag did not match the fetched body for ${docsUrl}.`,
337
+ );
338
+ }
339
+ body = get.body;
340
+ await atomicWrite(documentPath, body);
341
+ }
342
+
343
+ const outline = createOutline(body);
344
+ await atomicWrite(outlinePath, outline.markdown);
345
+ return {
346
+ outline: outline.markdown,
347
+ status: {
348
+ docsUrl,
349
+ etagMd5: expectedDigest,
350
+ fetchedMd5: bodyDigest(body),
351
+ contentMatchesEtag: true,
352
+ cacheStatus,
353
+ cacheDir: selectedCache,
354
+ docsPath: documentPath,
355
+ outlinePath,
356
+ checkedAt: new Date().toISOString(),
357
+ lineCount: outline.lineCount,
358
+ headingCount: outline.headingCount,
359
+ },
360
+ };
361
+ }
362
+
363
+ function parseArguments(argv) {
364
+ const result = {
365
+ docsUrl: DEFAULT_DOCS_URL,
366
+ cacheDir: undefined,
367
+ timeoutMs: 30000,
368
+ statusJson: false,
369
+ };
370
+ for (let index = 0; index < argv.length; index += 1) {
371
+ const value = argv[index];
372
+ if (value === "--docs-url") result.docsUrl = argv[++index];
373
+ else if (value === "--cache-dir") result.cacheDir = argv[++index];
374
+ else if (value === "--timeout-ms") result.timeoutMs = Number(argv[++index]);
375
+ else if (value === "--status-json") result.statusJson = true;
376
+ else throw new DocsFetchError(`Unknown argument: ${value}`);
377
+ }
378
+ if (!result.docsUrl) throw new DocsFetchError("--docs-url cannot be empty.");
379
+ if (!Number.isFinite(result.timeoutMs) || result.timeoutMs <= 0) {
380
+ throw new DocsFetchError("--timeout-ms must be a positive number.");
381
+ }
382
+ return result;
383
+ }
384
+
385
+ function outputFor(status, outline) {
386
+ return [
387
+ `Docs path: ${status.docsPath}`,
388
+ `Outline path: ${status.outlinePath}`,
389
+ status.cacheStatus === "hit"
390
+ ? "Docs status: local document was already current."
391
+ : "Docs status: local document was updated.",
392
+ "",
393
+ outline,
394
+ ].join("\n");
395
+ }
396
+
397
+ function errorChain(error) {
398
+ const messages = [];
399
+ let current = error;
400
+ while (current) {
401
+ messages.push(
402
+ current instanceof Error
403
+ ? `${current.name}: ${current.message}`
404
+ : String(current),
405
+ );
406
+ current = current?.cause;
407
+ }
408
+ return messages.join("\nCaused by: ");
409
+ }
410
+
411
+ async function main() {
412
+ const options = parseArguments(process.argv.slice(2));
413
+ const result = await fetchLettaDocs(options);
414
+ process.stdout.write(outputFor(result.status, result.outline));
415
+ if (options.statusJson) console.error(JSON.stringify(result.status));
416
+ }
417
+
418
+ if (
419
+ process.argv[1] &&
420
+ pathToFileURL(process.argv[1]).href === import.meta.url
421
+ ) {
422
+ main().catch((error) => {
423
+ console.error(`Error: ${error.message}`);
424
+ if (hasProxyEnvironment()) {
425
+ console.error(
426
+ "Hint: proxy variables are set, so curl is tried before native fetch.",
427
+ );
428
+ } else if (typeof fetch !== "function") {
429
+ console.error("Hint: install curl or use Node.js 18 or newer.");
430
+ } else if (process.platform === "win32") {
431
+ console.error("Hint: use a cache directory under %TEMP% or %TMP%.");
432
+ }
433
+ console.error("");
434
+ console.error("Details:");
435
+ console.error(errorChain(error));
436
+ process.exitCode = 1;
437
+ });
438
+ }
439
+
440
+ export { DEFAULT_DOCS_URL, createOutline, fetchLettaDocs };
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: self-configuration
3
- description: Modify Letta Code's own memory, model, context window, system prompt, compaction, permissions, toolsets, mods, skills, channels, schedules, and local runtime settings. Use when the user asks you to change how you behave or how the harness runs you.
3
+ description: Inspect or modify Letta Code's own memory, model, context window, system prompt, compaction, permissions, toolsets, mods, skills, channels, schedules, and local runtime settings. Use when the user asks how this agent or conversation is configured, or asks you to change how you behave or how the harness runs you.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -48,13 +48,28 @@ If a broken model or prompt prevents the agent from completing a turn, recover o
48
48
 
49
49
  Local settings, server state, and the current process are different sources of truth. Inspect the layer you intend to change before writing it.
50
50
 
51
- Start with the secret-safe local/runtime report:
51
+ Start with the authenticated, backend-aware active configuration report:
52
+
53
+ ```bash
54
+ letta agents config
55
+ ```
56
+
57
+ With no arguments it uses `AGENT_ID` and `CONVERSATION_ID` from the current session. To inspect an explicit scope:
58
+
59
+ ```bash
60
+ letta agents config --agent "$AGENT_ID"
61
+ letta agents config --conversation "$CONVERSATION_ID"
62
+ ```
63
+
64
+ The conversation form retrieves its parent agent automatically and reports both scopes plus the effective configured model. It works through the active API or local backend; do not read auth files, call REST directly, or decode local persistence paths yourself. A configured router handle such as `letta/auto` does not identify the underlying model selected for one inference.
65
+
66
+ Use the secret-safe local/runtime report for harness settings, permissions, and backend diagnostics:
52
67
 
53
68
  ```bash
54
69
  python3 <SKILL_DIR>/scripts/show_config.py --cwd "$PWD"
55
70
  ```
56
71
 
57
- Before changing server state, read the relevant scopes without printing full system prompts or credentials:
72
+ Before changing server state, the targeted helper can also read either scope without printing full system prompts or credentials:
58
73
 
59
74
  ```bash
60
75
  npx tsx <SKILL_DIR>/scripts/update-agent-settings.ts \
@@ -349,7 +364,7 @@ letta connect lmstudio --base-url http://127.0.0.1:1234/v1 --timeout 600s
349
364
  letta connect bedrock --method profile --profile "$AWS_PROFILE" --region "$AWS_REGION"
350
365
  ```
351
366
 
352
- Before connecting, verify whether the target agent/backend is API/Constellation or local. A provider saved to the wrong backend does not configure the current agent.
367
+ Before connecting, verify whether the target agent/backend is Letta Cloud or local. A provider saved to the wrong backend does not configure the current agent.
353
368
 
354
369
  Never print provider keys. Shell expansion such as `--api-key "$OPENAI_API_KEY"` still puts the resolved secret in process argv, where process listings may expose it. Prefer the command's interactive secret prompt in a trusted TTY. If no safer input path exists, stop for explicit user approval rather than passing a provider secret autonomously. Browser login, device-code confirmation, or account consent also requires human consent; do not claim success before it completes.
355
370