@tekmidian/pai 0.1.0 → 0.2.1

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/FEATURE.md CHANGED
@@ -19,7 +19,7 @@ different direction: persistent memory, session continuity, and deep Claude Code
19
19
  | **Language** | Python | TypeScript (Bun) |
20
20
  | **Primary interface** | CLI pipe (`echo "..." \| fabric -p pattern`) | MCP server + CLI (`pai`) |
21
21
  | **Prompt templates** | Yes — 200+ community "patterns" | No (out of scope) |
22
- | **YouTube transcript extraction** | Yes (built-in) | No |
22
+ | **YouTube transcript extraction** | Yes (built-in) | Yes — via [Scribe MCP](https://github.com/mnott/Scribe) |
23
23
  | **LLM pipe-through workflow** | Yes — core feature | No |
24
24
  | **Persistent session memory** | No | Yes — auto-indexed, 449K+ chunks |
25
25
  | **Session registry** | No | Yes — SQLite, tracks 77+ projects |
@@ -34,6 +34,7 @@ different direction: persistent memory, session continuity, and deep Claude Code
34
34
  | **Hook system** | No | Yes — pre-compact, session-stop, auto-cleanup |
35
35
  | **Backup / restore** | No | Yes — timestamped pg_dump + registry export |
36
36
  | **Multi-session concurrency** | n/a | Yes — daemon multiplexes Claude sessions |
37
+ | **Custom statusline** | No | Yes — model, MCPs, context meter, colors |
37
38
  | **Local / private** | Yes | Yes — no cloud, no external API for core |
38
39
  | **Docker required** | No | Only for full mode (PostgreSQL); SQLite mode needs none |
39
40
  | **macOS / Linux** | Yes | Yes |
@@ -95,7 +96,9 @@ To be clear about scope:
95
96
  - **Pipe-through LLM workflows** — Fabric's `echo "..." | fabric -p pattern` idiom is elegant
96
97
  for processing text at the command line. PAI doesn't replicate this.
97
98
  - **YouTube / web extraction** — Fabric can pull transcripts and content from URLs as input to
98
- patterns. PAI doesn't.
99
+ patterns. PAI covers YouTube transcription via the companion
100
+ [Scribe MCP](https://github.com/mnott/Scribe) server, but does not replicate Fabric's
101
+ web-scraping pipeline.
99
102
 
100
103
  If you want prompt patterns and CLI pipe-through workflows, use Fabric. If you want Claude
101
104
  Code to remember everything across sessions, use this.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekmidian/pai",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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",
@@ -14,6 +14,7 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "templates",
17
+ "statusline-command.sh",
17
18
  "README.md",
18
19
  "LICENSE",
19
20
  "ARCHITECTURE.md",
@@ -0,0 +1,348 @@
1
+ #!/opt/homebrew/bin/bash
2
+ #
3
+ # PAI Statusline - Customizable status display for Claude Code
4
+ #
5
+ # CUSTOMIZATION:
6
+ # - This script sources ${PAI_DIR}/.env for API keys and configuration
7
+ # - Set PAI_SIMPLE_COLORS=1 in settings.json env for basic ANSI colors
8
+ # (fixes display issues on some terminals)
9
+ # - To add features requiring API keys (e.g., quotes), add keys to .env
10
+ # - Comment out any printf lines you don't want displayed
11
+ #
12
+ # LINES DISPLAYED:
13
+ # 1. Greeting: DA name, model, directory
14
+ # 2. MCPs: Active MCP servers (wraps on narrow terminals)
15
+ # 3. Context: Current session context window usage (K / 200K)
16
+ #
17
+ # ENVIRONMENT VARIABLES (set in settings.json env section):
18
+ # DA - Your assistant's name (default: "Assistant")
19
+ # DA_COLOR - Name color: purple|blue|green|cyan|yellow|red|orange
20
+ # PAI_SIMPLE_COLORS - Set to "1" to use basic terminal colors
21
+ # PAI_NO_EMOJI - Set to "1" to disable emojis (for terminals that don't render them)
22
+ #
23
+
24
+ # Source .env for API keys and custom configuration
25
+ claude_env="${PAI_DIR:-$HOME/.claude}/.env"
26
+ [ -f "$claude_env" ] && source "$claude_env"
27
+
28
+ # Read JSON input from stdin
29
+ input=$(cat)
30
+
31
+ # Get Digital Assistant configuration from environment
32
+ DA_NAME="${DA:-Assistant}" # Assistant name
33
+ DA_COLOR="${DA_COLOR:-purple}" # Color for the assistant name
34
+
35
+ # Extract data from JSON input
36
+ current_dir=$(echo "$input" | jq -r '.workspace.current_dir')
37
+ model_name=$(echo "$input" | jq -r '.model.display_name')
38
+ cc_version=$(echo "$input" | jq -r '.version // "unknown"')
39
+
40
+ # Get directory name
41
+ dir_name=$(basename "$current_dir")
42
+
43
+ # Read Whazaa session name from iTerm2 user variable
44
+ pai_session_name=""
45
+ if [ -n "$ITERM_SESSION_ID" ]; then
46
+ ITERM_UUID="${ITERM_SESSION_ID##*:}"
47
+ pai_session_name=$(osascript << APPLESCRIPT 2>/dev/null
48
+ tell application "iTerm2"
49
+ repeat with aWindow in windows
50
+ repeat with aTab in tabs of aWindow
51
+ repeat with aSession in sessions of aTab
52
+ if id of aSession is "${ITERM_UUID}" then
53
+ tell aSession
54
+ try
55
+ return (variable named "user.paiName")
56
+ on error
57
+ return ""
58
+ end try
59
+ end tell
60
+ end if
61
+ end repeat
62
+ end repeat
63
+ end repeat
64
+ return ""
65
+ end tell
66
+ APPLESCRIPT
67
+ )
68
+ fi
69
+
70
+ # Build session suffix (only if different from dir_name)
71
+ session_suffix=""
72
+ if [ -n "$pai_session_name" ] && [ "$pai_session_name" != "$dir_name" ]; then
73
+ session_suffix=" • ${pai_session_name}"
74
+ fi
75
+
76
+ # Config directory
77
+ claude_dir="${PAI_DIR:-$HOME/.claude}"
78
+
79
+ # Count MCPs from all config sources (settings.json, .mcp.json, ~/.claude.json)
80
+ mcp_names_raw=""
81
+ mcps_count=0
82
+
83
+ # Helper: merge MCP names from a jq-compatible JSON file
84
+ _merge_mcps() {
85
+ local file="$1"
86
+ [ -f "$file" ] || return
87
+ local data
88
+ data=$(jq -r '.mcpServers | keys | join(" "), length' "$file" 2>/dev/null)
89
+ [ -n "$data" ] && [ "$data" != "null" ] || return
90
+ local names count
91
+ names=$(echo "$data" | head -1)
92
+ count=$(echo "$data" | tail -1)
93
+ [ -n "$names" ] || return
94
+ if [ -n "$mcp_names_raw" ]; then
95
+ mcp_names_raw="$mcp_names_raw $names"
96
+ else
97
+ mcp_names_raw="$names"
98
+ fi
99
+ mcps_count=$((mcps_count + count))
100
+ }
101
+
102
+ # Read from all three MCP config locations
103
+ _merge_mcps "$claude_dir/settings.json" # legacy
104
+ _merge_mcps "$claude_dir/.mcp.json" # project-level
105
+ _merge_mcps "$HOME/.claude.json" # user-level (e.g. Coogle, DEVONthink)
106
+
107
+ # Deduplicate MCP names (preserving order)
108
+ if [ -n "$mcp_names_raw" ]; then
109
+ mcp_names_raw=$(echo "$mcp_names_raw" | tr ' ' '\n' | awk '!seen[$0]++' | tr '\n' ' ' | sed 's/ $//')
110
+ mcps_count=$(echo "$mcp_names_raw" | wc -w | tr -d ' ')
111
+ fi
112
+
113
+ # Extract context window usage from Claude Code's JSON input (no JSONL parsing needed)
114
+ context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' 2>/dev/null)
115
+ context_size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000' 2>/dev/null)
116
+ context_used_k=$(( (context_pct * context_size / 100) / 1000 ))
117
+ context_max_k=$((context_size / 1000))
118
+
119
+ # Tokyo Night Storm Color Scheme
120
+ BACKGROUND='\033[48;2;36;40;59m'
121
+ BRIGHT_PURPLE='\033[38;2;187;154;247m'
122
+ BRIGHT_BLUE='\033[38;2;122;162;247m'
123
+ DARK_BLUE='\033[38;2;100;140;200m'
124
+ BRIGHT_GREEN='\033[38;2;158;206;106m'
125
+ DARK_GREEN='\033[38;2;130;170;90m'
126
+ BRIGHT_ORANGE='\033[38;2;255;158;100m'
127
+ BRIGHT_RED='\033[38;2;247;118;142m'
128
+ BRIGHT_CYAN='\033[38;2;125;207;255m'
129
+ BRIGHT_MAGENTA='\033[38;2;187;154;247m'
130
+ BRIGHT_YELLOW='\033[38;2;224;175;104m'
131
+
132
+ # Map DA_COLOR to actual ANSI color code
133
+ case "$DA_COLOR" in
134
+ "purple") DA_DISPLAY_COLOR='\033[38;2;147;112;219m' ;;
135
+ "blue") DA_DISPLAY_COLOR="$BRIGHT_BLUE" ;;
136
+ "green") DA_DISPLAY_COLOR="$BRIGHT_GREEN" ;;
137
+ "cyan") DA_DISPLAY_COLOR="$BRIGHT_CYAN" ;;
138
+ "magenta") DA_DISPLAY_COLOR="$BRIGHT_MAGENTA" ;;
139
+ "yellow") DA_DISPLAY_COLOR="$BRIGHT_YELLOW" ;;
140
+ "red") DA_DISPLAY_COLOR="$BRIGHT_RED" ;;
141
+ "orange") DA_DISPLAY_COLOR="$BRIGHT_ORANGE" ;;
142
+ *) DA_DISPLAY_COLOR='\033[38;2;147;112;219m' ;; # Default to purple
143
+ esac
144
+
145
+ # Line-specific colors
146
+ LINE1_PRIMARY="$BRIGHT_PURPLE"
147
+ LINE1_ACCENT='\033[38;2;160;130;210m'
148
+ MODEL_PURPLE='\033[38;2;138;99;210m'
149
+
150
+ LINE2_PRIMARY="$DARK_BLUE"
151
+ LINE2_ACCENT='\033[38;2;110;150;210m'
152
+
153
+ LINE3_PRIMARY="$DARK_GREEN"
154
+ LINE3_ACCENT='\033[38;2;140;180;100m'
155
+ COST_COLOR="$LINE3_ACCENT"
156
+ TOKENS_COLOR='\033[38;2;169;177;214m'
157
+
158
+ SEPARATOR_COLOR='\033[38;2;140;152;180m'
159
+ DIR_COLOR='\033[38;2;135;206;250m'
160
+
161
+ # MCP colors
162
+ MCP_DAEMON="$BRIGHT_BLUE"
163
+ MCP_STRIPE="$LINE2_ACCENT"
164
+ MCP_DEFAULT="$LINE2_PRIMARY"
165
+
166
+ # Reset includes explicit background clear for terminal compatibility
167
+ RESET='\033[0m\033[49m'
168
+
169
+ # Emoji definitions - can be disabled with PAI_NO_EMOJI=1
170
+ if [ "${PAI_NO_EMOJI:-0}" = "1" ]; then
171
+ EMOJI_WAVE=">"
172
+ EMOJI_BRAIN="*"
173
+ EMOJI_FOLDER="@"
174
+ EMOJI_PLUG="+"
175
+ EMOJI_BOOK="#"
176
+ EMOJI_GEM="$"
177
+ else
178
+ EMOJI_WAVE="👋"
179
+ EMOJI_BRAIN="🧠"
180
+ EMOJI_FOLDER="📁"
181
+ EMOJI_PLUG="🔌"
182
+ EMOJI_BOOK="📚"
183
+ EMOJI_GEM="💎"
184
+ fi
185
+
186
+ # Simple colors mode - set PAI_SIMPLE_COLORS=1 if you have terminal display issues
187
+ if [ "${PAI_SIMPLE_COLORS:-0}" = "1" ]; then
188
+ # Use basic ANSI colors instead of 24-bit RGB for terminal compatibility
189
+ BRIGHT_PURPLE='\033[35m'
190
+ BRIGHT_BLUE='\033[34m'
191
+ DARK_BLUE='\033[34m'
192
+ BRIGHT_GREEN='\033[32m'
193
+ DARK_GREEN='\033[32m'
194
+ BRIGHT_ORANGE='\033[33m'
195
+ BRIGHT_RED='\033[31m'
196
+ BRIGHT_CYAN='\033[36m'
197
+ BRIGHT_MAGENTA='\033[35m'
198
+ BRIGHT_YELLOW='\033[33m'
199
+ # Override derived colors
200
+ DA_DISPLAY_COLOR='\033[35m'
201
+ LINE1_PRIMARY='\033[35m'
202
+ LINE1_ACCENT='\033[35m'
203
+ MODEL_PURPLE='\033[35m'
204
+ LINE2_PRIMARY='\033[34m'
205
+ LINE2_ACCENT='\033[34m'
206
+ LINE3_PRIMARY='\033[32m'
207
+ LINE3_ACCENT='\033[32m'
208
+ COST_COLOR='\033[32m'
209
+ TOKENS_COLOR='\033[37m'
210
+ SEPARATOR_COLOR='\033[37m'
211
+ DIR_COLOR='\033[36m'
212
+ MCP_DAEMON='\033[34m'
213
+ MCP_STRIPE='\033[34m'
214
+ MCP_DEFAULT='\033[34m'
215
+ fi
216
+
217
+ # Format MCP names with terminal-width-aware wrapping
218
+ # Debug: log available width info (remove after testing)
219
+ # Terminal width for line truncation
220
+ # Claude Code's statusline subprocess can't detect resize (stty returns stale values).
221
+ # To set your width: echo 105 > ~/.claude/.statusline_width
222
+ # Default 80 is safe for any terminal; set higher (e.g., 105) for wide screens.
223
+ term_width=80
224
+ [ -f "${claude_dir}/.statusline_width" ] && read -r term_width < "${claude_dir}/.statusline_width" 2>/dev/null
225
+ [ "$term_width" -gt 0 ] 2>/dev/null || term_width=80
226
+ mcp_prefix_width=10 # visual width of "🔌 MCPs: " (emoji=2 + space + "MCPs: " = 10)
227
+
228
+ # Build MCP output — proactively split into two lines when there are many MCPs.
229
+ # No width detection needed: if total display chars > 60, split at the midpoint.
230
+ _mcp_display_name() {
231
+ case "$1" in
232
+ "daemon") echo "Daemon" ;;
233
+ "stripe") echo "Stripe" ;;
234
+ "httpx") echo "HTTPx" ;;
235
+ "brightdata") echo "BrightData" ;;
236
+ "naabu") echo "Naabu" ;;
237
+ "apify") echo "Apify" ;;
238
+ "content") echo "Content" ;;
239
+ "Ref") echo "Ref" ;;
240
+ "pai") echo "PAI" ;;
241
+ "playwright") echo "PW" ;;
242
+ "workspace") echo "Coogle" ;;
243
+ "macos_automator") echo "macOS" ;;
244
+ "claude_ai_Gmail") echo "Gmail" ;;
245
+ "claude_ai_Google_Calendar") echo "GCal" ;;
246
+ *) local n="$1"; echo "${n^}" ;;
247
+ esac
248
+ }
249
+
250
+ _mcp_formatted() {
251
+ local display_name="$1"
252
+ case "$display_name" in
253
+ "Daemon") printf "${MCP_DAEMON}%s${RESET}" "$display_name" ;;
254
+ "Stripe") printf "${MCP_STRIPE}%s${RESET}" "$display_name" ;;
255
+ *) printf "${MCP_DEFAULT}%s${RESET}" "$display_name" ;;
256
+ esac
257
+ }
258
+
259
+ # Collect all display names and calculate total width
260
+ mcp_display_names=()
261
+ mcp_formatted_strs=()
262
+ total_display_width=$mcp_prefix_width # start with "🔌 MCPs: " prefix
263
+ total_mcps=0
264
+
265
+ for mcp in $mcp_names_raw; do
266
+ dn=$(_mcp_display_name "$mcp")
267
+ fm=$(_mcp_formatted "$dn")
268
+ mcp_display_names+=("$dn")
269
+ mcp_formatted_strs+=("$fm")
270
+ if [ $total_mcps -gt 0 ]; then
271
+ total_display_width=$((total_display_width + 2)) # ", "
272
+ fi
273
+ total_display_width=$((total_display_width + ${#dn}))
274
+ total_mcps=$((total_mcps + 1))
275
+ done
276
+
277
+ # Decide: one line or two lines?
278
+ # If total display width > 60 chars, split at the midpoint
279
+ mcp_line1=""
280
+ mcp_line2=""
281
+
282
+ if [ $total_mcps -eq 0 ]; then
283
+ mcp_line1="none"
284
+ elif [ $total_display_width -le $term_width ]; then
285
+ # Single line — everything fits
286
+ for ((i=0; i<total_mcps; i++)); do
287
+ if [ $i -eq 0 ]; then
288
+ mcp_line1="${mcp_formatted_strs[$i]}"
289
+ else
290
+ mcp_line1="${mcp_line1}${SEPARATOR_COLOR}, ${mcp_formatted_strs[$i]}"
291
+ fi
292
+ done
293
+ else
294
+ # Two lines — split at midpoint
295
+ split_at=$(( (total_mcps + 1) / 2 ))
296
+ for ((i=0; i<split_at; i++)); do
297
+ if [ $i -eq 0 ]; then
298
+ mcp_line1="${mcp_formatted_strs[$i]}"
299
+ else
300
+ mcp_line1="${mcp_line1}${SEPARATOR_COLOR}, ${mcp_formatted_strs[$i]}"
301
+ fi
302
+ done
303
+ for ((i=split_at; i<total_mcps; i++)); do
304
+ if [ $i -eq $split_at ]; then
305
+ mcp_line2="${mcp_formatted_strs[$i]}"
306
+ else
307
+ mcp_line2="${mcp_line2}${SEPARATOR_COLOR}, ${mcp_formatted_strs[$i]}"
308
+ fi
309
+ done
310
+ fi
311
+
312
+ # Output the statusline
313
+ # LINE 1 - Greeting (adaptive: drop CC version when narrow, shorten further if very narrow)
314
+ line1_full="${EMOJI_WAVE} ${DA_DISPLAY_COLOR}\"${DA_NAME} here, ready to go...\"${RESET} ${MODEL_PURPLE}Running CC ${cc_version}${RESET}${LINE1_PRIMARY} with ${MODEL_PURPLE}${EMOJI_BRAIN} ${model_name}${RESET}${LINE1_PRIMARY} in ${DIR_COLOR}${EMOJI_FOLDER} ${dir_name}${BRIGHT_CYAN}${session_suffix}${RESET}"
315
+ line1_medium="${EMOJI_WAVE} ${DA_DISPLAY_COLOR}\"${DA_NAME}\"${RESET}${LINE1_PRIMARY} ${MODEL_PURPLE}${EMOJI_BRAIN} ${model_name}${RESET}${LINE1_PRIMARY} in ${DIR_COLOR}${EMOJI_FOLDER} ${dir_name}${BRIGHT_CYAN}${session_suffix}${RESET}"
316
+ line1_short="${EMOJI_WAVE} ${MODEL_PURPLE}${EMOJI_BRAIN} ${model_name}${RESET}${LINE1_PRIMARY} ${DIR_COLOR}${EMOJI_FOLDER} ${dir_name}${BRIGHT_CYAN}${session_suffix}${RESET}"
317
+
318
+ # Pick line 1 format based on width (plain-text lengths: full~85, medium~45, short~25)
319
+ if [ $term_width -ge 90 ]; then
320
+ printf "${line1_full}\n"
321
+ elif [ $term_width -ge 50 ]; then
322
+ printf "${line1_medium}\n"
323
+ else
324
+ printf "${line1_short}\n"
325
+ fi
326
+
327
+ # LINE 2 - MCPs (with optional wrap to second line)
328
+ printf "${LINE2_PRIMARY}${EMOJI_PLUG} MCPs${RESET}${LINE2_PRIMARY}${SEPARATOR_COLOR}: ${RESET}${mcp_line1}${RESET}\n"
329
+ if [ -n "$mcp_line2" ]; then
330
+ # Continuation line — indent to align with MCP names after "🔌 MCPs: "
331
+ printf "${LINE2_PRIMARY} ${RESET}${mcp_line2}${RESET}\n"
332
+ fi
333
+
334
+ # LINE 3 - Context meter (from Claude Code's JSON input)
335
+ if [ "$context_pct" -gt 0 ] 2>/dev/null; then
336
+ # Color based on usage: green < 50%, yellow 50-75%, red > 75%
337
+ if [ $context_pct -gt 75 ]; then
338
+ ctx_color="$BRIGHT_RED"
339
+ elif [ $context_pct -gt 50 ]; then
340
+ ctx_color="$BRIGHT_YELLOW"
341
+ else
342
+ ctx_color="$BRIGHT_GREEN"
343
+ fi
344
+
345
+ printf "${LINE3_PRIMARY}${EMOJI_GEM} Context${RESET}${LINE3_PRIMARY}${SEPARATOR_COLOR}: ${RESET}${ctx_color}${context_used_k}K${RESET}${LINE3_PRIMARY} / ${context_max_k}K${RESET}\n"
346
+ else
347
+ printf "${LINE3_PRIMARY}${EMOJI_GEM} Context${RESET}${LINE3_PRIMARY}${SEPARATOR_COLOR}: ${RESET}${LINE3_ACCENT}...${RESET}\n"
348
+ fi