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 +9 -4
- package/dist/git-ultrahope.js +263 -48
- package/dist/index.js +376 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,14 +12,19 @@ npm install -g ultrahope
|
|
|
12
12
|
|
|
13
13
|
### Login
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/git-ultrahope.js
CHANGED
|
@@ -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 = "
|
|
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:
|
|
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 = "
|
|
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
|
|
174
|
+
const plan2 = payload.plan === "pro" ? "pro" : "anonymous";
|
|
147
175
|
throw new InsufficientBalanceError(
|
|
148
176
|
payload.balance,
|
|
149
|
-
|
|
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 === "
|
|
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
|
|
189
|
-
"Content-Type": "application/json"
|
|
190
|
-
};
|
|
217
|
+
const authHeaders = {};
|
|
191
218
|
if (token) {
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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) {
|