@letta-ai/letta-code 0.28.13 → 0.28.14

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/letta.js CHANGED
@@ -4853,7 +4853,7 @@ var package_default;
4853
4853
  var init_package = __esm(() => {
4854
4854
  package_default = {
4855
4855
  name: "@letta-ai/letta-code",
4856
- version: "0.28.13",
4856
+ version: "0.28.14",
4857
4857
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
4858
4858
  type: "module",
4859
4859
  packageManager: "bun@1.3.0",
@@ -159124,6 +159124,8 @@ Creating crons:
159124
159124
  - Recurring monitoring/heartbeat: \`letta cron add --name <short-name> --description <description> --prompt <future-message> --every "2h"\` or \`--cron "0 9 * * *"\`
159125
159125
  Always include \`--name\`, \`--description\`, and \`--prompt\`. \`$AGENT_ID\` is automatically injected into the shell environment, and \`letta cron\` uses it by default, so you do not need to specify which agent to invoke unless overriding the current agent intentionally.
159126
159126
 
159127
+ Where crons run: for cloud agents, schedules default to durable Cloud schedules that fire from the cloud and execute in your cloud sandbox — they survive local shutdown, so this is the right default. If the scheduled work must run on a specific computer (e.g. it needs that computer's filesystem or local services), add \`--computer <deviceId>\` (from \`letta environments list\`) to keep the durable Cloud schedule but execute on that computer, with sandbox fallback if it is offline. Use \`--runner local\` only when that fallback is unacceptable; local schedules only fire while a Letta session is running on that computer.
159128
+
159127
159129
  # Harness Architecture
159128
159130
 
159129
159131
  You run within the Letta Code CLI on some machine (the environment). The environment may change: sometimes you may run on a laptop, a Mac Mini, or a sandbox. Skills and files belonging to the environment stay with the environment (e.g. \`AGENTS.md\` or \`.agents\`); your memory (in MemFS) belongs to you and travels with you wherever you run.
@@ -166511,6 +166513,12 @@ function cloneAccount(account) {
166511
166513
  ...account,
166512
166514
  allowedUsers: [...account.allowedUsers]
166513
166515
  };
166516
+ if (account.adminUsers) {
166517
+ cloned.adminUsers = [...account.adminUsers];
166518
+ }
166519
+ if (account.userAllowedCommands) {
166520
+ cloned.userAllowedCommands = [...account.userAllowedCommands];
166521
+ }
166514
166522
  if (isTelegramChannelAccount(account)) {
166515
166523
  cloned.binding = { ...account.binding };
166516
166524
  }
@@ -166882,9 +166890,12 @@ var init_accounts = __esm(() => {
166882
166890
  init_types7();
166883
166891
  SNAKE_TO_CAMEL = {
166884
166892
  account_uuid: "accountUuid",
166893
+ admin_users: "adminUsers",
166885
166894
  allowed_channels: "allowedChannels",
166886
166895
  allowed_groups: "allowedGroups",
166887
166896
  allow_bots: "allowBots",
166897
+ group_policy: "groupPolicy",
166898
+ user_allowed_commands: "userAllowedCommands",
166888
166899
  auto_thread_on_mention: "autoThreadOnMention",
166889
166900
  base_url: "baseUrl",
166890
166901
  acknowledge_message_reaction: "acknowledgeMessageReaction",
@@ -167334,7 +167345,551 @@ var init_account_display2 = __esm(() => {
167334
167345
  init_utils5();
167335
167346
  });
167336
167347
 
167337
- // src/channels/feedback.ts
167348
+ // src/channels/pairing.ts
167349
+ var exports_pairing = {};
167350
+ __export(exports_pairing, {
167351
+ rollbackPairingApproval: () => rollbackPairingApproval,
167352
+ removePairingStateForAccount: () => removePairingStateForAccount,
167353
+ loadPairingStore: () => loadPairingStore,
167354
+ isUserApproved: () => isUserApproved,
167355
+ getPendingPairings: () => getPendingPairings,
167356
+ getApprovedUsers: () => getApprovedUsers,
167357
+ createPairingCode: () => createPairingCode,
167358
+ consumePairingCode: () => consumePairingCode,
167359
+ clearPairingStores: () => clearPairingStores,
167360
+ __testOverrideSavePairingStore: () => __testOverrideSavePairingStore,
167361
+ __testOverrideLoadPairingStore: () => __testOverrideLoadPairingStore
167362
+ });
167363
+ import { randomInt } from "node:crypto";
167364
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
167365
+ function normalizeAccountId(accountId) {
167366
+ return accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
167367
+ }
167368
+ function getStore2(channelId) {
167369
+ let store = stores2.get(channelId);
167370
+ if (!store) {
167371
+ store = { pending: [], approved: [] };
167372
+ stores2.set(channelId, store);
167373
+ }
167374
+ return store;
167375
+ }
167376
+ function loadPairingStore(channelId) {
167377
+ if (loadPairingStoreOverride) {
167378
+ const overridden = loadPairingStoreOverride(channelId);
167379
+ if (overridden === null) {
167380
+ return;
167381
+ }
167382
+ stores2.set(channelId, {
167383
+ pending: [...overridden.pending],
167384
+ approved: [...overridden.approved]
167385
+ });
167386
+ return;
167387
+ }
167388
+ const path5 = getChannelPairingPath(channelId);
167389
+ if (!existsSync8(path5))
167390
+ return;
167391
+ try {
167392
+ const text = readFileSync7(path5, "utf-8");
167393
+ const parsed = JSON.parse(text);
167394
+ stores2.set(channelId, {
167395
+ pending: parsed.pending ?? [],
167396
+ approved: parsed.approved ?? []
167397
+ });
167398
+ } catch {}
167399
+ }
167400
+ function savePairingStore(channelId) {
167401
+ const store = getStore2(channelId);
167402
+ if (savePairingStoreOverride) {
167403
+ savePairingStoreOverride(channelId, {
167404
+ pending: [...store.pending],
167405
+ approved: [...store.approved]
167406
+ });
167407
+ return;
167408
+ }
167409
+ const dir = getChannelDir(channelId);
167410
+ mkdirSync5(dir, { recursive: true });
167411
+ writeFileSync3(getChannelPairingPath(channelId), `${JSON.stringify(store, null, 2)}
167412
+ `, "utf-8");
167413
+ }
167414
+ function generateCode(length = 6) {
167415
+ let code2 = "";
167416
+ for (let i2 = 0;i2 < length; i2++) {
167417
+ code2 += CODE_CHARS[randomInt(CODE_CHARS.length)];
167418
+ }
167419
+ return code2;
167420
+ }
167421
+ function isUserApproved(channelId, userId, accountId) {
167422
+ const store = getStore2(channelId);
167423
+ const normalizedAccountId = normalizeAccountId(accountId);
167424
+ return store.approved.some((u) => u.senderId === userId && normalizeAccountId(u.accountId) === normalizedAccountId);
167425
+ }
167426
+ function createPairingCode(channelId, userId, chatId, username, accountId) {
167427
+ const store = getStore2(channelId);
167428
+ const normalizedAccountId = normalizeAccountId(accountId);
167429
+ const nowMs = Date.now();
167430
+ const existing = store.pending.find((p) => p.senderId === userId && normalizeAccountId(p.accountId) === normalizedAccountId && new Date(p.expiresAt).getTime() > nowMs);
167431
+ if (existing) {
167432
+ return existing.code;
167433
+ }
167434
+ store.pending = store.pending.filter((p) => !(p.senderId === userId && normalizeAccountId(p.accountId) === normalizedAccountId));
167435
+ const now = Date.now();
167436
+ store.pending = store.pending.filter((p) => new Date(p.expiresAt).getTime() > now);
167437
+ while (store.pending.length >= MAX_PENDING_CODES) {
167438
+ store.pending.shift();
167439
+ }
167440
+ const code2 = generateCode();
167441
+ const pending = {
167442
+ accountId: normalizedAccountId,
167443
+ code: code2,
167444
+ senderId: userId,
167445
+ senderName: username,
167446
+ chatId,
167447
+ createdAt: new Date().toISOString(),
167448
+ expiresAt: new Date(now + PAIRING_CODE_TTL_MS).toISOString()
167449
+ };
167450
+ store.pending.push(pending);
167451
+ savePairingStore(channelId);
167452
+ return code2;
167453
+ }
167454
+ function consumePairingCode(channelId, code2, accountId) {
167455
+ const store = getStore2(channelId);
167456
+ const upperCode = code2.toUpperCase();
167457
+ const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId(accountId);
167458
+ const matches = store.pending.map((pending2, index2) => ({ pending: pending2, index: index2 })).filter(({ pending: pending2 }) => pending2.code === upperCode && (normalizedAccountId === undefined || normalizeAccountId(pending2.accountId) === normalizedAccountId));
167459
+ if (matches.length > 1) {
167460
+ return null;
167461
+ }
167462
+ const index = matches[0]?.index ?? -1;
167463
+ if (index === -1)
167464
+ return null;
167465
+ const pending = store.pending[index];
167466
+ const pendingAccountId = normalizeAccountId(pending.accountId);
167467
+ if (new Date(pending.expiresAt).getTime() < Date.now()) {
167468
+ store.pending.splice(index, 1);
167469
+ savePairingStore(channelId);
167470
+ return null;
167471
+ }
167472
+ store.pending.splice(index, 1);
167473
+ if (!store.approved.some((u) => u.senderId === pending.senderId && normalizeAccountId(u.accountId) === pendingAccountId)) {
167474
+ const approved = {
167475
+ accountId: pendingAccountId,
167476
+ senderId: pending.senderId,
167477
+ senderName: pending.senderName,
167478
+ approvedAt: new Date().toISOString()
167479
+ };
167480
+ store.approved.push(approved);
167481
+ }
167482
+ savePairingStore(channelId);
167483
+ return pending;
167484
+ }
167485
+ function getPendingPairings(channelId, accountId) {
167486
+ const store = getStore2(channelId);
167487
+ const now = Date.now();
167488
+ const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId(accountId);
167489
+ return store.pending.filter((p) => new Date(p.expiresAt).getTime() > now && (normalizedAccountId === undefined || normalizeAccountId(p.accountId) === normalizedAccountId));
167490
+ }
167491
+ function getApprovedUsers(channelId, accountId) {
167492
+ const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId(accountId);
167493
+ return getStore2(channelId).approved.filter((user) => normalizedAccountId === undefined || normalizeAccountId(user.accountId) === normalizedAccountId);
167494
+ }
167495
+ function rollbackPairingApproval(channelId, pending) {
167496
+ const store = getStore2(channelId);
167497
+ const normalizedAccountId = normalizeAccountId(pending.accountId);
167498
+ store.approved = store.approved.filter((u) => !(u.senderId === pending.senderId && normalizeAccountId(u.accountId) === normalizedAccountId));
167499
+ store.pending.push(pending);
167500
+ savePairingStore(channelId);
167501
+ }
167502
+ function removePairingStateForAccount(channelId, accountId) {
167503
+ const store = getStore2(channelId);
167504
+ const normalizedAccountId = normalizeAccountId(accountId);
167505
+ const nextPending = store.pending.filter((pending) => normalizeAccountId(pending.accountId) !== normalizedAccountId);
167506
+ const nextApproved = store.approved.filter((approved) => normalizeAccountId(approved.accountId) !== normalizedAccountId);
167507
+ const pendingRemoved = store.pending.length - nextPending.length;
167508
+ const approvedRemoved = store.approved.length - nextApproved.length;
167509
+ if (pendingRemoved === 0 && approvedRemoved === 0) {
167510
+ return { pendingRemoved, approvedRemoved };
167511
+ }
167512
+ store.pending = nextPending;
167513
+ store.approved = nextApproved;
167514
+ savePairingStore(channelId);
167515
+ return { pendingRemoved, approvedRemoved };
167516
+ }
167517
+ function clearPairingStores() {
167518
+ stores2.clear();
167519
+ }
167520
+ function __testOverrideLoadPairingStore(fn) {
167521
+ loadPairingStoreOverride = fn;
167522
+ }
167523
+ function __testOverrideSavePairingStore(fn) {
167524
+ savePairingStoreOverride = fn;
167525
+ }
167526
+ var PAIRING_CODE_TTL_MS, MAX_PENDING_CODES = 50, CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789", stores2, loadPairingStoreOverride = null, savePairingStoreOverride = null;
167527
+ var init_pairing = __esm(() => {
167528
+ init_accounts();
167529
+ init_config2();
167530
+ PAIRING_CODE_TTL_MS = 15 * 60 * 1000;
167531
+ stores2 = new Map;
167532
+ });
167533
+
167534
+ // src/channels/signal/target.ts
167535
+ function assertSignalHttpProtocol(url2) {
167536
+ if (url2.protocol !== "http:" && url2.protocol !== "https:") {
167537
+ throw new Error(`Signal base URL protocol must be http or https, got ${url2.protocol}`);
167538
+ }
167539
+ }
167540
+ function trimPrefix(value, prefix) {
167541
+ if (!value.toLowerCase().startsWith(prefix)) {
167542
+ return null;
167543
+ }
167544
+ return value.slice(prefix.length).trim();
167545
+ }
167546
+ function normalizeSignalBaseUrl(input) {
167547
+ const trimmed = input.trim();
167548
+ if (!trimmed) {
167549
+ return "";
167550
+ }
167551
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
167552
+ const withScheme = hasScheme ? trimmed : `http://${trimmed}`;
167553
+ const parsed = new URL(withScheme);
167554
+ assertSignalHttpProtocol(parsed);
167555
+ if (parsed.username || parsed.password) {
167556
+ throw new Error("Signal base URL must not include credentials.");
167557
+ }
167558
+ return withScheme.replace(/\/+$/, "");
167559
+ }
167560
+ function parseSignalTarget(input) {
167561
+ const trimmed = input.trim();
167562
+ if (!trimmed) {
167563
+ throw new Error("Signal target is required.");
167564
+ }
167565
+ const signalTarget = trimPrefix(trimmed, "signal:");
167566
+ const value = signalTarget !== null ? signalTarget : trimmed;
167567
+ if (!value) {
167568
+ throw new Error("Signal target is required.");
167569
+ }
167570
+ const groupId = trimPrefix(value, "group:");
167571
+ if (groupId) {
167572
+ return { kind: "group", groupId };
167573
+ }
167574
+ const username = trimPrefix(value, "username:");
167575
+ if (username) {
167576
+ return { kind: "username", username };
167577
+ }
167578
+ const usernameAlias = trimPrefix(value, "u:");
167579
+ if (usernameAlias) {
167580
+ return { kind: "username", username: `u:${usernameAlias}` };
167581
+ }
167582
+ const recipient = value;
167583
+ if (!recipient) {
167584
+ throw new Error("Signal recipient is required.");
167585
+ }
167586
+ return { kind: "recipient", recipient };
167587
+ }
167588
+ function signalTargetToSendRpcParams(target2) {
167589
+ switch (target2.kind) {
167590
+ case "group":
167591
+ return { groupId: target2.groupId };
167592
+ case "username":
167593
+ return { username: [target2.username] };
167594
+ case "recipient":
167595
+ return { recipient: [target2.recipient] };
167596
+ }
167597
+ }
167598
+ function signalTargetToReactionRpcParams(target2) {
167599
+ switch (target2.kind) {
167600
+ case "group":
167601
+ return { groupIds: [target2.groupId] };
167602
+ case "recipient":
167603
+ return { recipients: [target2.recipient] };
167604
+ case "username":
167605
+ throw new Error("Signal reactions require a recipient or group target.");
167606
+ }
167607
+ }
167608
+ function normalizeSignalSenderId(value) {
167609
+ if (!value) {
167610
+ return "";
167611
+ }
167612
+ const trimmed = value.trim();
167613
+ if (!trimmed) {
167614
+ return "";
167615
+ }
167616
+ return trimmed.toLowerCase();
167617
+ }
167618
+ function normalizeSignalPhone(value) {
167619
+ if (!value) {
167620
+ return "";
167621
+ }
167622
+ const trimmed = value.trim();
167623
+ if (!trimmed) {
167624
+ return "";
167625
+ }
167626
+ const withoutPrefix = trimmed.toLowerCase().startsWith("signal:") ? trimmed.slice("signal:".length) : trimmed;
167627
+ return withoutPrefix.replace(/[^0-9+]/g, "");
167628
+ }
167629
+ function signalAllowedUsersIncludes(allowedUsers, senderId) {
167630
+ const normalizedSender = normalizeSignalSenderId(senderId);
167631
+ const senderPhone = normalizeSignalPhone(senderId);
167632
+ return allowedUsers.some((entry) => {
167633
+ const normalizedEntry = normalizeSignalSenderId(entry);
167634
+ if (normalizedEntry === normalizedSender) {
167635
+ return true;
167636
+ }
167637
+ const entryPhone = normalizeSignalPhone(entry);
167638
+ return !!senderPhone && !!entryPhone && senderPhone === entryPhone;
167639
+ });
167640
+ }
167641
+ function isSignalGroupAllowed(allowedGroups, groupId) {
167642
+ if (!allowedGroups || allowedGroups.length === 0) {
167643
+ return true;
167644
+ }
167645
+ const normalized = groupId.trim();
167646
+ return allowedGroups.some((entry) => entry.trim() === normalized);
167647
+ }
167648
+ function matchesSignalMentionPatterns(text, mentionPatterns) {
167649
+ const normalizedText = text.toLowerCase();
167650
+ return (mentionPatterns ?? []).map((entry) => entry.trim().toLowerCase()).filter(Boolean).some((entry) => normalizedText.includes(entry));
167651
+ }
167652
+ var init_target2 = () => {};
167653
+
167654
+ // src/channels/whatsapp/jid.ts
167655
+ function stripDeviceSuffix(jid) {
167656
+ if (!jid)
167657
+ return "";
167658
+ return jid.replace(/:\d+(@|$)/, "$1");
167659
+ }
167660
+ function isLidJid(jid) {
167661
+ return !!jid && stripDeviceSuffix(jid).endsWith(WHATSAPP_LID_SUFFIX);
167662
+ }
167663
+ function isGroupJid(jid) {
167664
+ return !!jid && stripDeviceSuffix(jid).endsWith(WHATSAPP_GROUP_SUFFIX);
167665
+ }
167666
+ function isStatusOrBroadcastJid(jid) {
167667
+ if (!jid)
167668
+ return true;
167669
+ const normalized = stripDeviceSuffix(jid);
167670
+ return normalized === "status@broadcast" || normalized.endsWith("@broadcast") || normalized.endsWith("@newsletter");
167671
+ }
167672
+ function jidToDigits(jid) {
167673
+ if (!jid)
167674
+ return "";
167675
+ const base2 = stripDeviceSuffix(jid).split("@")[0] ?? "";
167676
+ return base2.replace(/\D/g, "");
167677
+ }
167678
+ function normalizePhoneLike(value) {
167679
+ if (!value)
167680
+ return "";
167681
+ return jidToDigits(value.trim());
167682
+ }
167683
+ function phoneDigitsToJid(phoneDigits) {
167684
+ const digits = normalizePhoneLike(phoneDigits);
167685
+ return digits ? `${digits}${WHATSAPP_PHONE_SUFFIX}` : "";
167686
+ }
167687
+ function normalizeMaybePhoneJid(value) {
167688
+ if (!value)
167689
+ return null;
167690
+ const trimmed = value.trim();
167691
+ if (!trimmed)
167692
+ return null;
167693
+ if (isLidJid(trimmed))
167694
+ return null;
167695
+ if (trimmed.includes("@"))
167696
+ return stripDeviceSuffix(trimmed);
167697
+ return phoneDigitsToJid(trimmed) || null;
167698
+ }
167699
+ function isSelfChat(remoteJid, selfPhoneJid, selfLid) {
167700
+ const remote = stripDeviceSuffix(remoteJid);
167701
+ if (!remote)
167702
+ return false;
167703
+ const phone = stripDeviceSuffix(selfPhoneJid);
167704
+ if (phone && remote === phone)
167705
+ return true;
167706
+ const lid = stripDeviceSuffix(selfLid);
167707
+ if (lid && remote === lid)
167708
+ return true;
167709
+ return false;
167710
+ }
167711
+ function senderIdFromJid(jid) {
167712
+ return jidToDigits(jid);
167713
+ }
167714
+ function allowedUsersIncludes(allowedUsers, senderId) {
167715
+ const normalizedSender = normalizePhoneLike(senderId);
167716
+ return allowedUsers.some((entry) => normalizePhoneLike(entry) === normalizedSender);
167717
+ }
167718
+ function resolveLidToPhoneJid(params) {
167719
+ const { lidJid, message, sock } = params;
167720
+ if (!isLidJid(lidJid))
167721
+ return normalizeMaybePhoneJid(lidJid);
167722
+ const msg = message;
167723
+ const senderPn = normalizeMaybePhoneJid(msg?.key?.senderPn ?? undefined);
167724
+ if (senderPn)
167725
+ return senderPn;
167726
+ const repo = sock?.signalRepository;
167727
+ const mapped3 = normalizeMaybePhoneJid(repo?.lidMapping?.get(stripDeviceSuffix(lidJid)));
167728
+ if (mapped3)
167729
+ return mapped3;
167730
+ return null;
167731
+ }
167732
+ function resolveSendJid(params) {
167733
+ const { chatId, selfPhoneJid, selfLid, lidToJid, sock } = params;
167734
+ if (!isLidJid(chatId))
167735
+ return stripDeviceSuffix(chatId);
167736
+ const normalized = stripDeviceSuffix(chatId);
167737
+ if (selfLid && normalized === stripDeviceSuffix(selfLid) && selfPhoneJid) {
167738
+ return stripDeviceSuffix(selfPhoneJid);
167739
+ }
167740
+ const mapped3 = normalizeMaybePhoneJid(lidToJid?.get(normalized));
167741
+ if (mapped3)
167742
+ return mapped3;
167743
+ const repo = sock?.signalRepository;
167744
+ const signalMapped = normalizeMaybePhoneJid(repo?.lidMapping?.get(normalized));
167745
+ if (signalMapped)
167746
+ return signalMapped;
167747
+ throw new Error(`Cannot send to unresolved WhatsApp LID: ${chatId}`);
167748
+ }
167749
+ function sanitizePathSegment(input) {
167750
+ const cleaned = input.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^_+|_+$/g, "");
167751
+ return cleaned || "whatsapp";
167752
+ }
167753
+ var WHATSAPP_PHONE_SUFFIX = "@s.whatsapp.net", WHATSAPP_LID_SUFFIX = "@lid", WHATSAPP_GROUP_SUFFIX = "@g.us";
167754
+
167755
+ // src/channels/access-control.ts
167756
+ function resolveChannelAccessScope(chatType) {
167757
+ return chatType === "channel" ? "group" : "dm";
167758
+ }
167759
+ function channelEnvKey(channelId, suffix) {
167760
+ const normalized = channelId.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
167761
+ return `LETTA_${normalized}_${suffix}`;
167762
+ }
167763
+ function parseUserList(raw) {
167764
+ if (!raw)
167765
+ return [];
167766
+ return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
167767
+ }
167768
+ function isEnvFlagEnabled(raw) {
167769
+ const normalized = (raw ?? "").trim().toLowerCase();
167770
+ return normalized === "1" || normalized === "true" || normalized === "yes";
167771
+ }
167772
+ function getChannelEnvAllowedUsers(channelId) {
167773
+ return [
167774
+ ...parseUserList(process.env[GLOBAL_ALLOWED_USERS_ENV]),
167775
+ ...parseUserList(process.env[channelEnvKey(channelId, "ALLOWED_USERS")])
167776
+ ];
167777
+ }
167778
+ function getChannelEnvAdminUsers(channelId) {
167779
+ return [
167780
+ ...parseUserList(process.env[GLOBAL_ADMIN_USERS_ENV]),
167781
+ ...parseUserList(process.env[channelEnvKey(channelId, "ADMIN_USERS")])
167782
+ ];
167783
+ }
167784
+ function isChannelAllowAllUsersEnabled(channelId) {
167785
+ return isEnvFlagEnabled(process.env[GLOBAL_ALLOW_ALL_ENV]) || isEnvFlagEnabled(process.env[channelEnvKey(channelId, "ALLOW_ALL_USERS")]);
167786
+ }
167787
+ function effectiveAllowedUsers(account, channelId) {
167788
+ return [
167789
+ ...account.allowedUsers,
167790
+ ...account.adminUsers ?? [],
167791
+ ...getChannelEnvAllowedUsers(channelId),
167792
+ ...getChannelEnvAdminUsers(channelId)
167793
+ ];
167794
+ }
167795
+ function allowlistMatches(channelId, allowed, senderId) {
167796
+ if (allowed.includes("*")) {
167797
+ return true;
167798
+ }
167799
+ if (!senderId) {
167800
+ return false;
167801
+ }
167802
+ if (channelId === "whatsapp") {
167803
+ return allowedUsersIncludes(allowed, senderId);
167804
+ }
167805
+ if (channelId === "signal") {
167806
+ return signalAllowedUsersIncludes(allowed, senderId);
167807
+ }
167808
+ return allowed.includes(senderId);
167809
+ }
167810
+ function isPairedApproved(channelId, senderId, accountId) {
167811
+ if (isUserApproved(channelId, senderId, accountId)) {
167812
+ return true;
167813
+ }
167814
+ loadPairingStore(channelId);
167815
+ return isUserApproved(channelId, senderId, accountId);
167816
+ }
167817
+ function evaluateChannelSenderAccess(input) {
167818
+ const { account, channelId, senderId, chatType } = input;
167819
+ if (isChannelAllowAllUsersEnabled(channelId)) {
167820
+ return "allow";
167821
+ }
167822
+ const allowed = effectiveAllowedUsers(account, channelId);
167823
+ if (allowlistMatches(channelId, allowed, senderId)) {
167824
+ return "allow";
167825
+ }
167826
+ const envAllowlistConfigured = getChannelEnvAllowedUsers(channelId).length > 0;
167827
+ const scope = resolveChannelAccessScope(chatType);
167828
+ const restrictive = envAllowlistConfigured || (scope === "group" ? (account.groupPolicy ?? "open") === "allowlist" : account.dmPolicy !== "open");
167829
+ if (!restrictive) {
167830
+ return "allow";
167831
+ }
167832
+ if (senderId && isPairedApproved(channelId, senderId, account.accountId)) {
167833
+ return "allow";
167834
+ }
167835
+ if (scope === "group") {
167836
+ return "deny";
167837
+ }
167838
+ if (account.dmPolicy === "pairing" && !envAllowlistConfigured) {
167839
+ return channelId === "slack" ? "allow" : "pair";
167840
+ }
167841
+ return "deny";
167842
+ }
167843
+ function buildChannelAccessDeniedMessage(channelId) {
167844
+ const noun = ACCESS_DENIED_NOUNS[channelId] ?? "bot";
167845
+ return `You are not on the allowed users list for this ${noun}.`;
167846
+ }
167847
+ function floorOnlyChannelCommandGate() {
167848
+ return {
167849
+ enabled: true,
167850
+ isAdmin: false,
167851
+ allowedCommands: [],
167852
+ pairingPending: true
167853
+ };
167854
+ }
167855
+ function normalizeCommandName(name) {
167856
+ return name.trim().replace(/^\/+/, "").toLowerCase();
167857
+ }
167858
+ function canonicalizeChannelCommandName(name) {
167859
+ const normalized = normalizeCommandName(name);
167860
+ return normalized === "reflect" ? "reflection" : normalized;
167861
+ }
167862
+ function resolveChannelCommandGate(params) {
167863
+ const admins = [
167864
+ ...params.account.adminUsers ?? [],
167865
+ ...getChannelEnvAdminUsers(params.channelId)
167866
+ ].filter((entry) => entry.length > 0);
167867
+ const enabled = admins.length > 0;
167868
+ const allowedCommands = (params.account.userAllowedCommands ?? []).map(canonicalizeChannelCommandName).filter((name) => name.length > 0);
167869
+ return {
167870
+ enabled,
167871
+ isAdmin: !enabled || allowlistMatches(params.channelId, admins, params.senderId),
167872
+ allowedCommands
167873
+ };
167874
+ }
167875
+ function canRunChannelCommand(gate, commandName) {
167876
+ if (!gate.enabled || gate.isAdmin) {
167877
+ return true;
167878
+ }
167879
+ const canonical = canonicalizeChannelCommandName(commandName);
167880
+ return CHANNEL_COMMAND_FLOOR.includes(canonical) || gate.allowedCommands.includes(canonical);
167881
+ }
167882
+ function runnableCommandsForUser(gate) {
167883
+ const seen = new Set;
167884
+ const runnable = [];
167885
+ for (const name of [...CHANNEL_COMMAND_FLOOR, ...gate.allowedCommands]) {
167886
+ if (!seen.has(name)) {
167887
+ seen.add(name);
167888
+ runnable.push(name);
167889
+ }
167890
+ }
167891
+ return runnable;
167892
+ }
167338
167893
  function channelDisplayName(channelId) {
167339
167894
  try {
167340
167895
  return getChannelDisplayName(channelId);
@@ -167342,8 +167897,60 @@ function channelDisplayName(channelId) {
167342
167897
  return channelId;
167343
167898
  }
167344
167899
  }
167900
+ function buildChannelCommandDeniedMessage(channelId, commandName, gate) {
167901
+ const runnable = runnableCommandsForUser(gate).map((name) => `/${name}`).join(", ");
167902
+ const restriction = gate.pairingPending ? "is available after pairing completes" : "is limited to admins here";
167903
+ return [
167904
+ `${channelDisplayName(channelId)}: /${normalizeCommandName(commandName)} ${restriction}.`,
167905
+ `Commands you can run: ${runnable}. Use /whoami to see your access.`
167906
+ ].join(`
167907
+ `);
167908
+ }
167909
+ function buildChannelWhoamiMessage(msg, gate) {
167910
+ const scope = resolveChannelAccessScope(msg.chatType) === "dm" ? "DM" : "group/channel";
167911
+ const who = msg.senderName ? `${msg.senderName} (${msg.senderId})` : msg.senderId;
167912
+ const lines = [
167913
+ `You — ${channelDisplayName(msg.channel)} (${scope})`,
167914
+ `User ID: ${who}`
167915
+ ];
167916
+ if (!gate || !gate.enabled) {
167917
+ lines.push("Tier: unrestricted (no admin list configured for this account)", "Commands: all available");
167918
+ } else if (gate.pairingPending) {
167919
+ const runnable = runnableCommandsForUser(gate).map((name) => `/${name}`).join(", ");
167920
+ lines.push("Tier: pending pairing", `Commands you can run: ${runnable}`);
167921
+ } else if (gate.isAdmin) {
167922
+ lines.push("Tier: admin", "Commands: all available");
167923
+ } else {
167924
+ const runnable = runnableCommandsForUser(gate).map((name) => `/${name}`).join(", ");
167925
+ lines.push("Tier: user", `Commands you can run: ${runnable}`);
167926
+ }
167927
+ return lines.join(`
167928
+ `);
167929
+ }
167930
+ var GLOBAL_ALLOWED_USERS_ENV = "LETTA_CHANNELS_ALLOWED_USERS", GLOBAL_ADMIN_USERS_ENV = "LETTA_CHANNELS_ADMIN_USERS", GLOBAL_ALLOW_ALL_ENV = "LETTA_CHANNELS_ALLOW_ALL_USERS", ACCESS_DENIED_NOUNS, CHANNEL_COMMAND_FLOOR;
167931
+ var init_access_control = __esm(() => {
167932
+ init_pairing();
167933
+ init_plugin_registry();
167934
+ init_target2();
167935
+ ACCESS_DENIED_NOUNS = {
167936
+ slack: "Slack app",
167937
+ discord: "Discord bot",
167938
+ whatsapp: "WhatsApp account",
167939
+ signal: "Signal account"
167940
+ };
167941
+ CHANNEL_COMMAND_FLOOR = ["help", "status", "whoami"];
167942
+ });
167943
+
167944
+ // src/channels/feedback.ts
167945
+ function channelDisplayName2(channelId) {
167946
+ try {
167947
+ return getChannelDisplayName(channelId);
167948
+ } catch {
167949
+ return channelId;
167950
+ }
167951
+ }
167345
167952
  function buildChannelFeedbackUsageMessage(channelId) {
167346
- const displayName = channelDisplayName(channelId);
167953
+ const displayName = channelDisplayName2(channelId);
167347
167954
  return [
167348
167955
  `${displayName} received /feedback without a message.`,
167349
167956
  "Usage: /feedback <message>"
@@ -167352,11 +167959,11 @@ function buildChannelFeedbackUsageMessage(channelId) {
167352
167959
  `);
167353
167960
  }
167354
167961
  function buildChannelFeedbackTooLongMessage(channelId, maxLength3 = CHANNEL_FEEDBACK_MESSAGE_MAX) {
167355
- const displayName = channelDisplayName(channelId);
167962
+ const displayName = channelDisplayName2(channelId);
167356
167963
  return `${displayName} feedback message is too long. Maximum is ${maxLength3.toLocaleString()} characters.`;
167357
167964
  }
167358
167965
  function buildChannelFeedbackNoRouteMessage(channelId) {
167359
- const displayName = channelDisplayName(channelId);
167966
+ const displayName = channelDisplayName2(channelId);
167360
167967
  const instruction = channelId === "slack" ? "Mention the app with a normal message in this chat or thread first so it can connect, then send /feedback <message> while mentioning the app." : "Send a normal message first and follow the pairing instructions, then try /feedback <message>.";
167361
167968
  return [
167362
167969
  `${displayName} cannot submit /feedback until this chat is connected to a Letta agent conversation.`,
@@ -167366,11 +167973,11 @@ function buildChannelFeedbackNoRouteMessage(channelId) {
167366
167973
  `);
167367
167974
  }
167368
167975
  function buildChannelFeedbackSubmittedMessage(channelId) {
167369
- const displayName = channelDisplayName(channelId);
167976
+ const displayName = channelDisplayName2(channelId);
167370
167977
  return `${displayName} feedback submitted. Thanks for helping improve Letta Code.`;
167371
167978
  }
167372
167979
  function buildChannelFeedbackFailedMessage(channelId) {
167373
- const displayName = channelDisplayName(channelId);
167980
+ const displayName = channelDisplayName2(channelId);
167374
167981
  return `${displayName} could not submit feedback right now. Please try again later.`;
167375
167982
  }
167376
167983
  function withDefinedValues2(payload) {
@@ -167433,7 +168040,7 @@ var init_feedback2 = __esm(() => {
167433
168040
  });
167434
168041
 
167435
168042
  // src/channels/commands.ts
167436
- function channelDisplayName2(channelId) {
168043
+ function channelDisplayName3(channelId) {
167437
168044
  try {
167438
168045
  return getChannelDisplayName(channelId);
167439
168046
  } catch {
@@ -167516,7 +168123,7 @@ function isSlackMentionControlCommand(msg, command) {
167516
168123
  return command.raw.startsWith("!") || isSlackMentionSlashCommand(msg, command);
167517
168124
  }
167518
168125
  function buildChannelHelpMessage(channelId) {
167519
- const displayName = channelDisplayName2(channelId);
168126
+ const displayName = channelDisplayName3(channelId);
167520
168127
  if (channelId === "slack") {
167521
168128
  return [
167522
168129
  `${displayName} is connected to Letta Code.`,
@@ -167548,7 +168155,7 @@ function buildChannelHelpMessage(channelId) {
167548
168155
  `);
167549
168156
  }
167550
168157
  function buildUnsupportedChannelCommandMessage(channelId, command) {
167551
- const displayName = channelDisplayName2(channelId);
168158
+ const displayName = channelDisplayName3(channelId);
167552
168159
  const isBang = command.raw.startsWith("!");
167553
168160
  const commandKind = isBang ? "bang" : "slash";
167554
168161
  const supportedCommands = isBang ? supportedBangCommandsText() : channelId === "slack" ? supportedSlackMentionSlashCommandsText() : supportedCommandsText();
@@ -167562,7 +168169,7 @@ function buildUnsupportedChannelCommandMessage(channelId, command) {
167562
168169
  `);
167563
168170
  }
167564
168171
  function buildChannelStatusMessage(msg, context3) {
167565
- const displayName = channelDisplayName2(msg.channel);
168172
+ const displayName = channelDisplayName3(msg.channel);
167566
168173
  const route = context3.route;
167567
168174
  const routeStatus = route ? "Connected to a Letta agent conversation." : "No route is connected for this chat yet.";
167568
168175
  const accountStatus = !context3.accountConfigured ? "No channel account is configured for this receiver." : context3.accountEnabled === false ? "Channel account is configured but disabled." : "Channel account is configured and enabled.";
@@ -167590,7 +168197,7 @@ function buildChannelStatusMessage(msg, context3) {
167590
168197
  `);
167591
168198
  }
167592
168199
  function buildChannelNoRouteMessage(channelId) {
167593
- const displayName = channelDisplayName2(channelId);
168200
+ const displayName = channelDisplayName3(channelId);
167594
168201
  return [
167595
168202
  `${displayName} could not find an existing route for this chat.`,
167596
168203
  "Send a normal message first and follow the pairing instructions, then try again."
@@ -167599,23 +168206,23 @@ function buildChannelNoRouteMessage(channelId) {
167599
168206
  `);
167600
168207
  }
167601
168208
  function buildChannelPausedMessage(channelId, route) {
167602
- const displayName = channelDisplayName2(channelId);
168209
+ const displayName = channelDisplayName3(channelId);
167603
168210
  const conversation = route.conversationId ? ` Conversation: ${route.conversationId}.` : "";
167604
168211
  return `${displayName} paused agent routing for this chat.${conversation} Send /resume here to turn replies back on.`;
167605
168212
  }
167606
168213
  function buildChannelAlreadyPausedMessage(channelId) {
167607
- return `${channelDisplayName2(channelId)} agent routing is already paused for this chat. Send /resume here to turn replies back on.`;
168214
+ return `${channelDisplayName3(channelId)} agent routing is already paused for this chat. Send /resume here to turn replies back on.`;
167608
168215
  }
167609
168216
  function buildChannelResumedMessage(channelId, route) {
167610
- const displayName = channelDisplayName2(channelId);
168217
+ const displayName = channelDisplayName3(channelId);
167611
168218
  const conversation = route.conversationId ? ` Conversation: ${route.conversationId}.` : "";
167612
168219
  return `${displayName} resumed agent routing for this chat.${conversation} Normal messages here will go to the connected agent again.`;
167613
168220
  }
167614
168221
  function buildChannelAlreadyActiveMessage(channelId) {
167615
- return `${channelDisplayName2(channelId)} agent routing is already active for this chat.`;
168222
+ return `${channelDisplayName3(channelId)} agent routing is already active for this chat.`;
167616
168223
  }
167617
168224
  function buildChannelCancelUnavailableMessage(channelId) {
167618
- const displayName = channelDisplayName2(channelId);
168225
+ const displayName = channelDisplayName3(channelId);
167619
168226
  return [
167620
168227
  `${displayName} received /cancel, but this chat is not connected to an active Letta Code conversation yet.`,
167621
168228
  "Send a normal message first to connect this chat to an agent."
@@ -167624,15 +168231,15 @@ function buildChannelCancelUnavailableMessage(channelId) {
167624
168231
  `);
167625
168232
  }
167626
168233
  function buildChannelCancelNoActiveTurnMessage(channelId) {
167627
- const displayName = channelDisplayName2(channelId);
168234
+ const displayName = channelDisplayName3(channelId);
167628
168235
  return `${displayName} received /cancel, but there is no in-progress agent turn to cancel for this chat.`;
167629
168236
  }
167630
168237
  function buildChannelCancelAcceptedMessage(channelId) {
167631
- const displayName = channelDisplayName2(channelId);
168238
+ const displayName = channelDisplayName3(channelId);
167632
168239
  return `${displayName} cancelled the in-progress agent turn for this chat.`;
167633
168240
  }
167634
168241
  function buildChannelChatLinkMessage(channelId, route, chatUrl) {
167635
- const displayName = channelDisplayName2(channelId);
168242
+ const displayName = channelDisplayName3(channelId);
167636
168243
  return [
167637
168244
  `${displayName} chat for this route: ${chatUrl}`,
167638
168245
  `Agent: ${route.agentId}.`,
@@ -167641,27 +168248,27 @@ function buildChannelChatLinkMessage(channelId, route, chatUrl) {
167641
168248
  `);
167642
168249
  }
167643
168250
  function buildChannelChatUnavailableMessage(channelId, route) {
167644
- const displayName = channelDisplayName2(channelId);
168251
+ const displayName = channelDisplayName3(channelId);
167645
168252
  return `${displayName} chat UI is not available for local backend agent ${route.agentId}.`;
167646
168253
  }
167647
168254
  function buildChannelDetachUnsupportedMessage(channelId) {
167648
- const displayName = channelDisplayName2(channelId);
168255
+ const displayName = channelDisplayName3(channelId);
167649
168256
  return `${displayName} can only detach Slack channel threads.`;
167650
168257
  }
167651
168258
  function buildChannelDetachedMessage(channelId) {
167652
- const displayName = channelDisplayName2(channelId);
168259
+ const displayName = channelDisplayName3(channelId);
167653
168260
  return `${displayName} detached this thread. I will ignore follow-up replies here until someone mentions the app again.`;
167654
168261
  }
167655
168262
  function buildChannelAlreadyDetachedMessage(channelId) {
167656
- const displayName = channelDisplayName2(channelId);
168263
+ const displayName = channelDisplayName3(channelId);
167657
168264
  return `${displayName} is already detached from this thread. Mention the app again to reattach.`;
167658
168265
  }
167659
168266
  function buildChannelNewConversationMessage(channelId, route) {
167660
- const displayName = channelDisplayName2(channelId);
168267
+ const displayName = channelDisplayName3(channelId);
167661
168268
  return `${displayName} started a new conversation for this chat. Conversation: ${route.conversationId}.`;
167662
168269
  }
167663
168270
  function buildChannelNewConversationUnavailableMessage(channelId) {
167664
- const displayName = channelDisplayName2(channelId);
168271
+ const displayName = channelDisplayName3(channelId);
167665
168272
  return `${displayName} cannot start a new conversation for this chat because no agent is configured.`;
167666
168273
  }
167667
168274
  function getModelEntryRank(entry) {
@@ -167720,7 +168327,7 @@ function buildChannelModelNotFoundText(channelId) {
167720
168327
  return `Model not found. Use ${modelCommandPrefix(channelId)} list to see available models.`;
167721
168328
  }
167722
168329
  function buildChannelCurrentModelMessage(channelId, params) {
167723
- const displayName = channelDisplayName2(channelId);
168330
+ const displayName = channelDisplayName3(channelId);
167724
168331
  const scope = params.scope === "agent" ? "agent" : "conversation";
167725
168332
  const handleText = params.modelHandle && params.modelHandle !== params.modelLabel ? ` (${params.modelHandle})` : "";
167726
168333
  const switchCommand = modelCommandPrefix(channelId);
@@ -167748,7 +168355,7 @@ function appendModelEntrySection(lines, channelId, title, entries, limit3) {
167748
168355
  }
167749
168356
  }
167750
168357
  function buildChannelModelListMessage(channelId, params) {
167751
- const displayName = channelDisplayName2(channelId);
168358
+ const displayName = channelDisplayName3(channelId);
167752
168359
  const limit3 = params.limit ?? DEFAULT_CHANNEL_MODEL_LIST_LIMIT;
167753
168360
  const entries = params.entries;
167754
168361
  const byHandle = buildModelEntriesByHandle(entries);
@@ -167781,33 +168388,33 @@ function buildChannelModelListMessage(channelId, params) {
167781
168388
  `);
167782
168389
  }
167783
168390
  function buildChannelModelListUnavailableMessage(channelId, error54) {
167784
- const displayName = channelDisplayName2(channelId);
168391
+ const displayName = channelDisplayName3(channelId);
167785
168392
  return `${displayName} could not load the model list: ${error54}`;
167786
168393
  }
167787
168394
  function buildChannelCurrentModelUnavailableMessage(channelId, error54) {
167788
- const displayName = channelDisplayName2(channelId);
168395
+ const displayName = channelDisplayName3(channelId);
167789
168396
  return `${displayName} could not load the current model: ${error54}`;
167790
168397
  }
167791
168398
  function buildChannelModelUpdatedMessage(channelId, params) {
167792
- const displayName = channelDisplayName2(channelId);
168399
+ const displayName = channelDisplayName3(channelId);
167793
168400
  const scope = params.appliedTo === "agent" ? "agent" : "conversation";
167794
168401
  const handleText = params.modelHandle === params.modelLabel ? "" : ` (${params.modelHandle})`;
167795
168402
  return `${displayName} updated this ${scope}'s model to ${params.modelLabel}${handleText}.`;
167796
168403
  }
167797
168404
  function buildChannelModelUpdateFailedMessage(channelId, identifier2, error54) {
167798
- const displayName = channelDisplayName2(channelId);
168405
+ const displayName = channelDisplayName3(channelId);
167799
168406
  return `${displayName} could not switch this chat's routed model to ${identifier2}: ${error54}`;
167800
168407
  }
167801
168408
  function buildChannelModelUnavailableMessage(channelId) {
167802
- const displayName = channelDisplayName2(channelId);
168409
+ const displayName = channelDisplayName3(channelId);
167803
168410
  return `${displayName} cannot use /model because the listener is not ready yet. Try again in a moment.`;
167804
168411
  }
167805
168412
  function buildChannelReflectionUnavailableMessage(channelId) {
167806
- const displayName = channelDisplayName2(channelId);
168413
+ const displayName = channelDisplayName3(channelId);
167807
168414
  return `${displayName} cannot start reflection for this chat because the listener is not ready yet. Try again in a moment.`;
167808
168415
  }
167809
168416
  function buildChannelReloadUnavailableMessage(channelId) {
167810
- const displayName = channelDisplayName2(channelId);
168417
+ const displayName = channelDisplayName3(channelId);
167811
168418
  return `${displayName} cannot reload listener settings for this chat because the listener is not ready yet. Try again in a moment.`;
167812
168419
  }
167813
168420
  async function handleScopedCommand(params) {
@@ -167844,10 +168451,17 @@ async function tryHandleChannelSlashCommand(adapter, msg, options3 = {}) {
167844
168451
  await adapter.sendDirectReply(msg.chatId, buildUnsupportedChannelCommandMessage(msg.channel, command), msg.threadId ? { replyToMessageId: msg.threadId } : undefined);
167845
168452
  return true;
167846
168453
  }
168454
+ const canonicalName = canonicalizeChannelCommandName(command.name);
168455
+ if (options3.commandGate && !canRunChannelCommand(options3.commandGate, canonicalName)) {
168456
+ await adapter.sendDirectReply(msg.chatId, buildChannelCommandDeniedMessage(msg.channel, canonicalName, options3.commandGate), msg.threadId ? { replyToMessageId: msg.threadId } : undefined);
168457
+ return true;
168458
+ }
167847
168459
  const reply = normalizeDirectReplyPayload(await (async () => {
167848
168460
  switch (command.name) {
167849
168461
  case "help":
167850
168462
  return buildChannelHelpMessage(msg.channel);
168463
+ case "whoami":
168464
+ return buildChannelWhoamiMessage(msg, options3.commandGate);
167851
168465
  case "status":
167852
168466
  return buildChannelStatusMessage(msg, options3.statusContext ?? {
167853
168467
  adapterRunning: adapter.isRunning(),
@@ -167941,6 +168555,7 @@ async function tryHandleChannelSlashCommand(adapter, msg, options3 = {}) {
167941
168555
  }
167942
168556
  var CHANNEL_SLASH_COMMANDS, SLACK_MENTION_COMMAND_NAMES, SLACK_MENTION_SLASH_COMMAND_EXAMPLES, DEFAULT_CHANNEL_MODEL_LIST_LIMIT = 8;
167943
168557
  var init_commands = __esm(() => {
168558
+ init_access_control();
167944
168559
  init_feedback2();
167945
168560
  init_plugin_registry();
167946
168561
  CHANNEL_SLASH_COMMANDS = [
@@ -167954,6 +168569,11 @@ var init_commands = __esm(() => {
167954
168569
  kind: "direct",
167955
168570
  summary: "Show this chat's channel connection status."
167956
168571
  },
168572
+ {
168573
+ name: "whoami",
168574
+ kind: "direct",
168575
+ summary: "Show your access tier and runnable commands here."
168576
+ },
167957
168577
  {
167958
168578
  name: "pause",
167959
168579
  kind: "direct",
@@ -168001,6 +168621,7 @@ var init_commands = __esm(() => {
168001
168621
  SLACK_MENTION_SLASH_COMMAND_EXAMPLES = [
168002
168622
  "@agent /help",
168003
168623
  "@agent /status",
168624
+ "@agent /whoami",
168004
168625
  "@agent /model",
168005
168626
  "@agent /model list",
168006
168627
  "@agent /model <handle-or-id>",
@@ -168100,19 +168721,24 @@ Handle: ${escapeSlackMrkdwn(params.current.modelHandle)}` : "";
168100
168721
  text: {
168101
168722
  type: "mrkdwn",
168102
168723
  text: "Choose a model for this routed conversation:"
168103
- },
168104
- accessory: {
168105
- type: "static_select",
168106
- action_id: SLACK_MODEL_SELECT_ACTION_ID,
168107
- placeholder: {
168108
- type: "plain_text",
168109
- text: "Select a model",
168110
- emoji: true
168111
- },
168112
- options: options3,
168113
- ...initialOption ? { initial_option: initialOption } : {}
168114
168724
  }
168115
168725
  },
168726
+ {
168727
+ type: "actions",
168728
+ elements: [
168729
+ {
168730
+ type: "static_select",
168731
+ action_id: SLACK_MODEL_SELECT_ACTION_ID,
168732
+ placeholder: {
168733
+ type: "plain_text",
168734
+ text: "Select a model",
168735
+ emoji: true
168736
+ },
168737
+ options: options3,
168738
+ ...initialOption ? { initial_option: initialOption } : {}
168739
+ }
168740
+ ]
168741
+ },
168116
168742
  {
168117
168743
  type: "context",
168118
168744
  elements: [
@@ -170977,9 +171603,9 @@ __export(exports_process_manager, {
170977
171603
  import {
170978
171604
  appendFileSync as appendFileSync2,
170979
171605
  chmodSync as chmodSync2,
170980
- mkdirSync as mkdirSync5,
171606
+ mkdirSync as mkdirSync6,
170981
171607
  mkdtempSync as mkdtempSync2,
170982
- writeFileSync as writeFileSync3
171608
+ writeFileSync as writeFileSync4
170983
171609
  } from "node:fs";
170984
171610
  import { tmpdir as tmpdir3 } from "node:os";
170985
171611
  import { join as join13 } from "node:path";
@@ -171138,7 +171764,7 @@ function __resetBackgroundOutputDirForTests() {
171138
171764
  backgroundOutputDir = undefined;
171139
171765
  }
171140
171766
  function ensureBackgroundOutputDir(dir) {
171141
- mkdirSync5(dir, { recursive: true, mode: 448 });
171767
+ mkdirSync6(dir, { recursive: true, mode: 448 });
171142
171768
  if (!process.env.LETTA_SCRATCHPAD) {
171143
171769
  chmodSync2(dir, 448);
171144
171770
  }
@@ -171147,7 +171773,7 @@ function createBackgroundOutputFile(id2) {
171147
171773
  const dir = getBackgroundOutputDir();
171148
171774
  ensureBackgroundOutputDir(dir);
171149
171775
  const filePath = join13(dir, `${id2}.log`);
171150
- writeFileSync3(filePath, "", { mode: 384 });
171776
+ writeFileSync4(filePath, "", { mode: 384 });
171151
171777
  chmodSync2(filePath, 384);
171152
171778
  return filePath;
171153
171779
  }
@@ -171240,12 +171866,12 @@ var init_attachment_task = __esm(() => {
171240
171866
  });
171241
171867
 
171242
171868
  // src/channels/targets.ts
171243
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
171244
- function getStore2(channelId) {
171245
- let store = stores2.get(channelId);
171869
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
171870
+ function getStore3(channelId) {
171871
+ let store = stores3.get(channelId);
171246
171872
  if (!store) {
171247
171873
  store = { targets: [] };
171248
- stores2.set(channelId, store);
171874
+ stores3.set(channelId, store);
171249
171875
  }
171250
171876
  return store;
171251
171877
  }
@@ -171255,35 +171881,35 @@ function loadTargetStore(channelId) {
171255
171881
  return;
171256
171882
  }
171257
171883
  const path5 = getChannelTargetsPath(channelId);
171258
- if (!existsSync8(path5)) {
171884
+ if (!existsSync9(path5)) {
171259
171885
  return;
171260
171886
  }
171261
171887
  try {
171262
- const text = readFileSync7(path5, "utf-8");
171888
+ const text = readFileSync8(path5, "utf-8");
171263
171889
  const parsed = JSON.parse(text);
171264
- stores2.set(channelId, {
171890
+ stores3.set(channelId, {
171265
171891
  targets: parsed.targets ?? []
171266
171892
  });
171267
171893
  } catch {}
171268
171894
  }
171269
171895
  function saveTargetStore(channelId) {
171270
171896
  if (saveTargetStoreOverride) {
171271
- saveTargetStoreOverride(channelId, getStore2(channelId));
171897
+ saveTargetStoreOverride(channelId, getStore3(channelId));
171272
171898
  return;
171273
171899
  }
171274
171900
  const dir = getChannelDir(channelId);
171275
- mkdirSync6(dir, { recursive: true });
171276
- writeFileSync4(getChannelTargetsPath(channelId), `${JSON.stringify(getStore2(channelId), null, 2)}
171901
+ mkdirSync7(dir, { recursive: true });
171902
+ writeFileSync5(getChannelTargetsPath(channelId), `${JSON.stringify(getStore3(channelId), null, 2)}
171277
171903
  `, "utf-8");
171278
171904
  }
171279
171905
  function listChannelTargets(channelId, accountId) {
171280
- const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId(accountId);
171281
- return getStore2(channelId).targets.filter((target2) => normalizedAccountId === undefined || normalizeAccountId(target2.accountId) === normalizedAccountId);
171906
+ const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId2(accountId);
171907
+ return getStore3(channelId).targets.filter((target2) => normalizedAccountId === undefined || normalizeAccountId2(target2.accountId) === normalizedAccountId);
171282
171908
  }
171283
171909
  function upsertChannelTarget(channelId, target2) {
171284
- const store = getStore2(channelId);
171285
- const normalizedAccountId = normalizeAccountId(target2.accountId);
171286
- const existingIndex = store.targets.findIndex((candidate) => candidate.targetId === target2.targetId && normalizeAccountId(candidate.accountId) === normalizedAccountId);
171910
+ const store = getStore3(channelId);
171911
+ const normalizedAccountId = normalizeAccountId2(target2.accountId);
171912
+ const existingIndex = store.targets.findIndex((candidate) => candidate.targetId === target2.targetId && normalizeAccountId2(candidate.accountId) === normalizedAccountId);
171287
171913
  if (existingIndex >= 0) {
171288
171914
  const existing = store.targets[existingIndex];
171289
171915
  if (!existing) {
@@ -171311,9 +171937,9 @@ function upsertChannelTarget(channelId, target2) {
171311
171937
  };
171312
171938
  }
171313
171939
  function removeChannelTarget(channelId, targetId, accountId) {
171314
- const store = getStore2(channelId);
171315
- const normalizedAccountId = normalizeAccountId(accountId);
171316
- const nextTargets = store.targets.filter((target2) => !(target2.targetId === targetId && normalizeAccountId(target2.accountId) === normalizedAccountId));
171940
+ const store = getStore3(channelId);
171941
+ const normalizedAccountId = normalizeAccountId2(accountId);
171942
+ const nextTargets = store.targets.filter((target2) => !(target2.targetId === targetId && normalizeAccountId2(target2.accountId) === normalizedAccountId));
171317
171943
  if (nextTargets.length === store.targets.length) {
171318
171944
  return false;
171319
171945
  }
@@ -171322,9 +171948,9 @@ function removeChannelTarget(channelId, targetId, accountId) {
171322
171948
  return true;
171323
171949
  }
171324
171950
  function removeChannelTargetsForAccount(channelId, accountId) {
171325
- const store = getStore2(channelId);
171326
- const normalizedAccountId = normalizeAccountId(accountId);
171327
- const nextTargets = store.targets.filter((target2) => normalizeAccountId(target2.accountId) !== normalizedAccountId);
171951
+ const store = getStore3(channelId);
171952
+ const normalizedAccountId = normalizeAccountId2(accountId);
171953
+ const nextTargets = store.targets.filter((target2) => normalizeAccountId2(target2.accountId) !== normalizedAccountId);
171328
171954
  const removed = store.targets.length - nextTargets.length;
171329
171955
  if (removed === 0) {
171330
171956
  return 0;
@@ -171333,14 +171959,14 @@ function removeChannelTargetsForAccount(channelId, accountId) {
171333
171959
  saveTargetStore(channelId);
171334
171960
  return removed;
171335
171961
  }
171336
- function normalizeAccountId(accountId) {
171962
+ function normalizeAccountId2(accountId) {
171337
171963
  return accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
171338
171964
  }
171339
- var stores2, loadTargetStoreOverride = null, saveTargetStoreOverride = null;
171965
+ var stores3, loadTargetStoreOverride = null, saveTargetStoreOverride = null;
171340
171966
  var init_targets = __esm(() => {
171341
171967
  init_accounts();
171342
171968
  init_config2();
171343
- stores2 = new Map;
171969
+ stores3 = new Map;
171344
171970
  });
171345
171971
 
171346
171972
  // src/channels/slack/target-resolution.ts
@@ -171933,12 +172559,12 @@ function resolveDiscordChannelMode(channelId, parentChannelId, isThread, allowed
171933
172559
 
171934
172560
  // src/channels/discord/media.ts
171935
172561
  import { randomUUID as randomUUID10 } from "node:crypto";
171936
- import { mkdirSync as mkdirSync7 } from "node:fs";
172562
+ import { mkdirSync as mkdirSync8 } from "node:fs";
171937
172563
  import { writeFile as writeFile6 } from "node:fs/promises";
171938
172564
  import { tmpdir as tmpdir4 } from "node:os";
171939
172565
  import { join as join14 } from "node:path";
171940
172566
  function ensureAttachmentsDir() {
171941
- mkdirSync7(DISCORD_ATTACHMENTS_DIR, { recursive: true });
172567
+ mkdirSync8(DISCORD_ATTACHMENTS_DIR, { recursive: true });
171942
172568
  return DISCORD_ATTACHMENTS_DIR;
171943
172569
  }
171944
172570
  function sanitizeDiscordPathSegment(input) {
@@ -173183,107 +173809,6 @@ var init_plugin4 = __esm(() => {
173183
173809
  };
173184
173810
  });
173185
173811
 
173186
- // src/channels/whatsapp/jid.ts
173187
- function stripDeviceSuffix(jid) {
173188
- if (!jid)
173189
- return "";
173190
- return jid.replace(/:\d+(@|$)/, "$1");
173191
- }
173192
- function isLidJid(jid) {
173193
- return !!jid && stripDeviceSuffix(jid).endsWith(WHATSAPP_LID_SUFFIX);
173194
- }
173195
- function isGroupJid(jid) {
173196
- return !!jid && stripDeviceSuffix(jid).endsWith(WHATSAPP_GROUP_SUFFIX);
173197
- }
173198
- function isStatusOrBroadcastJid(jid) {
173199
- if (!jid)
173200
- return true;
173201
- const normalized = stripDeviceSuffix(jid);
173202
- return normalized === "status@broadcast" || normalized.endsWith("@broadcast") || normalized.endsWith("@newsletter");
173203
- }
173204
- function jidToDigits(jid) {
173205
- if (!jid)
173206
- return "";
173207
- const base2 = stripDeviceSuffix(jid).split("@")[0] ?? "";
173208
- return base2.replace(/\D/g, "");
173209
- }
173210
- function normalizePhoneLike(value) {
173211
- if (!value)
173212
- return "";
173213
- return jidToDigits(value.trim());
173214
- }
173215
- function phoneDigitsToJid(phoneDigits) {
173216
- const digits = normalizePhoneLike(phoneDigits);
173217
- return digits ? `${digits}${WHATSAPP_PHONE_SUFFIX}` : "";
173218
- }
173219
- function normalizeMaybePhoneJid(value) {
173220
- if (!value)
173221
- return null;
173222
- const trimmed = value.trim();
173223
- if (!trimmed)
173224
- return null;
173225
- if (isLidJid(trimmed))
173226
- return null;
173227
- if (trimmed.includes("@"))
173228
- return stripDeviceSuffix(trimmed);
173229
- return phoneDigitsToJid(trimmed) || null;
173230
- }
173231
- function isSelfChat(remoteJid, selfPhoneJid, selfLid) {
173232
- const remote = stripDeviceSuffix(remoteJid);
173233
- if (!remote)
173234
- return false;
173235
- const phone = stripDeviceSuffix(selfPhoneJid);
173236
- if (phone && remote === phone)
173237
- return true;
173238
- const lid = stripDeviceSuffix(selfLid);
173239
- if (lid && remote === lid)
173240
- return true;
173241
- return false;
173242
- }
173243
- function senderIdFromJid(jid) {
173244
- return jidToDigits(jid);
173245
- }
173246
- function allowedUsersIncludes(allowedUsers, senderId) {
173247
- const normalizedSender = normalizePhoneLike(senderId);
173248
- return allowedUsers.some((entry) => normalizePhoneLike(entry) === normalizedSender);
173249
- }
173250
- function resolveLidToPhoneJid(params) {
173251
- const { lidJid, message, sock } = params;
173252
- if (!isLidJid(lidJid))
173253
- return normalizeMaybePhoneJid(lidJid);
173254
- const msg = message;
173255
- const senderPn = normalizeMaybePhoneJid(msg?.key?.senderPn ?? undefined);
173256
- if (senderPn)
173257
- return senderPn;
173258
- const repo = sock?.signalRepository;
173259
- const mapped3 = normalizeMaybePhoneJid(repo?.lidMapping?.get(stripDeviceSuffix(lidJid)));
173260
- if (mapped3)
173261
- return mapped3;
173262
- return null;
173263
- }
173264
- function resolveSendJid(params) {
173265
- const { chatId, selfPhoneJid, selfLid, lidToJid, sock } = params;
173266
- if (!isLidJid(chatId))
173267
- return stripDeviceSuffix(chatId);
173268
- const normalized = stripDeviceSuffix(chatId);
173269
- if (selfLid && normalized === stripDeviceSuffix(selfLid) && selfPhoneJid) {
173270
- return stripDeviceSuffix(selfPhoneJid);
173271
- }
173272
- const mapped3 = normalizeMaybePhoneJid(lidToJid?.get(normalized));
173273
- if (mapped3)
173274
- return mapped3;
173275
- const repo = sock?.signalRepository;
173276
- const signalMapped = normalizeMaybePhoneJid(repo?.lidMapping?.get(normalized));
173277
- if (signalMapped)
173278
- return signalMapped;
173279
- throw new Error(`Cannot send to unresolved WhatsApp LID: ${chatId}`);
173280
- }
173281
- function sanitizePathSegment(input) {
173282
- const cleaned = input.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^_+|_+$/g, "");
173283
- return cleaned || "whatsapp";
173284
- }
173285
- var WHATSAPP_PHONE_SUFFIX = "@s.whatsapp.net", WHATSAPP_LID_SUFFIX = "@lid", WHATSAPP_GROUP_SUFFIX = "@g.us";
173286
-
173287
173812
  // src/channels/whatsapp/media.ts
173288
173813
  import { randomUUID as randomUUID12 } from "node:crypto";
173289
173814
  import { mkdir as mkdir5, writeFile as writeFile7 } from "node:fs/promises";
@@ -173612,7 +174137,7 @@ var init_state = __esm(() => {
173612
174137
  });
173613
174138
 
173614
174139
  // src/channels/whatsapp/session.ts
173615
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
174140
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
173616
174141
  import { homedir as homedir6 } from "node:os";
173617
174142
  import { join as join16 } from "node:path";
173618
174143
  function shouldDropLine(line) {
@@ -173686,7 +174211,7 @@ function defaultIsProcessAlive(pid) {
173686
174211
  }
173687
174212
  function readLeaseOwner(lockDir) {
173688
174213
  try {
173689
- const owner = JSON.parse(readFileSync8(join16(lockDir, "owner.json"), "utf8"));
174214
+ const owner = JSON.parse(readFileSync9(join16(lockDir, "owner.json"), "utf8"));
173690
174215
  return {
173691
174216
  pid: typeof owner.pid === "number" ? owner.pid : undefined,
173692
174217
  command: typeof owner.command === "string" ? owner.command : undefined
@@ -173705,8 +174230,8 @@ function acquireWhatsAppSessionLease(accountId, options3 = {}) {
173705
174230
  }
173706
174231
  for (let attempt = 0;attempt < 2; attempt += 1) {
173707
174232
  try {
173708
- mkdirSync8(lockDir);
173709
- writeFileSync5(join16(lockDir, "owner.json"), `${JSON.stringify({
174233
+ mkdirSync9(lockDir);
174234
+ writeFileSync6(join16(lockDir, "owner.json"), `${JSON.stringify({
173710
174235
  accountId,
173711
174236
  pid,
173712
174237
  command: process.argv.join(" "),
@@ -173784,7 +174309,7 @@ function renderQrTerminal(qrMod, input) {
173784
174309
  async function createWhatsAppSocket(params) {
173785
174310
  installWhatsAppConsoleFilters();
173786
174311
  const authDir = getWhatsAppAuthDir(params.accountId);
173787
- mkdirSync8(authDir, { recursive: true });
174312
+ mkdirSync9(authDir, { recursive: true });
173788
174313
  const sessionLease = acquireWhatsAppSessionLease(params.accountId);
173789
174314
  setWhatsAppConnectionState(params.accountId, { status: "connecting" });
173790
174315
  try {
@@ -174584,126 +175109,6 @@ var init_plugin5 = __esm(() => {
174584
175109
  };
174585
175110
  });
174586
175111
 
174587
- // src/channels/signal/target.ts
174588
- function assertSignalHttpProtocol(url2) {
174589
- if (url2.protocol !== "http:" && url2.protocol !== "https:") {
174590
- throw new Error(`Signal base URL protocol must be http or https, got ${url2.protocol}`);
174591
- }
174592
- }
174593
- function trimPrefix(value, prefix) {
174594
- if (!value.toLowerCase().startsWith(prefix)) {
174595
- return null;
174596
- }
174597
- return value.slice(prefix.length).trim();
174598
- }
174599
- function normalizeSignalBaseUrl(input) {
174600
- const trimmed = input.trim();
174601
- if (!trimmed) {
174602
- return "";
174603
- }
174604
- const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
174605
- const withScheme = hasScheme ? trimmed : `http://${trimmed}`;
174606
- const parsed = new URL(withScheme);
174607
- assertSignalHttpProtocol(parsed);
174608
- if (parsed.username || parsed.password) {
174609
- throw new Error("Signal base URL must not include credentials.");
174610
- }
174611
- return withScheme.replace(/\/+$/, "");
174612
- }
174613
- function parseSignalTarget(input) {
174614
- const trimmed = input.trim();
174615
- if (!trimmed) {
174616
- throw new Error("Signal target is required.");
174617
- }
174618
- const signalTarget = trimPrefix(trimmed, "signal:");
174619
- const value = signalTarget !== null ? signalTarget : trimmed;
174620
- if (!value) {
174621
- throw new Error("Signal target is required.");
174622
- }
174623
- const groupId = trimPrefix(value, "group:");
174624
- if (groupId) {
174625
- return { kind: "group", groupId };
174626
- }
174627
- const username = trimPrefix(value, "username:");
174628
- if (username) {
174629
- return { kind: "username", username };
174630
- }
174631
- const usernameAlias = trimPrefix(value, "u:");
174632
- if (usernameAlias) {
174633
- return { kind: "username", username: `u:${usernameAlias}` };
174634
- }
174635
- const recipient = value;
174636
- if (!recipient) {
174637
- throw new Error("Signal recipient is required.");
174638
- }
174639
- return { kind: "recipient", recipient };
174640
- }
174641
- function signalTargetToSendRpcParams(target2) {
174642
- switch (target2.kind) {
174643
- case "group":
174644
- return { groupId: target2.groupId };
174645
- case "username":
174646
- return { username: [target2.username] };
174647
- case "recipient":
174648
- return { recipient: [target2.recipient] };
174649
- }
174650
- }
174651
- function signalTargetToReactionRpcParams(target2) {
174652
- switch (target2.kind) {
174653
- case "group":
174654
- return { groupIds: [target2.groupId] };
174655
- case "recipient":
174656
- return { recipients: [target2.recipient] };
174657
- case "username":
174658
- throw new Error("Signal reactions require a recipient or group target.");
174659
- }
174660
- }
174661
- function normalizeSignalSenderId(value) {
174662
- if (!value) {
174663
- return "";
174664
- }
174665
- const trimmed = value.trim();
174666
- if (!trimmed) {
174667
- return "";
174668
- }
174669
- return trimmed.toLowerCase();
174670
- }
174671
- function normalizeSignalPhone(value) {
174672
- if (!value) {
174673
- return "";
174674
- }
174675
- const trimmed = value.trim();
174676
- if (!trimmed) {
174677
- return "";
174678
- }
174679
- const withoutPrefix = trimmed.toLowerCase().startsWith("signal:") ? trimmed.slice("signal:".length) : trimmed;
174680
- return withoutPrefix.replace(/[^0-9+]/g, "");
174681
- }
174682
- function signalAllowedUsersIncludes(allowedUsers, senderId) {
174683
- const normalizedSender = normalizeSignalSenderId(senderId);
174684
- const senderPhone = normalizeSignalPhone(senderId);
174685
- return allowedUsers.some((entry) => {
174686
- const normalizedEntry = normalizeSignalSenderId(entry);
174687
- if (normalizedEntry === normalizedSender) {
174688
- return true;
174689
- }
174690
- const entryPhone = normalizeSignalPhone(entry);
174691
- return !!senderPhone && !!entryPhone && senderPhone === entryPhone;
174692
- });
174693
- }
174694
- function isSignalGroupAllowed(allowedGroups, groupId) {
174695
- if (!allowedGroups || allowedGroups.length === 0) {
174696
- return true;
174697
- }
174698
- const normalized = groupId.trim();
174699
- return allowedGroups.some((entry) => entry.trim() === normalized);
174700
- }
174701
- function matchesSignalMentionPatterns(text, mentionPatterns) {
174702
- const normalizedText = text.toLowerCase();
174703
- return (mentionPatterns ?? []).map((entry) => entry.trim().toLowerCase()).filter(Boolean).some((entry) => normalizedText.includes(entry));
174704
- }
174705
- var init_target2 = () => {};
174706
-
174707
175112
  // src/channels/signal/client.ts
174708
175113
  import { randomUUID as randomUUID14 } from "node:crypto";
174709
175114
  import { request as httpRequest } from "node:http";
@@ -175054,9 +175459,9 @@ var init_client5 = __esm(() => {
175054
175459
  import { randomUUID as randomUUID15 } from "node:crypto";
175055
175460
  import {
175056
175461
  copyFileSync,
175057
- mkdirSync as mkdirSync9,
175462
+ mkdirSync as mkdirSync10,
175058
175463
  readdirSync as readdirSync2,
175059
- readFileSync as readFileSync9,
175464
+ readFileSync as readFileSync10,
175060
175465
  realpathSync as realpathSync2,
175061
175466
  statSync as statSync3
175062
175467
  } from "node:fs";
@@ -175334,7 +175739,7 @@ function copySignalAttachment(params) {
175334
175739
  const mimeType = normalizeSignalMimeType(params.attachment.contentType) ?? inferSignalMimeTypeFromName(fileName);
175335
175740
  const kind = inferSignalAttachmentKind({ mimeType, fileName });
175336
175741
  const inboundDir = join17(getChannelDir("signal"), "inbound", sanitizeSignalPathSegment(params.accountId));
175337
- mkdirSync9(inboundDir, { recursive: true });
175742
+ mkdirSync10(inboundDir, { recursive: true });
175338
175743
  const localPath = join17(inboundDir, `${Date.now()}-${randomUUID15()}-${sanitizeSignalPathSegment(fileName)}`);
175339
175744
  copyFileSync(params.sourcePath, localPath);
175340
175745
  const attachment = {
@@ -175346,7 +175751,7 @@ function copySignalAttachment(params) {
175346
175751
  localPath
175347
175752
  };
175348
175753
  if (kind === "image" && sizeBytes <= MAX_SIGNAL_INLINE_IMAGE_BYTES) {
175349
- attachment.imageDataBase64 = readFileSync9(localPath).toString("base64");
175754
+ attachment.imageDataBase64 = readFileSync10(localPath).toString("base64");
175350
175755
  }
175351
175756
  return attachment;
175352
175757
  }
@@ -176034,7 +176439,7 @@ var init_runtime5 = __esm(() => {
176034
176439
 
176035
176440
  // src/channels/signal/setup-runtime.ts
176036
176441
  import { execFileSync as execFileSync2, spawn as spawn2 } from "node:child_process";
176037
- import { existsSync as existsSync9 } from "node:fs";
176442
+ import { existsSync as existsSync10 } from "node:fs";
176038
176443
  function getSignalDockerRunCommand() {
176039
176444
  return [
176040
176445
  "docker run -d",
@@ -176076,7 +176481,7 @@ function detectNativeSignalCliConfigDir() {
176076
176481
  }
176077
176482
  } catch {}
176078
176483
  const defaultDir = getDefaultSignalCliConfigDir();
176079
- return defaultDir && existsSync9(defaultDir) ? defaultDir : null;
176484
+ return defaultDir && existsSync10(defaultDir) ? defaultDir : null;
176080
176485
  }
176081
176486
  function runNativeSignalCli(args) {
176082
176487
  try {
@@ -176842,7 +177247,7 @@ __export(exports_plugin_registry, {
176842
177247
  getChannelDisplayName: () => getChannelDisplayName,
176843
177248
  __testClearUserChannelPluginCache: () => __testClearUserChannelPluginCache
176844
177249
  });
176845
- import { existsSync as existsSync10, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "node:fs";
177250
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync11 } from "node:fs";
176846
177251
  import { resolve as resolve4, sep } from "node:path";
176847
177252
  import { pathToFileURL as pathToFileURL2 } from "node:url";
176848
177253
  function isValidChannelId(value) {
@@ -176853,11 +177258,11 @@ function readStringArray(value) {
176853
177258
  }
176854
177259
  function readChannelManifest(channelDir) {
176855
177260
  const manifestPath = resolve4(channelDir, "channel.json");
176856
- if (!existsSync10(manifestPath)) {
177261
+ if (!existsSync11(manifestPath)) {
176857
177262
  return null;
176858
177263
  }
176859
177264
  try {
176860
- const parsed = JSON.parse(readFileSync10(manifestPath, "utf-8"));
177265
+ const parsed = JSON.parse(readFileSync11(manifestPath, "utf-8"));
176861
177266
  if (!isRecord(parsed)) {
176862
177267
  return null;
176863
177268
  }
@@ -176933,7 +177338,7 @@ function createUserChannelRegistration(manifest) {
176933
177338
  function discoverUserChannelRegistrations() {
176934
177339
  const registrations = new Map;
176935
177340
  const channelsRoot = getChannelsRoot();
176936
- if (!existsSync10(channelsRoot)) {
177341
+ if (!existsSync11(channelsRoot)) {
176937
177342
  return registrations;
176938
177343
  }
176939
177344
  let entries;
@@ -177096,186 +177501,6 @@ var init_plugin_registry = __esm(() => {
177096
177501
  loadedUserPlugins = new Map;
177097
177502
  });
177098
177503
 
177099
- // src/channels/pairing.ts
177100
- var exports_pairing = {};
177101
- __export(exports_pairing, {
177102
- rollbackPairingApproval: () => rollbackPairingApproval,
177103
- removePairingStateForAccount: () => removePairingStateForAccount,
177104
- loadPairingStore: () => loadPairingStore,
177105
- isUserApproved: () => isUserApproved,
177106
- getPendingPairings: () => getPendingPairings,
177107
- getApprovedUsers: () => getApprovedUsers,
177108
- createPairingCode: () => createPairingCode,
177109
- consumePairingCode: () => consumePairingCode,
177110
- clearPairingStores: () => clearPairingStores,
177111
- __testOverrideSavePairingStore: () => __testOverrideSavePairingStore,
177112
- __testOverrideLoadPairingStore: () => __testOverrideLoadPairingStore
177113
- });
177114
- import { existsSync as existsSync11, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
177115
- function normalizeAccountId2(accountId) {
177116
- return accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
177117
- }
177118
- function getStore3(channelId) {
177119
- let store = stores3.get(channelId);
177120
- if (!store) {
177121
- store = { pending: [], approved: [] };
177122
- stores3.set(channelId, store);
177123
- }
177124
- return store;
177125
- }
177126
- function loadPairingStore(channelId) {
177127
- if (loadPairingStoreOverride) {
177128
- const overridden = loadPairingStoreOverride(channelId);
177129
- if (overridden === null) {
177130
- return;
177131
- }
177132
- stores3.set(channelId, {
177133
- pending: [...overridden.pending],
177134
- approved: [...overridden.approved]
177135
- });
177136
- return;
177137
- }
177138
- const path5 = getChannelPairingPath(channelId);
177139
- if (!existsSync11(path5))
177140
- return;
177141
- try {
177142
- const text = readFileSync11(path5, "utf-8");
177143
- const parsed = JSON.parse(text);
177144
- stores3.set(channelId, {
177145
- pending: parsed.pending ?? [],
177146
- approved: parsed.approved ?? []
177147
- });
177148
- } catch {}
177149
- }
177150
- function savePairingStore(channelId) {
177151
- const store = getStore3(channelId);
177152
- if (savePairingStoreOverride) {
177153
- savePairingStoreOverride(channelId, {
177154
- pending: [...store.pending],
177155
- approved: [...store.approved]
177156
- });
177157
- return;
177158
- }
177159
- const dir = getChannelDir(channelId);
177160
- mkdirSync10(dir, { recursive: true });
177161
- writeFileSync6(getChannelPairingPath(channelId), `${JSON.stringify(store, null, 2)}
177162
- `, "utf-8");
177163
- }
177164
- function generateCode(length = 6) {
177165
- let code2 = "";
177166
- for (let i2 = 0;i2 < length; i2++) {
177167
- code2 += CODE_CHARS[Math.floor(Math.random() * CODE_CHARS.length)];
177168
- }
177169
- return code2;
177170
- }
177171
- function isUserApproved(channelId, userId, accountId) {
177172
- const store = getStore3(channelId);
177173
- const normalizedAccountId = normalizeAccountId2(accountId);
177174
- return store.approved.some((u) => u.senderId === userId && normalizeAccountId2(u.accountId) === normalizedAccountId);
177175
- }
177176
- function createPairingCode(channelId, userId, chatId, username, accountId) {
177177
- const store = getStore3(channelId);
177178
- const normalizedAccountId = normalizeAccountId2(accountId);
177179
- store.pending = store.pending.filter((p) => !(p.senderId === userId && normalizeAccountId2(p.accountId) === normalizedAccountId));
177180
- const now = Date.now();
177181
- store.pending = store.pending.filter((p) => new Date(p.expiresAt).getTime() > now);
177182
- while (store.pending.length >= MAX_PENDING_CODES) {
177183
- store.pending.shift();
177184
- }
177185
- const code2 = generateCode();
177186
- const pending = {
177187
- accountId: normalizedAccountId,
177188
- code: code2,
177189
- senderId: userId,
177190
- senderName: username,
177191
- chatId,
177192
- createdAt: new Date().toISOString(),
177193
- expiresAt: new Date(now + PAIRING_CODE_TTL_MS).toISOString()
177194
- };
177195
- store.pending.push(pending);
177196
- savePairingStore(channelId);
177197
- return code2;
177198
- }
177199
- function consumePairingCode(channelId, code2, accountId) {
177200
- const store = getStore3(channelId);
177201
- const upperCode = code2.toUpperCase();
177202
- const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId2(accountId);
177203
- const matches = store.pending.map((pending2, index2) => ({ pending: pending2, index: index2 })).filter(({ pending: pending2 }) => pending2.code === upperCode && (normalizedAccountId === undefined || normalizeAccountId2(pending2.accountId) === normalizedAccountId));
177204
- if (matches.length > 1) {
177205
- return null;
177206
- }
177207
- const index = matches[0]?.index ?? -1;
177208
- if (index === -1)
177209
- return null;
177210
- const pending = store.pending[index];
177211
- const pendingAccountId = normalizeAccountId2(pending.accountId);
177212
- if (new Date(pending.expiresAt).getTime() < Date.now()) {
177213
- store.pending.splice(index, 1);
177214
- savePairingStore(channelId);
177215
- return null;
177216
- }
177217
- store.pending.splice(index, 1);
177218
- if (!store.approved.some((u) => u.senderId === pending.senderId && normalizeAccountId2(u.accountId) === pendingAccountId)) {
177219
- const approved = {
177220
- accountId: pendingAccountId,
177221
- senderId: pending.senderId,
177222
- senderName: pending.senderName,
177223
- approvedAt: new Date().toISOString()
177224
- };
177225
- store.approved.push(approved);
177226
- }
177227
- savePairingStore(channelId);
177228
- return pending;
177229
- }
177230
- function getPendingPairings(channelId, accountId) {
177231
- const store = getStore3(channelId);
177232
- const now = Date.now();
177233
- const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId2(accountId);
177234
- return store.pending.filter((p) => new Date(p.expiresAt).getTime() > now && (normalizedAccountId === undefined || normalizeAccountId2(p.accountId) === normalizedAccountId));
177235
- }
177236
- function getApprovedUsers(channelId, accountId) {
177237
- const normalizedAccountId = accountId === undefined ? undefined : normalizeAccountId2(accountId);
177238
- return getStore3(channelId).approved.filter((user) => normalizedAccountId === undefined || normalizeAccountId2(user.accountId) === normalizedAccountId);
177239
- }
177240
- function rollbackPairingApproval(channelId, pending) {
177241
- const store = getStore3(channelId);
177242
- const normalizedAccountId = normalizeAccountId2(pending.accountId);
177243
- store.approved = store.approved.filter((u) => !(u.senderId === pending.senderId && normalizeAccountId2(u.accountId) === normalizedAccountId));
177244
- store.pending.push(pending);
177245
- savePairingStore(channelId);
177246
- }
177247
- function removePairingStateForAccount(channelId, accountId) {
177248
- const store = getStore3(channelId);
177249
- const normalizedAccountId = normalizeAccountId2(accountId);
177250
- const nextPending = store.pending.filter((pending) => normalizeAccountId2(pending.accountId) !== normalizedAccountId);
177251
- const nextApproved = store.approved.filter((approved) => normalizeAccountId2(approved.accountId) !== normalizedAccountId);
177252
- const pendingRemoved = store.pending.length - nextPending.length;
177253
- const approvedRemoved = store.approved.length - nextApproved.length;
177254
- if (pendingRemoved === 0 && approvedRemoved === 0) {
177255
- return { pendingRemoved, approvedRemoved };
177256
- }
177257
- store.pending = nextPending;
177258
- store.approved = nextApproved;
177259
- savePairingStore(channelId);
177260
- return { pendingRemoved, approvedRemoved };
177261
- }
177262
- function clearPairingStores() {
177263
- stores3.clear();
177264
- }
177265
- function __testOverrideLoadPairingStore(fn) {
177266
- loadPairingStoreOverride = fn;
177267
- }
177268
- function __testOverrideSavePairingStore(fn) {
177269
- savePairingStoreOverride = fn;
177270
- }
177271
- var PAIRING_CODE_TTL_MS, MAX_PENDING_CODES = 50, CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789", stores3, loadPairingStoreOverride = null, savePairingStoreOverride = null;
177272
- var init_pairing = __esm(() => {
177273
- init_accounts();
177274
- init_config2();
177275
- PAIRING_CODE_TTL_MS = 15 * 60 * 1000;
177276
- stores3 = new Map;
177277
- });
177278
-
177279
177504
  // src/cli/helpers/app-urls.ts
177280
177505
  function isLocalAgentId2(agentId) {
177281
177506
  return isLocalAgentId(agentId);
@@ -177320,7 +177545,7 @@ var init_app_urls = __esm(() => {
177320
177545
  });
177321
177546
 
177322
177547
  // src/channels/registry-presentation.ts
177323
- function channelDisplayName3(channelId) {
177548
+ function channelDisplayName4(channelId) {
177324
177549
  try {
177325
177550
  return getChannelDisplayName(channelId);
177326
177551
  } catch {
@@ -177338,7 +177563,7 @@ function getConfiguredAgentId(config3) {
177338
177563
  return normalizeAgentId(source2.agentId) ?? normalizeAgentId(source2.binding?.agentId);
177339
177564
  }
177340
177565
  function buildPairingInstructions(channelId, code2, options3 = {}) {
177341
- const displayName = channelDisplayName3(channelId);
177566
+ const displayName = channelDisplayName4(channelId);
177342
177567
  const configuredAgentId = normalizeAgentId(options3.agentId);
177343
177568
  const pairingCommand = `letta channels pair --channel ${channelId} --code ${code2} --agent ${configuredAgentId ?? "<agent-id>"}`;
177344
177569
  const agentLookupLines = configuredAgentId ? [] : ["Find the target agent with: letta agents list"];
@@ -177372,7 +177597,7 @@ function buildPairingInstructions(channelId, code2, options3 = {}) {
177372
177597
  `);
177373
177598
  }
177374
177599
  function buildUnboundRouteInstructions(channelId, chatId) {
177375
- const displayName = channelDisplayName3(channelId);
177600
+ const displayName = channelDisplayName4(channelId);
177376
177601
  if (!isFirstPartyChannelPlugin(channelId)) {
177377
177602
  return `This chat isn't connected to a Letta agent yet.
177378
177603
 
@@ -178220,6 +178445,9 @@ class ChannelControlRequests {
178220
178445
  this.requestIdByScope.delete(scopeKey);
178221
178446
  return false;
178222
178447
  }
178448
+ if (pending.event.source.senderId && pending.event.source.senderId !== msg.senderId) {
178449
+ return false;
178450
+ }
178223
178451
  if (msg.channel === "slack" && pending.event.kind === "generic_tool_approval") {
178224
178452
  return false;
178225
178453
  }
@@ -178789,10 +179017,27 @@ function createChannelInboundRouter(deps) {
178789
179017
  const adapter = deps.getAdapter(msg.channel, accountId);
178790
179018
  if (!adapter)
178791
179019
  return;
179020
+ const config3 = getChannelAccount(msg.channel, accountId);
179021
+ const senderAccess = config3 ? evaluateChannelSenderAccess({
179022
+ account: config3,
179023
+ channelId: msg.channel,
179024
+ senderId: msg.senderId,
179025
+ chatType: msg.chatType
179026
+ }) : null;
179027
+ if (senderAccess === "deny") {
179028
+ if (msg.reaction) {
179029
+ return;
179030
+ }
179031
+ if (resolveChannelAccessScope(msg.chatType) === "dm") {
179032
+ await adapter.sendDirectReply(msg.chatId, buildChannelAccessDeniedMessage(msg.channel));
179033
+ } else {
179034
+ console.log(`[channels] Dropped ${msg.channel} group message from unauthorized sender ${msg.senderId} in chat ${msg.chatId}`);
179035
+ }
179036
+ return;
179037
+ }
178792
179038
  if (await deps.controls.tryHandleInbound(adapter, msg)) {
178793
179039
  return;
178794
179040
  }
178795
- const config3 = getChannelAccount(msg.channel, accountId);
178796
179041
  if (deps.commands.shouldDropUnroutedSlackThreadInput(msg, accountId, config3)) {
178797
179042
  return;
178798
179043
  }
@@ -178822,7 +179067,12 @@ function createChannelInboundRouter(deps) {
178822
179067
  reload: async (_command, commandMsg) => deps.commands.handleReloadSlashCommand(commandMsg),
178823
179068
  resume: async () => deps.commands.handlePauseResumeSlashCommand("resume", msg)
178824
179069
  },
178825
- enableBangCommands: msg.channel === "slack" && msg.isMention === true
179070
+ enableBangCommands: msg.channel === "slack" && msg.isMention === true,
179071
+ commandGate: config3 ? senderAccess === "pair" ? floorOnlyChannelCommandGate() : resolveChannelCommandGate({
179072
+ account: config3,
179073
+ channelId: msg.channel,
179074
+ senderId: msg.senderId
179075
+ }) : undefined
178826
179076
  })) {
178827
179077
  return;
178828
179078
  }
@@ -178937,32 +179187,19 @@ function createChannelInboundRouter(deps) {
178937
179187
  });
178938
179188
  return;
178939
179189
  }
178940
- if (config3.dmPolicy === "allowlist") {
178941
- if (!config3.allowedUsers.includes(msg.senderId)) {
178942
- if (msg.reaction) {
178943
- return;
178944
- }
178945
- await adapter.sendDirectReply(msg.chatId, "You are not on the allowed users list for this bot.");
178946
- return;
178947
- }
178948
- } else if (config3.dmPolicy === "pairing") {
178949
- if (!isUserApproved(msg.channel, msg.senderId, accountId)) {
178950
- loadPairingStore(msg.channel);
178951
- }
178952
- if (!isUserApproved(msg.channel, msg.senderId, accountId)) {
178953
- if (msg.reaction) {
178954
- return;
178955
- }
178956
- const code2 = createPairingCode(msg.channel, msg.senderId, msg.chatId, msg.senderName, accountId);
178957
- deps.emitEvent({
178958
- type: "pairings_updated",
178959
- channelId: msg.channel
178960
- });
178961
- await adapter.sendDirectReply(msg.chatId, buildPairingInstructions(msg.channel, code2, {
178962
- agentId: getConfiguredAgentId(config3)
178963
- }));
179190
+ if (senderAccess === "pair") {
179191
+ if (msg.reaction) {
178964
179192
  return;
178965
179193
  }
179194
+ const code2 = createPairingCode(msg.channel, msg.senderId, msg.chatId, msg.senderName, accountId);
179195
+ deps.emitEvent({
179196
+ type: "pairings_updated",
179197
+ channelId: msg.channel
179198
+ });
179199
+ await adapter.sendDirectReply(msg.chatId, buildPairingInstructions(msg.channel, code2, {
179200
+ agentId: getConfiguredAgentId(config3)
179201
+ }));
179202
+ return;
178966
179203
  }
178967
179204
  let route = getRoute(msg.channel, msg.chatId, accountId, msg.threadId);
178968
179205
  if (!route) {
@@ -178984,6 +179221,7 @@ function createChannelInboundRouter(deps) {
178984
179221
  return { handleInboundMessage };
178985
179222
  }
178986
179223
  var init_registry_inbound = __esm(() => {
179224
+ init_access_control();
178987
179225
  init_accounts();
178988
179226
  init_commands();
178989
179227
  init_pairing();
@@ -179040,12 +179278,6 @@ function createChannelRouteProvisioner(deps) {
179040
179278
  await adapter.sendDirectReply(msg.chatId, buildSlackAppSetupInstructions(), buildDirectReplyOptions(msg));
179041
179279
  return null;
179042
179280
  }
179043
- if (msg.chatType === "direct") {
179044
- if (config3.dmPolicy === "allowlist" && !config3.allowedUsers.includes(msg.senderId)) {
179045
- await adapter.sendDirectReply(msg.chatId, "You are not on the allowed users list for this Slack app.", buildDirectReplyOptions(msg));
179046
- return null;
179047
- }
179048
- }
179049
179281
  const accountId = msg.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
179050
179282
  const routeThreadId = msg.channel === "slack" ? msg.threadId ?? null : null;
179051
179283
  let route = getRoute(msg.channel, msg.chatId, accountId, routeThreadId);
@@ -179194,10 +179426,6 @@ function createChannelRouteProvisioner(deps) {
179194
179426
  }
179195
179427
  return null;
179196
179428
  }
179197
- if (msg.chatType === "direct" && config3.dmPolicy === "allowlist" && !config3.allowedUsers.includes(msg.senderId)) {
179198
- await adapter.sendDirectReply(msg.chatId, "You are not on the allowed users list for this Discord bot.");
179199
- return null;
179200
- }
179201
179429
  const accountId = msg.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
179202
179430
  const routeThreadId = msg.threadId ?? null;
179203
179431
  let route = getRoute(msg.channel, msg.chatId, accountId, routeThreadId);
@@ -179245,10 +179473,6 @@ function createChannelRouteProvisioner(deps) {
179245
179473
  }
179246
179474
  return null;
179247
179475
  }
179248
- if (msg.chatType === "direct" && config3.dmPolicy === "allowlist" && !allowedUsersIncludes(config3.allowedUsers, msg.senderId)) {
179249
- await adapter.sendDirectReply(msg.chatId, "You are not on the allowed users list for this WhatsApp account.");
179250
- return null;
179251
- }
179252
179476
  const accountId = msg.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
179253
179477
  let route = getRoute(msg.channel, msg.chatId, accountId, null);
179254
179478
  if (!route) {
@@ -179310,12 +179534,6 @@ function createChannelRouteProvisioner(deps) {
179310
179534
  }
179311
179535
  return null;
179312
179536
  }
179313
- if (msg.chatType === "direct" && config3.dmPolicy === "allowlist" && !signalAllowedUsersIncludes(config3.allowedUsers, msg.senderId)) {
179314
- if (!msg.reaction) {
179315
- await adapter.sendDirectReply(msg.chatId, "You are not on the allowed users list for this Signal account.");
179316
- }
179317
- return null;
179318
- }
179319
179537
  const accountId = msg.accountId ?? LEGACY_CHANNEL_ACCOUNT_ID;
179320
179538
  let route = getRoute(msg.channel, msg.chatId, accountId, null);
179321
179539
  if (!route) {
@@ -179363,7 +179581,6 @@ var init_registry_routes = __esm(() => {
179363
179581
  init_accounts();
179364
179582
  init_registry_presentation();
179365
179583
  init_routing();
179366
- init_target2();
179367
179584
  init_targets();
179368
179585
  });
179369
179586
 
@@ -448250,6 +448467,100 @@ var init_cron = __esm(async () => {
448250
448467
  await init_scheduler();
448251
448468
  });
448252
448469
 
448470
+ // src/backend/api/environments.ts
448471
+ var exports_environments2 = {};
448472
+ __export(exports_environments2, {
448473
+ sendEnvironmentMessage: () => sendEnvironmentMessage,
448474
+ resolveEnvironmentConnectionId: () => resolveEnvironmentConnectionId,
448475
+ resolveAgentSandboxConnectionId: () => resolveAgentSandboxConnectionId,
448476
+ listEnvironments: () => listEnvironments,
448477
+ isEnvironmentOnline: () => isEnvironmentOnline,
448478
+ getEnvironmentConnection: () => getEnvironmentConnection,
448479
+ describeEnvironment: () => describeEnvironment,
448480
+ createAgentSandbox: () => createAgentSandbox
448481
+ });
448482
+ async function listEnvironments(options3 = {}) {
448483
+ return apiRequest("GET", "/v1/environments", undefined, {
448484
+ query: {
448485
+ limit: options3.limit,
448486
+ after: options3.after,
448487
+ onlineOnly: options3.onlineOnly
448488
+ }
448489
+ });
448490
+ }
448491
+ async function sendEnvironmentMessage(connectionId, body3) {
448492
+ return apiRequest("POST", `/v1/environments/${encodeURIComponent(connectionId)}/messages`, body3);
448493
+ }
448494
+ async function getEnvironmentConnection(deviceId) {
448495
+ return apiRequest("GET", `/v1/environments/${encodeURIComponent(deviceId)}`);
448496
+ }
448497
+ async function createAgentSandbox(agentId) {
448498
+ return apiRequest("POST", `/v1/agents/${encodeURIComponent(agentId)}/sandboxes`, {});
448499
+ }
448500
+ function isEnvironmentOnline(environment2) {
448501
+ return typeof environment2.connectionId === "string" && environment2.connectionId.length > 0 && typeof environment2.lastHeartbeat === "number" && Date.now() - environment2.lastHeartbeat < 120000;
448502
+ }
448503
+ function describeEnvironment(environment2) {
448504
+ const status = isEnvironmentOnline(environment2) ? "online" : "offline";
448505
+ return `${environment2.connectionName} (${environment2.deviceId}, ${status})`;
448506
+ }
448507
+ async function resolveEnvironmentConnectionId(selector) {
448508
+ const trimmed = selector.trim();
448509
+ if (!trimmed) {
448510
+ throw new Error("Environment selector must not be empty");
448511
+ }
448512
+ const response = await listEnvironments({ limit: 100 });
448513
+ const matches2 = response.connections.filter((environment3) => {
448514
+ return environment3.connectionId === trimmed || environment3.id === trimmed || environment3.deviceId === trimmed || environment3.connectionName === trimmed;
448515
+ });
448516
+ if (matches2.length === 0) {
448517
+ throw new Error(`Environment "${trimmed}" not found. Run \`letta environments list\` to discover available environments.`);
448518
+ }
448519
+ const onlineMatches = matches2.filter(isEnvironmentOnline);
448520
+ if (onlineMatches.length === 0) {
448521
+ throw new Error(`Environment "${trimmed}" is offline. Matched: ${matches2.map(describeEnvironment).join(", ")}`);
448522
+ }
448523
+ if (onlineMatches.length > 1) {
448524
+ throw new Error(`Environment "${trimmed}" is ambiguous. Matched: ${onlineMatches.map(describeEnvironment).join(", ")}`);
448525
+ }
448526
+ const environment2 = onlineMatches[0];
448527
+ if (!environment2) {
448528
+ throw new Error(`Environment "${trimmed}" is offline`);
448529
+ }
448530
+ if (!environment2.connectionId) {
448531
+ throw new Error(`Environment "${trimmed}" has no active connection id`);
448532
+ }
448533
+ return { connectionId: environment2.connectionId, environment: environment2 };
448534
+ }
448535
+ async function resolveAgentSandboxConnectionId(agentId, options3 = {}) {
448536
+ const timeoutMs = options3.timeoutMs ?? 3 * 60000;
448537
+ const pollIntervalMs = options3.pollIntervalMs ?? 2000;
448538
+ const sandbox = await createAgentSandbox(agentId);
448539
+ const deviceId = sandbox.deviceId || `sandbox-${agentId}`;
448540
+ const deadline = Date.now() + timeoutMs;
448541
+ let lastEnvironment = null;
448542
+ let lastError = null;
448543
+ while (Date.now() < deadline) {
448544
+ try {
448545
+ const environment2 = await getEnvironmentConnection(deviceId);
448546
+ lastEnvironment = environment2;
448547
+ if (isEnvironmentOnline(environment2) && environment2.connectionId) {
448548
+ return { connectionId: environment2.connectionId, environment: environment2 };
448549
+ }
448550
+ } catch (error54) {
448551
+ lastError = error54;
448552
+ }
448553
+ await new Promise((resolve30) => setTimeout(resolve30, pollIntervalMs));
448554
+ }
448555
+ if (lastEnvironment) {
448556
+ throw new Error(`Timed out waiting for cloud sandbox ${sandbox.connectionName} to come online. Last status: ${describeEnvironment(lastEnvironment)}`);
448557
+ }
448558
+ throw new Error(`Timed out waiting for cloud sandbox ${sandbox.connectionName} to register${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
448559
+ }
448560
+ var init_environments2 = __esm(() => {
448561
+ init_request();
448562
+ });
448563
+
448253
448564
  // src/backend/api/search.ts
448254
448565
  async function warmSearchCache(body3) {
448255
448566
  return apiRequest("POST", "/v1/_internal_search/cache-warm", body3);
@@ -450607,89 +450918,6 @@ var init_reflection_launcher = __esm(() => {
450607
450918
  pendingReflectionLaunches = new Map;
450608
450919
  });
450609
450920
 
450610
- // src/backend/api/environments.ts
450611
- async function listEnvironments(options3 = {}) {
450612
- return apiRequest("GET", "/v1/environments", undefined, {
450613
- query: {
450614
- limit: options3.limit,
450615
- after: options3.after,
450616
- onlineOnly: options3.onlineOnly
450617
- }
450618
- });
450619
- }
450620
- async function sendEnvironmentMessage(connectionId, body3) {
450621
- return apiRequest("POST", `/v1/environments/${encodeURIComponent(connectionId)}/messages`, body3);
450622
- }
450623
- async function getEnvironmentConnection(deviceId) {
450624
- return apiRequest("GET", `/v1/environments/${encodeURIComponent(deviceId)}`);
450625
- }
450626
- async function createAgentSandbox(agentId) {
450627
- return apiRequest("POST", `/v1/agents/${encodeURIComponent(agentId)}/sandboxes`, {});
450628
- }
450629
- function isEnvironmentOnline(environment2) {
450630
- return typeof environment2.connectionId === "string" && environment2.connectionId.length > 0 && typeof environment2.lastHeartbeat === "number" && Date.now() - environment2.lastHeartbeat < 120000;
450631
- }
450632
- function describeEnvironment(environment2) {
450633
- const status = isEnvironmentOnline(environment2) ? "online" : "offline";
450634
- return `${environment2.connectionName} (${environment2.deviceId}, ${status})`;
450635
- }
450636
- async function resolveEnvironmentConnectionId(selector) {
450637
- const trimmed = selector.trim();
450638
- if (!trimmed) {
450639
- throw new Error("Environment selector must not be empty");
450640
- }
450641
- const response = await listEnvironments({ limit: 100 });
450642
- const matches2 = response.connections.filter((environment3) => {
450643
- return environment3.connectionId === trimmed || environment3.id === trimmed || environment3.deviceId === trimmed || environment3.connectionName === trimmed;
450644
- });
450645
- if (matches2.length === 0) {
450646
- throw new Error(`Environment "${trimmed}" not found. Run \`letta environments list\` to discover available environments.`);
450647
- }
450648
- const onlineMatches = matches2.filter(isEnvironmentOnline);
450649
- if (onlineMatches.length === 0) {
450650
- throw new Error(`Environment "${trimmed}" is offline. Matched: ${matches2.map(describeEnvironment).join(", ")}`);
450651
- }
450652
- if (onlineMatches.length > 1) {
450653
- throw new Error(`Environment "${trimmed}" is ambiguous. Matched: ${onlineMatches.map(describeEnvironment).join(", ")}`);
450654
- }
450655
- const environment2 = onlineMatches[0];
450656
- if (!environment2) {
450657
- throw new Error(`Environment "${trimmed}" is offline`);
450658
- }
450659
- if (!environment2.connectionId) {
450660
- throw new Error(`Environment "${trimmed}" has no active connection id`);
450661
- }
450662
- return { connectionId: environment2.connectionId, environment: environment2 };
450663
- }
450664
- async function resolveAgentSandboxConnectionId(agentId, options3 = {}) {
450665
- const timeoutMs = options3.timeoutMs ?? 3 * 60000;
450666
- const pollIntervalMs = options3.pollIntervalMs ?? 2000;
450667
- const sandbox = await createAgentSandbox(agentId);
450668
- const deviceId = sandbox.deviceId || `sandbox-${agentId}`;
450669
- const deadline = Date.now() + timeoutMs;
450670
- let lastEnvironment = null;
450671
- let lastError = null;
450672
- while (Date.now() < deadline) {
450673
- try {
450674
- const environment2 = await getEnvironmentConnection(deviceId);
450675
- lastEnvironment = environment2;
450676
- if (isEnvironmentOnline(environment2) && environment2.connectionId) {
450677
- return { connectionId: environment2.connectionId, environment: environment2 };
450678
- }
450679
- } catch (error54) {
450680
- lastError = error54;
450681
- }
450682
- await new Promise((resolve31) => setTimeout(resolve31, pollIntervalMs));
450683
- }
450684
- if (lastEnvironment) {
450685
- throw new Error(`Timed out waiting for cloud sandbox ${sandbox.connectionName} to come online. Last status: ${describeEnvironment(lastEnvironment)}`);
450686
- }
450687
- throw new Error(`Timed out waiting for cloud sandbox ${sandbox.connectionName} to register${lastError instanceof Error ? `: ${lastError.message}` : ""}`);
450688
- }
450689
- var init_environments2 = __esm(() => {
450690
- init_request();
450691
- });
450692
-
450693
450921
  // node_modules/cli-spinners/spinners.json
450694
450922
  var require_spinners = __commonJS((exports, module3) => {
450695
450923
  module3.exports = {
@@ -473975,13 +474203,13 @@ function AgentSelector({
473975
474203
  });
473976
474204
  const renderAgentItem = (agent2, _index, isSelected, extra) => {
473977
474205
  const isCurrent = agent2.id === currentAgentId;
473978
- const isLocalAgent = isLocalAgentId(agent2.id);
474206
+ const isLocalAgent2 = isLocalAgentId(agent2.id);
473979
474207
  const relativeTime = formatRelativeTime3(agent2.last_run_completion);
473980
474208
  const blockCount = agent2.blocks?.length ?? 0;
473981
474209
  const modelStr = formatModel2(agent2);
473982
474210
  const metadataParts = [
473983
474211
  relativeTime,
473984
- ...isLocalAgent ? [] : [`${blockCount} memory block${blockCount === 1 ? "" : "s"}`],
474212
+ ...isLocalAgent2 ? [] : [`${blockCount} memory block${blockCount === 1 ? "" : "s"}`],
473985
474213
  modelStr
473986
474214
  ];
473987
474215
  const nameLen = (agent2.name || "Unnamed").length;
@@ -481959,7 +482187,7 @@ var init_reflection_arena_hf_upload = __esm(() => {
481959
482187
 
481960
482188
  // src/cli/helpers/reflection-arena.ts
481961
482189
  import { execFile as execFileCb6 } from "node:child_process";
481962
- import { randomInt, randomUUID as randomUUID32 } from "node:crypto";
482190
+ import { randomInt as randomInt2, randomUUID as randomUUID32 } from "node:crypto";
481963
482191
  import { appendFile as appendFile3, mkdir as mkdir18, readFile as readFile26, writeFile as writeFile20 } from "node:fs/promises";
481964
482192
  import { homedir as homedir39 } from "node:os";
481965
482193
  import { join as join71 } from "node:path";
@@ -481970,7 +482198,7 @@ function sampleReflectionArenaComparisonModel(excludedModels = []) {
481970
482198
  const totalWeight = candidates2.reduce((sum, entry) => sum + Math.max(0, entry.weight), 0);
481971
482199
  if (totalWeight <= 0)
481972
482200
  return "letta/auto";
481973
- let offset = randomInt(totalWeight);
482201
+ let offset = randomInt2(totalWeight);
481974
482202
  for (const candidate of candidates2) {
481975
482203
  offset -= Math.max(0, candidate.weight);
481976
482204
  if (offset < 0)
@@ -482035,7 +482263,7 @@ async function updateReflectionArenaRun(runId, update2) {
482035
482263
  return updated;
482036
482264
  }
482037
482265
  function shuffledLabels() {
482038
- return randomInt(2) === 0 ? ["1", "2"] : ["2", "1"];
482266
+ return randomInt2(2) === 0 ? ["1", "2"] : ["2", "1"];
482039
482267
  }
482040
482268
  function truncateReport(report) {
482041
482269
  if (!report?.trim())
@@ -492774,8 +493002,8 @@ var init_AgentInfoBar = __esm(async () => {
492774
493002
  return settingsManager.isAgentPinned(agentId);
492775
493003
  }, [agentId]);
492776
493004
  const isCloudUser = serverUrl?.includes("api.letta.com");
492777
- const isLocalAgent = agentId ? isLocalAgentId2(agentId) : false;
492778
- const showCloudLinks = Boolean(isCloudUser && agentId && !isLocalAgent);
493005
+ const isLocalAgent2 = agentId ? isLocalAgentId2(agentId) : false;
493006
+ const showCloudLinks = Boolean(isCloudUser && agentId && !isLocalAgent2);
492779
493007
  const adeConversationUrl = showCloudLinks && agentId && agentId !== "loading" ? buildChatUrl(agentId, { conversationId }) : "";
492780
493008
  const usageUrl = buildChatWebUrl("/preferences/usage");
492781
493009
  const showBottomBar = agentId && agentId !== "loading";
@@ -501159,8 +501387,8 @@ function MemfsTreeViewer({
501159
501387
  const terminalWidth = useTerminalWidth();
501160
501388
  const solidLine = SOLID_LINE15.repeat(Math.max(terminalWidth, 10));
501161
501389
  const isTmux = Boolean(process.env.TMUX);
501162
- const isLocalAgent = isLocalAgentId2(agentId);
501163
- const adeUrl = isLocalAgent ? null : buildChatUrl(agentId, { view: "memory", conversationId });
501390
+ const isLocalAgent2 = isLocalAgentId2(agentId);
501391
+ const adeUrl = isLocalAgent2 ? null : buildChatUrl(agentId, { view: "memory", conversationId });
501164
501392
  const [selectedIndex, setSelectedIndex] = import_react90.useState(0);
501165
501393
  const [treeScrollOffset, setTreeScrollOffset] = import_react90.useState(0);
501166
501394
  const [viewMode, setViewMode] = import_react90.useState("split");
@@ -501651,8 +501879,8 @@ function MemoryTabViewer({
501651
501879
  const terminalWidth = useTerminalWidth();
501652
501880
  const solidLine = SOLID_LINE16.repeat(Math.max(terminalWidth, 10));
501653
501881
  const isTmux = Boolean(process.env.TMUX);
501654
- const isLocalAgent = isLocalAgentId2(agentId);
501655
- const adeUrl = isLocalAgent ? null : buildChatUrl(agentId, { view: "memory", conversationId });
501882
+ const isLocalAgent2 = isLocalAgentId2(agentId);
501883
+ const adeUrl = isLocalAgent2 ? null : buildChatUrl(agentId, { view: "memory", conversationId });
501656
501884
  const [selectedTabIndex, setSelectedTabIndex] = import_react91.useState(0);
501657
501885
  const [scrollOffset, setScrollOffset] = import_react91.useState(0);
501658
501886
  const [freshBlocks, setFreshBlocks] = import_react91.useState(null);
@@ -548433,8 +548661,142 @@ Provider '${providerName}' saved.`);
548433
548661
  }
548434
548662
 
548435
548663
  // src/cli/subcommands/cron.ts
548436
- await init_cron();
548664
+ init_request();
548437
548665
  import { parseArgs as parseArgs5 } from "node:util";
548666
+
548667
+ // src/backend/api/schedules.ts
548668
+ init_request();
548669
+ function schedulePath(agentId) {
548670
+ return `/v1/agents/${encodeURIComponent(agentId)}/schedule`;
548671
+ }
548672
+ async function createCloudSchedule(agentId, input) {
548673
+ return apiRequest("POST", schedulePath(agentId), {
548674
+ ...input,
548675
+ use_sandbox: true
548676
+ });
548677
+ }
548678
+ async function listCloudSchedules(agentId, options3 = {}) {
548679
+ return apiRequest("GET", schedulePath(agentId), undefined, {
548680
+ query: {
548681
+ limit: options3.limit ?? 100,
548682
+ after: options3.after
548683
+ }
548684
+ });
548685
+ }
548686
+ async function getCloudSchedule(agentId, scheduleId) {
548687
+ return apiRequest("GET", `${schedulePath(agentId)}/${encodeURIComponent(scheduleId)}`);
548688
+ }
548689
+ async function deleteCloudSchedule(agentId, scheduleId) {
548690
+ await apiRequest("DELETE", `${schedulePath(agentId)}/${encodeURIComponent(scheduleId)}`, {});
548691
+ }
548692
+ async function listCloudScheduleHistory(agentId, scheduleId, options3 = {}) {
548693
+ return apiRequest("GET", `${schedulePath(agentId)}/${encodeURIComponent(scheduleId)}/history`, undefined, {
548694
+ query: {
548695
+ limit: options3.limit,
548696
+ offset: options3.offset
548697
+ }
548698
+ });
548699
+ }
548700
+
548701
+ // src/cli/subcommands/cron.ts
548702
+ init_backend_mode();
548703
+ await init_cron();
548704
+
548705
+ // src/cli/subcommands/cron-runner.ts
548706
+ var CLOUD_EXECUTION_TARGET = "cloud-sandbox";
548707
+ function isLocalAgent(agentId) {
548708
+ return agentId.startsWith("agent-local-");
548709
+ }
548710
+ function resolveCronRunner(params) {
548711
+ const { explicit, agentId, backendMode, cloudSchedulesSupported } = params;
548712
+ if (explicit !== undefined && explicit !== "local" && explicit !== "cloud") {
548713
+ return {
548714
+ error: `invalid --runner "${explicit}". Expected "local" or "cloud".`
548715
+ };
548716
+ }
548717
+ if (explicit === "local") {
548718
+ return { runner: "local", reason: "explicit --runner local" };
548719
+ }
548720
+ if (backendMode === "local" || isLocalAgent(agentId)) {
548721
+ if (explicit === "cloud") {
548722
+ return {
548723
+ error: "Cloud schedules are not available for local-backend agents. Use --runner local."
548724
+ };
548725
+ }
548726
+ return { runner: "local", reason: "local-backend agent" };
548727
+ }
548728
+ if (cloudSchedulesSupported === false) {
548729
+ if (explicit === "cloud") {
548730
+ return {
548731
+ error: "This Letta server does not serve Cloud schedule routes (self-hosted?). Use --runner local."
548732
+ };
548733
+ }
548734
+ return {
548735
+ runner: "local",
548736
+ reason: "server does not support Cloud schedules"
548737
+ };
548738
+ }
548739
+ return {
548740
+ runner: "cloud",
548741
+ reason: explicit === "cloud" ? "explicit --runner cloud" : "cloud agent defaults to durable Cloud schedules"
548742
+ };
548743
+ }
548744
+ var SYNTHETIC_CLOUD_DEVICE_ID = "__letta_cloud__";
548745
+ var SYNTHETIC_LOCAL_PLACEHOLDER_ID = "local";
548746
+ function validateTargetDevice(deviceId, environment2) {
548747
+ if (deviceId === SYNTHETIC_CLOUD_DEVICE_ID) {
548748
+ return {
548749
+ ok: false,
548750
+ error: `"Cloud" is the default execution target, not a computer. Omit --computer to run in the agent's cloud sandbox.`
548751
+ };
548752
+ }
548753
+ if (deviceId === SYNTHETIC_LOCAL_PLACEHOLDER_ID) {
548754
+ return {
548755
+ ok: false,
548756
+ error: '"local" is a placeholder entry, not a connected computer. Run `letta server` on the machine you want to target, then use its deviceId.'
548757
+ };
548758
+ }
548759
+ if (environment2?.organizationId === "local") {
548760
+ return {
548761
+ ok: false,
548762
+ error: `Device ${deviceId} is this computer's local desktop connection, not a computer connected to your Letta account. Cloud schedules can only target connected computers — run \`letta server\` on that machine (or enable remote access in the desktop app) to connect it.`
548763
+ };
548764
+ }
548765
+ return { ok: true };
548766
+ }
548767
+ var CLOUD_CRON_UTC_NOTE = "Recurring Cloud schedules currently interpret cron expressions in UTC (timezone support is tracked in LET-9815).";
548768
+ var CLOUD_DEVICE_FALLBACK_NOTE = "If the target computer is offline when the schedule fires, execution falls back to the agent's cloud sandbox.";
548769
+ function buildCloudScheduleInput(params) {
548770
+ const notes = [];
548771
+ let schedule;
548772
+ if (params.recurring) {
548773
+ schedule = { type: "recurring", cron_expression: params.cron };
548774
+ notes.push(CLOUD_CRON_UTC_NOTE);
548775
+ } else {
548776
+ const scheduledAt = params.scheduledFor?.getTime();
548777
+ if (!scheduledAt || Number.isNaN(scheduledAt)) {
548778
+ throw new Error("One-shot Cloud schedules require a resolved --at time.");
548779
+ }
548780
+ schedule = { type: "one-time", scheduled_at: scheduledAt };
548781
+ }
548782
+ const targetDeviceId = params.targetDeviceId?.trim();
548783
+ if (targetDeviceId) {
548784
+ notes.push(CLOUD_DEVICE_FALLBACK_NOTE);
548785
+ }
548786
+ return {
548787
+ input: {
548788
+ name: params.name,
548789
+ description: params.description,
548790
+ ...params.conversationId && { conversation_id: params.conversationId },
548791
+ messages: [{ role: "user", content: params.prompt }],
548792
+ schedule,
548793
+ ...targetDeviceId && { target_device_id: targetDeviceId }
548794
+ },
548795
+ notes
548796
+ };
548797
+ }
548798
+
548799
+ // src/cli/subcommands/cron.ts
548438
548800
  function printUsage4() {
548439
548801
  console.log(`
548440
548802
  Usage:
@@ -548442,10 +548804,10 @@ Usage:
548442
548804
  letta cron add --prompt <text> --at <time> [--once] [options]
548443
548805
  letta cron add --prompt <text> --cron <expr> [options]
548444
548806
  letta cron list [options]
548445
- letta cron get <id>
548446
- letta cron runs --id <id> [--limit <n>]
548447
- letta cron delete <id>
548448
- letta cron delete --all [--agent <id>]
548807
+ letta cron get <id> [--runner local|cloud]
548808
+ letta cron runs --id <id> [--limit <n>] [--runner local|cloud]
548809
+ letta cron delete <id> [--runner local|cloud]
548810
+ letta cron delete --all [--agent <id>] [--runner local|cloud]
548449
548811
 
548450
548812
  Add options:
548451
548813
  --prompt <text> Prompt to send to the agent (required)
@@ -548455,10 +548817,23 @@ Add options:
548455
548817
  --cron <expr> Raw 5-field cron expression
548456
548818
  --agent <id> Agent ID (defaults to LETTA_AGENT_ID)
548457
548819
  --conversation <id> Conversation ID (defaults to LETTA_CONVERSATION_ID or "default")
548820
+ --runner <runner> Where the schedule lives and fires:
548821
+ cloud - durable Cloud schedule; executes in the
548822
+ agent's managed cloud sandbox (default for
548823
+ cloud agents)
548824
+ local - this device's scheduler (~/.letta/crons.json);
548825
+ only fires while a session runs here (default
548826
+ for local-backend agents / self-hosted)
548827
+ --computer <id> (cloud runner only) Execute on one of your
548828
+ connected computers (deviceId from
548829
+ \`letta environments list\`) instead of the agent's
548830
+ cloud sandbox. Falls back to the sandbox if the
548831
+ computer is offline at fire time.
548458
548832
 
548459
548833
  List/filter options:
548460
548834
  --agent <id> Filter by agent ID
548461
548835
  --conversation <id> Filter by conversation ID
548836
+ --runner <runner> Only show tasks owned by this runner
548462
548837
 
548463
548838
  Delete options:
548464
548839
  --all Delete all tasks for the given agent
@@ -548480,7 +548855,9 @@ var CRON_OPTIONS = {
548480
548855
  all: { type: "boolean" },
548481
548856
  id: { type: "string" },
548482
548857
  limit: { type: "string" },
548483
- "run-id": { type: "string" }
548858
+ "run-id": { type: "string" },
548859
+ runner: { type: "string" },
548860
+ computer: { type: "string" }
548484
548861
  };
548485
548862
  function parseCronArgs(argv) {
548486
548863
  return parseArgs5({
@@ -548496,7 +548873,75 @@ function getAgentId3(fromArgs) {
548496
548873
  function getConversationId3(fromArgs) {
548497
548874
  return fromArgs || process.env.LETTA_CONVERSATION_ID || "default";
548498
548875
  }
548499
- function handleAdd(values2) {
548876
+ async function probeCloudScheduleSupport(agentId) {
548877
+ try {
548878
+ await listCloudSchedules(agentId, { limit: 1 });
548879
+ return true;
548880
+ } catch (err) {
548881
+ if (err instanceof ApiRequestError && (err.status === 404 || err.status === 405)) {
548882
+ return false;
548883
+ }
548884
+ return true;
548885
+ }
548886
+ }
548887
+ async function ensureSettingsForCloud() {
548888
+ const { settingsManager: settingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), exports_settings_manager));
548889
+ await settingsManager2.initialize();
548890
+ }
548891
+ async function getRunnerForAgent(explicit, agentId) {
548892
+ const backendMode = resolveBackendMode();
548893
+ const preliminary = resolveCronRunner({ explicit, agentId, backendMode });
548894
+ if ("error" in preliminary || preliminary.runner === "local") {
548895
+ return preliminary;
548896
+ }
548897
+ await ensureSettingsForCloud();
548898
+ const cloudSchedulesSupported = await probeCloudScheduleSupport(agentId);
548899
+ return resolveCronRunner({
548900
+ explicit,
548901
+ agentId,
548902
+ backendMode,
548903
+ cloudSchedulesSupported
548904
+ });
548905
+ }
548906
+ function isRunnerFlagValid(value) {
548907
+ return value === undefined || value === "local" || value === "cloud";
548908
+ }
548909
+ async function lookupEnvironmentForTarget(deviceId) {
548910
+ try {
548911
+ const { getEnvironmentConnection: getEnvironmentConnection2 } = await Promise.resolve().then(() => (init_environments2(), exports_environments2));
548912
+ return await getEnvironmentConnection2(deviceId);
548913
+ } catch {
548914
+ return null;
548915
+ }
548916
+ }
548917
+ function extractPromptFromCloudSchedule(schedule) {
548918
+ const messages = schedule.message?.messages;
548919
+ if (!Array.isArray(messages))
548920
+ return null;
548921
+ const first = messages[0];
548922
+ if (!first || typeof first.content !== "string")
548923
+ return null;
548924
+ return first.content;
548925
+ }
548926
+ function formatCloudScheduleOutput(schedule) {
548927
+ const targetDeviceId = schedule.target_device_id ?? null;
548928
+ return {
548929
+ id: schedule.id,
548930
+ runner: "cloud",
548931
+ execution_target: targetDeviceId ?? CLOUD_EXECUTION_TARGET,
548932
+ ...targetDeviceId && { target_device_id: targetDeviceId },
548933
+ agent_id: schedule.agent_id,
548934
+ conversation_id: schedule.conversation_id ?? "default",
548935
+ name: schedule.name ?? null,
548936
+ description: schedule.description ?? null,
548937
+ prompt: extractPromptFromCloudSchedule(schedule),
548938
+ schedule: schedule.schedule,
548939
+ recurring: schedule.schedule.type === "recurring",
548940
+ next_scheduled_time: schedule.next_scheduled_time,
548941
+ created_at: schedule.created_at ?? null
548942
+ };
548943
+ }
548944
+ async function handleAdd(values2) {
548500
548945
  const name = values2.name;
548501
548946
  if (!name || typeof name !== "string") {
548502
548947
  console.error("Error: --name is required.");
@@ -548568,6 +549013,37 @@ function handleAdd(values2) {
548568
549013
  console.error("Error: no schedule specified.");
548569
549014
  return 1;
548570
549015
  }
549016
+ const targetDeviceId = values2.computer?.trim() || undefined;
549017
+ const resolved = await getRunnerForAgent(values2.runner, agentId);
549018
+ if ("error" in resolved) {
549019
+ console.error(`Error: ${resolved.error}`);
549020
+ return 1;
549021
+ }
549022
+ if (targetDeviceId && resolved.runner !== "cloud") {
549023
+ console.error("Error: --computer requires the cloud runner. Run `letta cron add` on the target computer itself (with --runner local) to schedule there locally.");
549024
+ return 1;
549025
+ }
549026
+ if (targetDeviceId) {
549027
+ const validity = validateTargetDevice(targetDeviceId, await lookupEnvironmentForTarget(targetDeviceId));
549028
+ if (!validity.ok) {
549029
+ console.error(`Error: ${validity.error}`);
549030
+ return 1;
549031
+ }
549032
+ }
549033
+ if (resolved.runner === "cloud") {
549034
+ return handleCloudAdd({
549035
+ agentId,
549036
+ conversationId,
549037
+ name,
549038
+ description,
549039
+ prompt,
549040
+ cron,
549041
+ recurring,
549042
+ scheduledFor,
549043
+ note,
549044
+ targetDeviceId
549045
+ });
549046
+ }
548571
549047
  try {
548572
549048
  const result = addTask({
548573
549049
  agent_id: agentId,
@@ -548581,6 +549057,7 @@ function handleAdd(values2) {
548581
549057
  });
548582
549058
  const output = {
548583
549059
  id: result.task.id,
549060
+ runner: "local",
548584
549061
  status: result.task.status,
548585
549062
  cron: result.task.cron,
548586
549063
  recurring: result.task.recurring,
@@ -548601,37 +549078,152 @@ function handleAdd(values2) {
548601
549078
  output.warning = result.warning;
548602
549079
  }
548603
549080
  console.log(JSON.stringify(output, null, 2));
549081
+ console.error("Created local schedule: it only fires while a Letta session is running on this device.");
548604
549082
  return 0;
548605
549083
  } catch (err) {
548606
549084
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
548607
549085
  return 1;
548608
549086
  }
548609
549087
  }
548610
- function handleList(values2) {
549088
+ async function handleCloudAdd(params) {
549089
+ let built;
549090
+ try {
549091
+ built = buildCloudScheduleInput({
549092
+ name: params.name,
549093
+ description: params.description,
549094
+ prompt: params.prompt,
549095
+ conversationId: params.conversationId,
549096
+ cron: params.cron,
549097
+ recurring: params.recurring,
549098
+ scheduledFor: params.scheduledFor,
549099
+ targetDeviceId: params.targetDeviceId
549100
+ });
549101
+ } catch (err) {
549102
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
549103
+ return 1;
549104
+ }
549105
+ try {
549106
+ const result = await createCloudSchedule(params.agentId, built.input);
549107
+ const targetDeviceId = result.target_device_id ?? built.input.target_device_id ?? null;
549108
+ const output = {
549109
+ id: result.id,
549110
+ runner: "cloud",
549111
+ execution_target: targetDeviceId ?? CLOUD_EXECUTION_TARGET,
549112
+ ...targetDeviceId && { target_device_id: targetDeviceId },
549113
+ agent_id: params.agentId,
549114
+ conversation_id: params.conversationId,
549115
+ recurring: params.recurring,
549116
+ schedule: built.input.schedule,
549117
+ ...result.next_scheduled_at && {
549118
+ next_scheduled_at: result.next_scheduled_at
549119
+ }
549120
+ };
549121
+ const notes = [...built.notes];
549122
+ if (params.note)
549123
+ notes.unshift(params.note);
549124
+ if (notes.length > 0) {
549125
+ output.notes = notes;
549126
+ }
549127
+ console.log(JSON.stringify(output, null, 2));
549128
+ console.error(targetDeviceId ? `Created Cloud schedule: it fires from the cloud and runs on computer "${targetDeviceId}" (sandbox fallback if offline).` : "Created Cloud schedule: it fires from the cloud and runs in this agent's managed cloud sandbox (survives local shutdown).");
549129
+ return 0;
549130
+ } catch (err) {
549131
+ console.error(`Error: failed to create Cloud schedule: ${err instanceof Error ? err.message : String(err)}`);
549132
+ console.error("No schedule was created. Retry, or pass --runner local to schedule on this device instead.");
549133
+ return 1;
549134
+ }
549135
+ }
549136
+ async function handleList(values2) {
549137
+ if (!isRunnerFlagValid(values2.runner)) {
549138
+ console.error(`Error: invalid --runner "${values2.runner}". Expected "local" or "cloud".`);
549139
+ return 1;
549140
+ }
548611
549141
  const agentId = values2.agent || process.env.LETTA_AGENT_ID || undefined;
548612
549142
  const conversationId = values2.conversation || undefined;
548613
- const tasks2 = listTasks2({
548614
- agent_id: agentId,
548615
- conversation_id: conversationId
548616
- });
548617
- console.log(JSON.stringify(tasks2, null, 2));
549143
+ const includeLocal = values2.runner !== "cloud";
549144
+ const includeCloud = values2.runner !== "local";
549145
+ const output = [];
549146
+ if (includeLocal) {
549147
+ const tasks2 = listTasks2({
549148
+ agent_id: agentId,
549149
+ conversation_id: conversationId
549150
+ });
549151
+ for (const task2 of tasks2) {
549152
+ output.push({ ...task2, runner: "local" });
549153
+ }
549154
+ }
549155
+ if (includeCloud && agentId) {
549156
+ const resolved = await getRunnerForAgent(undefined, agentId);
549157
+ const cloudCapable = !("error" in resolved) && resolved.runner === "cloud";
549158
+ const cloudExplicit = values2.runner === "cloud";
549159
+ if (cloudCapable || cloudExplicit) {
549160
+ try {
549161
+ const response = await listCloudSchedules(agentId);
549162
+ for (const schedule of response.scheduled_messages) {
549163
+ if (conversationId && (schedule.conversation_id ?? "default") !== conversationId) {
549164
+ continue;
549165
+ }
549166
+ output.push(formatCloudScheduleOutput(schedule));
549167
+ }
549168
+ } catch (err) {
549169
+ console.error(`Warning: failed to list Cloud schedules: ${err instanceof Error ? err.message : String(err)}`);
549170
+ if (cloudExplicit) {
549171
+ return 1;
549172
+ }
549173
+ }
549174
+ }
549175
+ } else if (includeCloud && values2.runner === "cloud" && !agentId) {
549176
+ console.error("Error: --agent or LETTA_AGENT_ID required to list Cloud schedules.");
549177
+ return 1;
549178
+ }
549179
+ console.log(JSON.stringify(output, null, 2));
548618
549180
  return 0;
548619
549181
  }
548620
- function handleGet(positionals) {
549182
+ async function handleGet(values2, positionals) {
549183
+ if (!isRunnerFlagValid(values2.runner)) {
549184
+ console.error(`Error: invalid --runner "${values2.runner}". Expected "local" or "cloud".`);
549185
+ return 1;
549186
+ }
548621
549187
  const taskId = positionals[1];
548622
549188
  if (!taskId) {
548623
549189
  console.error("Error: task ID required. Usage: letta cron get <id>");
548624
549190
  return 1;
548625
549191
  }
548626
- const task2 = getTask2(taskId);
548627
- if (!task2) {
548628
- console.error(`Error: task ${taskId} not found.`);
549192
+ if (values2.runner !== "cloud") {
549193
+ const task2 = getTask2(taskId);
549194
+ if (task2) {
549195
+ console.log(JSON.stringify({ ...task2, runner: "local" }, null, 2));
549196
+ return 0;
549197
+ }
549198
+ if (values2.runner === "local") {
549199
+ console.error(`Error: task ${taskId} not found.`);
549200
+ return 1;
549201
+ }
549202
+ }
549203
+ const agentId = getAgentId3(values2.agent);
549204
+ if (!agentId) {
549205
+ console.error(`Error: task ${taskId} not found locally, and --agent or LETTA_AGENT_ID is required to look up Cloud schedules.`);
549206
+ return 1;
549207
+ }
549208
+ try {
549209
+ await ensureSettingsForCloud();
549210
+ const schedule = await getCloudSchedule(agentId, taskId);
549211
+ console.log(JSON.stringify(formatCloudScheduleOutput(schedule), null, 2));
549212
+ return 0;
549213
+ } catch (err) {
549214
+ if (err instanceof ApiRequestError && err.status === 404) {
549215
+ console.error(`Error: task ${taskId} not found.`);
549216
+ } else {
549217
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
549218
+ }
548629
549219
  return 1;
548630
549220
  }
548631
- console.log(JSON.stringify(task2, null, 2));
548632
- return 0;
548633
549221
  }
548634
- function handleRuns(values2) {
549222
+ async function handleRuns(values2) {
549223
+ if (!isRunnerFlagValid(values2.runner)) {
549224
+ console.error(`Error: invalid --runner "${values2.runner}". Expected "local" or "cloud".`);
549225
+ return 1;
549226
+ }
548635
549227
  const id2 = values2.id;
548636
549228
  if (!id2 || typeof id2 !== "string") {
548637
549229
  console.error("Error: --id is required. Usage: letta cron runs --id <id>");
@@ -548640,42 +549232,125 @@ function handleRuns(values2) {
548640
549232
  const limitRaw = Number.parseInt(String(values2.limit ?? "50"), 10);
548641
549233
  const limit3 = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 50;
548642
549234
  const runId = values2["run-id"];
549235
+ if (values2.runner !== "cloud" && getTask2(id2)) {
549236
+ try {
549237
+ const logPath = getCronRunLogPath(id2);
549238
+ const page = readCronRunLogEntriesPage(logPath, {
549239
+ jobId: id2,
549240
+ limit: limit3,
549241
+ ...typeof runId === "string" && runId.trim() ? { runId } : {}
549242
+ });
549243
+ console.log(JSON.stringify(page, null, 2));
549244
+ return 0;
549245
+ } catch (err) {
549246
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
549247
+ return 1;
549248
+ }
549249
+ }
549250
+ if (values2.runner === "local") {
549251
+ console.error(`Error: task ${id2} not found.`);
549252
+ return 1;
549253
+ }
549254
+ const agentId = getAgentId3(values2.agent);
549255
+ if (!agentId) {
549256
+ console.error(`Error: task ${id2} not found locally, and --agent or LETTA_AGENT_ID is required to look up Cloud schedule runs.`);
549257
+ return 1;
549258
+ }
548643
549259
  try {
548644
- const logPath = getCronRunLogPath(id2);
548645
- const page = readCronRunLogEntriesPage(logPath, {
548646
- jobId: id2,
548647
- limit: limit3,
548648
- ...typeof runId === "string" && runId.trim() ? { runId } : {}
548649
- });
548650
- console.log(JSON.stringify(page, null, 2));
549260
+ await ensureSettingsForCloud();
549261
+ const response = await listCloudScheduleHistory(agentId, id2, { limit: limit3 });
549262
+ console.log(JSON.stringify({
549263
+ runner: "cloud",
549264
+ entries: response.history,
549265
+ has_next_page: response.has_next_page
549266
+ }, null, 2));
548651
549267
  return 0;
548652
549268
  } catch (err) {
548653
549269
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
548654
549270
  return 1;
548655
549271
  }
548656
549272
  }
548657
- function handleDelete(values2, positionals) {
549273
+ async function handleDelete(values2, positionals) {
549274
+ if (!isRunnerFlagValid(values2.runner)) {
549275
+ console.error(`Error: invalid --runner "${values2.runner}". Expected "local" or "cloud".`);
549276
+ return 1;
549277
+ }
548658
549278
  if (values2.all) {
548659
- const agentId = getAgentId3(values2.agent);
548660
- if (!agentId) {
548661
- console.error("Error: --agent or LETTA_AGENT_ID required with --all.");
548662
- return 1;
548663
- }
548664
- const count = deleteAllTasks(agentId);
548665
- console.log(JSON.stringify({ deleted: count, agent_id: agentId }));
548666
- return 0;
549279
+ return handleDeleteAll(values2);
548667
549280
  }
548668
549281
  const taskId = positionals[1];
548669
549282
  if (!taskId) {
548670
549283
  console.error("Error: task ID required. Usage: letta cron delete <id> or --all --agent <id>");
548671
549284
  return 1;
548672
549285
  }
548673
- const found = deleteTask(taskId);
548674
- if (!found) {
548675
- console.error(`Error: task ${taskId} not found.`);
549286
+ if (values2.runner !== "cloud") {
549287
+ const found = deleteTask(taskId);
549288
+ if (found) {
549289
+ console.log(JSON.stringify({ deleted: taskId, runner: "local" }));
549290
+ return 0;
549291
+ }
549292
+ if (values2.runner === "local") {
549293
+ console.error(`Error: task ${taskId} not found.`);
549294
+ return 1;
549295
+ }
549296
+ }
549297
+ const agentId = getAgentId3(values2.agent);
549298
+ if (!agentId) {
549299
+ console.error(`Error: task ${taskId} not found locally, and --agent or LETTA_AGENT_ID is required to delete Cloud schedules.`);
549300
+ return 1;
549301
+ }
549302
+ try {
549303
+ await ensureSettingsForCloud();
549304
+ await getCloudSchedule(agentId, taskId);
549305
+ await deleteCloudSchedule(agentId, taskId);
549306
+ console.log(JSON.stringify({ deleted: taskId, runner: "cloud" }));
549307
+ return 0;
549308
+ } catch (err) {
549309
+ if (err instanceof ApiRequestError && err.status === 404) {
549310
+ console.error(`Error: task ${taskId} not found.`);
549311
+ } else {
549312
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
549313
+ }
549314
+ return 1;
549315
+ }
549316
+ }
549317
+ async function handleDeleteAll(values2) {
549318
+ const agentId = getAgentId3(values2.agent);
549319
+ if (!agentId) {
549320
+ console.error("Error: --agent or LETTA_AGENT_ID required with --all.");
548676
549321
  return 1;
548677
549322
  }
548678
- console.log(JSON.stringify({ deleted: taskId }));
549323
+ const includeLocal = values2.runner !== "cloud";
549324
+ const includeCloud = values2.runner !== "local";
549325
+ let localDeleted = 0;
549326
+ if (includeLocal) {
549327
+ localDeleted = deleteAllTasks(agentId);
549328
+ }
549329
+ let cloudDeleted = 0;
549330
+ if (includeCloud) {
549331
+ const resolved = await getRunnerForAgent(undefined, agentId);
549332
+ const cloudCapable = !("error" in resolved) && resolved.runner === "cloud";
549333
+ const cloudExplicit = values2.runner === "cloud";
549334
+ if (cloudCapable || cloudExplicit) {
549335
+ try {
549336
+ const response = await listCloudSchedules(agentId);
549337
+ for (const schedule of response.scheduled_messages) {
549338
+ await deleteCloudSchedule(agentId, schedule.id);
549339
+ cloudDeleted += 1;
549340
+ }
549341
+ } catch (err) {
549342
+ console.error(`Error: failed to delete Cloud schedules: ${err instanceof Error ? err.message : String(err)}`);
549343
+ console.error(`Deleted so far: ${localDeleted} local, ${cloudDeleted} cloud.`);
549344
+ return 1;
549345
+ }
549346
+ }
549347
+ }
549348
+ console.log(JSON.stringify({
549349
+ deleted: localDeleted + cloudDeleted,
549350
+ local_deleted: localDeleted,
549351
+ cloud_deleted: cloudDeleted,
549352
+ agent_id: agentId
549353
+ }));
548679
549354
  return 0;
548680
549355
  }
548681
549356
  async function runCronSubcommand(argv) {
@@ -548698,7 +549373,7 @@ async function runCronSubcommand(argv) {
548698
549373
  case "list":
548699
549374
  return handleList(parsed.values);
548700
549375
  case "get":
548701
- return handleGet(parsed.positionals);
549376
+ return handleGet(parsed.values, parsed.positionals);
548702
549377
  case "runs":
548703
549378
  return handleRuns(parsed.values);
548704
549379
  case "delete":
@@ -557864,4 +558539,4 @@ Error during initialization: ${message}`);
557864
558539
  }
557865
558540
  main2();
557866
558541
 
557867
- //# debugId=8849A89953C6484464756E2164756E21
558542
+ //# debugId=E8D52F75291B5E0B64756E2164756E21