oh-my-customcode 0.17.0 → 0.17.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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-customcode",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Batteries-included agent harness for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- # Opus | my-project | develop | CTX:42% | $0.05
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
- # 8. Context percentage with color
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
- # Build segments; omit git branch segment when unavailable
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 "${model_color}%s${COLOR_RESET} | %s | %s | ${ctx_color}%s${COLOR_RESET} | %s\n" \
141
- "$model_display" \
251
+ printf "${cost_color}%s${COLOR_RESET} | %s | %s%s | ${ctx_color}%s${COLOR_RESET}\n" \
252
+ "$cost_display" \
142
253
  "$project_name" \
143
- "$git_branch" \
144
- "$ctx_display" \
145
- "$cost_display"
254
+ "$branch_display" \
255
+ "$pr_segment" \
256
+ "$ctx_display"
146
257
  else
147
- printf "${model_color}%s${COLOR_RESET} | %s | ${ctx_color}%s${COLOR_RESET} | %s\n" \
148
- "$model_display" \
258
+ printf "${cost_color}%s${COLOR_RESET} | %s%s | ${ctx_color}%s${COLOR_RESET}\n" \
259
+ "$cost_display" \
149
260
  "$project_name" \
150
- "$ctx_display" \
151
- "$cost_display"
261
+ "$pr_segment" \
262
+ "$ctx_display"
152
263
  fi