linkshell-cli 0.5.20 → 0.5.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -708,6 +708,8 @@ const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
708
708
  "resume",
709
709
  "rewind",
710
710
  "settings",
711
+ "setup-bedrock",
712
+ "setup-vertex",
711
713
  "statusline",
712
714
  "stickers",
713
715
  "tasks",
@@ -757,6 +759,7 @@ const CLAUDE_BUILT_IN_COMMANDS = [
757
759
  { name: "bug", description: "Alias for /feedback" },
758
760
  { name: "fewer-permission-prompts", description: "Reduce common permission prompts", argsMode: "none" },
759
761
  { name: "focus", description: "Toggle the focus view", argsMode: "none" },
762
+ { name: "goal", description: "Set or clear a session goal the agent works toward", argsMode: "raw" },
760
763
  { name: "heapdump", description: "Write a JavaScript heap snapshot for diagnostics", argsMode: "none" },
761
764
  { name: "help", description: "Show help and available commands", argsMode: "none" },
762
765
  { name: "hooks", description: "View hook configurations", argsMode: "none" },
@@ -977,7 +980,20 @@ function mergeCommands(...groups) {
977
980
  map.set(key, {
978
981
  ...existing,
979
982
  ...command,
983
+ // Preserve curated metadata the discovered set doesn't carry: the SDK's
984
+ // SlashCommand has no destructive/category/argsMode/executionKind
985
+ // fields, and runtimeCommands hardcodes argsMode:"raw" + executionKind:
986
+ // "prompt". The merge order is (curated, discovered), so without these
987
+ // a discovered /clear would clobber the curated destructive flag
988
+ // (red-dot warning), the grouping, AND the curated argsMode:"none" that
989
+ // makes the web palette execute it on pick (vs insert-and-wait). For
990
+ // Codex it would also flip native commands to "prompt". Curated wins on
991
+ // collision; genuinely new discovered commands fall through to "raw".
992
+ destructive: command.destructive ?? existing?.destructive,
993
+ category: command.category ?? existing?.category,
980
994
  disabledReason: command.disabledReason ?? existing?.disabledReason,
995
+ argsMode: existing?.argsMode ?? command.argsMode,
996
+ executionKind: existing?.executionKind ?? command.executionKind,
981
997
  });
982
998
  }
983
999
  }
@@ -1023,7 +1039,7 @@ function runtimeCommands(provider, value) {
1023
1039
  })
1024
1040
  .filter((entry) => Boolean(entry));
1025
1041
  }
1026
- function parseModelListCapabilities(value) {
1042
+ function parseModelListCapabilities(provider, value) {
1027
1043
  const raw = asRecord(value);
1028
1044
  const modelsValue = Array.isArray(value) ? value :
1029
1045
  Array.isArray(raw?.data) ? raw.data :
@@ -1076,12 +1092,14 @@ function parseModelListCapabilities(value) {
1076
1092
  const reasoningEfforts = efforts.size
1077
1093
  ? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
1078
1094
  : undefined;
1079
- if (models.length === 0 && !defaultModel && !reasoningEfforts?.length)
1095
+ const commands = raw && Array.isArray(raw.commands) ? runtimeCommands(provider, raw.commands) : undefined;
1096
+ if (models.length === 0 && !defaultModel && !reasoningEfforts?.length && !commands?.length)
1080
1097
  return undefined;
1081
1098
  return {
1082
1099
  ...(models.length > 0 ? { models } : {}),
1083
1100
  ...(defaultModel ? { defaultModel } : {}),
1084
1101
  ...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
1102
+ ...(commands?.length ? { commands } : {}),
1085
1103
  };
1086
1104
  }
1087
1105
  function parseTimestamp(value) {
@@ -1115,6 +1133,11 @@ function parseRemoteSessions(value) {
1115
1133
  const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
1116
1134
  if (!id)
1117
1135
  continue;
1136
+ // Only accept a transcript-derived status the provider actually reported
1137
+ // (Claude's listSessions sets "running"/"idle" from the on-disk transcript).
1138
+ // Anything else is left undefined so the sync site keeps its own status.
1139
+ const reportedStatus = firstString(source, ["status"]);
1140
+ const status = reportedStatus === "running" || reportedStatus === "idle" ? reportedStatus : undefined;
1118
1141
  result.push({
1119
1142
  id,
1120
1143
  cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
@@ -1123,6 +1146,7 @@ function parseRemoteSessions(value) {
1123
1146
  createdAt: parseTimestamp(source.createdAt ?? source.created_at),
1124
1147
  lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
1125
1148
  usage: source.usage,
1149
+ ...(status ? { status } : {}),
1126
1150
  });
1127
1151
  }
1128
1152
  return result;
@@ -1687,7 +1711,7 @@ export class AgentWorkspaceProxy {
1687
1711
  if (typeof listModels === "function") {
1688
1712
  try {
1689
1713
  const result = await listModels.call(client);
1690
- runtimeCapabilities = parseModelListCapabilities(result);
1714
+ runtimeCapabilities = parseModelListCapabilities(provider, result);
1691
1715
  }
1692
1716
  catch (error) {
1693
1717
  if (this.input.verbose) {
@@ -1750,6 +1774,26 @@ export class AgentWorkspaceProxy {
1750
1774
  const usage = usageFields
1751
1775
  ? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
1752
1776
  : existing?.usage;
1777
+ // Status precedence. While LinkShell is driving a turn for this
1778
+ // conversation, the in-memory status is authoritative — the transcript
1779
+ // lags (the SDK hasn't flushed the in-flight turn to disk), so trusting
1780
+ // it would flicker a live "running" turn back to idle. Otherwise the
1781
+ // transcript-derived remote.status is authoritative for Claude (it
1782
+ // reports running/idle straight from disk): surface "running" while an
1783
+ // external process drives it, and let its "idle" clear a previously
1784
+ // observed "running" once the external turn finishes. We only fall
1785
+ // back to the existing status to keep error/waiting_permission sticky
1786
+ // across a re-sync — deriveTranscriptStatus never emits those, so they
1787
+ // live only in `existing`; defaulting everything else to idle avoids a
1788
+ // purely-external session getting stuck at "running" forever.
1789
+ const linkshellDriving = this.currentTurnIds.has(conversationId);
1790
+ const status = linkshellDriving
1791
+ ? existing?.status ?? "running"
1792
+ : remote.status === "running"
1793
+ ? "running"
1794
+ : existing?.status === "error" || existing?.status === "waiting_permission"
1795
+ ? existing.status
1796
+ : "idle";
1753
1797
  const conversation = {
1754
1798
  id: conversationId,
1755
1799
  agentSessionId,
@@ -1760,7 +1804,7 @@ export class AgentWorkspaceProxy {
1760
1804
  reasoningEffort: existing?.reasoningEffort,
1761
1805
  permissionMode: existing?.permissionMode,
1762
1806
  collaborationMode: existing?.collaborationMode,
1763
- status: existing?.status ?? "idle",
1807
+ status,
1764
1808
  archived: existing?.archived ?? false,
1765
1809
  lastMessagePreview: existing?.lastMessagePreview,
1766
1810
  lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
@@ -1854,6 +1898,22 @@ export class AgentWorkspaceProxy {
1854
1898
  const model = firstString(thread, ["model", "modelId", "currentModel"]);
1855
1899
  if (model && !conversation.model)
1856
1900
  conversation.model = model;
1901
+ // Seed a scroll-back cursor for Claude, which hydrates via readSession (full
1902
+ // transcript) yet pages via listTurns. The timeline only keeps the last
1903
+ // MAX_TIMELINE_ITEMS, so anything older is dropped here but reachable via
1904
+ // agent.v2.history.request. Cursor = total turn count (end-exclusive): the
1905
+ // first page overlaps the displayed tail (harmlessly deduped by the client's
1906
+ // mergeTimeline) and each subsequent page walks strictly older, terminating
1907
+ // when the cursor reaches 0. This integer-index cursor is Claude-specific —
1908
+ // Codex uses an opaque string cursor captured by the listTurns branch above,
1909
+ // so gate strictly on the provider rather than on listTurns presence (Codex
1910
+ // also exposes listTurns) to avoid poisoning Codex's cursor.
1911
+ if (conversation.provider === "claude" &&
1912
+ !this.historyCursors.has(conversation.id)) {
1913
+ const turnCount = Array.isArray(thread?.turns) ? thread.turns.length : 0;
1914
+ if (turnCount > 0)
1915
+ this.historyCursors.set(conversation.id, String(turnCount));
1916
+ }
1857
1917
  const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
1858
1918
  if (hydratedItems.length === 0)
1859
1919
  return;