@tekmidian/pai 0.40.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +1 -0
  2. package/dist/aibroker-client-BP74gGwD.mjs +215 -0
  3. package/dist/aibroker-client-BP74gGwD.mjs.map +1 -0
  4. package/dist/aibroker-client-C6m71p6O.mjs +3 -0
  5. package/dist/{auto-route-BwzmlW3W.mjs → auto-route-_5zzJz5q.mjs} +2 -2
  6. package/dist/{auto-route-BwzmlW3W.mjs.map → auto-route-_5zzJz5q.mjs.map} +1 -1
  7. package/dist/browser-mcp/index.d.mts +1 -0
  8. package/dist/browser-mcp/index.mjs +352 -0
  9. package/dist/browser-mcp/index.mjs.map +1 -0
  10. package/dist/{chain-DhVHVnmT.mjs → chain-DGXNoTwc.mjs} +93 -320
  11. package/dist/chain-DGXNoTwc.mjs.map +1 -0
  12. package/dist/cli/index.mjs +12 -10
  13. package/dist/cli/index.mjs.map +1 -1
  14. package/dist/cli/program.mjs +12 -10
  15. package/dist/{clusters-DRHhfli9.mjs → clusters-j0ZuNYp9.mjs} +1 -1
  16. package/dist/{clusters-DRHhfli9.mjs.map → clusters-j0ZuNYp9.mjs.map} +1 -1
  17. package/dist/{config-D1G9IFpn.mjs → config-B64vFg14.mjs} +3 -50
  18. package/dist/config-B64vFg14.mjs.map +1 -0
  19. package/dist/{config-DD52BPB-.mjs → config-C_ErGddD.mjs} +1 -1
  20. package/dist/{config-Dl8lT4Lu.mjs → config-DgzR5sfb.mjs} +2 -2
  21. package/dist/{config-Dl8lT4Lu.mjs.map → config-DgzR5sfb.mjs.map} +1 -1
  22. package/dist/{context-handover-cache-C6_rs7JT.mjs → context-handover-cache-dUnSZwaA.mjs} +4 -4
  23. package/dist/{context-handover-cache-C6_rs7JT.mjs.map → context-handover-cache-dUnSZwaA.mjs.map} +1 -1
  24. package/dist/daemon/index.mjs +13 -12
  25. package/dist/daemon/index.mjs.map +1 -1
  26. package/dist/daemon-CKBkhOsZ.mjs +23 -0
  27. package/dist/{daemon-CpdVUnl_.mjs → daemon-DsvP8KMy.mjs} +357 -27
  28. package/dist/daemon-DsvP8KMy.mjs.map +1 -0
  29. package/dist/daemon-mcp/index.mjs +20 -6
  30. package/dist/daemon-mcp/index.mjs.map +1 -1
  31. package/dist/detector-C-7KRi42.mjs +5 -0
  32. package/dist/{detector-smpCm7ku.mjs → detector-C73Nxm94.mjs} +1 -1
  33. package/dist/{detector-smpCm7ku.mjs.map → detector-C73Nxm94.mjs.map} +1 -1
  34. package/dist/{factory-Bc5EyN54.mjs → factory-DEb9NmHJ.mjs} +7 -7
  35. package/dist/{factory-Bc5EyN54.mjs.map → factory-DEb9NmHJ.mjs.map} +1 -1
  36. package/dist/factory-Dbo8yNPi.mjs +3 -0
  37. package/dist/{fallback-CdXv-Np0.mjs → fallback-5UJO-RyX.mjs} +5 -4
  38. package/dist/{fallback-CdXv-Np0.mjs.map → fallback-5UJO-RyX.mjs.map} +1 -1
  39. package/dist/hooks/worker-status-line.mjs +9 -4
  40. package/dist/hooks/worker-status-line.mjs.map +3 -3
  41. package/dist/hooks/worker-supervision.mjs +593 -0
  42. package/dist/hooks/worker-supervision.mjs.map +7 -0
  43. package/dist/{indexer-backend-rohVVKrA.mjs → indexer-backend-BPTzKFZU.mjs} +1 -1
  44. package/dist/{indexer-backend-rohVVKrA.mjs.map → indexer-backend-BPTzKFZU.mjs.map} +1 -1
  45. package/dist/{ipc-client-C7AVpsyv.mjs → ipc-client-BmypMNYk.mjs} +2 -2
  46. package/dist/{ipc-client-C7AVpsyv.mjs.map → ipc-client-BmypMNYk.mjs.map} +1 -1
  47. package/dist/{kg-entity-DbOMPdF9.mjs → kg-entity-r8duqhi9.mjs} +1 -1
  48. package/dist/{kg-entity-DbOMPdF9.mjs.map → kg-entity-r8duqhi9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-iNp5s55R.mjs → latent-ideas-CtSca4qG.mjs} +2 -2
  50. package/dist/{latent-ideas-iNp5s55R.mjs.map → latent-ideas-CtSca4qG.mjs.map} +1 -1
  51. package/dist/{link-boost-NNzADHX_.mjs → link-boost-Bucnz3n8.mjs} +1 -1
  52. package/dist/{link-boost-NNzADHX_.mjs.map → link-boost-Bucnz3n8.mjs.map} +1 -1
  53. package/dist/{main-resolver-CM1IHbuu.mjs → main-resolver-BgIrC-D9.mjs} +4 -214
  54. package/dist/main-resolver-BgIrC-D9.mjs.map +1 -0
  55. package/dist/main-resolver-DXGuygFG.mjs +7 -0
  56. package/dist/{neighborhood-AGL7oXQ2.mjs → neighborhood-CM7Jgb51.mjs} +1 -1
  57. package/dist/{neighborhood-AGL7oXQ2.mjs.map → neighborhood-CM7Jgb51.mjs.map} +1 -1
  58. package/dist/{note-context-BHwYpSFf.mjs → note-context-DCwE2A-M.mjs} +1 -1
  59. package/dist/{note-context-BHwYpSFf.mjs.map → note-context-DCwE2A-M.mjs.map} +1 -1
  60. package/dist/{planner-Cm3g6fWH.mjs → planner-BLDJt3ZS.mjs} +6 -5
  61. package/dist/{planner-Cm3g6fWH.mjs.map → planner-BLDJt3ZS.mjs.map} +1 -1
  62. package/dist/{postgres-Dwxe290H.mjs → postgres-CtOzfoQU.mjs} +1 -1
  63. package/dist/{postgres-Dwxe290H.mjs.map → postgres-CtOzfoQU.mjs.map} +1 -1
  64. package/dist/{program-AR0qlYRg.mjs → program-CruRnLtc.mjs} +99 -84
  65. package/dist/program-CruRnLtc.mjs.map +1 -0
  66. package/dist/query-feedback-DGoVRaWD.mjs +3 -0
  67. package/dist/{query-feedback-DZfjI34w.mjs → query-feedback-Dqz-HC__.mjs} +1 -1
  68. package/dist/{query-feedback-DZfjI34w.mjs.map → query-feedback-Dqz-HC__.mjs.map} +1 -1
  69. package/dist/router-BTIGZR2_.mjs +3 -0
  70. package/dist/{router-DHracTUC.mjs → router-DD6D4j31.mjs} +1 -1
  71. package/dist/{router-DHracTUC.mjs.map → router-DD6D4j31.mjs.map} +1 -1
  72. package/dist/runtime-paths-rni52zHX.mjs +50 -0
  73. package/dist/runtime-paths-rni52zHX.mjs.map +1 -0
  74. package/dist/scope-NIwCGxRI.mjs +351 -0
  75. package/dist/scope-NIwCGxRI.mjs.map +1 -0
  76. package/dist/{server-BOuAOj9b.mjs → server-Cp7tNzNC.mjs} +19 -2
  77. package/dist/server-Cp7tNzNC.mjs.map +1 -0
  78. package/dist/skills/Worker/SKILL.md +13 -0
  79. package/dist/{sources-ju6AA7lL.mjs → sources-CS43CpuS.mjs} +1 -1
  80. package/dist/{sources-ju6AA7lL.mjs.map → sources-CS43CpuS.mjs.map} +1 -1
  81. package/dist/{sqlite-kFZeWCJ2.mjs → sqlite-Cq3gJcq9.mjs} +1 -1
  82. package/dist/{sqlite-kFZeWCJ2.mjs.map → sqlite-Cq3gJcq9.mjs.map} +1 -1
  83. package/dist/{state-ahFAbD8b.mjs → state-BTa10SOM.mjs} +1 -1
  84. package/dist/{state-ahFAbD8b.mjs.map → state-BTa10SOM.mjs.map} +1 -1
  85. package/dist/{state-DH_mp5Xe.mjs → state-RXepvbnO.mjs} +1 -1
  86. package/dist/{themes-DqhNUUQk.mjs → themes-B7h9YDiy.mjs} +1 -1
  87. package/dist/{themes-DqhNUUQk.mjs.map → themes-B7h9YDiy.mjs.map} +1 -1
  88. package/dist/{tools-COna9vZR.mjs → tools-BW3NLMPk.mjs} +1 -1
  89. package/dist/{tools-CMlHa9lZ.mjs → tools-LlzcV_eK.mjs} +14 -14
  90. package/dist/{tools-CMlHa9lZ.mjs.map → tools-LlzcV_eK.mjs.map} +1 -1
  91. package/dist/{trace-DTP6k4B_.mjs → trace-u4hT-pLD.mjs} +1 -1
  92. package/dist/{trace-DTP6k4B_.mjs.map → trace-u4hT-pLD.mjs.map} +1 -1
  93. package/dist/{vault-indexer-YMH22mLU.mjs → vault-indexer-mata9S7b.mjs} +1 -1
  94. package/dist/{vault-indexer-YMH22mLU.mjs.map → vault-indexer-mata9S7b.mjs.map} +1 -1
  95. package/dist/{work-queue-worker-B3nl8yyn.mjs → work-queue-worker-7Vkli4Yh.mjs} +4 -4
  96. package/dist/{work-queue-worker-B3nl8yyn.mjs.map → work-queue-worker-7Vkli4Yh.mjs.map} +1 -1
  97. package/dist/work-queue-worker-D1p2V-ae.mjs +14 -0
  98. package/dist/{zettelkasten-DDvKRm2V.mjs → zettelkasten-F0D_XfQ4.mjs} +3 -3
  99. package/dist/{zettelkasten-DDvKRm2V.mjs.map → zettelkasten-F0D_XfQ4.mjs.map} +1 -1
  100. package/docs/commands/README.md +2 -2
  101. package/docs/commands/mcp.md +4 -4
  102. package/docs/commands/worker.md +3 -0
  103. package/docs/worker.md +40 -0
  104. package/package.json +1 -1
  105. package/plugins/productivity/hooks/hooks.json +5 -0
  106. package/src/hooks/ts/user-prompt/worker-supervision.ts +123 -0
  107. package/statusline-command.sh +65 -73
  108. package/dist/chain-DhVHVnmT.mjs.map +0 -1
  109. package/dist/config-D1G9IFpn.mjs.map +0 -1
  110. package/dist/daemon-BSL9sXxm.mjs +0 -22
  111. package/dist/daemon-CpdVUnl_.mjs.map +0 -1
  112. package/dist/detector-BLVOCiJ_.mjs +0 -5
  113. package/dist/factory-C7ua_3ik.mjs +0 -3
  114. package/dist/main-resolver-CM1IHbuu.mjs.map +0 -1
  115. package/dist/main-resolver-E5WlilhM.mjs +0 -6
  116. package/dist/program-AR0qlYRg.mjs.map +0 -1
  117. package/dist/query-feedback-CGcEiGtY.mjs +0 -3
  118. package/dist/router-CAtyVQud.mjs +0 -3
  119. package/dist/server-BOuAOj9b.mjs.map +0 -1
  120. package/dist/work-queue-worker-BM1GcAAq.mjs +0 -14
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * worker-supervision.ts
5
+ *
6
+ * UserPromptSubmit hook that surfaces worker supervision events into the
7
+ * session that owns the workers. The PAI daemon watches every worker's
8
+ * status file (src/workers/supervision.ts) and appends one JSON line per
9
+ * finished/failed/stalled event to
10
+ * <logDir>/supervision/<claude-session-id>.events. AIBroker pushes the same
11
+ * line into the terminal when it can; this hook is the channel that works
12
+ * without one — on the next prompt, every line the session has not yet seen
13
+ * is injected as context, and the cursor moves past it.
14
+ *
15
+ * Together the two channels replace polling: the orchestrator gets told its
16
+ * worker finished, failed or stalled, and never sleeps to find out.
17
+ */
18
+
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { isWorkerSession } from "../lib/worker-session.js";
22
+ import { readWorkersSection } from "../../../workers/config.js";
23
+ import { workersLogDir } from "../../../workers/paths.js";
24
+
25
+ interface HookInput {
26
+ session_id?: string;
27
+ }
28
+
29
+ /**
30
+ * Lines this session has already had injected. The count (not a byte offset)
31
+ * is the cursor because the file is append-only by construction; a recreated
32
+ * file simply clamps back to zero and replays its fresh content.
33
+ */
34
+ function readCursor(path: string): number {
35
+ try {
36
+ return Math.max(0, Number(readFileSync(path, "utf8").trim()) || 0);
37
+ } catch {
38
+ return 0;
39
+ }
40
+ }
41
+
42
+ function main(): void {
43
+ if (isWorkerSession()) return; // workers supervise nothing
44
+
45
+ let raw = "";
46
+ try {
47
+ raw = readFileSync(0, "utf8");
48
+ } catch {
49
+ return;
50
+ }
51
+ if (!raw.trim()) return;
52
+ let input: HookInput;
53
+ try {
54
+ input = JSON.parse(raw) as HookInput;
55
+ } catch {
56
+ return;
57
+ }
58
+ const session = input.session_id;
59
+ if (!session) return;
60
+
61
+ // logDir is wherever workers.logDir says (default ~/.claude/logs/workers);
62
+ // a missing or broken workers section means no supervision to surface
63
+ let logDir: string;
64
+ try {
65
+ logDir = workersLogDir(readWorkersSection().workers);
66
+ } catch {
67
+ return;
68
+ }
69
+
70
+ const dir = join(logDir, "supervision");
71
+ const eventsPath = join(dir, `${session}.events`);
72
+ const cursorPath = join(dir, `${session}.cursor`);
73
+ if (!existsSync(eventsPath)) return;
74
+
75
+ let lines: string[];
76
+ try {
77
+ lines = readFileSync(eventsPath, "utf8").split("\n").filter((l) => l.trim());
78
+ } catch {
79
+ return;
80
+ }
81
+ const fresh = lines.slice(readCursor(cursorPath));
82
+ if (fresh.length === 0) return;
83
+
84
+ // events AIBroker already typed into this terminal have a push receipt;
85
+ // this hook is the fallback channel and must not repeat them
86
+ let pushedIds = new Set<string>();
87
+ try {
88
+ pushedIds = new Set(
89
+ readFileSync(join(dir, `${session}.pushed`), "utf8")
90
+ .split("\n")
91
+ .map((l) => l.trim())
92
+ .filter(Boolean)
93
+ );
94
+ } catch {
95
+ // no receipts means nothing was pushed, everything surfaces here
96
+ }
97
+
98
+ const texts = fresh
99
+ .map((l) => {
100
+ try {
101
+ const ev = JSON.parse(l) as { id?: string; text?: string };
102
+ if (ev.id && pushedIds.has(ev.id)) return null;
103
+ return ev.text ?? null;
104
+ } catch {
105
+ return null;
106
+ }
107
+ })
108
+ .filter((t): t is string => t !== null);
109
+ try {
110
+ writeFileSync(cursorPath, String(lines.length), "utf8");
111
+ } catch {
112
+ // an unwritable cursor only means these lines surface once more
113
+ }
114
+ if (texts.length === 0) return; // damaged lines are consumed, not re-shown
115
+
116
+ process.stdout.write(
117
+ `<system-reminder>\nWORKER EVENTS (delivered by the daemon's supervision — no polling needed):\n` +
118
+ texts.join("\n") +
119
+ `\n</system-reminder>\n`
120
+ );
121
+ }
122
+
123
+ main();
@@ -11,7 +11,7 @@
11
11
  #
12
12
  # LINES DISPLAYED:
13
13
  # 1. Greeting: DA name, model, directory
14
- # 2. MCPs: Active MCP servers (wraps on narrow terminals)
14
+ # 2. MCPs: server count, then names that fit the width (rest shown as +N)
15
15
  # 3. Context: Current session context window usage (K / 200K)
16
16
  #
17
17
  # ENVIRONMENT VARIABLES (set in settings.json env section):
@@ -86,27 +86,26 @@ fi
86
86
  # Config directory
87
87
  claude_dir="${PAI_DIR:-$HOME/.claude}"
88
88
 
89
- # Count MCPs from all config sources (settings.json, .mcp.json, ~/.claude.json)
90
- mcp_names_raw=""
91
- mcps_count=0
89
+ # Collect MCP server names from all config sources (settings.json, .mcp.json,
90
+ # ~/.claude.json). Only entries that define a server (object with a command,
91
+ # type or url field) count: Claude Code also stores per-tool usage stats in
92
+ # mcpServers under tool-name keys ("Read", "mcp__pai__memory_search", …), and
93
+ # those must never reach the status line. Keys are newline-separated so names
94
+ # containing spaces survive.
95
+ mcp_names=""
92
96
 
93
- # Helper: merge MCP names from a jq-compatible JSON file
94
97
  _merge_mcps() {
95
98
  local file="$1"
96
99
  [ -f "$file" ] || return
97
- local data
98
- data=$(jq -r '.mcpServers | keys | join(" "), length' "$file" 2>/dev/null)
99
- [ -n "$data" ] && [ "$data" != "null" ] || return
100
- local names count
101
- names=$(echo "$data" | head -1)
102
- count=$(echo "$data" | tail -1)
100
+ local names
101
+ names=$(jq -r '.mcpServers // {} | to_entries[] | select(((.value | type) == "object") and (.value | (has("command") or has("type") or has("url")))) | .key' "$file" 2>/dev/null)
103
102
  [ -n "$names" ] || return
104
- if [ -n "$mcp_names_raw" ]; then
105
- mcp_names_raw="$mcp_names_raw $names"
103
+ if [ -n "$mcp_names" ]; then
104
+ mcp_names="${mcp_names}
105
+ ${names}"
106
106
  else
107
- mcp_names_raw="$names"
107
+ mcp_names="$names"
108
108
  fi
109
- mcps_count=$((mcps_count + count))
110
109
  }
111
110
 
112
111
  # Read from all three MCP config locations
@@ -114,11 +113,12 @@ _merge_mcps "$claude_dir/settings.json" # legacy
114
113
  _merge_mcps "$claude_dir/.mcp.json" # project-level
115
114
  _merge_mcps "$HOME/.claude.json" # user-level (e.g. Coogle, DEVONthink)
116
115
 
117
- # Deduplicate MCP names (preserving order)
118
- if [ -n "$mcp_names_raw" ]; then
119
- mcp_names_raw=$(echo "$mcp_names_raw" | tr ' ' '\n' | awk '!seen[$0]++' | tr '\n' ' ' | sed 's/ $//')
120
- mcps_count=$(echo "$mcp_names_raw" | wc -w | tr -d ' ')
121
- fi
116
+ # Deduplicate (case-insensitive macOS config keys drift in case), first
117
+ # spelling wins, order preserved
118
+ mcp_list=()
119
+ while IFS= read -r mcp_line; do
120
+ mcp_list+=("$mcp_line")
121
+ done < <(printf '%s\n' "$mcp_names" | awk 'NF && !seen[tolower($0)]++')
122
122
 
123
123
  # Extract context window usage from Claude Code's JSON input (no JSONL parsing needed)
124
124
  context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' 2>/dev/null)
@@ -247,8 +247,7 @@ if [ "${PAI_SIMPLE_COLORS:-0}" = "1" ]; then
247
247
  MCP_DEFAULT='\033[34m'
248
248
  fi
249
249
 
250
- # Format MCP names with terminal-width-aware wrapping
251
- # Debug: log available width info (remove after testing)
250
+ # Format the MCP segment (see the render block below)
252
251
  # Terminal width for line truncation
253
252
  # Claude Code's statusline subprocess can't detect resize (stty returns stale values).
254
253
  # To set your width: echo 105 > ~/.claude/.statusline_width
@@ -258,8 +257,7 @@ term_width=80
258
257
  [ "$term_width" -gt 0 ] 2>/dev/null || term_width=80
259
258
  mcp_prefix_width=10 # visual width of "🔌 MCPs: " (emoji=2 + space + "MCPs: " = 10)
260
259
 
261
- # Build MCP output proactively split into two lines when there are many MCPs.
262
- # No width detection needed: if total display chars > 60, split at the midpoint.
260
+ # Display-name mapping for known servers
263
261
  _mcp_display_name() {
264
262
  case "$1" in
265
263
  "daemon") echo "Daemon" ;;
@@ -275,7 +273,9 @@ _mcp_display_name() {
275
273
  "macos_automator") echo "macOS" ;;
276
274
  "claude_ai_Gmail") echo "Gmail" ;;
277
275
  "claude_ai_Google_Calendar") echo "GCal" ;;
278
- *) local n="$1"; echo "${n^}" ;;
276
+ # capitalize without bash-4 ${n^} so /bin/bash 3.2 renders names too
277
+ *) local n="$1"
278
+ printf '%s%s' "$(printf '%s' "$n" | cut -c1 | tr '[:lower:]' '[:upper:]')" "$(printf '%s' "$n" | cut -c2-)" ;;
279
279
  esac
280
280
  }
281
281
 
@@ -288,57 +288,53 @@ _mcp_formatted() {
288
288
  esac
289
289
  }
290
290
 
291
- # Collect all display names and calculate total width
292
- mcp_display_names=()
293
- mcp_formatted_strs=()
294
- total_display_width=$mcp_prefix_width # start with "🔌 MCPs: " prefix
295
- total_mcps=0
296
-
297
- for mcp in $mcp_names_raw; do
298
- dn=$(_mcp_display_name "$mcp")
299
- fm=$(_mcp_formatted "$dn")
300
- mcp_display_names+=("$dn")
301
- mcp_formatted_strs+=("$fm")
302
- if [ $total_mcps -gt 0 ]; then
303
- total_display_width=$((total_display_width + 2)) # ", "
304
- fi
305
- total_display_width=$((total_display_width + ${#dn}))
306
- total_mcps=$((total_mcps + 1))
307
- done
308
-
309
- # Decide: one line or two lines?
310
- # If total display width > 60 chars, split at the midpoint
291
+ # Build the MCP segment: "N: name1, name2, … +K" — total server count first,
292
+ # then as many display names as fit the width (hard cap regardless of width),
293
+ # the rest collapsed into "+K". Always a single line: what doesn't fit is
294
+ # counted, never printed.
295
+ mcp_total=${#mcp_list[@]}
311
296
  mcp_line1=""
312
- mcp_line2=""
313
297
 
314
- if [ $total_mcps -eq 0 ]; then
298
+ if [ "$mcp_total" -eq 0 ]; then
315
299
  mcp_line1="none"
316
- elif [ $total_display_width -le $term_width ]; then
317
- # Single line — everything fits
318
- for ((i=0; i<total_mcps; i++)); do
319
- if [ $i -eq 0 ]; then
320
- mcp_line1="${mcp_formatted_strs[$i]}"
321
- else
322
- mcp_line1="${mcp_line1}${SEPARATOR_COLOR}, ${mcp_formatted_strs[$i]}"
323
- fi
324
- done
325
300
  else
326
- # Two lines — split at midpoint
327
- split_at=$(( (total_mcps + 1) / 2 ))
328
- for ((i=0; i<split_at; i++)); do
329
- if [ $i -eq 0 ]; then
330
- mcp_line1="${mcp_formatted_strs[$i]}"
331
- else
332
- mcp_line1="${mcp_line1}${SEPARATOR_COLOR}, ${mcp_formatted_strs[$i]}"
301
+ mcp_max_names=6
302
+ # Width budget for the name list: terminal minus prefix, minus the leading
303
+ # "N: " and a reserve for the trailing " +K" overflow marker
304
+ mcp_budget=$(( term_width - mcp_prefix_width - ${#mcp_total} - 2 - 6 ))
305
+ [ "$mcp_budget" -lt 12 ] && mcp_budget=12
306
+ mcp_width=0
307
+ mcp_shown=0
308
+ mcp_overflow=0
309
+ for mcp in "${mcp_list[@]}"; do
310
+ dn=$(_mcp_display_name "$mcp")
311
+ w_add=${#dn}
312
+ [ "$mcp_shown" -gt 0 ] && w_add=$(( w_add + 2 )) # ", " separator
313
+ if [ "$mcp_shown" -ge "$mcp_max_names" ] || [ $(( mcp_width + w_add )) -gt "$mcp_budget" ]; then
314
+ mcp_overflow=$(( mcp_overflow + 1 ))
315
+ continue
333
316
  fi
334
- done
335
- for ((i=split_at; i<total_mcps; i++)); do
336
- if [ $i -eq $split_at ]; then
337
- mcp_line2="${mcp_formatted_strs[$i]}"
317
+ fm=$(_mcp_formatted "$dn")
318
+ if [ "$mcp_shown" -eq 0 ]; then
319
+ mcp_line1="${mcp_total}${SEPARATOR_COLOR}: ${RESET}${fm}"
320
+ mcp_width=${#mcp_total}
338
321
  else
339
- mcp_line2="${mcp_line2}${SEPARATOR_COLOR}, ${mcp_formatted_strs[$i]}"
322
+ mcp_line1="${mcp_line1}${SEPARATOR_COLOR}, ${fm}"
323
+ mcp_width=$(( mcp_width + 2 ))
340
324
  fi
325
+ mcp_width=$(( mcp_width + ${#dn} ))
326
+ mcp_shown=$(( mcp_shown + 1 ))
341
327
  done
328
+ if [ "$mcp_shown" -eq 0 ]; then
329
+ # Even the first name exceeds the budget — truncate that one name
330
+ dn=$(_mcp_display_name "${mcp_list[0]}")
331
+ avail=$(( mcp_budget - 1 ))
332
+ [ "$avail" -lt 3 ] && avail=3
333
+ dn="${dn:0:$((avail - 1))}…"
334
+ mcp_line1="${mcp_total}${SEPARATOR_COLOR}: ${RESET}$(_mcp_formatted "$dn")"
335
+ mcp_overflow=$(( mcp_total - 1 ))
336
+ fi
337
+ [ "$mcp_overflow" -gt 0 ] && mcp_line1="${mcp_line1}${SEPARATOR_COLOR} +${RESET}${LINE2_ACCENT}${mcp_overflow}${RESET}"
342
338
  fi
343
339
 
344
340
  # Output the statusline
@@ -356,12 +352,8 @@ else
356
352
  printf "${line1_short}\n"
357
353
  fi
358
354
 
359
- # LINE 2 - MCPs (with optional wrap to second line)
355
+ # LINE 2 - MCPs (single line, capped by width; overflow shown as +N)
360
356
  printf "${LINE2_PRIMARY}${EMOJI_PLUG} MCPs${RESET}${LINE2_PRIMARY}${SEPARATOR_COLOR}: ${RESET}${mcp_line1}${RESET}\n"
361
- if [ -n "$mcp_line2" ]; then
362
- # Continuation line — indent to align with MCP names after "🔌 MCPs: "
363
- printf "${LINE2_PRIMARY} ${RESET}${mcp_line2}${RESET}\n"
364
- fi
365
357
 
366
358
 
367
359
  # Usage suffix: provider-aware. glm sessions read the Z.ai plan quota