ultrahope 0.1.10 → 0.1.11
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 +5 -4
- package/dist/git-ultrahope.js +152 -42
- package/dist/index.js +175 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,14 +12,15 @@ 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 Anonymous 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.
|
|
23
24
|
|
|
24
25
|
### Translate
|
|
25
26
|
|
|
@@ -106,7 +107,7 @@ models = ["mistral/ministral-3b", "xai/grok-code-fast-1"]
|
|
|
106
107
|
|
|
107
108
|
### Credentials
|
|
108
109
|
|
|
109
|
-
Credentials are stored in `~/.config/ultrahope/credentials.json`.
|
|
110
|
+
Credentials and the local installation ID are stored in `~/.config/ultrahope/credentials.json`.
|
|
110
111
|
|
|
111
112
|
## Development
|
|
112
113
|
|
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: Anonymous 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) {
|
|
@@ -490,7 +518,7 @@ function createApiClient(token) {
|
|
|
490
518
|
async requestDeviceCode() {
|
|
491
519
|
const res = await fetch(`${API_BASE_URL}/api/auth/device/code`, {
|
|
492
520
|
method: "POST",
|
|
493
|
-
headers,
|
|
521
|
+
headers: jsonHeaders(),
|
|
494
522
|
body: JSON.stringify({ client_id: "ultrahope-cli" })
|
|
495
523
|
});
|
|
496
524
|
if (!res.ok) {
|
|
@@ -502,7 +530,7 @@ function createApiClient(token) {
|
|
|
502
530
|
async pollDeviceToken(deviceCode) {
|
|
503
531
|
const res = await fetch(`${API_BASE_URL}/api/auth/device/token`, {
|
|
504
532
|
method: "POST",
|
|
505
|
-
headers,
|
|
533
|
+
headers: jsonHeaders(),
|
|
506
534
|
body: JSON.stringify({
|
|
507
535
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
508
536
|
device_code: deviceCode,
|
|
@@ -514,6 +542,32 @@ function createApiClient(token) {
|
|
|
514
542
|
throw new Error(`API error: ${res.status} ${text}`);
|
|
515
543
|
}
|
|
516
544
|
return res.json();
|
|
545
|
+
},
|
|
546
|
+
async signInAnonymous() {
|
|
547
|
+
const res = await fetch(`${API_BASE_URL}/api/auth/sign-in/anonymous`, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers: jsonHeaders(),
|
|
550
|
+
body: JSON.stringify({})
|
|
551
|
+
});
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
const text = await res.text();
|
|
554
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
555
|
+
}
|
|
556
|
+
return res.json();
|
|
557
|
+
},
|
|
558
|
+
async deleteAnonymousUser() {
|
|
559
|
+
const res = await fetch(
|
|
560
|
+
`${API_BASE_URL}/api/auth/delete-anonymous-user`,
|
|
561
|
+
{
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: jsonHeaders(),
|
|
564
|
+
body: JSON.stringify({})
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
if (!res.ok) {
|
|
568
|
+
const text = await res.text();
|
|
569
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
570
|
+
}
|
|
517
571
|
}
|
|
518
572
|
};
|
|
519
573
|
}
|
|
@@ -565,6 +619,7 @@ function abortReasonForError(error) {
|
|
|
565
619
|
}
|
|
566
620
|
|
|
567
621
|
// lib/auth.ts
|
|
622
|
+
import { randomUUID } from "crypto";
|
|
568
623
|
import * as fs from "fs";
|
|
569
624
|
import * as os from "os";
|
|
570
625
|
import * as path from "path";
|
|
@@ -574,19 +629,74 @@ function getCredentialsPath() {
|
|
|
574
629
|
const filename = env && env !== "production" ? `credentials.${env}.json` : "credentials.json";
|
|
575
630
|
return path.join(configDir, "ultrahope", filename);
|
|
576
631
|
}
|
|
577
|
-
async function
|
|
632
|
+
async function getCredentials() {
|
|
578
633
|
const credPath = getCredentialsPath();
|
|
579
634
|
try {
|
|
580
635
|
const content = await fs.promises.readFile(credPath, "utf-8");
|
|
581
636
|
const creds = JSON.parse(content);
|
|
582
|
-
|
|
637
|
+
if (typeof creds.access_token !== "string" || creds.access_token.length === 0) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
accessToken: creds.access_token,
|
|
642
|
+
authKind: creds.auth_kind === "anonymous" ? "anonymous" : "authenticated",
|
|
643
|
+
installationId: await ensureInstallationId(creds)
|
|
644
|
+
};
|
|
583
645
|
} catch {
|
|
584
646
|
return null;
|
|
585
647
|
}
|
|
586
648
|
}
|
|
649
|
+
async function writeCredentials(creds) {
|
|
650
|
+
const credPath = getCredentialsPath();
|
|
651
|
+
const dir = path.dirname(credPath);
|
|
652
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
653
|
+
await fs.promises.writeFile(credPath, JSON.stringify(creds, null, 2), {
|
|
654
|
+
mode: 384
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async function ensureInstallationId(creds) {
|
|
658
|
+
if (creds?.installation_id && creds.installation_id.length > 0) {
|
|
659
|
+
return creds.installation_id;
|
|
660
|
+
}
|
|
661
|
+
const installationId = randomUUID();
|
|
662
|
+
await writeCredentials({
|
|
663
|
+
access_token: creds?.access_token ?? "",
|
|
664
|
+
auth_kind: creds?.auth_kind,
|
|
665
|
+
installation_id: installationId
|
|
666
|
+
});
|
|
667
|
+
return installationId;
|
|
668
|
+
}
|
|
669
|
+
async function getInstallationId() {
|
|
670
|
+
const credPath = getCredentialsPath();
|
|
671
|
+
try {
|
|
672
|
+
const content = await fs.promises.readFile(credPath, "utf-8");
|
|
673
|
+
const creds = JSON.parse(content);
|
|
674
|
+
return await ensureInstallationId(creds);
|
|
675
|
+
} catch {
|
|
676
|
+
return await ensureInstallationId(null);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function getToken() {
|
|
680
|
+
const existing = await getCredentials();
|
|
681
|
+
if (existing) {
|
|
682
|
+
return existing.accessToken;
|
|
683
|
+
}
|
|
684
|
+
const api = createApiClient();
|
|
685
|
+
const anonymousSession = await api.signInAnonymous();
|
|
686
|
+
await saveToken(anonymousSession.token, "anonymous");
|
|
687
|
+
return anonymousSession.token;
|
|
688
|
+
}
|
|
689
|
+
async function saveToken(token, authKind = "authenticated") {
|
|
690
|
+
const installationId = await getInstallationId();
|
|
691
|
+
await writeCredentials({
|
|
692
|
+
access_token: token,
|
|
693
|
+
auth_kind: authKind,
|
|
694
|
+
installation_id: installationId
|
|
695
|
+
});
|
|
696
|
+
}
|
|
587
697
|
|
|
588
698
|
// lib/command-execution.ts
|
|
589
|
-
import { randomUUID } from "crypto";
|
|
699
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
590
700
|
|
|
591
701
|
// lib/daily-limit-prompt.ts
|
|
592
702
|
import * as readline from "readline";
|
|
@@ -730,9 +840,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
730
840
|
);
|
|
731
841
|
}
|
|
732
842
|
console.log("");
|
|
733
|
-
console.log(
|
|
734
|
-
`${theme.primary}Commit message generation was skipped${theme.reset}`
|
|
735
|
-
);
|
|
843
|
+
console.log(`${theme.primary}Generation was skipped${theme.reset}`);
|
|
736
844
|
console.log("");
|
|
737
845
|
console.log(
|
|
738
846
|
ui.bullet(`Daily request limit reached (${info.count} / ${info.limit})`)
|
|
@@ -745,7 +853,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
745
853
|
);
|
|
746
854
|
console.log(` ${ui.link("ultrahope jj describe")}`);
|
|
747
855
|
console.log("");
|
|
748
|
-
console.log(`${theme.primary}Or upgrade
|
|
856
|
+
console.log(`${theme.primary}Or upgrade to Pro:${theme.reset}`);
|
|
749
857
|
console.log(` ${ui.link(PRICING_URL)}`);
|
|
750
858
|
return;
|
|
751
859
|
}
|
|
@@ -755,7 +863,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
755
863
|
`${theme.secondary} 1) Retry after the daily limit resets${theme.reset}`
|
|
756
864
|
);
|
|
757
865
|
console.log(
|
|
758
|
-
`${theme.secondary} 2) Upgrade
|
|
866
|
+
`${theme.secondary} 2) Upgrade to Pro to continue immediately${theme.reset}`
|
|
759
867
|
);
|
|
760
868
|
console.log("");
|
|
761
869
|
const choice = await promptChoice();
|
|
@@ -883,12 +991,13 @@ async function handleUpgrade() {
|
|
|
883
991
|
|
|
884
992
|
// lib/command-execution.ts
|
|
885
993
|
function startCommandExecution(options) {
|
|
886
|
-
const commandExecutionId =
|
|
994
|
+
const commandExecutionId = randomUUID2();
|
|
887
995
|
const cliSessionId = commandExecutionId;
|
|
888
996
|
const abortController = new AbortController();
|
|
889
997
|
const commandExecutionPromise = options.api.commandExecution({
|
|
890
998
|
commandExecutionId,
|
|
891
999
|
cliSessionId,
|
|
1000
|
+
installationId: options.installationId,
|
|
892
1001
|
command: options.command,
|
|
893
1002
|
args: options.args,
|
|
894
1003
|
api: options.apiPath,
|
|
@@ -933,8 +1042,12 @@ async function handleCommandExecutionError(error, options) {
|
|
|
933
1042
|
console.error(error.formatMessage());
|
|
934
1043
|
process.exit(1);
|
|
935
1044
|
}
|
|
1045
|
+
if (error instanceof SubscriptionRequiredError) {
|
|
1046
|
+
console.error(error.formatMessage());
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
936
1049
|
if (error instanceof InputLengthExceededError) {
|
|
937
|
-
console.error("\x1B[31m\u2716\x1B[0m Input is too long for
|
|
1050
|
+
console.error("\x1B[31m\u2716\x1B[0m Input is too long for anonymous usage.");
|
|
938
1051
|
console.error(
|
|
939
1052
|
` Max allowed characters: ${error.limit}. Received: ${error.count}.`
|
|
940
1053
|
);
|
|
@@ -1064,10 +1177,7 @@ async function* generateCommitMessages(options) {
|
|
|
1064
1177
|
models
|
|
1065
1178
|
});
|
|
1066
1179
|
const token = await getToken();
|
|
1067
|
-
|
|
1068
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
1069
|
-
process.exit(1);
|
|
1070
|
-
}
|
|
1180
|
+
const installationId = await getInstallationId();
|
|
1071
1181
|
const api = createApiClient(token);
|
|
1072
1182
|
const generateWithRetry = async function* (payload) {
|
|
1073
1183
|
const maxAttempts = 3;
|
|
@@ -1077,6 +1187,7 @@ async function* generateCommitMessages(options) {
|
|
|
1077
1187
|
const stream = options.refine ? api.streamCommitMessageRefine(
|
|
1078
1188
|
{
|
|
1079
1189
|
cliSessionId,
|
|
1190
|
+
installationId,
|
|
1080
1191
|
model: payload.model,
|
|
1081
1192
|
originalMessage: options.refine.originalMessage,
|
|
1082
1193
|
refineInstruction: options.refine.refineInstruction
|
|
@@ -1085,6 +1196,7 @@ async function* generateCommitMessages(options) {
|
|
|
1085
1196
|
) : api.streamCommitMessage(
|
|
1086
1197
|
{
|
|
1087
1198
|
...payload,
|
|
1199
|
+
installationId,
|
|
1088
1200
|
input: diff,
|
|
1089
1201
|
guide: options.guide
|
|
1090
1202
|
},
|
|
@@ -3882,10 +3994,7 @@ async function commit(args2) {
|
|
|
3882
3994
|
}
|
|
3883
3995
|
try {
|
|
3884
3996
|
const token = await getToken();
|
|
3885
|
-
|
|
3886
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
3887
|
-
process.exit(1);
|
|
3888
|
-
}
|
|
3997
|
+
const installationId = await getInstallationId();
|
|
3889
3998
|
const api = createApiClient(token);
|
|
3890
3999
|
const apiClient = api;
|
|
3891
4000
|
let guideHint;
|
|
@@ -3912,6 +4021,7 @@ async function commit(args2) {
|
|
|
3912
4021
|
const apiPath = isRefineAttempt ? "/v1/commit-message/refine" : "/v1/commit-message";
|
|
3913
4022
|
const { commandExecutionPromise, abortController, cliSessionId } = startCommandExecution({
|
|
3914
4023
|
api,
|
|
4024
|
+
installationId,
|
|
3915
4025
|
command: "commit",
|
|
3916
4026
|
args: args2,
|
|
3917
4027
|
apiPath,
|
package/dist/index.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: Anonymous 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) {
|
|
@@ -490,7 +518,7 @@ function createApiClient(token) {
|
|
|
490
518
|
async requestDeviceCode() {
|
|
491
519
|
const res = await fetch(`${API_BASE_URL}/api/auth/device/code`, {
|
|
492
520
|
method: "POST",
|
|
493
|
-
headers,
|
|
521
|
+
headers: jsonHeaders(),
|
|
494
522
|
body: JSON.stringify({ client_id: "ultrahope-cli" })
|
|
495
523
|
});
|
|
496
524
|
if (!res.ok) {
|
|
@@ -502,7 +530,7 @@ function createApiClient(token) {
|
|
|
502
530
|
async pollDeviceToken(deviceCode) {
|
|
503
531
|
const res = await fetch(`${API_BASE_URL}/api/auth/device/token`, {
|
|
504
532
|
method: "POST",
|
|
505
|
-
headers,
|
|
533
|
+
headers: jsonHeaders(),
|
|
506
534
|
body: JSON.stringify({
|
|
507
535
|
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
508
536
|
device_code: deviceCode,
|
|
@@ -514,6 +542,32 @@ function createApiClient(token) {
|
|
|
514
542
|
throw new Error(`API error: ${res.status} ${text}`);
|
|
515
543
|
}
|
|
516
544
|
return res.json();
|
|
545
|
+
},
|
|
546
|
+
async signInAnonymous() {
|
|
547
|
+
const res = await fetch(`${API_BASE_URL}/api/auth/sign-in/anonymous`, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers: jsonHeaders(),
|
|
550
|
+
body: JSON.stringify({})
|
|
551
|
+
});
|
|
552
|
+
if (!res.ok) {
|
|
553
|
+
const text = await res.text();
|
|
554
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
555
|
+
}
|
|
556
|
+
return res.json();
|
|
557
|
+
},
|
|
558
|
+
async deleteAnonymousUser() {
|
|
559
|
+
const res = await fetch(
|
|
560
|
+
`${API_BASE_URL}/api/auth/delete-anonymous-user`,
|
|
561
|
+
{
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: jsonHeaders(),
|
|
564
|
+
body: JSON.stringify({})
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
if (!res.ok) {
|
|
568
|
+
const text = await res.text();
|
|
569
|
+
throw new Error(`API error: ${res.status} ${text}`);
|
|
570
|
+
}
|
|
517
571
|
}
|
|
518
572
|
};
|
|
519
573
|
}
|
|
@@ -565,6 +619,7 @@ function abortReasonForError(error) {
|
|
|
565
619
|
}
|
|
566
620
|
|
|
567
621
|
// lib/auth.ts
|
|
622
|
+
import { randomUUID } from "crypto";
|
|
568
623
|
import * as fs from "fs";
|
|
569
624
|
import * as os from "os";
|
|
570
625
|
import * as path from "path";
|
|
@@ -574,29 +629,74 @@ function getCredentialsPath() {
|
|
|
574
629
|
const filename = env && env !== "production" ? `credentials.${env}.json` : "credentials.json";
|
|
575
630
|
return path.join(configDir, "ultrahope", filename);
|
|
576
631
|
}
|
|
577
|
-
async function
|
|
632
|
+
async function getCredentials() {
|
|
578
633
|
const credPath = getCredentialsPath();
|
|
579
634
|
try {
|
|
580
635
|
const content = await fs.promises.readFile(credPath, "utf-8");
|
|
581
636
|
const creds = JSON.parse(content);
|
|
582
|
-
|
|
637
|
+
if (typeof creds.access_token !== "string" || creds.access_token.length === 0) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
accessToken: creds.access_token,
|
|
642
|
+
authKind: creds.auth_kind === "anonymous" ? "anonymous" : "authenticated",
|
|
643
|
+
installationId: await ensureInstallationId(creds)
|
|
644
|
+
};
|
|
583
645
|
} catch {
|
|
584
646
|
return null;
|
|
585
647
|
}
|
|
586
648
|
}
|
|
587
|
-
async function
|
|
649
|
+
async function writeCredentials(creds) {
|
|
588
650
|
const credPath = getCredentialsPath();
|
|
589
651
|
const dir = path.dirname(credPath);
|
|
590
652
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
591
|
-
await fs.promises.writeFile(
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
653
|
+
await fs.promises.writeFile(credPath, JSON.stringify(creds, null, 2), {
|
|
654
|
+
mode: 384
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async function ensureInstallationId(creds) {
|
|
658
|
+
if (creds?.installation_id && creds.installation_id.length > 0) {
|
|
659
|
+
return creds.installation_id;
|
|
660
|
+
}
|
|
661
|
+
const installationId = randomUUID();
|
|
662
|
+
await writeCredentials({
|
|
663
|
+
access_token: creds?.access_token ?? "",
|
|
664
|
+
auth_kind: creds?.auth_kind,
|
|
665
|
+
installation_id: installationId
|
|
666
|
+
});
|
|
667
|
+
return installationId;
|
|
668
|
+
}
|
|
669
|
+
async function getInstallationId() {
|
|
670
|
+
const credPath = getCredentialsPath();
|
|
671
|
+
try {
|
|
672
|
+
const content = await fs.promises.readFile(credPath, "utf-8");
|
|
673
|
+
const creds = JSON.parse(content);
|
|
674
|
+
return await ensureInstallationId(creds);
|
|
675
|
+
} catch {
|
|
676
|
+
return await ensureInstallationId(null);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function getToken() {
|
|
680
|
+
const existing = await getCredentials();
|
|
681
|
+
if (existing) {
|
|
682
|
+
return existing.accessToken;
|
|
683
|
+
}
|
|
684
|
+
const api = createApiClient();
|
|
685
|
+
const anonymousSession = await api.signInAnonymous();
|
|
686
|
+
await saveToken(anonymousSession.token, "anonymous");
|
|
687
|
+
return anonymousSession.token;
|
|
688
|
+
}
|
|
689
|
+
async function saveToken(token, authKind = "authenticated") {
|
|
690
|
+
const installationId = await getInstallationId();
|
|
691
|
+
await writeCredentials({
|
|
692
|
+
access_token: token,
|
|
693
|
+
auth_kind: authKind,
|
|
694
|
+
installation_id: installationId
|
|
695
|
+
});
|
|
596
696
|
}
|
|
597
697
|
|
|
598
698
|
// lib/command-execution.ts
|
|
599
|
-
import { randomUUID } from "crypto";
|
|
699
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
600
700
|
|
|
601
701
|
// lib/daily-limit-prompt.ts
|
|
602
702
|
import * as readline from "readline";
|
|
@@ -740,9 +840,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
740
840
|
);
|
|
741
841
|
}
|
|
742
842
|
console.log("");
|
|
743
|
-
console.log(
|
|
744
|
-
`${theme.primary}Commit message generation was skipped${theme.reset}`
|
|
745
|
-
);
|
|
843
|
+
console.log(`${theme.primary}Generation was skipped${theme.reset}`);
|
|
746
844
|
console.log("");
|
|
747
845
|
console.log(
|
|
748
846
|
ui.bullet(`Daily request limit reached (${info.count} / ${info.limit})`)
|
|
@@ -755,7 +853,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
755
853
|
);
|
|
756
854
|
console.log(` ${ui.link("ultrahope jj describe")}`);
|
|
757
855
|
console.log("");
|
|
758
|
-
console.log(`${theme.primary}Or upgrade
|
|
856
|
+
console.log(`${theme.primary}Or upgrade to Pro:${theme.reset}`);
|
|
759
857
|
console.log(` ${ui.link(PRICING_URL)}`);
|
|
760
858
|
return;
|
|
761
859
|
}
|
|
@@ -765,7 +863,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
765
863
|
`${theme.secondary} 1) Retry after the daily limit resets${theme.reset}`
|
|
766
864
|
);
|
|
767
865
|
console.log(
|
|
768
|
-
`${theme.secondary} 2) Upgrade
|
|
866
|
+
`${theme.secondary} 2) Upgrade to Pro to continue immediately${theme.reset}`
|
|
769
867
|
);
|
|
770
868
|
console.log("");
|
|
771
869
|
const choice = await promptChoice();
|
|
@@ -893,12 +991,13 @@ async function handleUpgrade() {
|
|
|
893
991
|
|
|
894
992
|
// lib/command-execution.ts
|
|
895
993
|
function startCommandExecution(options) {
|
|
896
|
-
const commandExecutionId =
|
|
994
|
+
const commandExecutionId = randomUUID2();
|
|
897
995
|
const cliSessionId = commandExecutionId;
|
|
898
996
|
const abortController = new AbortController();
|
|
899
997
|
const commandExecutionPromise = options.api.commandExecution({
|
|
900
998
|
commandExecutionId,
|
|
901
999
|
cliSessionId,
|
|
1000
|
+
installationId: options.installationId,
|
|
902
1001
|
command: options.command,
|
|
903
1002
|
args: options.args,
|
|
904
1003
|
api: options.apiPath,
|
|
@@ -943,8 +1042,12 @@ async function handleCommandExecutionError(error, options) {
|
|
|
943
1042
|
console.error(error.formatMessage());
|
|
944
1043
|
process.exit(1);
|
|
945
1044
|
}
|
|
1045
|
+
if (error instanceof SubscriptionRequiredError) {
|
|
1046
|
+
console.error(error.formatMessage());
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
946
1049
|
if (error instanceof InputLengthExceededError) {
|
|
947
|
-
console.error("\x1B[31m\u2716\x1B[0m Input is too long for
|
|
1050
|
+
console.error("\x1B[31m\u2716\x1B[0m Input is too long for anonymous usage.");
|
|
948
1051
|
console.error(
|
|
949
1052
|
` Max allowed characters: ${error.limit}. Received: ${error.count}.`
|
|
950
1053
|
);
|
|
@@ -1074,10 +1177,7 @@ async function* generateCommitMessages(options) {
|
|
|
1074
1177
|
models
|
|
1075
1178
|
});
|
|
1076
1179
|
const token = await getToken();
|
|
1077
|
-
|
|
1078
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
1079
|
-
process.exit(1);
|
|
1080
|
-
}
|
|
1180
|
+
const installationId = await getInstallationId();
|
|
1081
1181
|
const api = createApiClient(token);
|
|
1082
1182
|
const generateWithRetry = async function* (payload) {
|
|
1083
1183
|
const maxAttempts = 3;
|
|
@@ -1087,6 +1187,7 @@ async function* generateCommitMessages(options) {
|
|
|
1087
1187
|
const stream = options.refine ? api.streamCommitMessageRefine(
|
|
1088
1188
|
{
|
|
1089
1189
|
cliSessionId,
|
|
1190
|
+
installationId,
|
|
1090
1191
|
model: payload.model,
|
|
1091
1192
|
originalMessage: options.refine.originalMessage,
|
|
1092
1193
|
refineInstruction: options.refine.refineInstruction
|
|
@@ -1095,6 +1196,7 @@ async function* generateCommitMessages(options) {
|
|
|
1095
1196
|
) : api.streamCommitMessage(
|
|
1096
1197
|
{
|
|
1097
1198
|
...payload,
|
|
1199
|
+
installationId,
|
|
1098
1200
|
input: diff,
|
|
1099
1201
|
guide: options.guide
|
|
1100
1202
|
},
|
|
@@ -3898,15 +4000,12 @@ function assertDiffAvailable(revision, diff) {
|
|
|
3898
4000
|
process.exit(1);
|
|
3899
4001
|
}
|
|
3900
4002
|
}
|
|
3901
|
-
async function initCommandExecutionContext(args2, models, diff, apiPath, guide, isSessionActive) {
|
|
4003
|
+
async function initCommandExecutionContext(args2, models, diff, apiPath, installationId, guide, isSessionActive) {
|
|
3902
4004
|
const token = await getToken();
|
|
3903
|
-
if (!token) {
|
|
3904
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
3905
|
-
process.exit(1);
|
|
3906
|
-
}
|
|
3907
4005
|
const api = createApiClient(token);
|
|
3908
4006
|
const { commandExecutionPromise, abortController, cliSessionId } = startCommandExecution({
|
|
3909
4007
|
api,
|
|
4008
|
+
installationId,
|
|
3910
4009
|
command: "jj",
|
|
3911
4010
|
args: ["describe", ...args2],
|
|
3912
4011
|
apiPath,
|
|
@@ -3995,6 +4094,7 @@ async function describe(args2) {
|
|
|
3995
4094
|
let refineMessage;
|
|
3996
4095
|
let commandExecutionRun = 0;
|
|
3997
4096
|
let isEscalation = false;
|
|
4097
|
+
const installationId = await getInstallationId();
|
|
3998
4098
|
while (true) {
|
|
3999
4099
|
const sessionId = ++commandExecutionRun;
|
|
4000
4100
|
const isRefineAttempt = refineMessage !== void 0;
|
|
@@ -4003,6 +4103,7 @@ async function describe(args2) {
|
|
|
4003
4103
|
models,
|
|
4004
4104
|
diff,
|
|
4005
4105
|
isRefineAttempt ? "/v1/commit-message/refine" : "/v1/commit-message/stream",
|
|
4106
|
+
installationId,
|
|
4006
4107
|
composeGuidance(options.guide, guideHint),
|
|
4007
4108
|
() => sessionId === commandExecutionRun
|
|
4008
4109
|
);
|
|
@@ -4150,6 +4251,8 @@ async function jj(args2) {
|
|
|
4150
4251
|
// commands/login.ts
|
|
4151
4252
|
async function login(_args) {
|
|
4152
4253
|
const api = createApiClient();
|
|
4254
|
+
const existingCredentials = await getCredentials();
|
|
4255
|
+
const anonymousToken = existingCredentials?.authKind === "anonymous" ? existingCredentials.accessToken : null;
|
|
4153
4256
|
console.log("Requesting device code...");
|
|
4154
4257
|
const deviceCode = await api.requestDeviceCode();
|
|
4155
4258
|
console.log();
|
|
@@ -4163,8 +4266,18 @@ async function login(_args) {
|
|
|
4163
4266
|
deviceCode.interval,
|
|
4164
4267
|
deviceCode.expires_in
|
|
4165
4268
|
);
|
|
4166
|
-
await saveToken(token);
|
|
4269
|
+
await saveToken(token, "authenticated");
|
|
4167
4270
|
await ensureGlobalConfigFile();
|
|
4271
|
+
if (anonymousToken) {
|
|
4272
|
+
try {
|
|
4273
|
+
await createApiClient(anonymousToken).deleteAnonymousUser();
|
|
4274
|
+
} catch (error) {
|
|
4275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4276
|
+
console.error(
|
|
4277
|
+
`Warning: Failed to delete anonymous trial session. ${message}`
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4168
4281
|
console.log("Successfully authenticated!");
|
|
4169
4282
|
}
|
|
4170
4283
|
async function pollForToken(api, deviceCode, interval, expiresIn) {
|
|
@@ -4247,10 +4360,7 @@ async function handleVcsCommitMessage(input, options, args2) {
|
|
|
4247
4360
|
const models = resolveModels(options.cliModels);
|
|
4248
4361
|
try {
|
|
4249
4362
|
const token = await getToken();
|
|
4250
|
-
|
|
4251
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
4252
|
-
process.exit(1);
|
|
4253
|
-
}
|
|
4363
|
+
const installationId = await getInstallationId();
|
|
4254
4364
|
const api = createApiClient(token);
|
|
4255
4365
|
const apiClient = api;
|
|
4256
4366
|
let guideHint;
|
|
@@ -4277,6 +4387,7 @@ async function handleVcsCommitMessage(input, options, args2) {
|
|
|
4277
4387
|
const apiPath = isRefineAttempt && options.target === "vcs-commit-message" ? "/v1/commit-message/refine" : TARGET_TO_API_PATH[options.target];
|
|
4278
4388
|
const { commandExecutionPromise, abortController, cliSessionId } = startCommandExecution({
|
|
4279
4389
|
api,
|
|
4390
|
+
installationId,
|
|
4280
4391
|
command: "translate",
|
|
4281
4392
|
args: args2,
|
|
4282
4393
|
apiPath,
|
|
@@ -4354,10 +4465,7 @@ async function handleVcsCommitMessage(input, options, args2) {
|
|
|
4354
4465
|
}
|
|
4355
4466
|
async function handleGenericTarget(input, options, args2) {
|
|
4356
4467
|
const token = await getToken();
|
|
4357
|
-
|
|
4358
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
4359
|
-
process.exit(1);
|
|
4360
|
-
}
|
|
4468
|
+
const installationId = await getInstallationId();
|
|
4361
4469
|
const api = createApiClient(token);
|
|
4362
4470
|
const models = resolveModels(options.cliModels);
|
|
4363
4471
|
const requestPayload = models.length === 1 ? { input, target: options.target, model: models[0] } : { input, target: options.target, models };
|
|
@@ -4367,6 +4475,7 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
4367
4475
|
commandExecutionPromise
|
|
4368
4476
|
} = startCommandExecution({
|
|
4369
4477
|
api,
|
|
4478
|
+
installationId,
|
|
4370
4479
|
command: "translate",
|
|
4371
4480
|
args: args2,
|
|
4372
4481
|
apiPath: TARGET_TO_API_PATH[options.target],
|
|
@@ -4394,7 +4503,7 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
4394
4503
|
const maxAttempts = 3;
|
|
4395
4504
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4396
4505
|
try {
|
|
4397
|
-
return await generateFn({ cliSessionId, input, model });
|
|
4506
|
+
return await generateFn({ cliSessionId, installationId, input, model });
|
|
4398
4507
|
} catch (error) {
|
|
4399
4508
|
if (isAbortError2(error) || abortController.signal.aborted) throw error;
|
|
4400
4509
|
if (isInvalidCliSessionIdError2(error) && attempt < maxAttempts - 1) {
|
|
@@ -4533,7 +4642,7 @@ function parseArgs(args2) {
|
|
|
4533
4642
|
// package.json
|
|
4534
4643
|
var package_default = {
|
|
4535
4644
|
name: "ultrahope",
|
|
4536
|
-
version: "0.1.
|
|
4645
|
+
version: "0.1.11",
|
|
4537
4646
|
description: "LLM-powered development workflow assistant",
|
|
4538
4647
|
type: "module",
|
|
4539
4648
|
license: "MIT",
|
|
@@ -4618,11 +4727,15 @@ Usage: ultrahope <command>
|
|
|
4618
4727
|
Commands:
|
|
4619
4728
|
translate Translate input to various formats
|
|
4620
4729
|
jj Jujutsu integration commands
|
|
4621
|
-
login Authenticate with device flow
|
|
4730
|
+
login Authenticate with device flow and unlock full account usage
|
|
4622
4731
|
|
|
4623
4732
|
Options:
|
|
4624
4733
|
--version, -v Show version
|
|
4625
|
-
--help, -h Show this help message
|
|
4734
|
+
--help, -h Show this help message
|
|
4735
|
+
|
|
4736
|
+
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`);
|
|
4626
4739
|
}
|
|
4627
4740
|
main().catch((err) => {
|
|
4628
4741
|
console.error(err);
|