oh-my-customcode 0.17.0 → 0.18.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.
- package/dist/cli/index.js +54 -0
- package/dist/index.js +54 -0
- package/package.json +1 -1
- package/templates/.claude/skills/codex-exec/SKILL.md +29 -1
- package/templates/.claude/skills/codex-exec/scripts/codex-wrapper.cjs +17 -0
- package/templates/.claude/skills/intent-detection/SKILL.md +62 -0
- package/templates/.claude/skills/intent-detection/patterns/agent-triggers.yaml +36 -0
- package/templates/.claude/statusline.sh +128 -17
package/dist/cli/index.js
CHANGED
|
@@ -12805,6 +12805,11 @@ async function listFiles(dir2, options = {}) {
|
|
|
12805
12805
|
}
|
|
12806
12806
|
return files;
|
|
12807
12807
|
}
|
|
12808
|
+
async function copyFile(src, dest) {
|
|
12809
|
+
const fs = await import("node:fs/promises");
|
|
12810
|
+
await ensureDirectory(dirname2(dest));
|
|
12811
|
+
await fs.copyFile(src, dest);
|
|
12812
|
+
}
|
|
12808
12813
|
function matchesPattern(filename, pattern) {
|
|
12809
12814
|
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
12810
12815
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
@@ -13984,6 +13989,53 @@ async function installSingleComponent(targetDir, component, options, result) {
|
|
|
13984
13989
|
result.warnings.push(`Failed to install ${component}: ${message}`);
|
|
13985
13990
|
}
|
|
13986
13991
|
}
|
|
13992
|
+
async function installStatusline(targetDir, options, _result) {
|
|
13993
|
+
const layout = getProviderLayout();
|
|
13994
|
+
const srcPath = resolveTemplatePath(join4(layout.rootDir, "statusline.sh"));
|
|
13995
|
+
const destPath = join4(targetDir, layout.rootDir, "statusline.sh");
|
|
13996
|
+
if (!await fileExists(srcPath)) {
|
|
13997
|
+
debug("install.statusline_not_found", { path: srcPath });
|
|
13998
|
+
return;
|
|
13999
|
+
}
|
|
14000
|
+
if (await fileExists(destPath)) {
|
|
14001
|
+
if (!options.force && !options.backup) {
|
|
14002
|
+
debug("install.statusline_skipped", { reason: "exists" });
|
|
14003
|
+
return;
|
|
14004
|
+
}
|
|
14005
|
+
}
|
|
14006
|
+
await copyFile(srcPath, destPath);
|
|
14007
|
+
const fs2 = await import("node:fs/promises");
|
|
14008
|
+
await fs2.chmod(destPath, 493);
|
|
14009
|
+
debug("install.statusline_installed", {});
|
|
14010
|
+
}
|
|
14011
|
+
async function installSettingsLocal(targetDir, result) {
|
|
14012
|
+
const layout = getProviderLayout();
|
|
14013
|
+
const settingsPath = join4(targetDir, layout.rootDir, "settings.local.json");
|
|
14014
|
+
const statusLineConfig = {
|
|
14015
|
+
statusLine: {
|
|
14016
|
+
type: "command",
|
|
14017
|
+
command: ".claude/statusline.sh",
|
|
14018
|
+
padding: 0
|
|
14019
|
+
}
|
|
14020
|
+
};
|
|
14021
|
+
if (await fileExists(settingsPath)) {
|
|
14022
|
+
try {
|
|
14023
|
+
const existing = await readJsonFile(settingsPath);
|
|
14024
|
+
if (!existing.statusLine) {
|
|
14025
|
+
existing.statusLine = statusLineConfig.statusLine;
|
|
14026
|
+
await writeJsonFile(settingsPath, existing);
|
|
14027
|
+
debug("install.settings_local_merged", {});
|
|
14028
|
+
} else {
|
|
14029
|
+
debug("install.settings_local_skipped", { reason: "statusLine exists" });
|
|
14030
|
+
}
|
|
14031
|
+
} catch {
|
|
14032
|
+
result.warnings.push("Failed to parse existing settings.local.json, skipping statusLine config");
|
|
14033
|
+
}
|
|
14034
|
+
return;
|
|
14035
|
+
}
|
|
14036
|
+
await writeJsonFile(settingsPath, statusLineConfig);
|
|
14037
|
+
debug("install.settings_local_created", {});
|
|
14038
|
+
}
|
|
13987
14039
|
async function installEntryDocWithTracking(targetDir, options, result) {
|
|
13988
14040
|
const language = options.language ?? DEFAULT_LANGUAGE2;
|
|
13989
14041
|
const overwrite = !!(options.force || options.backup);
|
|
@@ -14010,6 +14062,8 @@ async function install(options) {
|
|
|
14010
14062
|
await checkAndWarnExisting(options.targetDir, !!options.force, !!options.backup, result);
|
|
14011
14063
|
await verifyTemplateDirectory();
|
|
14012
14064
|
await installAllComponents(options.targetDir, options, result);
|
|
14065
|
+
await installStatusline(options.targetDir, options, result);
|
|
14066
|
+
await installSettingsLocal(options.targetDir, result);
|
|
14013
14067
|
await installEntryDocWithTracking(options.targetDir, options, result);
|
|
14014
14068
|
await updateInstallConfig(options.targetDir, options, result.installedComponents);
|
|
14015
14069
|
result.success = true;
|
package/dist/index.js
CHANGED
|
@@ -163,6 +163,11 @@ function resolveTemplatePath(relativePath) {
|
|
|
163
163
|
const packageRoot = getPackageRoot();
|
|
164
164
|
return join(packageRoot, "templates", relativePath);
|
|
165
165
|
}
|
|
166
|
+
async function copyFile(src, dest) {
|
|
167
|
+
const fs = await import("node:fs/promises");
|
|
168
|
+
await ensureDirectory(dirname(dest));
|
|
169
|
+
await fs.copyFile(src, dest);
|
|
170
|
+
}
|
|
166
171
|
function matchesPattern(filename, pattern) {
|
|
167
172
|
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
168
173
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
@@ -846,6 +851,53 @@ async function installSingleComponent(targetDir, component, options, result) {
|
|
|
846
851
|
result.warnings.push(`Failed to install ${component}: ${message}`);
|
|
847
852
|
}
|
|
848
853
|
}
|
|
854
|
+
async function installStatusline(targetDir, options, _result) {
|
|
855
|
+
const layout = getProviderLayout();
|
|
856
|
+
const srcPath = resolveTemplatePath(join3(layout.rootDir, "statusline.sh"));
|
|
857
|
+
const destPath = join3(targetDir, layout.rootDir, "statusline.sh");
|
|
858
|
+
if (!await fileExists(srcPath)) {
|
|
859
|
+
debug("install.statusline_not_found", { path: srcPath });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (await fileExists(destPath)) {
|
|
863
|
+
if (!options.force && !options.backup) {
|
|
864
|
+
debug("install.statusline_skipped", { reason: "exists" });
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
await copyFile(srcPath, destPath);
|
|
869
|
+
const fs = await import("node:fs/promises");
|
|
870
|
+
await fs.chmod(destPath, 493);
|
|
871
|
+
debug("install.statusline_installed", {});
|
|
872
|
+
}
|
|
873
|
+
async function installSettingsLocal(targetDir, result) {
|
|
874
|
+
const layout = getProviderLayout();
|
|
875
|
+
const settingsPath = join3(targetDir, layout.rootDir, "settings.local.json");
|
|
876
|
+
const statusLineConfig = {
|
|
877
|
+
statusLine: {
|
|
878
|
+
type: "command",
|
|
879
|
+
command: ".claude/statusline.sh",
|
|
880
|
+
padding: 0
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
if (await fileExists(settingsPath)) {
|
|
884
|
+
try {
|
|
885
|
+
const existing = await readJsonFile(settingsPath);
|
|
886
|
+
if (!existing.statusLine) {
|
|
887
|
+
existing.statusLine = statusLineConfig.statusLine;
|
|
888
|
+
await writeJsonFile(settingsPath, existing);
|
|
889
|
+
debug("install.settings_local_merged", {});
|
|
890
|
+
} else {
|
|
891
|
+
debug("install.settings_local_skipped", { reason: "statusLine exists" });
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
result.warnings.push("Failed to parse existing settings.local.json, skipping statusLine config");
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
await writeJsonFile(settingsPath, statusLineConfig);
|
|
899
|
+
debug("install.settings_local_created", {});
|
|
900
|
+
}
|
|
849
901
|
async function installEntryDocWithTracking(targetDir, options, result) {
|
|
850
902
|
const language = options.language ?? DEFAULT_LANGUAGE;
|
|
851
903
|
const overwrite = !!(options.force || options.backup);
|
|
@@ -872,6 +924,8 @@ async function install(options) {
|
|
|
872
924
|
await checkAndWarnExisting(options.targetDir, !!options.force, !!options.backup, result);
|
|
873
925
|
await verifyTemplateDirectory();
|
|
874
926
|
await installAllComponents(options.targetDir, options, result);
|
|
927
|
+
await installStatusline(options.targetDir, options, result);
|
|
928
|
+
await installSettingsLocal(options.targetDir, result);
|
|
875
929
|
await installEntryDocWithTracking(options.targetDir, options, result);
|
|
876
930
|
await updateInstallConfig(options.targetDir, options, result.installedComponents);
|
|
877
931
|
result.success = true;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codex-exec
|
|
3
3
|
description: Execute OpenAI Codex CLI prompts and return results
|
|
4
|
-
argument-hint: "<prompt> [--json] [--output <path>] [--model <name>] [--timeout <ms>]"
|
|
4
|
+
argument-hint: "<prompt> [--json] [--output <path>] [--model <name>] [--timeout <ms>] [--effort <level>]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Codex Exec Skill
|
|
@@ -18,6 +18,10 @@ Execute OpenAI Codex CLI prompts in non-interactive mode and return structured r
|
|
|
18
18
|
--timeout <ms> Execution timeout (default: 120000, max: 600000)
|
|
19
19
|
--full-auto Enable auto-approval mode (codex -a full-auto)
|
|
20
20
|
--working-dir Working directory for Codex execution
|
|
21
|
+
--effort <level> Set reasoning effort level (minimal, low, medium, high, xhigh)
|
|
22
|
+
Maps to Codex CLI's model_reasoning_effort config
|
|
23
|
+
Default: uses Codex CLI's configured default
|
|
24
|
+
Recommended: xhigh for research/analysis tasks
|
|
21
25
|
```
|
|
22
26
|
|
|
23
27
|
## Workflow
|
|
@@ -147,3 +151,27 @@ Orchestrator delegates generation task
|
|
|
147
151
|
→ Reviewer validates quality
|
|
148
152
|
→ Iterate if needed
|
|
149
153
|
```
|
|
154
|
+
|
|
155
|
+
## Research Workflow
|
|
156
|
+
|
|
157
|
+
When the orchestrator detects a research/information gathering request:
|
|
158
|
+
|
|
159
|
+
1. **Check Codex availability**: Verify `codex` binary and `OPENAI_API_KEY`
|
|
160
|
+
2. **If available**: Execute with xhigh reasoning effort for thorough research
|
|
161
|
+
3. **If unavailable**: Fall back to Claude's WebFetch/WebSearch
|
|
162
|
+
|
|
163
|
+
### Research Command Pattern
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
/codex-exec "Research and analyze: {topic}. Provide structured findings with sources." --effort xhigh --full-auto --json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Effort Level Guide
|
|
170
|
+
|
|
171
|
+
| Level | Use Case | Speed | Depth |
|
|
172
|
+
|-------|----------|-------|-------|
|
|
173
|
+
| minimal | Quick lookups | Fastest | Surface |
|
|
174
|
+
| low | Simple queries | Fast | Basic |
|
|
175
|
+
| medium | General tasks | Balanced | Standard |
|
|
176
|
+
| high | Complex analysis | Slower | Deep |
|
|
177
|
+
| xhigh | Research & investigation | Slowest | Maximum |
|
|
@@ -51,6 +51,7 @@ function parseArgs() {
|
|
|
51
51
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
52
52
|
fullAuto: false,
|
|
53
53
|
workingDir: null,
|
|
54
|
+
effort: null,
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
for (let i = 2; i < process.argv.length; i++) {
|
|
@@ -91,6 +92,12 @@ function parseArgs() {
|
|
|
91
92
|
args.workingDir = process.argv[++i];
|
|
92
93
|
}
|
|
93
94
|
break;
|
|
95
|
+
case '--effort':
|
|
96
|
+
case '--reasoning-effort':
|
|
97
|
+
if (i + 1 < process.argv.length) {
|
|
98
|
+
args.effort = process.argv[++i];
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -160,6 +167,16 @@ function buildCommand(options) {
|
|
|
160
167
|
args.push('-C', options.workingDir);
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
// Reasoning effort (maps to -c model_reasoning_effort="value")
|
|
171
|
+
if (options.effort) {
|
|
172
|
+
const validEfforts = ['minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
173
|
+
if (validEfforts.includes(options.effort)) {
|
|
174
|
+
args.push('-c', `model_reasoning_effort="${options.effort}"`);
|
|
175
|
+
} else {
|
|
176
|
+
process.stderr.write(`Warning: Invalid effort level "${options.effort}". Valid: ${validEfforts.join(', ')}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
// Add prompt as last argument
|
|
164
181
|
args.push(options.prompt);
|
|
165
182
|
|
|
@@ -228,3 +228,65 @@ intent_detection:
|
|
|
228
228
|
max_alternatives: 3
|
|
229
229
|
korean_support: true
|
|
230
230
|
```
|
|
231
|
+
|
|
232
|
+
## Research Intent Routing
|
|
233
|
+
|
|
234
|
+
When a research/information gathering intent is detected:
|
|
235
|
+
|
|
236
|
+
### Detection Keywords
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
# Korean
|
|
240
|
+
korean:
|
|
241
|
+
- "조사" → research
|
|
242
|
+
- "검색" → search
|
|
243
|
+
- "리서치" → research
|
|
244
|
+
- "탐색" → explore
|
|
245
|
+
- "찾아" → look up
|
|
246
|
+
- "알아봐" → find out
|
|
247
|
+
- "자료" → gather materials
|
|
248
|
+
- "정보 수집" → gather information
|
|
249
|
+
|
|
250
|
+
# English
|
|
251
|
+
english:
|
|
252
|
+
- "research"
|
|
253
|
+
- "investigate"
|
|
254
|
+
- "search for"
|
|
255
|
+
- "look up"
|
|
256
|
+
- "gather information"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Routing Logic
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
Research intent detected (confidence >= 70%)
|
|
263
|
+
↓
|
|
264
|
+
Check Codex CLI availability
|
|
265
|
+
├─ Available (codex binary + OPENAI_API_KEY)
|
|
266
|
+
│ → Use codex-exec skill with --effort xhigh
|
|
267
|
+
│ → Prompt: "Research and analyze: {user_request}"
|
|
268
|
+
│ → Returns: structured findings for orchestrator
|
|
269
|
+
└─ Unavailable
|
|
270
|
+
→ Fall back to Claude's WebFetch/WebSearch
|
|
271
|
+
→ Orchestrator handles directly or via general-purpose agent
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Confidence Scoring
|
|
275
|
+
|
|
276
|
+
| Factor | Weight | Example |
|
|
277
|
+
|--------|--------|---------|
|
|
278
|
+
| Research keyword match | +40 | "조사해줘", "research" |
|
|
279
|
+
| Action verb match | +30 | "찾아", "investigate" |
|
|
280
|
+
| URL/topic present | +20 | specific URL or topic mentioned |
|
|
281
|
+
| Context (previous research) | +10 | follow-up research request |
|
|
282
|
+
|
|
283
|
+
### Output Format
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
[Intent Detected]
|
|
287
|
+
├── Input: "{user input}"
|
|
288
|
+
├── Workflow: research-workflow
|
|
289
|
+
├── Confidence: {percentage}%
|
|
290
|
+
├── Method: codex-exec (xhigh) | WebFetch fallback
|
|
291
|
+
└── Reason: {explanation}
|
|
292
|
+
```
|
|
@@ -288,6 +288,42 @@ agents:
|
|
|
288
288
|
supported_actions: [save, recall, remember]
|
|
289
289
|
base_confidence: 40
|
|
290
290
|
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Research / Information Gathering (skill-based, not agent)
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
research-workflow:
|
|
295
|
+
keywords:
|
|
296
|
+
korean:
|
|
297
|
+
- 조사
|
|
298
|
+
- 검색
|
|
299
|
+
- 리서치
|
|
300
|
+
- 탐색
|
|
301
|
+
- 찾아
|
|
302
|
+
- 알아봐
|
|
303
|
+
- 자료
|
|
304
|
+
- 정보 수집
|
|
305
|
+
- 웹에서
|
|
306
|
+
english:
|
|
307
|
+
- research
|
|
308
|
+
- investigate
|
|
309
|
+
- search for
|
|
310
|
+
- look up
|
|
311
|
+
- gather information
|
|
312
|
+
- web fetch
|
|
313
|
+
- find out
|
|
314
|
+
- explore topic
|
|
315
|
+
file_patterns: []
|
|
316
|
+
supported_actions:
|
|
317
|
+
- search
|
|
318
|
+
- fetch
|
|
319
|
+
- investigate
|
|
320
|
+
- research
|
|
321
|
+
- analyze
|
|
322
|
+
- gather
|
|
323
|
+
base_confidence: 50
|
|
324
|
+
action_weight: 30
|
|
325
|
+
routing_note: "Uses codex-exec skill with xhigh effort when available, falls back to WebFetch/WebSearch"
|
|
326
|
+
|
|
291
327
|
# Managers (continued)
|
|
292
328
|
mgr-gitnerd:
|
|
293
329
|
keywords:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Reads JSON from stdin (Claude Code statusline API, ~300ms intervals)
|
|
5
5
|
# and outputs a formatted status line, e.g.:
|
|
6
6
|
#
|
|
7
|
-
#
|
|
7
|
+
# $0.05 | my-project | develop | PR #160 | CTX:42%
|
|
8
8
|
#
|
|
9
9
|
# JSON input structure:
|
|
10
10
|
# {
|
|
@@ -71,6 +71,7 @@ IFS=$'\t' read -r model_name project_dir ctx_pct ctx_size cost_usd <<< "$(
|
|
|
71
71
|
|
|
72
72
|
# ---------------------------------------------------------------------------
|
|
73
73
|
# 5. Model display name + color (bash 3.2 compatible case pattern matching)
|
|
74
|
+
# Model detection (kept for internal reference, not displayed in statusline)
|
|
74
75
|
# ---------------------------------------------------------------------------
|
|
75
76
|
case "$model_name" in
|
|
76
77
|
*[Oo]pus*) model_display="Opus"; model_color="${COLOR_OPUS}" ;;
|
|
@@ -79,6 +80,30 @@ case "$model_name" in
|
|
|
79
80
|
*) model_display="$model_name"; model_color="${COLOR_RESET}" ;;
|
|
80
81
|
esac
|
|
81
82
|
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# 5b. Cost display — format and colorize session API cost
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Ensure cost_usd is a valid number (fallback to 0)
|
|
87
|
+
if [[ -z "$cost_usd" ]] || ! printf '%f' "$cost_usd" >/dev/null 2>&1; then
|
|
88
|
+
cost_usd="0"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
cost_display=$(printf '$%.2f' "$cost_usd")
|
|
92
|
+
|
|
93
|
+
# Color by cost threshold (cents for integer comparison)
|
|
94
|
+
cost_cents=$(printf '%.0f' "$(echo "$cost_usd * 100" | bc 2>/dev/null || echo 0)")
|
|
95
|
+
if ! [[ "$cost_cents" =~ ^[0-9]+$ ]]; then
|
|
96
|
+
cost_cents=0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [[ "$cost_cents" -ge 500 ]]; then
|
|
100
|
+
cost_color="${COLOR_CTX_CRIT}" # Red (>= $5.00)
|
|
101
|
+
elif [[ "$cost_cents" -ge 100 ]]; then
|
|
102
|
+
cost_color="${COLOR_CTX_WARN}" # Yellow ($1.00 - $4.99)
|
|
103
|
+
else
|
|
104
|
+
cost_color="${COLOR_CTX_OK}" # Green (< $1.00)
|
|
105
|
+
fi
|
|
106
|
+
|
|
82
107
|
# ---------------------------------------------------------------------------
|
|
83
108
|
# 6. Project name — basename of workspace current_dir
|
|
84
109
|
# ---------------------------------------------------------------------------
|
|
@@ -108,7 +133,85 @@ if [[ -f "$git_head_file" ]]; then
|
|
|
108
133
|
fi
|
|
109
134
|
|
|
110
135
|
# ---------------------------------------------------------------------------
|
|
111
|
-
#
|
|
136
|
+
# 7b. Branch URL — for OSC 8 clickable link
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
branch_url=""
|
|
139
|
+
if [[ -n "$git_branch" && -n "$project_dir" ]]; then
|
|
140
|
+
# Get remote URL from git config
|
|
141
|
+
git_config="${project_dir}/.git/config"
|
|
142
|
+
if [[ -f "$git_config" ]]; then
|
|
143
|
+
# Extract remote origin URL from git config (no subprocess)
|
|
144
|
+
remote_url=""
|
|
145
|
+
in_remote_origin=false
|
|
146
|
+
while IFS= read -r line; do
|
|
147
|
+
case "$line" in
|
|
148
|
+
'[remote "origin"]')
|
|
149
|
+
in_remote_origin=true
|
|
150
|
+
;;
|
|
151
|
+
'['*)
|
|
152
|
+
in_remote_origin=false
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
if $in_remote_origin; then
|
|
156
|
+
case "$line" in
|
|
157
|
+
*url\ =*)
|
|
158
|
+
remote_url="${line#*url = }"
|
|
159
|
+
;;
|
|
160
|
+
esac
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
esac
|
|
164
|
+
done < "$git_config"
|
|
165
|
+
|
|
166
|
+
# Convert remote URL to HTTPS browse URL
|
|
167
|
+
if [[ -n "$remote_url" ]]; then
|
|
168
|
+
case "$remote_url" in
|
|
169
|
+
git@github.com:*)
|
|
170
|
+
# git@github.com:owner/repo.git → https://github.com/owner/repo
|
|
171
|
+
repo_path="${remote_url#git@github.com:}"
|
|
172
|
+
repo_path="${repo_path%.git}"
|
|
173
|
+
branch_url="https://github.com/${repo_path}/tree/${git_branch}"
|
|
174
|
+
;;
|
|
175
|
+
https://github.com/*)
|
|
176
|
+
# https://github.com/owner/repo.git → https://github.com/owner/repo
|
|
177
|
+
repo_path="${remote_url#https://github.com/}"
|
|
178
|
+
repo_path="${repo_path%.git}"
|
|
179
|
+
branch_url="https://github.com/${repo_path}/tree/${git_branch}"
|
|
180
|
+
;;
|
|
181
|
+
esac
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# 8. PR number — cached by branch to avoid gh call on every refresh
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
pr_display=""
|
|
190
|
+
if [[ -n "$git_branch" ]] && command -v gh >/dev/null 2>&1; then
|
|
191
|
+
cache_file="/tmp/statusline-pr-${project_name}"
|
|
192
|
+
cached_branch=""
|
|
193
|
+
cached_pr=""
|
|
194
|
+
|
|
195
|
+
if [[ -f "$cache_file" ]]; then
|
|
196
|
+
IFS=$'\t' read -r cached_branch cached_pr < "$cache_file"
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
if [[ "$cached_branch" == "$git_branch" ]]; then
|
|
200
|
+
# Cache hit — use cached PR number
|
|
201
|
+
pr_number="$cached_pr"
|
|
202
|
+
else
|
|
203
|
+
# Cache miss — query gh and update cache
|
|
204
|
+
pr_number="$(gh pr view --json number -q .number 2>/dev/null || echo "")"
|
|
205
|
+
printf '%s\t%s\n' "$git_branch" "$pr_number" > "$cache_file"
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if [[ -n "$pr_number" ]]; then
|
|
209
|
+
pr_display="PR #${pr_number}"
|
|
210
|
+
fi
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# 9. Context percentage with color
|
|
112
215
|
# ---------------------------------------------------------------------------
|
|
113
216
|
# ctx_pct may arrive as a float (e.g. 42.5); truncate to integer for comparison
|
|
114
217
|
ctx_int="${ctx_pct%%.*}"
|
|
@@ -127,26 +230,34 @@ fi
|
|
|
127
230
|
|
|
128
231
|
ctx_display="CTX:${ctx_int}%"
|
|
129
232
|
|
|
130
|
-
# ---------------------------------------------------------------------------
|
|
131
|
-
# 9. Cost formatting — always two decimal places
|
|
132
|
-
# ---------------------------------------------------------------------------
|
|
133
|
-
cost_display="$(printf '$%.2f' "$cost_usd")"
|
|
134
|
-
|
|
135
233
|
# ---------------------------------------------------------------------------
|
|
136
234
|
# 10. Assemble and output the status line
|
|
137
235
|
# ---------------------------------------------------------------------------
|
|
138
|
-
#
|
|
236
|
+
# Format branch with optional OSC 8 hyperlink
|
|
237
|
+
if [[ -n "$branch_url" && -n "${COLOR_RESET}" ]]; then
|
|
238
|
+
# OSC 8 hyperlink: ESC]8;;URL BEL visible-text ESC]8;; BEL
|
|
239
|
+
branch_display=$'\033]8;;'"${branch_url}"$'\a'"${git_branch}"$'\033]8;;\a'
|
|
240
|
+
else
|
|
241
|
+
branch_display="$git_branch"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Build the PR segment (with separator) if present
|
|
245
|
+
pr_segment=""
|
|
246
|
+
if [[ -n "$pr_display" ]]; then
|
|
247
|
+
pr_segment=" | ${pr_display}"
|
|
248
|
+
fi
|
|
249
|
+
|
|
139
250
|
if [[ -n "$git_branch" ]]; then
|
|
140
|
-
printf "${
|
|
141
|
-
"$
|
|
251
|
+
printf "${cost_color}%s${COLOR_RESET} | %s | %s%s | ${ctx_color}%s${COLOR_RESET}\n" \
|
|
252
|
+
"$cost_display" \
|
|
142
253
|
"$project_name" \
|
|
143
|
-
"$
|
|
144
|
-
"$
|
|
145
|
-
"$
|
|
254
|
+
"$branch_display" \
|
|
255
|
+
"$pr_segment" \
|
|
256
|
+
"$ctx_display"
|
|
146
257
|
else
|
|
147
|
-
printf "${
|
|
148
|
-
"$
|
|
258
|
+
printf "${cost_color}%s${COLOR_RESET} | %s%s | ${ctx_color}%s${COLOR_RESET}\n" \
|
|
259
|
+
"$cost_display" \
|
|
149
260
|
"$project_name" \
|
|
150
|
-
"$
|
|
151
|
-
"$
|
|
261
|
+
"$pr_segment" \
|
|
262
|
+
"$ctx_display"
|
|
152
263
|
fi
|