@claudinho/cli 0.5.2 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +157 -61
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -28,6 +28,7 @@ claudinho markets [target] # prediction-market signals: today | <date> | <id> |
28
28
  claudinho share [target] # copy-pasteable snippet: today | live | <date> | <id> | next <TEAM> | table <GROUP>
29
29
  claudinho prompt # one compact status line (for statusline/tmux/Starship)
30
30
  claudinho init-statusline # wire it into the Claude Code statusline
31
+ claudinho init-cursor-statusline # wire it into the Cursor CLI statusline
31
32
  claudinho hook # live-score context for a Claude Code hook (silent off-match)
32
33
  claudinho init-hook # make Claude itself score-aware (UserPromptSubmit)
33
34
  claudinho vibe # a matchday-coder one-liner (#VibingLaVidaLoca)
@@ -177,6 +178,32 @@ Wires `claudinho hook` into Claude Code's `UserPromptSubmit`. During a match,
177
178
  the live score is injected into Claude's context so it can mention it naturally;
178
179
  off-match it's silent (zero added tokens). Restart Claude Code to activate.
179
180
 
181
+ ## Statusline (Cursor CLI)
182
+
183
+ ```bash
184
+ claudinho init-cursor-statusline # patches ~/.cursor/cli-config.json (backs up first)
185
+ claudinho init-cursor-statusline --print # just print the snippet
186
+ ```
187
+
188
+ Uses the same `claudinho prompt` hot path as Claude Code. Cursor-specific tuning
189
+ is applied automatically (`updateIntervalMs: 1000`, `timeoutMs: 1500`).
190
+
191
+ Optional second line with session meta (model, context %, worktree, vim mode) **below** the score:
192
+
193
+ ```bash
194
+ export CLAUDINHO_CURSOR_META=auto # recommended for Cursor CLI
195
+ ```
196
+
197
+ Custom command (local dev or monorepo checkout):
198
+
199
+ ```bash
200
+ claudinho init-cursor-statusline --command "node ./packages/cli/dist/index.js prompt"
201
+ ```
202
+
203
+ > Cursor's `beforeSubmitPrompt` hook does not yet reliably inject live-score
204
+ > context into the model. Use `init-hook` for Claude Code; Cursor CLI is
205
+ > statusline-only until hook injection lands.
206
+
180
207
  ## How it works
181
208
 
182
209
  The full fixture list (104 matches, groups, venues, host cities, kickoffs) ships **bundled**
package/dist/index.js CHANGED
@@ -4114,12 +4114,65 @@ function spawnRefresh(source) {
4114
4114
  }
4115
4115
  }
4116
4116
 
4117
+ // src/cursorPayload.ts
4118
+ import { readFileSync as readFileSync3 } from "fs";
4119
+ function parseCursorPayload(raw) {
4120
+ try {
4121
+ const trimmed = raw.trim();
4122
+ if (!trimmed) return void 0;
4123
+ return JSON.parse(trimmed);
4124
+ } catch {
4125
+ return void 0;
4126
+ }
4127
+ }
4128
+ function readCursorPayload() {
4129
+ try {
4130
+ if (process.stdin.isTTY) return void 0;
4131
+ return parseCursorPayload(readFileSync3(0, "utf8"));
4132
+ } catch {
4133
+ return void 0;
4134
+ }
4135
+ }
4136
+ function looksLikeCursorPayload(payload) {
4137
+ return !!(payload.model?.display_name || payload.context_window != null || payload.render_width_chars != null || payload.worktree?.name || payload.vim?.mode);
4138
+ }
4139
+ function cursorMetaEnabled(payload) {
4140
+ const v = (process.env.CLAUDINHO_CURSOR_META ?? "").toLowerCase();
4141
+ if (v === "0" || v === "false" || v === "no") return false;
4142
+ if (v === "1" || v === "true" || v === "yes") return !!payload;
4143
+ if (v === "auto") return !!payload && looksLikeCursorPayload(payload);
4144
+ return false;
4145
+ }
4146
+ function renderCursorMetaLine(payload) {
4147
+ const parts = [];
4148
+ const model = payload.model?.display_name;
4149
+ if (model) {
4150
+ let label = model;
4151
+ if (payload.model?.param_summary) label += ` ${payload.model.param_summary}`;
4152
+ parts.push(label);
4153
+ }
4154
+ const pct2 = payload.context_window?.used_percentage;
4155
+ if (typeof pct2 === "number" && Number.isFinite(pct2)) {
4156
+ parts.push(`ctx ${Math.floor(pct2)}%`);
4157
+ }
4158
+ if (payload.worktree?.name) parts.push(`wt ${payload.worktree.name}`);
4159
+ if (payload.vim?.mode) parts.push(payload.vim.mode);
4160
+ if (parts.length === 0) return void 0;
4161
+ return `\x1B[90m${parts.join(" ")}\x1B[0m`;
4162
+ }
4163
+ function renderPromptOutput(scoreLine, payload) {
4164
+ const meta = payload && cursorMetaEnabled(payload) ? renderCursorMetaLine(payload) : void 0;
4165
+ if (meta) return `${scoreLine}
4166
+ ${meta}`;
4167
+ return scoreLine;
4168
+ }
4169
+
4117
4170
  // src/install.ts
4118
4171
  import {
4119
4172
  copyFileSync,
4120
4173
  existsSync,
4121
4174
  mkdirSync as mkdirSync3,
4122
- readFileSync as readFileSync3,
4175
+ readFileSync as readFileSync4,
4123
4176
  writeFileSync as writeFileSync2
4124
4177
  } from "fs";
4125
4178
  import { homedir as homedir3 } from "os";
@@ -4127,80 +4180,116 @@ import { dirname, join as join3 } from "path";
4127
4180
  function claudeSettingsPath() {
4128
4181
  return join3(homedir3(), ".claude", "settings.json");
4129
4182
  }
4183
+ function cursorCliConfigPath() {
4184
+ return join3(homedir3(), ".cursor", "cli-config.json");
4185
+ }
4186
+ function isSameCommand(configured, requested) {
4187
+ return configured === requested;
4188
+ }
4130
4189
  function backupOnce(path) {
4131
4190
  const bak = `${path}.claudinho.bak`;
4132
4191
  if (existsSync(path) && !existsSync(bak)) copyFileSync(path, bak);
4133
4192
  }
4134
- function initStatusline(opts = {}) {
4135
- const path = opts.path ?? claudeSettingsPath();
4136
- const sl = { type: "command", command: opts.command ?? "claudinho prompt" };
4193
+ var DEFAULT_PROMPT_COMMAND = "claudinho prompt";
4194
+ var HOOK_COMMAND = "claudinho hook";
4195
+ var CURSOR_STATUSLINE_DEFAULTS = {
4196
+ padding: 0,
4197
+ updateIntervalMs: 1e3,
4198
+ timeoutMs: 1500
4199
+ };
4200
+ function configPathFor(target, override) {
4201
+ if (override) return override;
4202
+ return target === "cursor" ? cursorCliConfigPath() : claudeSettingsPath();
4203
+ }
4204
+ function defaultStatusLineConfig(target, command) {
4205
+ const base = { type: "command", command };
4206
+ if (target === "cursor") return { ...base, ...CURSOR_STATUSLINE_DEFAULTS };
4207
+ return base;
4208
+ }
4209
+ function restartMessage(target) {
4210
+ return target === "cursor" ? "Restart Cursor CLI (or start a new session) to see it." : "Restart Claude Code to see it.";
4211
+ }
4212
+ function readSettings(path, snippet) {
4213
+ if (!existsSync(path)) return {};
4214
+ try {
4215
+ return JSON.parse(readFileSync4(path, "utf8"));
4216
+ } catch {
4217
+ return {
4218
+ action: "manual",
4219
+ path,
4220
+ message: `Could not parse ${path}. Add this manually:
4221
+ ${snippet}`
4222
+ };
4223
+ }
4224
+ }
4225
+ function isInitResult(v) {
4226
+ return "action" in v && typeof v.action === "string";
4227
+ }
4228
+ function initStatuslineFor(target, opts = {}) {
4229
+ const path = configPathFor(target, opts.path);
4230
+ const command = opts.command ?? DEFAULT_PROMPT_COMMAND;
4231
+ const sl = defaultStatusLineConfig(target, command);
4137
4232
  const snippet = JSON.stringify({ statusLine: sl }, null, 2);
4138
4233
  if (opts.print) {
4139
4234
  return { action: "printed", path, message: snippet };
4140
4235
  }
4141
- let settings = {};
4142
- if (existsSync(path)) {
4143
- try {
4144
- settings = JSON.parse(readFileSync3(path, "utf8"));
4145
- } catch {
4146
- return {
4147
- action: "manual",
4148
- path,
4149
- message: `Could not parse ${path}. Add this manually:
4150
- ${snippet}`
4151
- };
4152
- }
4153
- }
4236
+ const parsed = readSettings(path, snippet);
4237
+ if (isInitResult(parsed)) return parsed;
4238
+ const settings = parsed;
4154
4239
  const existing = settings.statusLine;
4155
- if (existing?.command?.includes("claudinho")) {
4156
- return { action: "already", path, message: `Statusline already uses claudinho (${path}).` };
4240
+ if (isSameCommand(existing?.command, command)) {
4241
+ const label = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
4242
+ return { action: "already", path, message: `${label} already configured (${path}).` };
4157
4243
  }
4158
4244
  backupOnce(path);
4159
4245
  settings.statusLine = sl;
4160
4246
  mkdirSync3(dirname(path), { recursive: true });
4161
4247
  writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
4248
+ const surface = target === "cursor" ? "Cursor CLI statusline" : "Statusline";
4162
4249
  return {
4163
4250
  action: "written",
4164
4251
  path,
4165
- message: `Statusline configured in ${path}. Restart Claude Code to see it.`
4252
+ message: `${surface} configured in ${path}. ${restartMessage(target)}`
4166
4253
  };
4167
4254
  }
4168
- var HOOK_EVENT = "UserPromptSubmit";
4169
- var HOOK_COMMAND = "claudinho hook";
4255
+ function initStatusline(opts = {}) {
4256
+ return initStatuslineFor("claude", opts);
4257
+ }
4258
+ function initCursorStatusline(opts = {}) {
4259
+ return initStatuslineFor("cursor", opts);
4260
+ }
4261
+ var CLAUDE_HOOK_EVENT = "UserPromptSubmit";
4262
+ function claudeHookCommands(settings) {
4263
+ const hooks = settings.hooks;
4264
+ const matchers = hooks?.[CLAUDE_HOOK_EVENT] ?? [];
4265
+ return matchers.flatMap(
4266
+ (m) => (m.hooks ?? []).map((h) => h.command).filter((c) => typeof c === "string")
4267
+ );
4268
+ }
4170
4269
  function initHook(opts = {}) {
4171
4270
  const path = opts.path ?? claudeSettingsPath();
4172
4271
  const command = opts.command ?? HOOK_COMMAND;
4173
4272
  const snippet = JSON.stringify(
4174
- { hooks: { [HOOK_EVENT]: [{ hooks: [{ type: "command", command }] }] } },
4273
+ { hooks: { [CLAUDE_HOOK_EVENT]: [{ hooks: [{ type: "command", command }] }] } },
4175
4274
  null,
4176
4275
  2
4177
4276
  );
4178
4277
  if (opts.print) return { action: "printed", path, message: snippet };
4179
- let settings = {};
4180
- if (existsSync(path)) {
4181
- try {
4182
- settings = JSON.parse(readFileSync3(path, "utf8"));
4183
- } catch {
4184
- return {
4185
- action: "manual",
4186
- path,
4187
- message: `Could not parse ${path}. Add this manually:
4188
- ${snippet}`
4189
- };
4190
- }
4278
+ const parsed = readSettings(path, snippet);
4279
+ if (isInitResult(parsed)) return parsed;
4280
+ const settings = parsed;
4281
+ if (claudeHookCommands(settings).some((c) => isSameCommand(c, command))) {
4282
+ return {
4283
+ action: "already",
4284
+ path,
4285
+ message: `UserPromptSubmit hook already configured (${path}).`
4286
+ };
4191
4287
  }
4288
+ backupOnce(path);
4192
4289
  settings.hooks ??= {};
4193
4290
  const hooks = settings.hooks;
4194
- hooks[HOOK_EVENT] ??= [];
4195
- const matchers = hooks[HOOK_EVENT];
4196
- const already = matchers.some(
4197
- (m) => (m.hooks ?? []).some((h) => typeof h.command === "string" && h.command.includes("claudinho"))
4198
- );
4199
- if (already) {
4200
- return { action: "already", path, message: `UserPromptSubmit hook already uses claudinho (${path}).` };
4201
- }
4202
- backupOnce(path);
4203
- matchers.push({ hooks: [{ type: "command", command }] });
4291
+ hooks[CLAUDE_HOOK_EVENT] ??= [];
4292
+ hooks[CLAUDE_HOOK_EVENT].push({ hooks: [{ type: "command", command }] });
4204
4293
  mkdirSync3(dirname(path), { recursive: true });
4205
4294
  writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
4206
4295
  return {
@@ -4439,6 +4528,7 @@ async function cmdTable(group, ctx) {
4439
4528
  }
4440
4529
  function cmdPrompt({ cfg }) {
4441
4530
  try {
4531
+ const payload = readCursorPayload();
4442
4532
  const team = process.env.CLAUDINHO_TEAM;
4443
4533
  const compact = !["0", "false", "no"].includes(
4444
4534
  (process.env.CLAUDINHO_COMPACT ?? "").toLowerCase()
@@ -4446,7 +4536,8 @@ function cmdPrompt({ cfg }) {
4446
4536
  const maxRaw = Number.parseInt(process.env.CLAUDINHO_MAX ?? "", 10);
4447
4537
  const max = Number.isFinite(maxRaw) && maxRaw > 0 ? maxRaw : void 0;
4448
4538
  const state = readCurrentState(cfg.source, resolveCompetition());
4449
- out(renderPrompt(state, { team, compact, max }));
4539
+ const scoreLine = renderPrompt(state, { team, compact, max });
4540
+ out(renderPromptOutput(scoreLine, payload));
4450
4541
  if (!state || shouldRefresh()) spawnRefresh(cfg.source);
4451
4542
  } catch {
4452
4543
  out("");
@@ -4465,8 +4556,7 @@ function cmdHook({ cfg }) {
4465
4556
  async function cmdRefresh({ cfg }) {
4466
4557
  await runRefresh({ source: cfg.source });
4467
4558
  }
4468
- function cmdInitStatusline(opts, { cfg }) {
4469
- const res = initStatusline({ print: opts.print });
4559
+ function printInitResult(res, cfg) {
4470
4560
  const c = painterFor(cfg);
4471
4561
  if (res.action === "printed") {
4472
4562
  out(res.message);
@@ -4475,15 +4565,14 @@ function cmdInitStatusline(opts, { cfg }) {
4475
4565
  const mark = res.action === "written" ? c.green("\u2713") : res.action === "already" ? c.cyan("\u2022") : c.yellow("!");
4476
4566
  out(`${mark} ${res.message}`);
4477
4567
  }
4568
+ function cmdInitStatusline(opts, { cfg }) {
4569
+ printInitResult(initStatusline({ print: opts.print, command: opts.command }), cfg);
4570
+ }
4478
4571
  function cmdInitHook(opts, { cfg }) {
4479
- const res = initHook({ print: opts.print });
4480
- const c = painterFor(cfg);
4481
- if (res.action === "printed") {
4482
- out(res.message);
4483
- return;
4484
- }
4485
- const mark = res.action === "written" ? c.green("\u2713") : res.action === "already" ? c.cyan("\u2022") : c.yellow("!");
4486
- out(`${mark} ${res.message}`);
4572
+ printInitResult(initHook({ print: opts.print, command: opts.command }), cfg);
4573
+ }
4574
+ function cmdInitCursorStatusline(opts, { cfg }) {
4575
+ printInitResult(initCursorStatusline({ print: opts.print, command: opts.command }), cfg);
4487
4576
  }
4488
4577
  async function cmdMatch(id, ctx) {
4489
4578
  const { cfg, t } = ctx;
@@ -4914,7 +5003,7 @@ function handlePipeError(stream) {
4914
5003
  }
4915
5004
  handlePipeError(process.stdout);
4916
5005
  handlePipeError(process.stderr);
4917
- var VERSION = "0.5.2";
5006
+ var VERSION = "0.6.0";
4918
5007
  var DISCLAIMER = "Claudinho is an independent fan project. Not affiliated with or endorsed by FIFA or Anthropic.";
4919
5008
  function ctxFrom(cmd) {
4920
5009
  const root = cmd.parent ?? cmd;
@@ -4930,7 +5019,7 @@ function fail(err) {
4930
5019
  }
4931
5020
  var program = new Command();
4932
5021
  program.name("claudinho").description(
4933
- "The 2026 men\u2019s football tournament in your terminal, your Claude Code statusline, and any MCP client.\n" + DISCLAIMER
5022
+ "The 2026 men\u2019s football tournament in your terminal, your Claude Code / Cursor CLI statusline, and any MCP client.\n" + DISCLAIMER
4934
5023
  ).version(VERSION, "-v, --version").option("--lang <code>", "language: en, es, pt, fr").option("--tz <zone>", "IANA timezone, e.g. America/Mexico_City").option("--json", "output JSON (for scripting)").option("--no-color", "disable ANSI colors").option("--source <name>", "live data provider (advanced)").option("--flavor <level>", "commentary flair: off, subtle, full (default: full)").option("--no-markets", "hide prediction-market signals (informational only)");
4935
5024
  program.addHelpText("after", "\n#VibingLaVidaLoca \u26BD");
4936
5025
  program.command("today").description("show a day's fixtures (default: today)").argument("[date]", "date as YYYY-MM-DD").action(async (date, _opts, cmd) => {
@@ -4985,20 +5074,27 @@ program.command("share").description("print a shareable, copy-pasteable match sn
4985
5074
  fail(e);
4986
5075
  }
4987
5076
  });
4988
- program.command("prompt").description("print a one-line status (Claude Code statusline, tmux, Starship, \u2026)").action((_opts, cmd) => {
5077
+ program.command("prompt").description("print a status line (Claude Code / Cursor CLI statusline, tmux, Starship, \u2026)").action((_opts, cmd) => {
4989
5078
  cmdPrompt(ctxFrom(cmd));
4990
5079
  });
4991
- program.command("init-statusline").description("configure the Claude Code statusline to use claudinho").option("--print", "print the settings snippet instead of writing it").action((opts, cmd) => {
5080
+ program.command("init-statusline").description("configure the Claude Code statusline to use claudinho").option("--print", "print the settings snippet instead of writing it").option("--command <cmd>", "command to run (default: claudinho prompt)").action((opts, cmd) => {
4992
5081
  try {
4993
5082
  cmdInitStatusline(opts, ctxFrom(cmd));
4994
5083
  } catch (e) {
4995
5084
  fail(e);
4996
5085
  }
4997
5086
  });
5087
+ program.command("init-cursor-statusline").description("configure the Cursor CLI statusline to use claudinho").option("--print", "print the settings snippet instead of writing it").option("--command <cmd>", "command to run (default: claudinho prompt)").action((opts, cmd) => {
5088
+ try {
5089
+ cmdInitCursorStatusline(opts, ctxFrom(cmd));
5090
+ } catch (e) {
5091
+ fail(e);
5092
+ }
5093
+ });
4998
5094
  program.command("hook").description("print live-score context for a Claude Code UserPromptSubmit hook (silent off-match)").action((_opts, cmd) => {
4999
5095
  cmdHook(ctxFrom(cmd));
5000
5096
  });
5001
- program.command("init-hook").description("wire the live-score hook into Claude Code (UserPromptSubmit)").option("--print", "print the settings snippet instead of writing it").action((opts, cmd) => {
5097
+ program.command("init-hook").description("wire the live-score hook into Claude Code (UserPromptSubmit)").option("--print", "print the settings snippet instead of writing it").option("--command <cmd>", "command to run (default: claudinho hook)").action((opts, cmd) => {
5002
5098
  try {
5003
5099
  cmdInitHook(opts, ctxFrom(cmd));
5004
5100
  } catch (e) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@claudinho/cli",
3
- "version": "0.5.2",
4
- "description": "Live scores, fixtures, group tables, and read-only prediction-market signals for the 2026 men's football tournament — in your terminal and Claude Code statusline. No API keys. Not affiliated with FIFA or Anthropic.",
3
+ "version": "0.6.0",
4
+ "description": "Live scores, fixtures, group tables, and read-only prediction-market signals for the 2026 men's football tournament — in your terminal, Claude Code, and Cursor CLI statusline. No API keys. Not affiliated with FIFA or Anthropic.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Arturo Garrido",
@@ -43,6 +43,7 @@
43
43
  "terminal",
44
44
  "statusline",
45
45
  "claude-code",
46
+ "cursor-cli",
46
47
  "2026",
47
48
  "vibinglavidaloca"
48
49
  ],
@@ -56,7 +57,7 @@
56
57
  "tsup": "^8.0.0",
57
58
  "typescript": "^5.7.0",
58
59
  "vitest": "^4.1.0",
59
- "@claudinho/core": "0.5.2"
60
+ "@claudinho/core": "0.6.0"
60
61
  },
61
62
  "scripts": {
62
63
  "build": "tsup",