ultrahope 0.1.10 → 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,14 +12,19 @@ npm install -g ultrahope
12
12
 
13
13
  ### Login
14
14
 
15
- Authenticate with your ultrahope account using device flow:
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
+
17
+ When you want to keep going, authenticate with your Ultrahope account using device flow:
16
18
 
17
19
  ```bash
18
20
  ultrahope login
19
21
  ```
20
22
 
21
- This will display a URL and code. Open the URL in your browser, sign in, and enter the code to authorize the CLI.
22
- On first successful login, `${XDG_CONFIG_HOME:-~/.config}/ultrahope/config.toml` is created automatically if missing.
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
+
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.
23
28
 
24
29
  ### Translate
25
30
 
@@ -106,7 +111,7 @@ models = ["mistral/ministral-3b", "xai/grok-code-fast-1"]
106
111
 
107
112
  ### Credentials
108
113
 
109
- Credentials are stored in `~/.config/ultrahope/credentials.json`.
114
+ Credentials and the local installation ID are stored in `~/.config/ultrahope/credentials.json`.
110
115
 
111
116
  ## Development
112
117
 
@@ -36,7 +36,7 @@ function log(message, data) {
36
36
  // lib/api-client.ts
37
37
  var API_BASE_URL = process.env.ULTRAHOPE_API_URL ?? "https://ultrahope.dev";
38
38
  var InsufficientBalanceError = class extends Error {
39
- constructor(balance, plan = "free", hint, actions) {
39
+ constructor(balance, plan = "anonymous", hint, actions) {
40
40
  super("Token balance exhausted");
41
41
  this.balance = balance;
42
42
  this.plan = plan;
@@ -55,7 +55,7 @@ var InsufficientBalanceError = class extends Error {
55
55
  }
56
56
  } else {
57
57
  lines.push(
58
- "Error: Your free credit has been exhausted. Upgrade to Pro for unlimited requests with $5 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}`);
@@ -65,11 +65,12 @@ var InsufficientBalanceError = class extends Error {
65
65
  }
66
66
  };
67
67
  var DailyLimitExceededError = class extends Error {
68
- constructor(count, limit, resetsAt) {
68
+ constructor(count, limit, resetsAt, plan = "anonymous") {
69
69
  super("Daily request limit reached");
70
70
  this.count = count;
71
71
  this.limit = limit;
72
72
  this.resetsAt = resetsAt;
73
+ this.plan = plan;
73
74
  this.name = "DailyLimitExceededError";
74
75
  }
75
76
  };
@@ -79,6 +80,26 @@ var UnauthorizedError = class extends Error {
79
80
  this.name = "UnauthorizedError";
80
81
  }
81
82
  };
83
+ var SubscriptionRequiredError = class extends Error {
84
+ constructor(subscribeUrl, hint) {
85
+ super("Active Pro subscription required");
86
+ this.subscribeUrl = subscribeUrl;
87
+ this.hint = hint;
88
+ this.name = "SubscriptionRequiredError";
89
+ }
90
+ formatMessage() {
91
+ const lines = [
92
+ "Error: This signed-in account requires an active Pro subscription."
93
+ ];
94
+ if (this.hint) {
95
+ lines.push(` ${this.hint}`);
96
+ }
97
+ if (this.subscribeUrl) {
98
+ lines.push(` Subscribe: ${this.subscribeUrl}`);
99
+ }
100
+ return lines.join("\n");
101
+ }
102
+ };
82
103
  var InvalidModelError = class extends Error {
83
104
  constructor(model, allowedModels, message) {
84
105
  super(message ?? `Model '${model}' is not supported.`);
@@ -88,7 +109,7 @@ var InvalidModelError = class extends Error {
88
109
  }
89
110
  };
90
111
  var InputLengthExceededError = class extends Error {
91
- constructor(count, limit, plan = "free", message) {
112
+ constructor(count, limit, plan = "anonymous", message) {
92
113
  super(
93
114
  message ?? `Input length ${count} exceeds the ${plan} plan limit of ${limit} characters.`
94
115
  );
@@ -141,12 +162,19 @@ function parseSseEvents(buffer) {
141
162
  }
142
163
  function handle402Error(error) {
143
164
  const payload = error;
165
+ if (payload?.error === "subscription_required") {
166
+ log("generate error (402 subscription_required)", error);
167
+ throw new SubscriptionRequiredError(
168
+ payload.actions?.subscribe,
169
+ payload.hint
170
+ );
171
+ }
144
172
  if (typeof payload?.balance === "number") {
145
173
  log("generate error (402 insufficient_balance)", error);
146
- const plan = payload.plan === "pro" || payload.plan === "free" ? payload.plan : "free";
174
+ const plan2 = payload.plan === "pro" ? "pro" : "anonymous";
147
175
  throw new InsufficientBalanceError(
148
176
  payload.balance,
149
- plan,
177
+ plan2,
150
178
  payload.hint,
151
179
  payload.actions
152
180
  );
@@ -154,14 +182,15 @@ function handle402Error(error) {
154
182
  const count = typeof payload?.count === "number" ? payload.count : 0;
155
183
  const limit = typeof payload?.limit === "number" ? payload.limit : 0;
156
184
  const resetsAt = payload?.resetsAt ?? "";
185
+ const plan = payload?.plan === "pro" ? "pro" : "anonymous";
157
186
  log("generate error (402 daily_limit)", error);
158
- throw new DailyLimitExceededError(count, limit, resetsAt);
187
+ throw new DailyLimitExceededError(count, limit, resetsAt, plan);
159
188
  }
160
189
  function throwInputLengthExceededError(error) {
161
190
  const payload = error;
162
191
  const count = typeof payload?.count === "number" ? payload.count : 0;
163
192
  const limit = typeof payload?.limit === "number" ? payload.limit : 0;
164
- const plan = payload?.plan === "free" ? payload.plan : "free";
193
+ const plan = payload?.plan === "anonymous" ? "anonymous" : "anonymous";
165
194
  const message = typeof payload?.message === "string" ? payload.message : `Input length ${count} exceeds the ${plan} plan limit of ${limit} characters.`;
166
195
  log("generate error (400 input_too_long)", error);
167
196
  throw new InputLengthExceededError(count, limit, plan, message);
@@ -185,25 +214,27 @@ function handle400Error(error) {
185
214
  throwInvalidModelError(error);
186
215
  }
187
216
  function createApiClient(token) {
188
- const headers = {
189
- "Content-Type": "application/json"
190
- };
217
+ const authHeaders = {};
191
218
  if (token) {
192
- headers.Authorization = `Bearer ${token}`;
219
+ authHeaders.Authorization = `Bearer ${token}`;
220
+ }
221
+ function jsonHeaders(extra) {
222
+ return {
223
+ ...authHeaders,
224
+ "Content-Type": "application/json",
225
+ ...extra
226
+ };
193
227
  }
194
228
  const client = createClient({
195
229
  baseUrl: API_BASE_URL,
196
- headers
230
+ headers: authHeaders
197
231
  });
198
232
  return {
199
233
  async *streamCommitMessage(req, options) {
200
234
  log("streamCommitMessage request", req);
201
235
  const res = await fetch(`${API_BASE_URL}/api/v1/commit-message/stream`, {
202
236
  method: "POST",
203
- headers: {
204
- ...headers,
205
- Accept: "text/event-stream"
206
- },
237
+ headers: jsonHeaders({ Accept: "text/event-stream" }),
207
238
  body: JSON.stringify(req),
208
239
  signal: options?.signal
209
240
  });
@@ -269,10 +300,7 @@ function createApiClient(token) {
269
300
  `${API_BASE_URL}/api/v1/commit-message/refine/stream`,
270
301
  {
271
302
  method: "POST",
272
- headers: {
273
- ...headers,
274
- Accept: "text/event-stream"
275
- },
303
+ headers: jsonHeaders({ Accept: "text/event-stream" }),
276
304
  body: JSON.stringify(req),
277
305
  signal: options?.signal
278
306
  }
@@ -337,7 +365,7 @@ function createApiClient(token) {
337
365
  log("generation_score request", req);
338
366
  const res = await fetch(`${API_BASE_URL}/api/v1/generation_score`, {
339
367
  method: "POST",
340
- headers,
368
+ headers: jsonHeaders(),
341
369
  body: JSON.stringify(req)
342
370
  });
343
371
  if (!res.ok) {
@@ -376,6 +404,23 @@ function createApiClient(token) {
376
404
  log("command_execution response", data);
377
405
  return data;
378
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
+ },
379
424
  async generateCommitMessage(req, options) {
380
425
  log("generateCommitMessage request", req);
381
426
  const { data, error, response } = await client.POST(
@@ -490,7 +535,7 @@ function createApiClient(token) {
490
535
  async requestDeviceCode() {
491
536
  const res = await fetch(`${API_BASE_URL}/api/auth/device/code`, {
492
537
  method: "POST",
493
- headers,
538
+ headers: jsonHeaders(),
494
539
  body: JSON.stringify({ client_id: "ultrahope-cli" })
495
540
  });
496
541
  if (!res.ok) {
@@ -502,7 +547,7 @@ function createApiClient(token) {
502
547
  async pollDeviceToken(deviceCode) {
503
548
  const res = await fetch(`${API_BASE_URL}/api/auth/device/token`, {
504
549
  method: "POST",
505
- headers,
550
+ headers: jsonHeaders(),
506
551
  body: JSON.stringify({
507
552
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
508
553
  device_code: deviceCode,
@@ -514,6 +559,32 @@ function createApiClient(token) {
514
559
  throw new Error(`API error: ${res.status} ${text}`);
515
560
  }
516
561
  return res.json();
562
+ },
563
+ async signInAnonymous() {
564
+ const res = await fetch(`${API_BASE_URL}/api/auth/sign-in/anonymous`, {
565
+ method: "POST",
566
+ headers: jsonHeaders(),
567
+ body: JSON.stringify({})
568
+ });
569
+ if (!res.ok) {
570
+ const text = await res.text();
571
+ throw new Error(`API error: ${res.status} ${text}`);
572
+ }
573
+ return res.json();
574
+ },
575
+ async deleteAnonymousUser() {
576
+ const res = await fetch(
577
+ `${API_BASE_URL}/api/auth/delete-anonymous-user`,
578
+ {
579
+ method: "POST",
580
+ headers: jsonHeaders(),
581
+ body: JSON.stringify({})
582
+ }
583
+ );
584
+ if (!res.ok) {
585
+ const text = await res.text();
586
+ throw new Error(`API error: ${res.status} ${text}`);
587
+ }
517
588
  }
518
589
  };
519
590
  }
@@ -565,6 +636,7 @@ function abortReasonForError(error) {
565
636
  }
566
637
 
567
638
  // lib/auth.ts
639
+ import { randomUUID } from "crypto";
568
640
  import * as fs from "fs";
569
641
  import * as os from "os";
570
642
  import * as path from "path";
@@ -574,19 +646,74 @@ function getCredentialsPath() {
574
646
  const filename = env && env !== "production" ? `credentials.${env}.json` : "credentials.json";
575
647
  return path.join(configDir, "ultrahope", filename);
576
648
  }
577
- async function getToken() {
649
+ async function getCredentials() {
578
650
  const credPath = getCredentialsPath();
579
651
  try {
580
652
  const content = await fs.promises.readFile(credPath, "utf-8");
581
653
  const creds = JSON.parse(content);
582
- return creds.access_token ?? null;
654
+ if (typeof creds.access_token !== "string" || creds.access_token.length === 0) {
655
+ return null;
656
+ }
657
+ return {
658
+ accessToken: creds.access_token,
659
+ authKind: creds.auth_kind === "anonymous" ? "anonymous" : "authenticated",
660
+ installationId: await ensureInstallationId(creds)
661
+ };
583
662
  } catch {
584
663
  return null;
585
664
  }
586
665
  }
666
+ async function writeCredentials(creds) {
667
+ const credPath = getCredentialsPath();
668
+ const dir = path.dirname(credPath);
669
+ await fs.promises.mkdir(dir, { recursive: true });
670
+ await fs.promises.writeFile(credPath, JSON.stringify(creds, null, 2), {
671
+ mode: 384
672
+ });
673
+ }
674
+ async function ensureInstallationId(creds) {
675
+ if (creds?.installation_id && creds.installation_id.length > 0) {
676
+ return creds.installation_id;
677
+ }
678
+ const installationId = randomUUID();
679
+ await writeCredentials({
680
+ access_token: creds?.access_token ?? "",
681
+ auth_kind: creds?.auth_kind,
682
+ installation_id: installationId
683
+ });
684
+ return installationId;
685
+ }
686
+ async function getInstallationId() {
687
+ const credPath = getCredentialsPath();
688
+ try {
689
+ const content = await fs.promises.readFile(credPath, "utf-8");
690
+ const creds = JSON.parse(content);
691
+ return await ensureInstallationId(creds);
692
+ } catch {
693
+ return await ensureInstallationId(null);
694
+ }
695
+ }
696
+ async function getToken() {
697
+ const existing = await getCredentials();
698
+ if (existing) {
699
+ return existing.accessToken;
700
+ }
701
+ const api = createApiClient();
702
+ const anonymousSession = await api.signInAnonymous();
703
+ await saveToken(anonymousSession.token, "anonymous");
704
+ return anonymousSession.token;
705
+ }
706
+ async function saveToken(token, authKind = "authenticated") {
707
+ const installationId = await getInstallationId();
708
+ await writeCredentials({
709
+ access_token: token,
710
+ auth_kind: authKind,
711
+ installation_id: installationId
712
+ });
713
+ }
587
714
 
588
715
  // lib/command-execution.ts
589
- import { randomUUID } from "crypto";
716
+ import { randomUUID as randomUUID2 } from "crypto";
590
717
 
591
718
  // lib/daily-limit-prompt.ts
592
719
  import * as readline from "readline";
@@ -730,9 +857,7 @@ async function showDailyLimitPrompt(info) {
730
857
  );
731
858
  }
732
859
  console.log("");
733
- console.log(
734
- `${theme.primary}Commit message generation was skipped${theme.reset}`
735
- );
860
+ console.log(`${theme.primary}Generation was skipped${theme.reset}`);
736
861
  console.log("");
737
862
  console.log(
738
863
  ui.bullet(`Daily request limit reached (${info.count} / ${info.limit})`)
@@ -745,7 +870,7 @@ async function showDailyLimitPrompt(info) {
745
870
  );
746
871
  console.log(` ${ui.link("ultrahope jj describe")}`);
747
872
  console.log("");
748
- console.log(`${theme.primary}Or upgrade your plan:${theme.reset}`);
873
+ console.log(`${theme.primary}Or upgrade to Pro:${theme.reset}`);
749
874
  console.log(` ${ui.link(PRICING_URL)}`);
750
875
  return;
751
876
  }
@@ -755,7 +880,7 @@ async function showDailyLimitPrompt(info) {
755
880
  `${theme.secondary} 1) Retry after the daily limit resets${theme.reset}`
756
881
  );
757
882
  console.log(
758
- `${theme.secondary} 2) Upgrade your plan to continue immediately${theme.reset}`
883
+ `${theme.secondary} 2) Upgrade to Pro to continue immediately${theme.reset}`
759
884
  );
760
885
  console.log("");
761
886
  const choice = await promptChoice();
@@ -883,12 +1008,13 @@ async function handleUpgrade() {
883
1008
 
884
1009
  // lib/command-execution.ts
885
1010
  function startCommandExecution(options) {
886
- const commandExecutionId = randomUUID();
1011
+ const commandExecutionId = randomUUID2();
887
1012
  const cliSessionId = commandExecutionId;
888
1013
  const abortController = new AbortController();
889
1014
  const commandExecutionPromise = options.api.commandExecution({
890
1015
  commandExecutionId,
891
1016
  cliSessionId,
1017
+ installationId: options.installationId,
892
1018
  command: options.command,
893
1019
  args: options.args,
894
1020
  api: options.apiPath,
@@ -933,8 +1059,12 @@ async function handleCommandExecutionError(error, options) {
933
1059
  console.error(error.formatMessage());
934
1060
  process.exit(1);
935
1061
  }
1062
+ if (error instanceof SubscriptionRequiredError) {
1063
+ console.error(error.formatMessage());
1064
+ process.exit(1);
1065
+ }
936
1066
  if (error instanceof InputLengthExceededError) {
937
- console.error("\x1B[31m\u2716\x1B[0m Input is too long for the Free plan.");
1067
+ console.error("\x1B[31m\u2716\x1B[0m Input is too long for anonymous usage.");
938
1068
  console.error(
939
1069
  ` Max allowed characters: ${error.limit}. Received: ${error.count}.`
940
1070
  );
@@ -1064,10 +1194,7 @@ async function* generateCommitMessages(options) {
1064
1194
  models
1065
1195
  });
1066
1196
  const token = await getToken();
1067
- if (!token) {
1068
- console.error("Error: Not authenticated. Run `ultrahope login` first.");
1069
- process.exit(1);
1070
- }
1197
+ const installationId = await getInstallationId();
1071
1198
  const api = createApiClient(token);
1072
1199
  const generateWithRetry = async function* (payload) {
1073
1200
  const maxAttempts = 3;
@@ -1077,6 +1204,7 @@ async function* generateCommitMessages(options) {
1077
1204
  const stream = options.refine ? api.streamCommitMessageRefine(
1078
1205
  {
1079
1206
  cliSessionId,
1207
+ installationId,
1080
1208
  model: payload.model,
1081
1209
  originalMessage: options.refine.originalMessage,
1082
1210
  refineInstruction: options.refine.refineInstruction
@@ -1085,6 +1213,7 @@ async function* generateCommitMessages(options) {
1085
1213
  ) : api.streamCommitMessage(
1086
1214
  {
1087
1215
  ...payload,
1216
+ installationId,
1088
1217
  input: diff,
1089
1218
  guide: options.guide
1090
1219
  },
@@ -1378,6 +1507,77 @@ function formatDiffStats(stats) {
1378
1507
  return parts.join(", ");
1379
1508
  }
1380
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
+
1381
1581
  // lib/renderer.ts
1382
1582
  import * as readline2 from "readline";
1383
1583
 
@@ -2955,6 +3155,12 @@ var selectorRenderCapabilities = {
2955
3155
  escalate: true,
2956
3156
  clickConfirm: false
2957
3157
  };
3158
+ function resolveSelectorCapabilities(options) {
3159
+ return {
3160
+ ...selectorRenderCapabilities,
3161
+ escalate: options?.escalate ?? selectorRenderCapabilities.escalate
3162
+ };
3163
+ }
2958
3164
  function renderError(error, slotsLength, output) {
2959
3165
  const readyCount = slotsLength;
2960
3166
  const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
@@ -3103,9 +3309,13 @@ async function selectCandidate(options) {
3103
3309
  let generationRun = 0;
3104
3310
  let generationController = null;
3105
3311
  let isPromptOpen = false;
3312
+ let dynamicCapabilities = resolveSelectorCapabilities(options.capabilities);
3106
3313
  const ttyReader = ttyInput;
3107
3314
  const ttyWriter = ttyOutput;
3108
3315
  const renderer = createRenderer(ttyWriter);
3316
+ const updateDynamicCapabilities = () => {
3317
+ dynamicCapabilities = resolveSelectorCapabilities(options.capabilities);
3318
+ };
3109
3319
  const setRawModeSafe2 = (enabled) => {
3110
3320
  try {
3111
3321
  const r = ttyReader;
@@ -3117,6 +3327,7 @@ async function selectCandidate(options) {
3117
3327
  setRawModeSafe2(true);
3118
3328
  ttyReader.resume();
3119
3329
  const render = () => {
3330
+ updateDynamicCapabilities();
3120
3331
  const allowPromptRender = context.mode === "prompt" && context.promptKind === "edit";
3121
3332
  if (!cleanedUp && (!isPromptOpen || allowPromptRender)) {
3122
3333
  const frame = selectorRenderFrame({
@@ -3129,7 +3340,7 @@ async function selectCandidate(options) {
3129
3340
  nowMs: Date.now(),
3130
3341
  spinnerFrames: SPINNER_FRAMES,
3131
3342
  copy: renderCopy,
3132
- capabilities: selectorRenderCapabilities
3343
+ capabilities: dynamicCapabilities
3133
3344
  });
3134
3345
  renderer.render(renderSelectorTextFromRenderFrame(frame));
3135
3346
  }
@@ -3145,7 +3356,7 @@ async function selectCandidate(options) {
3145
3356
  nowMs: Date.now(),
3146
3357
  spinnerFrames: SPINNER_FRAMES,
3147
3358
  copy: renderCopy,
3148
- capabilities: selectorRenderCapabilities
3359
+ capabilities: dynamicCapabilities
3149
3360
  });
3150
3361
  const selected = result.selectedCandidate?.content ?? result.selected ?? "";
3151
3362
  const selectedTitle = normalizeCandidateContentForDisplay(selected) || selected;
@@ -3270,7 +3481,7 @@ async function selectCandidate(options) {
3270
3481
  nowMs: Date.now(),
3271
3482
  spinnerFrames: SPINNER_FRAMES,
3272
3483
  copy: renderCopy,
3273
- capabilities: selectorRenderCapabilities
3484
+ capabilities: dynamicCapabilities
3274
3485
  });
3275
3486
  const costSuffix = frame.viewModel.header.totalCostLabel ? ` (total: ${frame.viewModel.header.totalCostLabel})` : "";
3276
3487
  const generatedLine = `${frame.viewModel.header.generatedLabel}${costSuffix}`;
@@ -3391,7 +3602,7 @@ async function selectCandidate(options) {
3391
3602
  nowMs: Date.now(),
3392
3603
  spinnerFrames: SPINNER_FRAMES,
3393
3604
  copy: renderCopy,
3394
- capabilities: selectorRenderCapabilities,
3605
+ capabilities: dynamicCapabilities,
3395
3606
  bufferText: buffer.getText()
3396
3607
  });
3397
3608
  const prompt = frame.prompt;
@@ -3467,7 +3678,7 @@ async function selectCandidate(options) {
3467
3678
  nowMs: Date.now(),
3468
3679
  spinnerFrames: SPINNER_FRAMES,
3469
3680
  copy: renderCopy,
3470
- capabilities: selectorRenderCapabilities,
3681
+ capabilities: dynamicCapabilities,
3471
3682
  bufferText: buffer.getText()
3472
3683
  });
3473
3684
  const prompt = frame.prompt;
@@ -3551,6 +3762,8 @@ async function selectCandidate(options) {
3551
3762
  }
3552
3763
  if (key.name === "e" && key.shift) {
3553
3764
  if (!hasReadySlot(context.slots)) return;
3765
+ updateDynamicCapabilities();
3766
+ if (!dynamicCapabilities.escalate) return;
3554
3767
  applyResult(transitionSelectorFlow(context, { type: "ESCALATE" }));
3555
3768
  return;
3556
3769
  }
@@ -3881,12 +4094,12 @@ async function commit(args2) {
3881
4094
  process.exit(1);
3882
4095
  }
3883
4096
  try {
4097
+ const existingCredentials = await getCredentials();
4098
+ const authKind = existingCredentials?.authKind ?? "anonymous";
3884
4099
  const token = await getToken();
3885
- if (!token) {
3886
- console.error("Error: Not authenticated. Run `ultrahope login` first.");
3887
- process.exit(1);
3888
- }
4100
+ const installationId = await getInstallationId();
3889
4101
  const api = createApiClient(token);
4102
+ const capabilities = await resolveEntitlementCapability(api, authKind);
3890
4103
  const apiClient = api;
3891
4104
  let guideHint;
3892
4105
  let refineMessage;
@@ -3912,6 +4125,7 @@ async function commit(args2) {
3912
4125
  const apiPath = isRefineAttempt ? "/v1/commit-message/refine" : "/v1/commit-message";
3913
4126
  const { commandExecutionPromise, abortController, cliSessionId } = startCommandExecution({
3914
4127
  api,
4128
+ installationId,
3915
4129
  command: "commit",
3916
4130
  args: args2,
3917
4131
  apiPath,
@@ -3963,7 +4177,8 @@ async function commit(args2) {
3963
4177
  models,
3964
4178
  inlineEditPrompt: true,
3965
4179
  initialGuideHint: guideHint,
3966
- isEscalation
4180
+ isEscalation,
4181
+ capabilities
3967
4182
  });
3968
4183
  if (result.action === "abort") {
3969
4184
  if (result.error instanceof InvalidModelError) {