@letta-ai/letta-code 0.23.6 → 0.23.7

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 (2) hide show
  1. package/letta.js +636 -34
  2. package/package.json +1 -1
package/letta.js CHANGED
@@ -3269,7 +3269,7 @@ var package_default;
3269
3269
  var init_package = __esm(() => {
3270
3270
  package_default = {
3271
3271
  name: "@letta-ai/letta-code",
3272
- version: "0.23.6",
3272
+ version: "0.23.7",
3273
3273
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
3274
3274
  type: "module",
3275
3275
  bin: {
@@ -40138,6 +40138,357 @@ var init_types = __esm(() => {
40138
40138
  SUPPORTED_CHANNEL_IDS = ["telegram", "slack"];
40139
40139
  });
40140
40140
 
40141
+ // src/channels/interactive.ts
40142
+ function normalizeWhitespace(text) {
40143
+ return text.replace(/\s+/g, " ").trim();
40144
+ }
40145
+ function isAffirmativeResponse(text) {
40146
+ const normalized = normalizeWhitespace(text).toLowerCase();
40147
+ return [
40148
+ "approve",
40149
+ "approved",
40150
+ "allow",
40151
+ "yes",
40152
+ "y",
40153
+ "ok",
40154
+ "okay",
40155
+ "continue",
40156
+ "go ahead",
40157
+ "looks good",
40158
+ "lgtm",
40159
+ "sgtm",
40160
+ "ship it"
40161
+ ].includes(normalized);
40162
+ }
40163
+ function isNegativeResponse(text) {
40164
+ const normalized = normalizeWhitespace(text).toLowerCase();
40165
+ return [
40166
+ "deny",
40167
+ "denied",
40168
+ "reject",
40169
+ "rejected",
40170
+ "no",
40171
+ "n",
40172
+ "cancel",
40173
+ "skip",
40174
+ "keep planning"
40175
+ ].includes(normalized);
40176
+ }
40177
+ function stripApprovalPrefix(text) {
40178
+ return normalizeWhitespace(text.replace(/^(approve|allow|yes|y|ok|okay|deny|reject|no|n)\s*[:-]?\s*/i, ""));
40179
+ }
40180
+ function summarizeControlRequestInput(input) {
40181
+ const serialized = JSON.stringify(input, null, 2);
40182
+ if (!serialized || serialized === "{}") {
40183
+ return null;
40184
+ }
40185
+ if (serialized.length <= 1200) {
40186
+ return serialized;
40187
+ }
40188
+ return `${serialized.slice(0, 1197).trimEnd()}...`;
40189
+ }
40190
+ function summarizePlanPreview(planContent) {
40191
+ const normalized = planContent.trim();
40192
+ if (!normalized) {
40193
+ return "";
40194
+ }
40195
+ const maxLength = 1800;
40196
+ if (normalized.length <= maxLength) {
40197
+ return normalized;
40198
+ }
40199
+ return `${normalized.slice(0, maxLength).trimEnd()}
40200
+
40201
+ [Plan preview truncated for channel delivery.]`;
40202
+ }
40203
+ function buildQuestionPrompt(question, index) {
40204
+ const lines = [
40205
+ `${index + 1}. ${question.question ?? `Question ${index + 1}`}`
40206
+ ];
40207
+ const options = question.options ?? [];
40208
+ options.forEach((option, optionIndex) => {
40209
+ const label = option.label?.trim() || `Option ${optionIndex + 1}`;
40210
+ const description = option.description?.trim();
40211
+ lines.push(description ? ` ${optionIndex + 1}) ${label} — ${description}` : ` ${optionIndex + 1}) ${label}`);
40212
+ });
40213
+ if (question.multiSelect) {
40214
+ lines.push(" Choose one or more options. Separate multiple answers with commas.");
40215
+ }
40216
+ return lines;
40217
+ }
40218
+ function matchQuestionOption(question, text) {
40219
+ const trimmed = normalizeWhitespace(text);
40220
+ const options = question.options ?? [];
40221
+ if (!trimmed || options.length === 0) {
40222
+ return trimmed;
40223
+ }
40224
+ const numberMatch = trimmed.match(/^(\d+)$/);
40225
+ if (numberMatch?.[1]) {
40226
+ const option = options[Number(numberMatch[1]) - 1];
40227
+ if (option?.label?.trim()) {
40228
+ return option.label.trim();
40229
+ }
40230
+ }
40231
+ const exactLabel = options.find((option) => option.label && normalizeWhitespace(option.label).toLowerCase() === trimmed.toLowerCase());
40232
+ if (exactLabel?.label?.trim()) {
40233
+ return exactLabel.label.trim();
40234
+ }
40235
+ return trimmed;
40236
+ }
40237
+ function matchQuestionAnswer(question, text) {
40238
+ if (!question.multiSelect) {
40239
+ return matchQuestionOption(question, text);
40240
+ }
40241
+ const normalized = normalizeWhitespace(text);
40242
+ if (!normalized) {
40243
+ return normalized;
40244
+ }
40245
+ const selections = normalized.replace(/\band\b/gi, ",").split(/\s*(?:,|\/|;)\s*/).map((entry) => normalizeWhitespace(entry)).filter(Boolean);
40246
+ if (selections.length <= 1) {
40247
+ return matchQuestionOption(question, normalized);
40248
+ }
40249
+ const matchedSelections = Array.from(new Set(selections.map((selection) => matchQuestionOption(question, selection)).filter(Boolean)));
40250
+ return matchedSelections.length > 0 ? matchedSelections.join(", ") : normalized;
40251
+ }
40252
+ function parseNumberedAnswers(rawText, questions) {
40253
+ const matches = Array.from(rawText.matchAll(/(?:^|\n)\s*(\d+)[).:-]\s*(.+?)(?=(?:\n\s*\d+[).:-]\s*)|$)/gs));
40254
+ if (matches.length === 0) {
40255
+ return null;
40256
+ }
40257
+ const answers = {};
40258
+ for (const match of matches) {
40259
+ const questionIndex = Number(match[1]) - 1;
40260
+ const question = questions[questionIndex];
40261
+ const answerText = match[2]?.trim();
40262
+ if (!question?.question || !answerText) {
40263
+ continue;
40264
+ }
40265
+ answers[question.question] = matchQuestionAnswer(question, answerText);
40266
+ }
40267
+ return Object.keys(answers).length > 0 ? answers : null;
40268
+ }
40269
+ function buildAllowResponse(requestId, decision) {
40270
+ return {
40271
+ request_id: requestId,
40272
+ decision
40273
+ };
40274
+ }
40275
+ function buildDenyResponse(requestId, message) {
40276
+ return {
40277
+ request_id: requestId,
40278
+ decision: {
40279
+ behavior: "deny",
40280
+ message
40281
+ }
40282
+ };
40283
+ }
40284
+ function getAskUserQuestionInput(input) {
40285
+ return input;
40286
+ }
40287
+ function formatAskUserQuestionPrompt(event) {
40288
+ const input = getAskUserQuestionInput(event.input);
40289
+ const questions = (input.questions ?? []).filter((question) => normalizeWhitespace(question.question ?? ""));
40290
+ const lines = [
40291
+ "The agent needs an answer before it can continue.",
40292
+ "",
40293
+ ...questions.flatMap((question, index) => buildQuestionPrompt(question, index)),
40294
+ ""
40295
+ ];
40296
+ if (questions.length <= 1) {
40297
+ const singleQuestion = questions[0];
40298
+ lines.push(singleQuestion?.multiSelect ? "Reply with one or more option numbers/labels separated by commas, or just send a freeform answer in your next message." : "Reply with an option number/label, or just send a freeform answer in your next message.");
40299
+ } else {
40300
+ lines.push("Reply with numbered lines, for example:", "1: your answer", "2: your answer", "", "You can also use option numbers or option labels. For multi-select questions, separate multiple answers with commas.");
40301
+ }
40302
+ return lines.join(`
40303
+ `);
40304
+ }
40305
+ function formatEnterPlanModePrompt() {
40306
+ return [
40307
+ "The agent wants to enter plan mode before making changes.",
40308
+ "",
40309
+ "Reply `approve` to let it plan first, or reply `deny` to skip planning and continue normally."
40310
+ ].join(`
40311
+ `);
40312
+ }
40313
+ function formatExitPlanModePrompt(event) {
40314
+ const lines = [
40315
+ "The agent is ready to leave plan mode and start implementing."
40316
+ ];
40317
+ if (event.planContent?.trim()) {
40318
+ lines.push("", "Proposed plan:", summarizePlanPreview(event.planContent));
40319
+ if (event.planFilePath?.trim()) {
40320
+ lines.push("", `Plan file: ${event.planFilePath.trim()}`);
40321
+ }
40322
+ }
40323
+ lines.push("", "Reply `approve` to accept the plan and start coding.", "Reply with feedback instead if you want the agent to keep planning.");
40324
+ return lines.join(`
40325
+ `);
40326
+ }
40327
+ function formatGenericToolApprovalPrompt(event) {
40328
+ const inputSummary = summarizeControlRequestInput(event.input);
40329
+ const lines = [`The agent wants approval to run \`${event.toolName}\`.`];
40330
+ if (inputSummary) {
40331
+ lines.push("", "Tool input:", inputSummary);
40332
+ }
40333
+ lines.push("", "Reply `approve` to allow it.", "Reply with feedback instead if you want to deny it.");
40334
+ return lines.join(`
40335
+ `);
40336
+ }
40337
+ function formatChannelControlRequestPrompt(event) {
40338
+ switch (event.kind) {
40339
+ case "ask_user_question":
40340
+ return formatAskUserQuestionPrompt(event);
40341
+ case "enter_plan_mode":
40342
+ return formatEnterPlanModePrompt();
40343
+ case "exit_plan_mode":
40344
+ return formatExitPlanModePrompt(event);
40345
+ case "generic_tool_approval":
40346
+ return formatGenericToolApprovalPrompt(event);
40347
+ default: {
40348
+ const exhaustiveCheck = event.kind;
40349
+ return exhaustiveCheck;
40350
+ }
40351
+ }
40352
+ }
40353
+ function parseAskUserQuestionResponse(event, rawText) {
40354
+ const input = getAskUserQuestionInput(event.input);
40355
+ const questions = (input.questions ?? []).filter((question) => normalizeWhitespace(question.question ?? ""));
40356
+ if (questions.length === 0) {
40357
+ return {
40358
+ type: "reprompt",
40359
+ message: "I couldn't find the original question payload. Please ask the agent to try again."
40360
+ };
40361
+ }
40362
+ if (questions.length === 1) {
40363
+ const [question] = questions;
40364
+ if (!question?.question) {
40365
+ return {
40366
+ type: "reprompt",
40367
+ message: "I couldn't find the original question text. Please ask the agent to try again."
40368
+ };
40369
+ }
40370
+ const answer = matchQuestionAnswer(question, rawText);
40371
+ return {
40372
+ type: "response",
40373
+ response: buildAllowResponse(event.requestId, {
40374
+ behavior: "allow",
40375
+ updated_input: {
40376
+ ...event.input,
40377
+ answers: {
40378
+ ...input.answers ?? {},
40379
+ [question.question]: answer
40380
+ }
40381
+ }
40382
+ })
40383
+ };
40384
+ }
40385
+ const numberedAnswers = parseNumberedAnswers(rawText, questions);
40386
+ if (!numberedAnswers) {
40387
+ return {
40388
+ type: "reprompt",
40389
+ message: `Please answer with numbered lines so I can map each reply to the right question.
40390
+ Example:
40391
+ 1: your answer
40392
+ 2: your answer`
40393
+ };
40394
+ }
40395
+ const missingQuestions = questions.filter((question) => question.question && !Object.hasOwn(numberedAnswers, question.question));
40396
+ if (missingQuestions.length > 0) {
40397
+ return {
40398
+ type: "reprompt",
40399
+ message: `I still need answers for: ${missingQuestions.map((question) => question.question).join(", ")}`
40400
+ };
40401
+ }
40402
+ return {
40403
+ type: "response",
40404
+ response: buildAllowResponse(event.requestId, {
40405
+ behavior: "allow",
40406
+ updated_input: {
40407
+ ...event.input,
40408
+ answers: {
40409
+ ...input.answers ?? {},
40410
+ ...numberedAnswers
40411
+ }
40412
+ }
40413
+ })
40414
+ };
40415
+ }
40416
+ function parseEnterPlanModeResponse(event, rawText) {
40417
+ if (isAffirmativeResponse(rawText)) {
40418
+ return {
40419
+ type: "response",
40420
+ response: buildAllowResponse(event.requestId, {
40421
+ behavior: "allow"
40422
+ })
40423
+ };
40424
+ }
40425
+ if (isNegativeResponse(rawText)) {
40426
+ return {
40427
+ type: "response",
40428
+ response: buildDenyResponse(event.requestId, "User chose to skip plan mode and continue implementing directly.")
40429
+ };
40430
+ }
40431
+ return {
40432
+ type: "reprompt",
40433
+ message: "Reply `approve` to let the agent enter plan mode, or `deny` to skip planning."
40434
+ };
40435
+ }
40436
+ function parseExitPlanModeResponse(event, rawText) {
40437
+ if (isAffirmativeResponse(rawText)) {
40438
+ return {
40439
+ type: "response",
40440
+ response: buildAllowResponse(event.requestId, {
40441
+ behavior: "allow"
40442
+ })
40443
+ };
40444
+ }
40445
+ const feedback = stripApprovalPrefix(rawText);
40446
+ return {
40447
+ type: "response",
40448
+ response: buildDenyResponse(event.requestId, feedback || "Please keep planning and revise the proposal.")
40449
+ };
40450
+ }
40451
+ function parseGenericToolApprovalResponse(event, rawText) {
40452
+ if (isAffirmativeResponse(rawText)) {
40453
+ const message = stripApprovalPrefix(rawText);
40454
+ return {
40455
+ type: "response",
40456
+ response: buildAllowResponse(event.requestId, {
40457
+ behavior: "allow",
40458
+ ...message ? { message } : {}
40459
+ })
40460
+ };
40461
+ }
40462
+ const feedback = stripApprovalPrefix(rawText);
40463
+ return {
40464
+ type: "response",
40465
+ response: buildDenyResponse(event.requestId, feedback || "Denied by channel user.")
40466
+ };
40467
+ }
40468
+ function parseChannelControlRequestResponse(event, rawText) {
40469
+ const trimmed = rawText.trim();
40470
+ if (!trimmed) {
40471
+ return {
40472
+ type: "reprompt",
40473
+ message: formatChannelControlRequestPrompt(event)
40474
+ };
40475
+ }
40476
+ switch (event.kind) {
40477
+ case "ask_user_question":
40478
+ return parseAskUserQuestionResponse(event, trimmed);
40479
+ case "enter_plan_mode":
40480
+ return parseEnterPlanModeResponse(event, trimmed);
40481
+ case "exit_plan_mode":
40482
+ return parseExitPlanModeResponse(event, trimmed);
40483
+ case "generic_tool_approval":
40484
+ return parseGenericToolApprovalResponse(event, trimmed);
40485
+ default: {
40486
+ const exhaustiveCheck = event.kind;
40487
+ return exhaustiveCheck;
40488
+ }
40489
+ }
40490
+ }
40491
+
40141
40492
  // src/channels/telegram/media.ts
40142
40493
  import { randomUUID as randomUUID2 } from "node:crypto";
40143
40494
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
@@ -41121,6 +41472,13 @@ function createTelegramAdapter(config) {
41121
41472
  } : undefined;
41122
41473
  await telegramBot.api.sendMessage(chatId, text, reply_parameters ? { reply_parameters } : {});
41123
41474
  },
41475
+ async handleControlRequestEvent(event) {
41476
+ const telegramBot = await ensureBot();
41477
+ const reply_parameters = event.source.messageId || event.source.threadId ? {
41478
+ message_id: Number(event.source.threadId ?? event.source.messageId)
41479
+ } : undefined;
41480
+ await telegramBot.api.sendMessage(event.source.chatId, formatChannelControlRequestPrompt(event), reply_parameters ? { reply_parameters } : {});
41481
+ },
41124
41482
  onMessage: undefined
41125
41483
  };
41126
41484
  return adapter;
@@ -42280,6 +42638,16 @@ function createSlackAdapter(config) {
42280
42638
  });
42281
42639
  rememberMessageThread(response.ts, options?.replyToMessageId ?? response.ts ?? null);
42282
42640
  },
42641
+ async handleControlRequestEvent(event) {
42642
+ await ensureApp();
42643
+ const slackClient = await ensureWriteClient();
42644
+ const response = await slackClient.chat.postMessage({
42645
+ channel: event.source.chatId,
42646
+ text: formatChannelControlRequestPrompt(event),
42647
+ ...event.source.threadId ?? event.source.messageId ? { thread_ts: event.source.threadId ?? event.source.messageId } : {}
42648
+ });
42649
+ rememberMessageThread(response.ts, event.source.threadId ?? event.source.messageId ?? response.ts ?? null);
42650
+ },
42283
42651
  async prepareInboundMessage(msg, options) {
42284
42652
  if (!options?.isFirstRouteTurn || msg.channel !== "slack" || msg.chatType !== "channel" || !isNonEmptyString2(msg.threadId) || !isNonEmptyString2(msg.messageId)) {
42285
42653
  return msg;
@@ -43347,6 +43715,14 @@ function buildChannelTurnSource(route, msg) {
43347
43715
  conversationId: route.conversationId
43348
43716
  };
43349
43717
  }
43718
+ function getChannelApprovalScopeKey(params) {
43719
+ return [
43720
+ params.channel,
43721
+ params.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID,
43722
+ params.chatId,
43723
+ params.threadId ?? ""
43724
+ ].join(":");
43725
+ }
43350
43726
  function getChannelRegistry() {
43351
43727
  return instance;
43352
43728
  }
@@ -43364,7 +43740,10 @@ class ChannelRegistry {
43364
43740
  ready = false;
43365
43741
  messageHandler = null;
43366
43742
  eventHandler = null;
43743
+ approvalResponseHandler = null;
43367
43744
  buffer = [];
43745
+ pendingControlRequestsById = new Map;
43746
+ pendingControlRequestIdByScope = new Map;
43368
43747
  constructor() {
43369
43748
  if (instance) {
43370
43749
  throw new Error("ChannelRegistry is a singleton — use getChannelRegistry()");
@@ -43434,9 +43813,57 @@ class ChannelRegistry {
43434
43813
  setMessageHandler(handler) {
43435
43814
  this.messageHandler = handler;
43436
43815
  }
43816
+ setApprovalResponseHandler(handler) {
43817
+ this.approvalResponseHandler = handler;
43818
+ }
43437
43819
  setEventHandler(handler) {
43438
43820
  this.eventHandler = handler;
43439
43821
  }
43822
+ async registerPendingControlRequest(event) {
43823
+ const scopeKey = getChannelApprovalScopeKey({
43824
+ channel: event.source.channel,
43825
+ accountId: event.source.accountId,
43826
+ chatId: event.source.chatId,
43827
+ threadId: event.source.threadId
43828
+ });
43829
+ const existingRequestId = this.pendingControlRequestIdByScope.get(scopeKey);
43830
+ if (existingRequestId) {
43831
+ this.clearPendingControlRequest(existingRequestId);
43832
+ }
43833
+ this.pendingControlRequestsById.set(event.requestId, event);
43834
+ this.pendingControlRequestIdByScope.set(scopeKey, event.requestId);
43835
+ const adapter = this.getAdapter(event.source.channel, event.source.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID);
43836
+ if (!adapter) {
43837
+ return;
43838
+ }
43839
+ try {
43840
+ if (adapter.handleControlRequestEvent) {
43841
+ await adapter.handleControlRequestEvent(event);
43842
+ return;
43843
+ }
43844
+ await adapter.sendDirectReply(event.source.chatId, formatChannelControlRequestPrompt(event), {
43845
+ replyToMessageId: event.source.threadId ?? event.source.messageId
43846
+ });
43847
+ } catch (error) {
43848
+ console.error(`[Channels] Failed to deliver control request prompt for ${event.source.channel}/${event.source.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID}:`, error instanceof Error ? error.message : error);
43849
+ }
43850
+ }
43851
+ clearPendingControlRequest(requestId) {
43852
+ const pending = this.pendingControlRequestsById.get(requestId);
43853
+ if (!pending) {
43854
+ return;
43855
+ }
43856
+ this.pendingControlRequestsById.delete(requestId);
43857
+ const scopeKey = getChannelApprovalScopeKey({
43858
+ channel: pending.source.channel,
43859
+ accountId: pending.source.accountId,
43860
+ chatId: pending.source.chatId,
43861
+ threadId: pending.source.threadId
43862
+ });
43863
+ if (this.pendingControlRequestIdByScope.get(scopeKey) === requestId) {
43864
+ this.pendingControlRequestIdByScope.delete(scopeKey);
43865
+ }
43866
+ }
43440
43867
  setReady() {
43441
43868
  this.ready = true;
43442
43869
  this.flushBuffer();
@@ -43528,6 +43955,7 @@ class ChannelRegistry {
43528
43955
  this.ready = false;
43529
43956
  this.messageHandler = null;
43530
43957
  this.eventHandler = null;
43958
+ this.approvalResponseHandler = null;
43531
43959
  }
43532
43960
  async stopAll() {
43533
43961
  for (const adapter of Array.from(this.adapters.values())) {
@@ -43538,13 +43966,63 @@ class ChannelRegistry {
43538
43966
  this.ready = false;
43539
43967
  this.messageHandler = null;
43540
43968
  this.eventHandler = null;
43969
+ this.approvalResponseHandler = null;
43970
+ this.pendingControlRequestsById.clear();
43971
+ this.pendingControlRequestIdByScope.clear();
43541
43972
  instance = null;
43542
43973
  }
43974
+ async tryHandlePendingControlRequest(adapter, msg) {
43975
+ const scopeKey = getChannelApprovalScopeKey({
43976
+ channel: msg.channel,
43977
+ accountId: msg.accountId,
43978
+ chatId: msg.chatId,
43979
+ threadId: msg.threadId
43980
+ });
43981
+ const requestId = this.pendingControlRequestIdByScope.get(scopeKey);
43982
+ if (!requestId) {
43983
+ return false;
43984
+ }
43985
+ const pending = this.pendingControlRequestsById.get(requestId);
43986
+ if (!pending) {
43987
+ this.pendingControlRequestIdByScope.delete(scopeKey);
43988
+ return false;
43989
+ }
43990
+ const parsed = parseChannelControlRequestResponse(pending, msg.text);
43991
+ if (parsed.type === "reprompt") {
43992
+ await adapter.sendDirectReply(msg.chatId, parsed.message, {
43993
+ replyToMessageId: msg.threadId ?? msg.messageId
43994
+ });
43995
+ return true;
43996
+ }
43997
+ if (!this.approvalResponseHandler) {
43998
+ await adapter.sendDirectReply(msg.chatId, "I’m reconnecting to Letta Code right now, so I couldn’t use that reply yet. Please send it again in a moment.", {
43999
+ replyToMessageId: msg.threadId ?? msg.messageId
44000
+ });
44001
+ return true;
44002
+ }
44003
+ const handled = await this.approvalResponseHandler({
44004
+ runtime: {
44005
+ agent_id: pending.source.agentId,
44006
+ conversation_id: pending.source.conversationId
44007
+ },
44008
+ response: parsed.response
44009
+ });
44010
+ this.clearPendingControlRequest(requestId);
44011
+ if (!handled) {
44012
+ await adapter.sendDirectReply(msg.chatId, "That approval prompt expired before I could use your reply. Please ask the agent to try again.", {
44013
+ replyToMessageId: msg.threadId ?? msg.messageId
44014
+ });
44015
+ }
44016
+ return true;
44017
+ }
43543
44018
  async handleInboundMessage(msg) {
43544
44019
  const accountId = msg.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
43545
44020
  const adapter = this.getAdapter(msg.channel, accountId);
43546
44021
  if (!adapter)
43547
44022
  return;
44023
+ if (await this.tryHandlePendingControlRequest(adapter, msg)) {
44024
+ return;
44025
+ }
43548
44026
  const config = getChannelAccount(msg.channel, accountId);
43549
44027
  if (!config)
43550
44028
  return;
@@ -52599,6 +53077,7 @@ function createConversationRuntime(listener, agentId, conversationId) {
52599
53077
  key: runtimeKey,
52600
53078
  agentId: normalizedAgentId,
52601
53079
  conversationId: normalizedConversationId,
53080
+ activeChannelTurnSources: null,
52602
53081
  messageQueue: Promise.resolve(),
52603
53082
  pendingApprovalResolvers: new Map,
52604
53083
  recoveredApprovalState: null,
@@ -77282,13 +77761,58 @@ function writeCronFile(data) {
77282
77761
  writeFileSync14(tmp, JSON.stringify(data, null, 2), { flush: true });
77283
77762
  renameSync2(tmp, path20);
77284
77763
  }
77285
- function isProcessAlive(pid) {
77764
+ function readLinuxProcessIdentity(pid) {
77765
+ try {
77766
+ const stat5 = readFileSync15(`/proc/${pid}/stat`, "utf8");
77767
+ const endCommand = stat5.lastIndexOf(")");
77768
+ if (endCommand === -1) {
77769
+ return null;
77770
+ }
77771
+ const fields = stat5.slice(endCommand + 2).trim().split(/\s+/);
77772
+ const startTicks = fields[19] ?? null;
77773
+ if (!startTicks) {
77774
+ return null;
77775
+ }
77776
+ let bootId = null;
77777
+ try {
77778
+ bootId = readFileSync15("/proc/sys/kernel/random/boot_id", "utf8").trim() || null;
77779
+ } catch {}
77780
+ return { startTicks, bootId };
77781
+ } catch {
77782
+ return null;
77783
+ }
77784
+ }
77785
+ function readProcessIdentity(pid) {
77786
+ if (readProcessIdentityOverride) {
77787
+ return readProcessIdentityOverride(pid);
77788
+ }
77789
+ return readLinuxProcessIdentity(pid);
77790
+ }
77791
+ function captureProcessIdentity(pid) {
77792
+ const identity = readProcessIdentity(pid);
77793
+ return {
77794
+ process_start_ticks: identity?.startTicks ?? null,
77795
+ boot_id: identity?.bootId ?? null
77796
+ };
77797
+ }
77798
+ function isProcessAlive(pid, owner) {
77286
77799
  try {
77287
77800
  process.kill(pid, 0);
77288
- return true;
77289
77801
  } catch {
77290
77802
  return false;
77291
77803
  }
77804
+ if (owner) {
77805
+ const identity = readProcessIdentity(pid);
77806
+ if (identity) {
77807
+ if (owner.boot_id && identity.bootId && owner.boot_id !== identity.bootId) {
77808
+ return false;
77809
+ }
77810
+ if (owner.process_start_ticks && identity.startTicks && owner.process_start_ticks !== identity.startTicks) {
77811
+ return false;
77812
+ }
77813
+ }
77814
+ }
77815
+ return true;
77292
77816
  }
77293
77817
  function readLockOwner(lockDir) {
77294
77818
  try {
@@ -77311,7 +77835,7 @@ function isLockStale(lockDir) {
77311
77835
  return true;
77312
77836
  }
77313
77837
  }
77314
- const pidDead = !isProcessAlive(owner.pid);
77838
+ const pidDead = !isProcessAlive(owner.pid, owner);
77315
77839
  const isOld = Date.now() - owner.acquired_at > LOCK_STALE_AGE_MS;
77316
77840
  return pidDead && isOld;
77317
77841
  }
@@ -77330,7 +77854,8 @@ function acquireLock() {
77330
77854
  const owner = {
77331
77855
  pid: process.pid,
77332
77856
  token,
77333
- acquired_at: Date.now()
77857
+ acquired_at: Date.now(),
77858
+ ...captureProcessIdentity(process.pid)
77334
77859
  };
77335
77860
  writeLockOwner(lockDir, owner);
77336
77861
  return {
@@ -77435,7 +77960,7 @@ function addTask(input) {
77435
77960
  data.tasks.push(task2);
77436
77961
  writeCronFile(data);
77437
77962
  let warning;
77438
- if (!data.scheduler_owner || !isProcessAlive(data.scheduler_owner.pid)) {
77963
+ if (!data.scheduler_owner || !isProcessAlive(data.scheduler_owner.pid, data.scheduler_owner)) {
77439
77964
  warning = "No letta server is currently running. This task will only execute when a WS listener is active.";
77440
77965
  }
77441
77966
  return { task: task2, warning };
@@ -77483,15 +78008,17 @@ function claimSchedulerLease() {
77483
78008
  const data = readCronFile();
77484
78009
  const token = randomBytes(4).toString("hex");
77485
78010
  if (data.scheduler_owner) {
77486
- const { pid, token: existingToken } = data.scheduler_owner;
77487
- if (isProcessAlive(pid)) {
78011
+ const existingOwner = data.scheduler_owner;
78012
+ const { pid, token: existingToken } = existingOwner;
78013
+ if (isProcessAlive(pid, existingOwner)) {
77488
78014
  throw new Error(`Scheduler lease held by PID ${pid} (token ${existingToken}). Cannot claim.`);
77489
78015
  }
77490
78016
  }
77491
78017
  data.scheduler_owner = {
77492
78018
  pid: process.pid,
77493
78019
  token,
77494
- started_at: new Date().toISOString()
78020
+ started_at: new Date().toISOString(),
78021
+ ...captureProcessIdentity(process.pid)
77495
78022
  };
77496
78023
  writeCronFile(data);
77497
78024
  return token;
@@ -77551,7 +78078,7 @@ function getCronFileMtime() {
77551
78078
  return 0;
77552
78079
  }
77553
78080
  }
77554
- var CRON_FILE_NAME = "crons.json", LOCK_DIR_NAME = "crons.lock", LOCK_TOKEN_FILE = "owner.json", LOCK_TIMEOUT_MS = 5000, LOCK_RETRY_MS = 50, LOCK_STALE_AGE_MS = 30000, MAX_ACTIVE_TASKS_PER_AGENT = 50, TASK_ID_BYTES = 4, GC_AGE_MS;
78081
+ var CRON_FILE_NAME = "crons.json", LOCK_DIR_NAME = "crons.lock", LOCK_TOKEN_FILE = "owner.json", LOCK_TIMEOUT_MS = 5000, LOCK_RETRY_MS = 50, LOCK_STALE_AGE_MS = 30000, MAX_ACTIVE_TASKS_PER_AGENT = 50, TASK_ID_BYTES = 4, GC_AGE_MS, readProcessIdentityOverride = null;
77555
78082
  var init_cronFile = __esm(() => {
77556
78083
  init_parseInterval();
77557
78084
  GC_AGE_MS = 24 * 60 * 60 * 1000;
@@ -81837,6 +82364,18 @@ var MIN_SPLIT_LENGTH = 1500;
81837
82364
  function isInteractiveApprovalTool(toolName) {
81838
82365
  return INTERACTIVE_APPROVAL_TOOLS.has(toolName);
81839
82366
  }
82367
+ function getInteractiveApprovalKind(toolName) {
82368
+ switch (toolName) {
82369
+ case "AskUserQuestion":
82370
+ return "ask_user_question";
82371
+ case "EnterPlanMode":
82372
+ return "enter_plan_mode";
82373
+ case "ExitPlanMode":
82374
+ return "exit_plan_mode";
82375
+ default:
82376
+ return null;
82377
+ }
82378
+ }
81840
82379
  function isHeadlessAutoAllowTool(toolName) {
81841
82380
  return HEADLESS_AUTO_ALLOW_TOOLS.has(toolName);
81842
82381
  }
@@ -88493,7 +89032,46 @@ var init_send = __esm(async () => {
88493
89032
  });
88494
89033
 
88495
89034
  // src/websocket/listener/turn-approval.ts
89035
+ import { readFile as readFile11 } from "node:fs/promises";
88496
89036
  import WebSocket2 from "ws";
89037
+ function getChannelApprovalSourceScopeKey(source) {
89038
+ return [
89039
+ source.channel,
89040
+ source.accountId ?? "",
89041
+ source.chatId,
89042
+ source.threadId ?? ""
89043
+ ].join(":");
89044
+ }
89045
+ function resolveChannelApprovalSource(runtime) {
89046
+ const sources = runtime.activeChannelTurnSources ?? [];
89047
+ if (sources.length === 0) {
89048
+ return null;
89049
+ }
89050
+ const sourcesByScope = new Map;
89051
+ for (const source of sources) {
89052
+ sourcesByScope.set(getChannelApprovalSourceScopeKey(source), source);
89053
+ }
89054
+ if (sourcesByScope.size !== 1) {
89055
+ return null;
89056
+ }
89057
+ return [...sourcesByScope.values()].at(-1) ?? null;
89058
+ }
89059
+ async function maybeReadPlanPreview(toolName, turnPermissionModeState) {
89060
+ if (toolName !== "ExitPlanMode" || !turnPermissionModeState.planFilePath) {
89061
+ return {};
89062
+ }
89063
+ try {
89064
+ const planContent = await readFile11(turnPermissionModeState.planFilePath, "utf8");
89065
+ return {
89066
+ planFilePath: turnPermissionModeState.planFilePath,
89067
+ planContent
89068
+ };
89069
+ } catch {
89070
+ return {
89071
+ planFilePath: turnPermissionModeState.planFilePath
89072
+ };
89073
+ }
89074
+ }
88497
89075
  async function handleApprovalStop(params) {
88498
89076
  const {
88499
89077
  approvals,
@@ -88631,6 +89209,18 @@ async function handleApprovalStop(params) {
88631
89209
  agent_id: agentId,
88632
89210
  conversation_id: conversationId
88633
89211
  };
89212
+ const registry = getChannelRegistry();
89213
+ const channelSource = resolveChannelApprovalSource(runtime);
89214
+ if (registry && channelSource) {
89215
+ await registry.registerPendingControlRequest({
89216
+ requestId,
89217
+ kind: getInteractiveApprovalKind(ac.approval.toolName) ?? "generic_tool_approval",
89218
+ source: channelSource,
89219
+ toolName: ac.approval.toolName,
89220
+ input: ac.parsedArgs,
89221
+ ...await maybeReadPlanPreview(ac.approval.toolName, turnPermissionModeState)
89222
+ });
89223
+ }
88634
89224
  let responseBody;
88635
89225
  try {
88636
89226
  responseBody = await requestApprovalOverWS(runtime, socket, requestId, controlRequest);
@@ -88639,6 +89229,8 @@ async function handleApprovalStop(params) {
88639
89229
  return interruptTermination();
88640
89230
  }
88641
89231
  throw error;
89232
+ } finally {
89233
+ registry?.clearPendingControlRequest(requestId);
88642
89234
  }
88643
89235
  if (shouldInterrupt()) {
88644
89236
  return interruptTermination();
@@ -88841,6 +89433,7 @@ async function handleApprovalStop(params) {
88841
89433
  };
88842
89434
  }
88843
89435
  var init_turn_approval = __esm(async () => {
89436
+ init_registry();
88844
89437
  init_diffPreview();
88845
89438
  init_interactivePolicy();
88846
89439
  init_skill_injection();
@@ -90684,6 +91277,7 @@ async function drainQueuedMessages(runtime, socket, opts, processQueuedTurn) {
90684
91277
  }
90685
91278
  let turnError;
90686
91279
  let didThrow = false;
91280
+ runtime.activeChannelTurnSources = channelTurnSources;
90687
91281
  try {
90688
91282
  await processQueuedTurn(queuedTurn, dequeuedBatch);
90689
91283
  } catch (error) {
@@ -90691,6 +91285,7 @@ async function drainQueuedMessages(runtime, socket, opts, processQueuedTurn) {
90691
91285
  turnError = error instanceof Error ? error.message : String(error);
90692
91286
  throw error;
90693
91287
  } finally {
91288
+ runtime.activeChannelTurnSources = null;
90694
91289
  if (channelTurnSources.length > 0) {
90695
91290
  await dispatchChannelTurnLifecycleEvent({
90696
91291
  type: "finished",
@@ -93471,6 +94066,13 @@ function wireChannelIngress(listener, socket, opts, processQueuedTurn) {
93471
94066
  }
93472
94067
  scheduleQueuePump(conversationRuntime, socket, opts, processQueuedTurn);
93473
94068
  });
94069
+ registry.setApprovalResponseHandler(async ({ runtime, response }) => handleApprovalResponseInput(listener, {
94070
+ runtime,
94071
+ response,
94072
+ socket,
94073
+ opts,
94074
+ processQueuedTurn
94075
+ }));
93474
94076
  registry.setEventHandler((event) => {
93475
94077
  handleChannelRegistryEvent(event, socket, listener);
93476
94078
  });
@@ -94380,8 +94982,8 @@ async function connectWithRetry(runtime, opts, attempt = 0, startTime = Date.now
94380
94982
  console.log(`[Listen] Received read_file command: path=${parsed.path}, request_id=${parsed.request_id}`);
94381
94983
  runDetachedListenerTask("read_file", async () => {
94382
94984
  try {
94383
- const { readFile: readFile11 } = await import("node:fs/promises");
94384
- const content = await readFile11(parsed.path, "utf-8");
94985
+ const { readFile: readFile12 } = await import("node:fs/promises");
94986
+ const content = await readFile12(parsed.path, "utf-8");
94385
94987
  console.log(`[Listen] read_file success: ${parsed.path} (${content.length} bytes)`);
94386
94988
  safeSocketSend(socket, {
94387
94989
  type: "read_file_response",
@@ -94411,10 +95013,10 @@ async function connectWithRetry(runtime, opts, attempt = 0, startTime = Date.now
94411
95013
  try {
94412
95014
  const { edit: edit2 } = await Promise.resolve().then(() => (init_Edit2(), exports_Edit));
94413
95015
  const { write: write2 } = await Promise.resolve().then(() => (init_Write2(), exports_Write));
94414
- const { readFile: readFile11 } = await import("node:fs/promises");
95016
+ const { readFile: readFile12 } = await import("node:fs/promises");
94415
95017
  let currentContent = null;
94416
95018
  try {
94417
- currentContent = await readFile11(parsed.path, "utf-8");
95019
+ currentContent = await readFile12(parsed.path, "utf-8");
94418
95020
  } catch (readErr) {
94419
95021
  const e = readErr;
94420
95022
  if (e.code !== "ENOENT")
@@ -94522,7 +95124,7 @@ async function connectWithRetry(runtime, opts, attempt = 0, startTime = Date.now
94522
95124
  console.log(`[Listen] Received edit_file command: file_path=${parsed.file_path}, request_id=${parsed.request_id}`);
94523
95125
  runDetachedListenerTask("edit_file", async () => {
94524
95126
  try {
94525
- const { readFile: readFile11 } = await import("node:fs/promises");
95127
+ const { readFile: readFile12 } = await import("node:fs/promises");
94526
95128
  const { edit: edit2 } = await Promise.resolve().then(() => (init_Edit2(), exports_Edit));
94527
95129
  console.log(`[Listen] Executing edit: old_string="${parsed.old_string.slice(0, 50)}${parsed.old_string.length > 50 ? "..." : ""}"`);
94528
95130
  const result = await edit2({
@@ -94538,7 +95140,7 @@ async function connectWithRetry(runtime, opts, attempt = 0, startTime = Date.now
94538
95140
  }
94539
95141
  if (result.replacements > 0) {
94540
95142
  try {
94541
- const contentAfter = await readFile11(parsed.file_path, "utf-8");
95143
+ const contentAfter = await readFile12(parsed.file_path, "utf-8");
94542
95144
  safeSocketSend(socket, {
94543
95145
  type: "file_ops",
94544
95146
  path: parsed.file_path,
@@ -95453,7 +96055,7 @@ __export(exports_skills2, {
95453
96055
  GLOBAL_SKILLS_DIR: () => GLOBAL_SKILLS_DIR2
95454
96056
  });
95455
96057
  import { existsSync as existsSync28 } from "node:fs";
95456
- import { readdir as readdir8, readFile as readFile11, realpath as realpath4, stat as stat7 } from "node:fs/promises";
96058
+ import { readdir as readdir8, readFile as readFile12, realpath as realpath4, stat as stat7 } from "node:fs/promises";
95457
96059
  import { dirname as dirname13, join as join35 } from "node:path";
95458
96060
  import { fileURLToPath as fileURLToPath8 } from "node:url";
95459
96061
  function getBundledSkillsPath2() {
@@ -95584,7 +96186,7 @@ async function findSkillFiles2(currentPath, rootPath, skills, errors, source, vi
95584
96186
  }
95585
96187
  }
95586
96188
  async function parseSkillFile2(filePath, rootPath, source) {
95587
- const content = await readFile11(filePath, "utf-8");
96189
+ const content = await readFile12(filePath, "utf-8");
95588
96190
  const { frontmatter, body } = parseFrontmatter(content);
95589
96191
  const normalizedRoot = rootPath.endsWith("/") ? rootPath.slice(0, -1) : rootPath;
95590
96192
  const relativePath = filePath.slice(normalizedRoot.length + 1);
@@ -95643,7 +96245,7 @@ __export(exports_fs, {
95643
96245
  writeJsonFile: () => writeJsonFile,
95644
96246
  writeFile: () => writeFile10,
95645
96247
  readJsonFile: () => readJsonFile,
95646
- readFile: () => readFile12,
96248
+ readFile: () => readFile13,
95647
96249
  mkdir: () => mkdir9,
95648
96250
  exists: () => exists2
95649
96251
  });
@@ -95654,7 +96256,7 @@ import {
95654
96256
  mkdirSync as mkdirSync21
95655
96257
  } from "node:fs";
95656
96258
  import { dirname as dirname14 } from "node:path";
95657
- async function readFile12(path24) {
96259
+ async function readFile13(path24) {
95658
96260
  return fsReadFileSync2(path24, { encoding: "utf-8" });
95659
96261
  }
95660
96262
  async function writeFile10(path24, content) {
@@ -95671,7 +96273,7 @@ async function mkdir9(path24, options) {
95671
96273
  mkdirSync21(path24, options);
95672
96274
  }
95673
96275
  async function readJsonFile(path24) {
95674
- const text = await readFile12(path24);
96276
+ const text = await readFile13(path24);
95675
96277
  return JSON.parse(text);
95676
96278
  }
95677
96279
  async function writeJsonFile(path24, data, options) {
@@ -97510,7 +98112,7 @@ __export(exports_import, {
97510
98112
  extractSkillsFromAf: () => extractSkillsFromAf
97511
98113
  });
97512
98114
  import { createReadStream } from "node:fs";
97513
- import { chmod, mkdir as mkdir10, readFile as readFile13, writeFile as writeFile11 } from "node:fs/promises";
98115
+ import { chmod, mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
97514
98116
  import { dirname as dirname15, resolve as resolve28 } from "node:path";
97515
98117
  async function importAgentFromFile(options) {
97516
98118
  const client = await getClient();
@@ -97543,7 +98145,7 @@ async function importAgentFromFile(options) {
97543
98145
  }
97544
98146
  async function extractSkillsFromAf(afPath, destDir) {
97545
98147
  const extracted = [];
97546
- const content = await readFile13(afPath, "utf-8");
98148
+ const content = await readFile14(afPath, "utf-8");
97547
98149
  const afData = JSON.parse(content);
97548
98150
  if (!afData.skills || !Array.isArray(afData.skills)) {
97549
98151
  return [];
@@ -124440,7 +125042,7 @@ __export(exports_custom, {
124440
125042
  COMMANDS_DIR: () => COMMANDS_DIR
124441
125043
  });
124442
125044
  import { existsSync as existsSync36 } from "node:fs";
124443
- import { readdir as readdir10, readFile as readFile14 } from "node:fs/promises";
125045
+ import { readdir as readdir10, readFile as readFile15 } from "node:fs/promises";
124444
125046
  import { basename as basename11, dirname as dirname17, join as join44 } from "node:path";
124445
125047
  async function getCustomCommands() {
124446
125048
  if (cachedCommands !== null) {
@@ -124499,7 +125101,7 @@ async function findCommandFiles(currentPath, rootPath, commands2, source2) {
124499
125101
  } catch (_error) {}
124500
125102
  }
124501
125103
  async function parseCommandFile(filePath, rootPath, source2) {
124502
- const content = await readFile14(filePath, "utf-8");
125104
+ const content = await readFile15(filePath, "utf-8");
124503
125105
  const { frontmatter, body } = parseFrontmatter(content);
124504
125106
  const id = basename11(filePath, ".md");
124505
125107
  const relativePath = dirname17(filePath).slice(rootPath.length);
@@ -137231,7 +137833,7 @@ var init_PersonalitySelector = __esm(async () => {
137231
137833
  });
137232
137834
 
137233
137835
  // src/utils/aws-credentials.ts
137234
- import { readFile as readFile15 } from "node:fs/promises";
137836
+ import { readFile as readFile16 } from "node:fs/promises";
137235
137837
  import { homedir as homedir36 } from "node:os";
137236
137838
  import { join as join48 } from "node:path";
137237
137839
  async function parseAwsCredentials() {
@@ -137239,11 +137841,11 @@ async function parseAwsCredentials() {
137239
137841
  const configPath = join48(homedir36(), ".aws", "config");
137240
137842
  const profiles = new Map;
137241
137843
  try {
137242
- const content = await readFile15(credentialsPath, "utf-8");
137844
+ const content = await readFile16(credentialsPath, "utf-8");
137243
137845
  parseIniFile(content, profiles, false);
137244
137846
  } catch {}
137245
137847
  try {
137246
- const content = await readFile15(configPath, "utf-8");
137848
+ const content = await readFile16(configPath, "utf-8");
137247
137849
  parseIniFile(content, profiles, true);
137248
137850
  } catch {}
137249
137851
  return Array.from(profiles.values());
@@ -143908,7 +144510,7 @@ var exports_export = {};
143908
144510
  __export(exports_export, {
143909
144511
  packageSkills: () => packageSkills
143910
144512
  });
143911
- import { readdir as readdir11, readFile as readFile16 } from "node:fs/promises";
144513
+ import { readdir as readdir11, readFile as readFile17 } from "node:fs/promises";
143912
144514
  import { relative as relative17, resolve as resolve32 } from "node:path";
143913
144515
  async function packageSkills(agentId, skillsDir) {
143914
144516
  const skills = [];
@@ -143929,7 +144531,7 @@ async function packageSkills(agentId, skillsDir) {
143929
144531
  const skillDir = resolve32(baseDir, entry.name);
143930
144532
  const skillMdPath = resolve32(skillDir, "SKILL.md");
143931
144533
  try {
143932
- await readFile16(skillMdPath, "utf-8");
144534
+ await readFile17(skillMdPath, "utf-8");
143933
144535
  } catch {
143934
144536
  console.warn(`Skipping invalid skill ${entry.name}: missing SKILL.md`);
143935
144537
  continue;
@@ -143961,7 +144563,7 @@ async function readSkillFiles(skillDir) {
143961
144563
  if (entry.isDirectory()) {
143962
144564
  await walk(fullPath);
143963
144565
  } else {
143964
- const content = await readFile16(fullPath, "utf-8");
144566
+ const content = await readFile17(fullPath, "utf-8");
143965
144567
  const relativePath = relative17(skillDir, fullPath).replace(/\\/g, "/");
143966
144568
  files[relativePath] = content;
143967
144569
  }
@@ -154739,7 +155341,7 @@ __export(exports_import2, {
154739
155341
  extractSkillsFromAf: () => extractSkillsFromAf2
154740
155342
  });
154741
155343
  import { createReadStream as createReadStream2 } from "node:fs";
154742
- import { chmod as chmod2, mkdir as mkdir11, readFile as readFile17, writeFile as writeFile12 } from "node:fs/promises";
155344
+ import { chmod as chmod2, mkdir as mkdir11, readFile as readFile18, writeFile as writeFile12 } from "node:fs/promises";
154743
155345
  import { dirname as dirname20, resolve as resolve33 } from "node:path";
154744
155346
  async function importAgentFromFile2(options) {
154745
155347
  const client = await getClient();
@@ -154772,7 +155374,7 @@ async function importAgentFromFile2(options) {
154772
155374
  }
154773
155375
  async function extractSkillsFromAf2(afPath, destDir) {
154774
155376
  const extracted = [];
154775
- const content = await readFile17(afPath, "utf-8");
155377
+ const content = await readFile18(afPath, "utf-8");
154776
155378
  const afData = JSON.parse(content);
154777
155379
  if (!afData.skills || !Array.isArray(afData.skills)) {
154778
155380
  return [];
@@ -164230,4 +164832,4 @@ Error during initialization: ${message}`);
164230
164832
  }
164231
164833
  main();
164232
164834
 
164233
- //# debugId=B587994115421B1064756E2164756E21
164835
+ //# debugId=13707EE7EA6B933364756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.23.6",
3
+ "version": "0.23.7",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {