ultrahope 0.1.11 → 0.1.12

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/README.md CHANGED
@@ -12,7 +12,7 @@ npm install -g ultrahope
12
12
 
13
13
  ### Login
14
14
 
15
- You can try Ultrahope without logging in first. The CLI automatically creates an anonymous session and allows up to 5 requests per day with the Anonymous plan limits.
15
+ You can try Ultrahope without logging in first. The CLI automatically creates an anonymous session and allows up to 5 requests per day with the Free plan limits.
16
16
 
17
17
  When you want to keep going, authenticate with your Ultrahope account using device flow:
18
18
 
@@ -22,6 +22,10 @@ ultrahope login
22
22
 
23
23
  This will display a URL and code. Open the URL in your browser, sign in, and enter the code to authorize the CLI. On successful login, the CLI replaces the anonymous session with your authenticated one while keeping the local installation identity.
24
24
 
25
+ Escalation (`Shift+E`) uses the Pro model set (`anthropic/claude-sonnet-4.6`,
26
+ `openai/gpt-5.3-codex`). If your account is not Pro, escalation is not shown and
27
+ requesting Pro-only models is rejected by the API.
28
+
25
29
  ### Translate
26
30
 
27
31
  Translate input to various formats. Pipe content to the command:
@@ -55,7 +55,7 @@ var InsufficientBalanceError = class extends Error {
55
55
  }
56
56
  } else {
57
57
  lines.push(
58
- "Error: Anonymous usage is limited. Upgrade to Pro for unlimited requests with $1 included credit."
58
+ "Error: Free plan usage is limited. Upgrade to Pro for unlimited requests with $1 included credit."
59
59
  );
60
60
  if (this.actions?.upgrade) {
61
61
  lines.push(` Upgrade: ${this.actions.upgrade}`);
@@ -404,6 +404,23 @@ function createApiClient(token) {
404
404
  log("command_execution response", data);
405
405
  return data;
406
406
  },
407
+ async getEntitlement() {
408
+ log("entitlement request");
409
+ const res = await fetch(`${API_BASE_URL}/api/v1/entitlement`, {
410
+ method: "GET",
411
+ headers: jsonHeaders()
412
+ });
413
+ if (res.status === 401) {
414
+ log("entitlement error (401)");
415
+ throw new UnauthorizedError();
416
+ }
417
+ if (!res.ok) {
418
+ const text = await getErrorText(res, null);
419
+ log("entitlement error", { status: res.status, text });
420
+ throw new Error(`API error: ${res.status} ${text}`);
421
+ }
422
+ return res.json();
423
+ },
407
424
  async generateCommitMessage(req, options) {
408
425
  log("generateCommitMessage request", req);
409
426
  const { data, error, response } = await client.POST(
@@ -1490,6 +1507,77 @@ function formatDiffStats(stats) {
1490
1507
  return parts.join(", ");
1491
1508
  }
1492
1509
 
1510
+ // lib/entitlement-cache.ts
1511
+ import fs3 from "fs/promises";
1512
+ import * as os3 from "os";
1513
+ import path3 from "path";
1514
+ var ENTITLEMENT_CACHE_TTL_MS = 1e3 * 60 * 15;
1515
+ function getEntitlementCachePath() {
1516
+ const configDir = process.env.XDG_CONFIG_HOME ?? path3.join(os3.homedir(), ".config");
1517
+ const filename = "entitlement-cache.json";
1518
+ return path3.join(configDir, "ultrahope", filename);
1519
+ }
1520
+ async function readEntitlementCache() {
1521
+ const cachePath = getEntitlementCachePath();
1522
+ try {
1523
+ const raw = await fs3.readFile(cachePath, "utf-8");
1524
+ const parsed = JSON.parse(raw);
1525
+ if (parsed && typeof parsed === "object" && "entitlement" in parsed && "fetchedAt" in parsed && typeof parsed.entitlement === "string" && typeof parsed.fetchedAt === "string") {
1526
+ if (parsed.entitlement === "anonymous" || parsed.entitlement === "authenticated_unpaid" || parsed.entitlement === "pro") {
1527
+ return {
1528
+ entitlement: parsed.entitlement,
1529
+ fetchedAt: parsed.fetchedAt
1530
+ };
1531
+ }
1532
+ }
1533
+ return null;
1534
+ } catch {
1535
+ return null;
1536
+ }
1537
+ }
1538
+ function normalizeCachedAt(fetchedAt) {
1539
+ const value = Date.parse(fetchedAt);
1540
+ return Number.isFinite(value) ? value : NaN;
1541
+ }
1542
+ function isEntitlementCacheFresh(record) {
1543
+ const fetchedAt = normalizeCachedAt(record.fetchedAt);
1544
+ if (!Number.isFinite(fetchedAt)) return false;
1545
+ return Date.now() - fetchedAt <= ENTITLEMENT_CACHE_TTL_MS;
1546
+ }
1547
+ async function writeEntitlementCache(entitlement) {
1548
+ const cachePath = getEntitlementCachePath();
1549
+ const dir = path3.dirname(cachePath);
1550
+ await fs3.mkdir(dir, { recursive: true });
1551
+ const payload = {
1552
+ entitlement,
1553
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1554
+ };
1555
+ await fs3.writeFile(cachePath, JSON.stringify(payload), { mode: 384 });
1556
+ }
1557
+
1558
+ // lib/entitlement-capability.ts
1559
+ async function resolveEntitlementCapability(api, authKind) {
1560
+ if (authKind !== "authenticated") {
1561
+ return { escalate: false };
1562
+ }
1563
+ const cache = await readEntitlementCache();
1564
+ if (cache && isEntitlementCacheFresh(cache)) {
1565
+ return {
1566
+ escalate: cache.entitlement === "pro"
1567
+ };
1568
+ }
1569
+ const capability = { escalate: true };
1570
+ void (async () => {
1571
+ try {
1572
+ const response = await api.getEntitlement();
1573
+ capability.escalate = response.entitlement === "pro";
1574
+ await writeEntitlementCache(response.entitlement);
1575
+ } catch {
1576
+ }
1577
+ })();
1578
+ return capability;
1579
+ }
1580
+
1493
1581
  // lib/renderer.ts
1494
1582
  import * as readline2 from "readline";
1495
1583
 
@@ -3067,6 +3155,12 @@ var selectorRenderCapabilities = {
3067
3155
  escalate: true,
3068
3156
  clickConfirm: false
3069
3157
  };
3158
+ function resolveSelectorCapabilities(options) {
3159
+ return {
3160
+ ...selectorRenderCapabilities,
3161
+ escalate: options?.escalate ?? selectorRenderCapabilities.escalate
3162
+ };
3163
+ }
3070
3164
  function renderError(error, slotsLength, output) {
3071
3165
  const readyCount = slotsLength;
3072
3166
  const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
@@ -3215,9 +3309,13 @@ async function selectCandidate(options) {
3215
3309
  let generationRun = 0;
3216
3310
  let generationController = null;
3217
3311
  let isPromptOpen = false;
3312
+ let dynamicCapabilities = resolveSelectorCapabilities(options.capabilities);
3218
3313
  const ttyReader = ttyInput;
3219
3314
  const ttyWriter = ttyOutput;
3220
3315
  const renderer = createRenderer(ttyWriter);
3316
+ const updateDynamicCapabilities = () => {
3317
+ dynamicCapabilities = resolveSelectorCapabilities(options.capabilities);
3318
+ };
3221
3319
  const setRawModeSafe2 = (enabled) => {
3222
3320
  try {
3223
3321
  const r = ttyReader;
@@ -3229,6 +3327,7 @@ async function selectCandidate(options) {
3229
3327
  setRawModeSafe2(true);
3230
3328
  ttyReader.resume();
3231
3329
  const render = () => {
3330
+ updateDynamicCapabilities();
3232
3331
  const allowPromptRender = context.mode === "prompt" && context.promptKind === "edit";
3233
3332
  if (!cleanedUp && (!isPromptOpen || allowPromptRender)) {
3234
3333
  const frame = selectorRenderFrame({
@@ -3241,7 +3340,7 @@ async function selectCandidate(options) {
3241
3340
  nowMs: Date.now(),
3242
3341
  spinnerFrames: SPINNER_FRAMES,
3243
3342
  copy: renderCopy,
3244
- capabilities: selectorRenderCapabilities
3343
+ capabilities: dynamicCapabilities
3245
3344
  });
3246
3345
  renderer.render(renderSelectorTextFromRenderFrame(frame));
3247
3346
  }
@@ -3257,7 +3356,7 @@ async function selectCandidate(options) {
3257
3356
  nowMs: Date.now(),
3258
3357
  spinnerFrames: SPINNER_FRAMES,
3259
3358
  copy: renderCopy,
3260
- capabilities: selectorRenderCapabilities
3359
+ capabilities: dynamicCapabilities
3261
3360
  });
3262
3361
  const selected = result.selectedCandidate?.content ?? result.selected ?? "";
3263
3362
  const selectedTitle = normalizeCandidateContentForDisplay(selected) || selected;
@@ -3382,7 +3481,7 @@ async function selectCandidate(options) {
3382
3481
  nowMs: Date.now(),
3383
3482
  spinnerFrames: SPINNER_FRAMES,
3384
3483
  copy: renderCopy,
3385
- capabilities: selectorRenderCapabilities
3484
+ capabilities: dynamicCapabilities
3386
3485
  });
3387
3486
  const costSuffix = frame.viewModel.header.totalCostLabel ? ` (total: ${frame.viewModel.header.totalCostLabel})` : "";
3388
3487
  const generatedLine = `${frame.viewModel.header.generatedLabel}${costSuffix}`;
@@ -3503,7 +3602,7 @@ async function selectCandidate(options) {
3503
3602
  nowMs: Date.now(),
3504
3603
  spinnerFrames: SPINNER_FRAMES,
3505
3604
  copy: renderCopy,
3506
- capabilities: selectorRenderCapabilities,
3605
+ capabilities: dynamicCapabilities,
3507
3606
  bufferText: buffer.getText()
3508
3607
  });
3509
3608
  const prompt = frame.prompt;
@@ -3579,7 +3678,7 @@ async function selectCandidate(options) {
3579
3678
  nowMs: Date.now(),
3580
3679
  spinnerFrames: SPINNER_FRAMES,
3581
3680
  copy: renderCopy,
3582
- capabilities: selectorRenderCapabilities,
3681
+ capabilities: dynamicCapabilities,
3583
3682
  bufferText: buffer.getText()
3584
3683
  });
3585
3684
  const prompt = frame.prompt;
@@ -3663,6 +3762,8 @@ async function selectCandidate(options) {
3663
3762
  }
3664
3763
  if (key.name === "e" && key.shift) {
3665
3764
  if (!hasReadySlot(context.slots)) return;
3765
+ updateDynamicCapabilities();
3766
+ if (!dynamicCapabilities.escalate) return;
3666
3767
  applyResult(transitionSelectorFlow(context, { type: "ESCALATE" }));
3667
3768
  return;
3668
3769
  }
@@ -3993,9 +4094,12 @@ async function commit(args2) {
3993
4094
  process.exit(1);
3994
4095
  }
3995
4096
  try {
4097
+ const existingCredentials = await getCredentials();
4098
+ const authKind = existingCredentials?.authKind ?? "anonymous";
3996
4099
  const token = await getToken();
3997
4100
  const installationId = await getInstallationId();
3998
4101
  const api = createApiClient(token);
4102
+ const capabilities = await resolveEntitlementCapability(api, authKind);
3999
4103
  const apiClient = api;
4000
4104
  let guideHint;
4001
4105
  let refineMessage;
@@ -4073,7 +4177,8 @@ async function commit(args2) {
4073
4177
  models,
4074
4178
  inlineEditPrompt: true,
4075
4179
  initialGuideHint: guideHint,
4076
- isEscalation
4180
+ isEscalation,
4181
+ capabilities
4077
4182
  });
4078
4183
  if (result.action === "abort") {
4079
4184
  if (result.error instanceof InvalidModelError) {
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ var InsufficientBalanceError = class extends Error {
55
55
  }
56
56
  } else {
57
57
  lines.push(
58
- "Error: Anonymous usage is limited. Upgrade to Pro for unlimited requests with $1 included credit."
58
+ "Error: Free plan usage is limited. Upgrade to Pro for unlimited requests with $1 included credit."
59
59
  );
60
60
  if (this.actions?.upgrade) {
61
61
  lines.push(` Upgrade: ${this.actions.upgrade}`);
@@ -404,6 +404,23 @@ function createApiClient(token) {
404
404
  log("command_execution response", data);
405
405
  return data;
406
406
  },
407
+ async getEntitlement() {
408
+ log("entitlement request");
409
+ const res = await fetch(`${API_BASE_URL}/api/v1/entitlement`, {
410
+ method: "GET",
411
+ headers: jsonHeaders()
412
+ });
413
+ if (res.status === 401) {
414
+ log("entitlement error (401)");
415
+ throw new UnauthorizedError();
416
+ }
417
+ if (!res.ok) {
418
+ const text = await getErrorText(res, null);
419
+ log("entitlement error", { status: res.status, text });
420
+ throw new Error(`API error: ${res.status} ${text}`);
421
+ }
422
+ return res.json();
423
+ },
407
424
  async generateCommitMessage(req, options) {
408
425
  log("generateCommitMessage request", req);
409
426
  const { data, error, response } = await client.POST(
@@ -1508,6 +1525,77 @@ function formatDiffStats(stats) {
1508
1525
  return parts.join(", ");
1509
1526
  }
1510
1527
 
1528
+ // lib/entitlement-cache.ts
1529
+ import fs3 from "fs/promises";
1530
+ import * as os3 from "os";
1531
+ import path3 from "path";
1532
+ var ENTITLEMENT_CACHE_TTL_MS = 1e3 * 60 * 15;
1533
+ function getEntitlementCachePath() {
1534
+ const configDir = process.env.XDG_CONFIG_HOME ?? path3.join(os3.homedir(), ".config");
1535
+ const filename = "entitlement-cache.json";
1536
+ return path3.join(configDir, "ultrahope", filename);
1537
+ }
1538
+ async function readEntitlementCache() {
1539
+ const cachePath = getEntitlementCachePath();
1540
+ try {
1541
+ const raw = await fs3.readFile(cachePath, "utf-8");
1542
+ const parsed = JSON.parse(raw);
1543
+ if (parsed && typeof parsed === "object" && "entitlement" in parsed && "fetchedAt" in parsed && typeof parsed.entitlement === "string" && typeof parsed.fetchedAt === "string") {
1544
+ if (parsed.entitlement === "anonymous" || parsed.entitlement === "authenticated_unpaid" || parsed.entitlement === "pro") {
1545
+ return {
1546
+ entitlement: parsed.entitlement,
1547
+ fetchedAt: parsed.fetchedAt
1548
+ };
1549
+ }
1550
+ }
1551
+ return null;
1552
+ } catch {
1553
+ return null;
1554
+ }
1555
+ }
1556
+ function normalizeCachedAt(fetchedAt) {
1557
+ const value = Date.parse(fetchedAt);
1558
+ return Number.isFinite(value) ? value : NaN;
1559
+ }
1560
+ function isEntitlementCacheFresh(record) {
1561
+ const fetchedAt = normalizeCachedAt(record.fetchedAt);
1562
+ if (!Number.isFinite(fetchedAt)) return false;
1563
+ return Date.now() - fetchedAt <= ENTITLEMENT_CACHE_TTL_MS;
1564
+ }
1565
+ async function writeEntitlementCache(entitlement) {
1566
+ const cachePath = getEntitlementCachePath();
1567
+ const dir = path3.dirname(cachePath);
1568
+ await fs3.mkdir(dir, { recursive: true });
1569
+ const payload = {
1570
+ entitlement,
1571
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1572
+ };
1573
+ await fs3.writeFile(cachePath, JSON.stringify(payload), { mode: 384 });
1574
+ }
1575
+
1576
+ // lib/entitlement-capability.ts
1577
+ async function resolveEntitlementCapability(api, authKind) {
1578
+ if (authKind !== "authenticated") {
1579
+ return { escalate: false };
1580
+ }
1581
+ const cache = await readEntitlementCache();
1582
+ if (cache && isEntitlementCacheFresh(cache)) {
1583
+ return {
1584
+ escalate: cache.entitlement === "pro"
1585
+ };
1586
+ }
1587
+ const capability = { escalate: true };
1588
+ void (async () => {
1589
+ try {
1590
+ const response = await api.getEntitlement();
1591
+ capability.escalate = response.entitlement === "pro";
1592
+ await writeEntitlementCache(response.entitlement);
1593
+ } catch {
1594
+ }
1595
+ })();
1596
+ return capability;
1597
+ }
1598
+
1511
1599
  // lib/renderer.ts
1512
1600
  import * as readline2 from "readline";
1513
1601
 
@@ -3085,6 +3173,12 @@ var selectorRenderCapabilities = {
3085
3173
  escalate: true,
3086
3174
  clickConfirm: false
3087
3175
  };
3176
+ function resolveSelectorCapabilities(options) {
3177
+ return {
3178
+ ...selectorRenderCapabilities,
3179
+ escalate: options?.escalate ?? selectorRenderCapabilities.escalate
3180
+ };
3181
+ }
3088
3182
  function renderError(error, slotsLength, output) {
3089
3183
  const readyCount = slotsLength;
3090
3184
  const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
@@ -3233,9 +3327,13 @@ async function selectCandidate(options) {
3233
3327
  let generationRun = 0;
3234
3328
  let generationController = null;
3235
3329
  let isPromptOpen = false;
3330
+ let dynamicCapabilities = resolveSelectorCapabilities(options.capabilities);
3236
3331
  const ttyReader = ttyInput;
3237
3332
  const ttyWriter = ttyOutput;
3238
3333
  const renderer = createRenderer(ttyWriter);
3334
+ const updateDynamicCapabilities = () => {
3335
+ dynamicCapabilities = resolveSelectorCapabilities(options.capabilities);
3336
+ };
3239
3337
  const setRawModeSafe2 = (enabled) => {
3240
3338
  try {
3241
3339
  const r = ttyReader;
@@ -3247,6 +3345,7 @@ async function selectCandidate(options) {
3247
3345
  setRawModeSafe2(true);
3248
3346
  ttyReader.resume();
3249
3347
  const render = () => {
3348
+ updateDynamicCapabilities();
3250
3349
  const allowPromptRender = context.mode === "prompt" && context.promptKind === "edit";
3251
3350
  if (!cleanedUp && (!isPromptOpen || allowPromptRender)) {
3252
3351
  const frame = selectorRenderFrame({
@@ -3259,7 +3358,7 @@ async function selectCandidate(options) {
3259
3358
  nowMs: Date.now(),
3260
3359
  spinnerFrames: SPINNER_FRAMES,
3261
3360
  copy: renderCopy,
3262
- capabilities: selectorRenderCapabilities
3361
+ capabilities: dynamicCapabilities
3263
3362
  });
3264
3363
  renderer.render(renderSelectorTextFromRenderFrame(frame));
3265
3364
  }
@@ -3275,7 +3374,7 @@ async function selectCandidate(options) {
3275
3374
  nowMs: Date.now(),
3276
3375
  spinnerFrames: SPINNER_FRAMES,
3277
3376
  copy: renderCopy,
3278
- capabilities: selectorRenderCapabilities
3377
+ capabilities: dynamicCapabilities
3279
3378
  });
3280
3379
  const selected = result.selectedCandidate?.content ?? result.selected ?? "";
3281
3380
  const selectedTitle = normalizeCandidateContentForDisplay(selected) || selected;
@@ -3400,7 +3499,7 @@ async function selectCandidate(options) {
3400
3499
  nowMs: Date.now(),
3401
3500
  spinnerFrames: SPINNER_FRAMES,
3402
3501
  copy: renderCopy,
3403
- capabilities: selectorRenderCapabilities
3502
+ capabilities: dynamicCapabilities
3404
3503
  });
3405
3504
  const costSuffix = frame.viewModel.header.totalCostLabel ? ` (total: ${frame.viewModel.header.totalCostLabel})` : "";
3406
3505
  const generatedLine = `${frame.viewModel.header.generatedLabel}${costSuffix}`;
@@ -3521,7 +3620,7 @@ async function selectCandidate(options) {
3521
3620
  nowMs: Date.now(),
3522
3621
  spinnerFrames: SPINNER_FRAMES,
3523
3622
  copy: renderCopy,
3524
- capabilities: selectorRenderCapabilities,
3623
+ capabilities: dynamicCapabilities,
3525
3624
  bufferText: buffer.getText()
3526
3625
  });
3527
3626
  const prompt = frame.prompt;
@@ -3597,7 +3696,7 @@ async function selectCandidate(options) {
3597
3696
  nowMs: Date.now(),
3598
3697
  spinnerFrames: SPINNER_FRAMES,
3599
3698
  copy: renderCopy,
3600
- capabilities: selectorRenderCapabilities,
3699
+ capabilities: dynamicCapabilities,
3601
3700
  bufferText: buffer.getText()
3602
3701
  });
3603
3702
  const prompt = frame.prompt;
@@ -3681,6 +3780,8 @@ async function selectCandidate(options) {
3681
3780
  }
3682
3781
  if (key.name === "e" && key.shift) {
3683
3782
  if (!hasReadySlot(context.slots)) return;
3783
+ updateDynamicCapabilities();
3784
+ if (!dynamicCapabilities.escalate) return;
3684
3785
  applyResult(transitionSelectorFlow(context, { type: "ESCALATE" }));
3685
3786
  return;
3686
3787
  }
@@ -4063,7 +4164,7 @@ function createCandidateFactory(diff, models, context, captureRecorder, baseGuid
4063
4164
  } : void 0
4064
4165
  });
4065
4166
  }
4066
- async function runInteractiveDescribe(models, createCandidates, context, guideHint, isEscalation) {
4167
+ async function runInteractiveDescribe(models, createCandidates, context, guideHint, isEscalation, capabilities) {
4067
4168
  return selectCandidate({
4068
4169
  createCandidates,
4069
4170
  maxSlots: models.length,
@@ -4071,7 +4172,8 @@ async function runInteractiveDescribe(models, createCandidates, context, guideHi
4071
4172
  models,
4072
4173
  inlineEditPrompt: true,
4073
4174
  initialGuideHint: guideHint,
4074
- isEscalation
4175
+ isEscalation,
4176
+ capabilities
4075
4177
  });
4076
4178
  }
4077
4179
  async function describe(args2) {
@@ -4088,6 +4190,11 @@ async function describe(args2) {
4088
4190
  apiPath: "/v1/commit-message/stream"
4089
4191
  });
4090
4192
  try {
4193
+ const existingCredentials = await getCredentials();
4194
+ const authKind = existingCredentials?.authKind ?? "anonymous";
4195
+ const token = await getToken();
4196
+ const api = createApiClient(token);
4197
+ const capabilities = await resolveEntitlementCapability(api, authKind);
4091
4198
  const stats = getJjDiffStats(options.revision);
4092
4199
  console.log(ui.success(`Found ${formatDiffStats(stats)}`));
4093
4200
  let guideHint;
@@ -4120,7 +4227,8 @@ async function describe(args2) {
4120
4227
  createCandidates,
4121
4228
  context,
4122
4229
  guideHint,
4123
- isEscalation
4230
+ isEscalation,
4231
+ capabilities
4124
4232
  );
4125
4233
  if (result.action === "abort") {
4126
4234
  if (result.error instanceof InvalidModelError) {
@@ -4329,6 +4437,48 @@ function composeGuidance2(guideHint) {
4329
4437
  const normalizedGuideHint = guideHint?.trim() ?? "";
4330
4438
  return normalizedGuideHint || void 0;
4331
4439
  }
4440
+ function decideVcsCommitMessageSelection(state, result, escalationModels) {
4441
+ if (result.action === "escalate") {
4442
+ return {
4443
+ kind: "escalate",
4444
+ state: {
4445
+ ...state,
4446
+ models: escalationModels,
4447
+ guideHint: void 0,
4448
+ refineMessage: void 0,
4449
+ isEscalation: true
4450
+ }
4451
+ };
4452
+ }
4453
+ if (result.action === "refine") {
4454
+ return {
4455
+ kind: "refine",
4456
+ state: {
4457
+ ...state,
4458
+ guideHint: result.guide,
4459
+ refineMessage: result.selected ?? result.selectedCandidate?.content
4460
+ },
4461
+ guideHint: result.guide,
4462
+ refineMessage: result.selected ?? result.selectedCandidate?.content
4463
+ };
4464
+ }
4465
+ if (result.action === "confirm" && result.selected) {
4466
+ return {
4467
+ kind: "confirm",
4468
+ state,
4469
+ selected: result.selected,
4470
+ selectedCandidateGenerationId: result.selectedCandidate?.generationId
4471
+ };
4472
+ }
4473
+ if (result.action === "abort") {
4474
+ return {
4475
+ kind: "abort",
4476
+ state,
4477
+ error: result.error
4478
+ };
4479
+ }
4480
+ return { kind: "continue", state };
4481
+ }
4332
4482
  async function translate(args2) {
4333
4483
  const options = parseArgs(args2);
4334
4484
  const input = await stdin();
@@ -4357,14 +4507,20 @@ async function handleVcsCommitMessage(input, options, args2) {
4357
4507
  args: args2,
4358
4508
  apiPath: "/v1/commit-message/stream"
4359
4509
  });
4360
- const models = resolveModels(options.cliModels);
4510
+ const baseModels = resolveModels(options.cliModels);
4511
+ const escalationModels = resolveEscalationModels();
4512
+ let state = {
4513
+ models: baseModels,
4514
+ isEscalation: false
4515
+ };
4361
4516
  try {
4517
+ const existingCredentials = await getCredentials();
4518
+ const authKind = existingCredentials?.authKind ?? "anonymous";
4362
4519
  const token = await getToken();
4363
4520
  const installationId = await getInstallationId();
4364
4521
  const api = createApiClient(token);
4522
+ const capabilities = await resolveEntitlementCapability(api, authKind);
4365
4523
  const apiClient = api;
4366
- let guideHint;
4367
- let refineMessage;
4368
4524
  let commandExecutionRun = 0;
4369
4525
  const recordSelection2 = async (generationId) => {
4370
4526
  if (!generationId || !apiClient) return;
@@ -4381,10 +4537,10 @@ async function handleVcsCommitMessage(input, options, args2) {
4381
4537
  console.error(`Warning: Failed to record selection. ${message}`);
4382
4538
  }
4383
4539
  };
4384
- const startCommandExecutionSession = (isRefineAttempt) => {
4540
+ const startCommandExecutionSession = (isRefineAttempt, currentModels) => {
4385
4541
  const sessionId = ++commandExecutionRun;
4386
- const requestGuide = composeGuidance2(guideHint);
4387
- const apiPath = isRefineAttempt && options.target === "vcs-commit-message" ? "/v1/commit-message/refine" : TARGET_TO_API_PATH[options.target];
4542
+ const requestGuide = composeGuidance2(state.guideHint);
4543
+ const apiPath = isRefineAttempt ? "/v1/commit-message/refine" : TARGET_TO_API_PATH[options.target];
4388
4544
  const { commandExecutionPromise, abortController, cliSessionId } = startCommandExecution({
4389
4545
  api,
4390
4546
  installationId,
@@ -4392,7 +4548,7 @@ async function handleVcsCommitMessage(input, options, args2) {
4392
4548
  args: args2,
4393
4549
  apiPath,
4394
4550
  requestPayload: {
4395
- ...models.length === 1 ? { input, target: "vcs-commit-message", model: models[0] } : { input, target: "vcs-commit-message", models },
4551
+ ...currentModels.length === 1 ? { input, target: "vcs-commit-message", model: currentModels[0] } : { input, target: "vcs-commit-message", models: currentModels },
4396
4552
  ...requestGuide ? { guide: requestGuide } : {}
4397
4553
  }
4398
4554
  });
@@ -4402,7 +4558,7 @@ async function handleVcsCommitMessage(input, options, args2) {
4402
4558
  }
4403
4559
  abortController.abort(abortReasonForError(error));
4404
4560
  await handleCommandExecutionError(error, {
4405
- progress: { ready: 0, total: models.length }
4561
+ progress: { ready: 0, total: currentModels.length }
4406
4562
  });
4407
4563
  });
4408
4564
  return {
@@ -4412,32 +4568,40 @@ async function handleVcsCommitMessage(input, options, args2) {
4412
4568
  };
4413
4569
  };
4414
4570
  while (true) {
4415
- const isRefineAttempt = refineMessage !== void 0;
4416
- const { commandExecutionSignal, commandExecutionPromise, cliSessionId } = startCommandExecutionSession(isRefineAttempt);
4571
+ const isRefineAttempt = state.refineMessage !== void 0;
4572
+ const { commandExecutionSignal, commandExecutionPromise, cliSessionId } = startCommandExecutionSession(isRefineAttempt, state.models);
4417
4573
  const createCandidates = (signal) => generateCommitMessages({
4418
4574
  diff: input,
4419
- models,
4420
- guide: composeGuidance2(guideHint),
4575
+ models: state.models,
4576
+ guide: composeGuidance2(state.guideHint),
4421
4577
  signal: mergeAbortSignals(signal, commandExecutionSignal),
4422
4578
  cliSessionId,
4423
4579
  commandExecutionPromise,
4424
4580
  streamCaptureRecorder: captureRecorder,
4425
- refine: refineMessage !== void 0 ? {
4426
- originalMessage: refineMessage,
4427
- refineInstruction: guideHint
4581
+ refine: state.refineMessage !== void 0 ? {
4582
+ originalMessage: state.refineMessage,
4583
+ refineInstruction: state.guideHint
4428
4584
  } : void 0
4429
4585
  });
4430
4586
  const result = await selectCandidate({
4431
4587
  createCandidates,
4432
- maxSlots: models.length,
4588
+ maxSlots: state.models.length,
4433
4589
  abortSignal: commandExecutionSignal,
4434
- models,
4590
+ models: state.models,
4435
4591
  inlineEditPrompt: true,
4436
- initialGuideHint: guideHint
4592
+ initialGuideHint: state.guideHint,
4593
+ isEscalation: state.isEscalation,
4594
+ capabilities
4437
4595
  });
4438
- if (result.action === "abort") {
4439
- if (result.error instanceof InvalidModelError) {
4440
- exitWithInvalidModelError2(result.error);
4596
+ const transition = decideVcsCommitMessageSelection(
4597
+ state,
4598
+ result,
4599
+ escalationModels
4600
+ );
4601
+ state = transition.state;
4602
+ if (transition.kind === "abort") {
4603
+ if (transition.error instanceof InvalidModelError) {
4604
+ exitWithInvalidModelError2(transition.error);
4441
4605
  }
4442
4606
  if (isCommandExecutionAbort(commandExecutionSignal)) {
4443
4607
  return;
@@ -4445,14 +4609,16 @@ async function handleVcsCommitMessage(input, options, args2) {
4445
4609
  console.error("Aborted.");
4446
4610
  process.exit(1);
4447
4611
  }
4448
- if (result.action === "refine") {
4449
- guideHint = result.guide;
4450
- refineMessage = result.selected ?? result.selectedCandidate?.content;
4612
+ if (transition.kind === "escalate") {
4613
+ console.log(ui.hint(" -> Escalate"));
4451
4614
  continue;
4452
4615
  }
4453
- if (result.action === "confirm" && result.selected) {
4454
- await recordSelection2(result.selectedCandidate?.generationId);
4455
- console.log(result.selected);
4616
+ if (transition.kind === "refine") {
4617
+ continue;
4618
+ }
4619
+ if (transition.kind === "confirm") {
4620
+ await recordSelection2(transition.selectedCandidateGenerationId);
4621
+ console.log(transition.selected);
4456
4622
  return;
4457
4623
  }
4458
4624
  }
@@ -4642,7 +4808,7 @@ function parseArgs(args2) {
4642
4808
  // package.json
4643
4809
  var package_default = {
4644
4810
  name: "ultrahope",
4645
- version: "0.1.11",
4811
+ version: "0.1.12",
4646
4812
  description: "LLM-powered development workflow assistant",
4647
4813
  type: "module",
4648
4814
  license: "MIT",
@@ -4734,8 +4900,8 @@ Options:
4734
4900
  --help, -h Show this help message
4735
4901
 
4736
4902
  Plans:
4737
- Anonymous: 5 requests/day and 40,000 chars/request in the CLI without login
4738
- Pro: login required, paid usage, no anonymous limits`);
4903
+ Free: 5 requests/day and 40,000 chars/request in the CLI without login
4904
+ Pro: login required, paid usage, no Free plan limits`);
4739
4905
  }
4740
4906
  main().catch((err) => {
4741
4907
  console.error(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultrahope",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "LLM-powered development workflow assistant",
5
5
  "type": "module",
6
6
  "license": "MIT",