@tekmidian/pai 0.40.0 → 0.41.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.
@@ -25,12 +25,22 @@ pai worker run --label "short task label" --class research \\
25
25
  `pai worker run --chain draft,implement …` — the draft stage turns the brief
26
26
  into a spec file, implement runs with it, `--chain draft,implement,review`
27
27
  adds a review pass. `--class spotcheck` for verification runs.
28
+ - Grant MCP tools by naming mcp__server__tool in --allowedTools (the server loads automatically).
28
29
  - The answer is in the `result` field of the JSON it prints. Review the diff yourself.
29
30
  - `--no-pane` suppresses the iTerm follow pane; panes open automatically otherwise.
30
31
  - `--agent <name>` runs a definition from ~/.claude/agents/<name>.md (the agent
31
32
  library runs on workers: body becomes the system prompt, tools the allowlist,
32
33
  model the class).
33
34
 
35
+ ### Waiting
36
+
37
+ - NEVER busy-wait for a worker. No sleep loops, no `sleep N; pai worker ps`
38
+ polling, no manual retry loops. Two sanctioned waits: run workers as
39
+ background Bash tasks (the harness fires a completion notification), or
40
+ call `pai worker wait <id...>` which blocks until they finish and prints
41
+ each result as one JSON line. If you catch yourself sleeping to re-check a
42
+ worker, stop — you already get notified.
43
+
34
44
  ### Watching
35
45
 
36
46
  - `worker_ps` — running + last finished workers (chains show as trees).
@@ -68,8 +68,8 @@ Generated man pages for every `pai` command area. Read any page in the terminal
68
68
  | `pai kg query` | Query KG triples by subject, predicate, object, time, or project |
69
69
  | `pai kg stats` | Show triple counts and contradiction count |
70
70
  | `pai mcp` | MCP server management: install and status |
71
- | `pai mcp install` | Register the PAI MCP server in ~/.claude.json (restart Claude Code to activate) |
72
- | `pai mcp status` | Show whether the PAI MCP server is registered and the binary exists |
71
+ | `pai mcp install` | Register the PAI MCP server (pai) in ~/.claude.json (restart Claude Code to activate) |
72
+ | `pai mcp status` | Show whether the PAI MCP server (pai) is registered and the binary exists |
73
73
  | `pai memory` | Memory engine: index, search, and status |
74
74
  | `pai memory embed [project-slug]` | Generate embeddings for un-embedded chunks (Phase 2.5) |
75
75
  | `pai memory index [project-slug]` | Index memory files for one project or all projects |
@@ -14,17 +14,17 @@ pai mcp <subcommand> [options]
14
14
 
15
15
  | Command | Description |
16
16
  |---------|-------------|
17
- | [`pai mcp install`](#pai-mcp-install) | Register the PAI MCP server in ~/.claude.json (restart Claude Code to activate) |
18
- | [`pai mcp status`](#pai-mcp-status) | Show whether the PAI MCP server is registered and the binary exists |
17
+ | [`pai mcp install`](#pai-mcp-install) | Register the PAI MCP server (pai) in ~/.claude.json (restart Claude Code to activate) |
18
+ | [`pai mcp status`](#pai-mcp-status) | Show whether the PAI MCP server (pai) is registered and the binary exists |
19
19
 
20
20
  ### pai mcp install
21
21
 
22
- Register the PAI MCP server in ~/.claude.json (restart Claude Code to activate)
22
+ Register the PAI MCP server (pai) in ~/.claude.json (restart Claude Code to activate)
23
23
 
24
24
 
25
25
  ### pai mcp status
26
26
 
27
- Show whether the PAI MCP server is registered and the binary exists
27
+ Show whether the PAI MCP server (pai) is registered and the binary exists
28
28
 
29
29
 
30
30
  ## See also
@@ -47,6 +47,7 @@ Run one claude-code worker through the configured provider.
47
47
 
48
48
  Unknown options are passed to claude verbatim (e.g. -p, --allowedTools);
49
49
  --output-format/--verbose are handled here.
50
+ Grant MCP tools by naming mcp__server__tool in --allowedTools (the server loads automatically);
50
51
  --chain draft,implement[,review] runs a spec-first pipeline;
51
52
  --agent <name> runs an agent definition from ~/.claude/agents.
52
53
 
@@ -192,6 +193,8 @@ Merge a worker's worktree branch (worker/<id>) into the original checkout, then
192
193
 
193
194
  Poll workers until they finish; prints each result as one JSON line, exit 1 on failure or timeout
194
195
 
196
+ Never busy-wait for a worker: no sleep loops, no sleep-then-`pai worker ps` polling, no manual retry loops. Two sanctioned waits: run workers as background Bash tasks (the harness notifies on completion), or call `pai worker wait`, which blocks until they finish and prints each result. If you catch yourself sleeping to re-check a worker, stop — you already get notified.
197
+
195
198
  **Arguments**
196
199
 
197
200
  | Argument | Kind |
@@ -1,22 +1,20 @@
1
1
  # Provider Independence
2
2
 
3
- How to free yourself from Claude — so a maxed-out subscription stops being your problem. How to run any cloud model and any local model inside your Claude Code (or OpenCode) harness. How to run models in parallel, each doing what it is best at all of it purely local, no need for yet another Hermes-or-whatever harness. How to have workers run by default, visibly, each with its own chat you can talk to.
3
+ I started my Thursday at 5am, which is normaland at about 80%, which is not.
4
4
 
5
- My journey into PAI and AIBroker started nine months ago, when OpenClawd burned through all my credits in four hours. I decided then and there that there had to be a better way: instead of buying API tokens, use the Claude Max subscription I was already paying for.
5
+ Because those 80% are not motivation (that was certainly higher). That is the weekly usage of my Claude Max 200 subscription.
6
6
 
7
- Nine months later, I hit the next wall: maxing out Claude Max itself. The next level of independence was required. I built it in one day. Here is what happened.
7
+ With still two days to go, I was getting worried. Last week I had already hit 100% hard despite all my efficiency gains, including creating my own "Agentish" language.
8
8
 
9
- I started my Thursday at 5am, which is normal, and at about 80%, which is not.
9
+ At 9am I was above 90%, and there was no way I would make it through to Saturday 8am, the weekly reset.
10
10
 
11
- That 80% is not motivation although that was certainly higher. It is the weekly usage of my Claude Max 200 subscription.
11
+ So I decided to solve this problem. At 10pm I am at 97% and that number has not changed since noon.
12
12
 
13
- With two days to go until the Saturday 8am reset, I was getting worried. Last week I had already hit 100%despite all my efficiency gains, including inventing my own "Agentish" language.
13
+ This page shows how to become provider-independent. How to free yourself from Claude, so a maxed-out subscription stops being your problem. How to use any cloud model and any local model inside your Claude Code (or OpenCode) harness. How to run models in parallel, each for what it is good at — all purely local, without resorting to yet another Hermes-or-whatever harness. How to have workers run by default, visibly and how to interact with each of them in its own chat.
14
14
 
15
- By 9am I was above 90%. There was no way I would make it to the reset.
15
+ My whole journey into PAI and AIBroker started nine months ago, when I used OpenClawd for four hours and had burned through all my credits. I decided then and there that there had to be a better way: instead of API tokens, use the Claude Max subscription I was paying for anyway.
16
16
 
17
- So I decided to solve this problem instead of waiting it out. By 10pm I was at 97% a number that has not moved since noon, because since noon, almost nothing has run on Claude.
18
-
19
- This is how I did it.
17
+ Nine months later, I ran into the next wall: maxing out the Claude Max subscription itself. So the next level of independence was required and I made that happen, today. Here is what happened.
20
18
 
21
19
  I made myself independent of Claude and Fable. They are great — but I hate being locked in, and I suspect most of us do. I want to decide which model I use: Claude, GLM, Grok, whatever. I need to abstract away from that choice.
22
20
 
@@ -35,18 +33,7 @@ So the requirements were:
35
33
 
36
34
  The whole implementation cost 7 of my last 10 percentage points of Claude Max 200. Then I switched to GLM for everything else. And what can I say — it works beautifully.
37
35
 
38
- ---
39
-
40
- The rest of this page is the operator's guide: switching providers, starting sessions, and the three copy-paste steps to your own setup.
41
-
42
- > My AI budget hit 91% before lunch. So I changed one rule: the expensive
43
- > assistant keeps the thinking, a cheaper crew does the building. The feature
44
- > shipped anyway.
45
- >
46
- > Your AI bill is not a fact of nature. It is a design decision.
47
36
 
48
- Your assistant runs its helper crew on whichever AI provider you choose —
49
- and so can the session itself.
50
37
 
51
38
  ## Switching
52
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "PAI Knowledge OS — Personal AI Infrastructure with federated memory and project management",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -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