@humanu/orchestra 0.5.58 → 0.5.61

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.
@@ -2,6 +2,31 @@
2
2
 
3
3
  # shellcheck shell=bash
4
4
 
5
+ ai_choose_provider() {
6
+ local primary="${AI_PRIMARY_PROVIDER:-anthropic}"
7
+ primary="$(printf '%s' "$primary" | tr '[:upper:]' '[:lower:]')"
8
+ if [[ "$primary" == "openai" ]]; then
9
+ if [[ -n "${OPENAI_API_KEY-}" ]]; then
10
+ echo "openai"
11
+ return 0
12
+ fi
13
+ if [[ -n "${ANTHROPIC_API_KEY-}" ]]; then
14
+ echo "anthropic"
15
+ return 0
16
+ fi
17
+ else
18
+ if [[ -n "${ANTHROPIC_API_KEY-}" ]]; then
19
+ echo "anthropic"
20
+ return 0
21
+ fi
22
+ if [[ -n "${OPENAI_API_KEY-}" ]]; then
23
+ echo "openai"
24
+ return 0
25
+ fi
26
+ fi
27
+ return 1
28
+ }
29
+
5
30
  # AI generate name from base64 content
6
31
  bridge_ai_generate_name_from_base64() {
7
32
  if [[ -z "${1:-}" ]]; then
@@ -9,19 +34,66 @@ bridge_ai_generate_name_from_base64() {
9
34
  return 1
10
35
  fi
11
36
  content_b64="$1"
37
+ load_ai_primary_provider
38
+ load_openai_model
39
+ load_openai_api_key
12
40
  load_anthropic_api_key
13
- if [[ -z "${ANTHROPIC_API_KEY-}" ]]; then
41
+ local provider
42
+ if ! provider="$(ai_choose_provider)"; then
14
43
  echo "\"missing_api_key\""
15
44
  return 0
16
45
  fi
17
46
  # Build request via Python to ensure proper JSON encoding
18
- request_body=$(python3 - "$content_b64" <<'PY'
19
- import sys, json, base64
47
+ request_payload=$(python3 - "$content_b64" "$provider" "${OPENAI_MODEL:-gpt-4o-mini}" <<'PY'
48
+ import sys, json, base64, re
49
+
20
50
  b64 = sys.argv[1]
51
+ provider = sys.argv[2] if len(sys.argv) > 2 else "anthropic"
52
+ openai_model = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else "gpt-4o-mini"
21
53
  try:
22
- content = base64.b64decode(b64.encode('utf-8')).decode('utf-8','ignore')
54
+ content = base64.b64decode(b64.encode('utf-8')).decode('utf-8', 'ignore')
23
55
  except Exception:
24
56
  content = ''
57
+
58
+ def extract_command(line: str) -> str:
59
+ stripped = line.strip()
60
+ for lead in ("$ ", "> "):
61
+ if stripped.startswith(lead):
62
+ return stripped[len(lead):].strip()
63
+ for marker in ("$ ", "# "):
64
+ if marker in line:
65
+ return line.split(marker)[-1].strip()
66
+ return ""
67
+
68
+ def normalize_app(cmd: str) -> str:
69
+ cmd = cmd.strip()
70
+ if not cmd:
71
+ return ""
72
+ for lead in ("$ ", "> "):
73
+ if cmd.startswith(lead):
74
+ cmd = cmd[len(lead):].strip()
75
+ parts = cmd.split()
76
+ if not parts:
77
+ return ""
78
+ if parts[0] in ("sudo", "env", "command"):
79
+ parts = parts[1:]
80
+ if not parts:
81
+ return ""
82
+ base = parts[0].split("/")[-1].lower()
83
+ base = re.sub(r"[^a-z0-9]+", "_", base).strip("_")
84
+ if base in {"bash", "zsh", "sh", "fish", "tmux", "login", "sudo", "man", "less", "more", "cat", "tail", "watch", "source", "export", "set", "alias", "unalias", "history", "bindkey"}:
85
+ return ""
86
+ return base
87
+
88
+ lines = content.splitlines()
89
+ last_cmd = ""
90
+ for line in reversed(lines):
91
+ candidate = extract_command(line)
92
+ if candidate:
93
+ last_cmd = candidate
94
+ break
95
+
96
+ app_prefix = normalize_app(last_cmd) or ""
25
97
  prompt = f"""Analyze this terminal session and create a descriptive name based ONLY on the console output and commands executed.
26
98
 
27
99
  Terminal output:
@@ -36,26 +108,15 @@ IMPORTANT: Look carefully at the terminal output for:
36
108
 
37
109
  CRITICAL GIT DETECTION: Check for ANY of these Git indicators in the last outputs:
38
110
  - Git commands: git status, git diff, git add, git commit, git push, git pull, git checkout, git branch, git merge, git rebase, git log, git stash, etc.
39
- - Git output patterns: \"On branch\", \"Your branch is\", \"Changes not staged\", \"Changes to be committed\", \"Untracked files\", \"modified:\", \"deleted:\", \"new file:\", \"renamed:\"
111
+ - Git output patterns: "On branch", "Your branch is", "Changes not staged", "Changes to be committed", "Untracked files", "modified:", "deleted:", "new file:", "renamed:"
40
112
  - Git diff output: Lines starting with +, -, @@, diff --git
41
- - Git merge/rebase messages: \"CONFLICT\", \"Merge branch\", \"Rebase\", \"Cherry-pick\"
42
- - Git status indicators: \"nothing to commit\", \"working tree clean\", \"ahead of\", \"behind\"
43
- - Git error messages: \"fatal:\", \"error:\", \"warning:\" (when preceded by git commands)
44
-
45
- Use these prefixes based on detected application/context (PRIORITIZE git_ if Git patterns found):
46
- - git_ : ANY Git operations or Git output patterns detected (HIGHEST PRIORITY if found)
47
- - opencode_ : If you see Claude Code, OpenCode, Cursor, AI assistant interactions, or AI-powered coding
48
- - running_ : Dev servers (npm run dev, yarn dev, python manage.py runserver, rails server, etc.)
49
- - build_ : Build processes (npm run build, cargo build, make, webpack, etc.)
50
- - test_ : Testing (npm test, pytest, jest, cargo test, etc.)
51
- - docker_ : Docker commands, container operations
52
- - ssh_ : SSH connections, remote operations
53
- - db_ : Database operations (psql, mysql, mongo, migrations)
54
- - debug_ : Debugging sessions, error investigation
55
- - deploy_ : Deployment operations
56
- - Otherwise, no prefix for general development
57
-
58
- If Git patterns are detected, ALWAYS use git_ prefix, even if other activities are present.
113
+ - Git merge/rebase messages: "CONFLICT", "Merge branch", "Rebase", "Cherry-pick"
114
+ - Git status indicators: "nothing to commit", "working tree clean", "ahead of", "behind"
115
+ - Git error messages: "fatal:", "error:", "warning:" (when preceded by git commands)
116
+
117
+ DESCRIPTION ONLY: Return a short description of the activity.
118
+ - Do NOT include the app/tool name or any prefix in the output.
119
+ - If Git patterns are detected, the description should still reflect the git activity, but do not add "git_".
59
120
 
60
121
  Describe what the user was actually doing based on the console output, not just the current state.
61
122
 
@@ -63,29 +124,63 @@ The name should be max 100 chars total, use underscores only (no colons or speci
63
124
 
64
125
  Respond with ONLY the session name, nothing else."""
65
126
 
66
- req = {
67
- "model": "claude-3-5-haiku-latest",
68
- "max_tokens": 100,
69
- "messages": [{"role":"user","content": prompt}]
70
- }
127
+ if provider == "openai":
128
+ req = {
129
+ "model": openai_model,
130
+ "max_tokens": 100,
131
+ "temperature": 0.2,
132
+ "messages": [{"role": "user", "content": prompt}],
133
+ }
134
+ else:
135
+ req = {
136
+ "model": "claude-3-5-haiku-latest",
137
+ "max_tokens": 100,
138
+ "messages": [{"role": "user", "content": prompt}],
139
+ }
140
+ print(app_prefix)
71
141
  print(json.dumps(req))
72
142
  PY
73
- )
143
+ )
144
+ app_prefix="${request_payload%%$'\n'*}"
145
+ request_body="${request_payload#*$'\n'}"
146
+ if [[ "$request_body" == "$request_payload" ]]; then
147
+ request_body="$request_payload"
148
+ app_prefix=""
149
+ fi
150
+ if [[ "$provider" == "openai" ]]; then
151
+ response=$(curl -s -X POST https://api.openai.com/v1/chat/completions \
152
+ --max-time 20 \
153
+ -H "Content-Type: application/json" \
154
+ -H "Authorization: Bearer $OPENAI_API_KEY" \
155
+ -d "$request_body" 2>/dev/null)
156
+ new_name=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('choices',[{}])[0].get('message',{}).get('content',''))" 2>/dev/null)
157
+ else
74
158
  response=$(curl -s -X POST https://api.anthropic.com/v1/messages \
75
159
  --max-time 20 \
76
160
  -H "Content-Type: application/json" \
77
161
  -H "x-api-key: $ANTHROPIC_API_KEY" \
78
162
  -H "anthropic-version: 2023-06-01" \
79
163
  -d "$request_body" 2>/dev/null)
80
- # Extract text
81
164
  new_name=$(echo "$response" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('content',[{}])[0].get('text',''))" 2>/dev/null)
82
- # Clean and truncate
83
- new_name=$(echo "$new_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)
84
- if [[ -z "$new_name" ]]; then
85
- echo "\"ai_generation_failed\""
86
- else
87
- printf '"%s"\n' "$new_name"
165
+ fi
166
+ # Clean and truncate
167
+ new_name=$(echo "$new_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)
168
+ if [[ -z "$new_name" ]]; then
169
+ echo "\"ai_generation_failed\""
170
+ else
171
+ app_prefix="$(printf '%s' "$app_prefix" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '_')"
172
+ app_prefix="${app_prefix//__/_}"
173
+ app_prefix="${app_prefix##_}"
174
+ app_prefix="${app_prefix%%_}"
175
+ if [[ -n "$app_prefix" ]]; then
176
+ if [[ "$new_name" == "${app_prefix}_"* ]]; then
177
+ new_name="${new_name#${app_prefix}_}"
178
+ fi
179
+ new_name="${app_prefix}_${new_name}"
88
180
  fi
181
+ new_name="${new_name:0:100}"
182
+ printf '"%s"\n' "$new_name"
183
+ fi
89
184
  }
90
185
 
91
186
  # Rename session with AI
@@ -95,48 +190,78 @@ bridge_rename_session() {
95
190
  return 1
96
191
  fi
97
192
  original_session_name="$1"
98
-
193
+
99
194
  # Load API key from config file or fallback to .env
195
+ load_openai_api_key
100
196
  load_anthropic_api_key
101
-
102
- if [[ -z "${ANTHROPIC_API_KEY-}" ]]; then
197
+
198
+ if [[ -z "${OPENAI_API_KEY-}" && -z "${ANTHROPIC_API_KEY-}" ]]; then
103
199
  echo "\"missing_api_key\""
104
200
  return 0
105
201
  fi
106
-
202
+
107
203
  # Check if tmux is available and session exists
108
204
  if ! tmux_available; then
109
205
  echo "\"tmux_not_available\""
110
206
  return 0
111
207
  fi
112
-
208
+
113
209
  if ! tmux_session_exists "$original_session_name"; then
114
210
  echo "\"session_not_found\""
115
211
  return 0
116
212
  fi
117
-
118
- # Generate AI name using the CURRENT session (no renaming to temp name)
119
- ai_name="$(tmux_generate_ai_session_name "$original_session_name" 2>/dev/null)"
120
-
121
- if [[ -n "$ai_name" ]] && [[ "$ai_name" != *"error"* ]] && [[ "$ai_name" != *"Failed"* ]]; then
122
- # Clean the AI name for session naming
213
+
214
+ # Generate AI description using the CURRENT session (no renaming to temp name)
215
+ TMUX_AI_APP_SOURCE=""
216
+ local err_file
217
+ if [[ "$OSTYPE" == "darwin"* ]]; then
218
+ err_file=$(mktemp -t gw_ai_err)
219
+ else
220
+ err_file=$(mktemp)
221
+ fi
222
+ local ai_status=0
223
+ if ! ai_name="$(tmux_generate_ai_session_name "$original_session_name" 2>"$err_file")"; then
224
+ ai_status=$?
225
+ fi
226
+ local ai_err=""
227
+ if [[ -s "$err_file" ]]; then
228
+ ai_err=$(tail -n 1 "$err_file")
229
+ fi
230
+ rm -f "$err_file"
231
+
232
+ if [[ -n "$ai_name" ]]; then
233
+ # Clean the AI description for session naming
123
234
  ai_name="$(echo "$ai_name" | tr ' ' '_' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-100)"
124
-
125
- # Remove any prefix from AI name to avoid duplication
126
- ai_name="$(echo "$ai_name" | sed -E 's/^(opencode|running|ssh|docker|k8s|git|db)_//')"
127
-
128
- # Remove any leading underscores
129
235
  ai_name="$(echo "$ai_name" | sed 's/^_*//')"
130
-
236
+ if [[ -z "$ai_name" ]]; then
237
+ echo "\"ai_generation_failed\""
238
+ return 0
239
+ fi
240
+
131
241
  # Perform the rename preserving canonical prefix and delimiter
132
242
  if tmux_rename_session "$original_session_name" "$ai_name" >/dev/null; then
133
- echo "\"success\""
243
+ local app_source="${TMUX_AI_APP_SOURCE-}"
244
+ app_source="$(printf '%s' "$app_source" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '_')"
245
+ app_source="${app_source//__/_}"
246
+ app_source="${app_source##_}"
247
+ app_source="${app_source%%_}"
248
+ if [[ -n "$app_source" ]]; then
249
+ echo "\"success:app_source:${app_source}\""
250
+ else
251
+ echo "\"success\""
252
+ fi
134
253
  else
135
254
  echo "\"rename_failed\""
136
255
  fi
137
256
  else
138
257
  # AI generation failed, keep original name
139
- echo "\"ai_generation_failed\""
258
+ if [[ -n "$ai_err" ]]; then
259
+ echo "\"ai_generation_failed: ${ai_err}\""
260
+ elif [[ $ai_status -ne 0 ]]; then
261
+ echo "\"ai_generation_failed: ai helper exited ${ai_status}\""
262
+ else
263
+ echo "\"ai_generation_failed\""
264
+ fi
140
265
  fi
141
266
  }
142
267
 
@@ -148,24 +273,24 @@ bridge_manual_rename_session() {
148
273
  fi
149
274
  old_name="$1"
150
275
  new_display_name="$2"
151
-
276
+
152
277
  if ! tmux_available; then
153
278
  echo "\"tmux_not_available\""
154
279
  return 0
155
280
  fi
156
-
281
+
157
282
  # Check if session exists
158
283
  if ! tmux has-session -t "$old_name" 2>/dev/null; then
159
284
  echo "\"session_not_found\""
160
285
  return 0
161
286
  fi
162
-
287
+
163
288
  # Validate new display name (alphanumeric, underscore, hyphen only)
164
289
  if [[ ! "$new_display_name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
165
290
  echo "\"invalid_name\""
166
291
  return 0
167
292
  fi
168
-
293
+
169
294
  # Perform rename via tmux API to preserve delimiter and canonical prefix
170
295
  if tmux_rename_session "$old_name" "$new_display_name" >/dev/null; then
171
296
  echo "\"success\""
@@ -448,3 +448,169 @@ NODE
448
448
  fi
449
449
  fi
450
450
  }
451
+
452
+ # Helper function to load OpenAI API key from config or .env file
453
+ load_openai_api_key() {
454
+ local config_file="$HOME/.orchestra/config.json"
455
+ if [[ -f "$config_file" ]]; then
456
+ if have_cmd jq; then
457
+ local api_key
458
+ api_key="$(jq -r '.openai_api_key // empty' "$config_file" 2>/dev/null)"
459
+ if [[ -n "$api_key" ]] && [[ "$api_key" != "null" ]]; then
460
+ export OPENAI_API_KEY="$api_key"
461
+ return 0
462
+ fi
463
+ else
464
+ bridge_init_json_backend
465
+ local api_key
466
+ case "$BRIDGE_JSON_BACKEND" in
467
+ python)
468
+ api_key="$(python3 - "$config_file" <<'PY'
469
+ import json
470
+ import sys
471
+ try:
472
+ data = json.load(open(sys.argv[1], 'r'))
473
+ except Exception:
474
+ data = {}
475
+ value = data.get('openai_api_key') or ''
476
+ print(value)
477
+ PY
478
+ )"
479
+ ;;
480
+ node)
481
+ api_key="$(node - "$config_file" <<'NODE'
482
+ const fs = require('fs');
483
+ let value = '';
484
+ try {
485
+ const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
486
+ value = data.openai_api_key || '';
487
+ } catch (err) {
488
+ value = '';
489
+ }
490
+ process.stdout.write(value);
491
+ NODE
492
+ )"
493
+ ;;
494
+ esac
495
+ if [[ -n "${api_key:-}" ]]; then
496
+ export OPENAI_API_KEY="$api_key"
497
+ return 0
498
+ fi
499
+ fi
500
+ fi
501
+
502
+ if [[ -f ".env" ]]; then
503
+ source .env
504
+ fi
505
+
506
+ if [[ -z "${OPENAI_API_KEY-}" ]]; then
507
+ local repo_root
508
+ repo_root="$(git_repo_root 2>/dev/null)"
509
+ if [[ -n "$repo_root" ]] && [[ -f "$repo_root/.env" ]]; then
510
+ source "$repo_root/.env"
511
+ fi
512
+ fi
513
+ }
514
+
515
+ # Helper function to load primary AI provider
516
+ load_ai_primary_provider() {
517
+ if [[ -n "${AI_PRIMARY_PROVIDER:-}" ]]; then
518
+ return 0
519
+ fi
520
+
521
+ local config_file="$HOME/.orchestra/config.json"
522
+ local provider=""
523
+ if [[ -f "$config_file" ]]; then
524
+ if have_cmd jq; then
525
+ provider="$(jq -r '.ai_primary_provider // empty' "$config_file" 2>/dev/null)"
526
+ else
527
+ bridge_init_json_backend
528
+ case "$BRIDGE_JSON_BACKEND" in
529
+ python)
530
+ provider="$(python3 - "$config_file" <<'PY'
531
+ import json
532
+ import sys
533
+ try:
534
+ data = json.load(open(sys.argv[1], 'r'))
535
+ except Exception:
536
+ data = {}
537
+ value = data.get('ai_primary_provider') or ''
538
+ print(value)
539
+ PY
540
+ )"
541
+ ;;
542
+ node)
543
+ provider="$(node - "$config_file" <<'NODE'
544
+ const fs = require('fs');
545
+ let value = '';
546
+ try {
547
+ const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
548
+ value = data.ai_primary_provider || '';
549
+ } catch (err) {
550
+ value = '';
551
+ }
552
+ process.stdout.write(value);
553
+ NODE
554
+ )"
555
+ ;;
556
+ esac
557
+ fi
558
+ fi
559
+
560
+ provider="$(printf '%s' "$provider" | tr '[:upper:]' '[:lower:]')"
561
+ if [[ "$provider" != "openai" && "$provider" != "anthropic" ]]; then
562
+ provider="anthropic"
563
+ fi
564
+ export AI_PRIMARY_PROVIDER="$provider"
565
+ }
566
+
567
+ # Helper function to load OpenAI model
568
+ load_openai_model() {
569
+ if [[ -n "${OPENAI_MODEL:-}" ]]; then
570
+ return 0
571
+ fi
572
+
573
+ local config_file="$HOME/.orchestra/config.json"
574
+ local model=""
575
+ if [[ -f "$config_file" ]]; then
576
+ if have_cmd jq; then
577
+ model="$(jq -r '.openai_model // empty' "$config_file" 2>/dev/null)"
578
+ else
579
+ bridge_init_json_backend
580
+ case "$BRIDGE_JSON_BACKEND" in
581
+ python)
582
+ model="$(python3 - "$config_file" <<'PY'
583
+ import json
584
+ import sys
585
+ try:
586
+ data = json.load(open(sys.argv[1], 'r'))
587
+ except Exception:
588
+ data = {}
589
+ value = data.get('openai_model') or ''
590
+ print(value)
591
+ PY
592
+ )"
593
+ ;;
594
+ node)
595
+ model="$(node - "$config_file" <<'NODE'
596
+ const fs = require('fs');
597
+ let value = '';
598
+ try {
599
+ const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
600
+ value = data.openai_model || '';
601
+ } catch (err) {
602
+ value = '';
603
+ }
604
+ process.stdout.write(value);
605
+ NODE
606
+ )"
607
+ ;;
608
+ esac
609
+ fi
610
+ fi
611
+
612
+ if [[ -z "$model" || "$model" == "null" ]]; then
613
+ model="gpt-4o-mini"
614
+ fi
615
+ export OPENAI_MODEL="$model"
616
+ }
@@ -30,7 +30,7 @@ Navigation:
30
30
  d Delete worktree or kill session
31
31
  r Rename session (AI-powered)
32
32
  c Create new worktree
33
- o Configure Anthropic API key
33
+ o Configure AI keys and provider
34
34
  Ctrl+R Refresh data
35
35
  q/Esc Quit
36
36