ultrahope 0.1.2 → 0.1.4
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 +31 -0
- package/dist/git-ultrahope.js +822 -401
- package/dist/index.js +1010 -373
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// index.ts
|
|
4
|
-
import { createRequire } from "module";
|
|
5
|
-
|
|
6
3
|
// commands/jj.ts
|
|
7
|
-
import { execSync as execSync2, spawnSync } from "child_process";
|
|
4
|
+
import { execFileSync, execSync as execSync2, spawnSync } from "child_process";
|
|
8
5
|
|
|
9
6
|
// lib/api-client.ts
|
|
10
7
|
import createClient from "openapi-fetch";
|
|
@@ -39,11 +36,33 @@ function log(message, data) {
|
|
|
39
36
|
// lib/api-client.ts
|
|
40
37
|
var API_BASE_URL = process.env.ULTRAHOPE_API_URL ?? "https://ultrahope.dev";
|
|
41
38
|
var InsufficientBalanceError = class extends Error {
|
|
42
|
-
constructor(balance) {
|
|
39
|
+
constructor(balance, plan = "free", hint, actions) {
|
|
43
40
|
super("Token balance exhausted");
|
|
44
41
|
this.balance = balance;
|
|
42
|
+
this.plan = plan;
|
|
43
|
+
this.hint = hint;
|
|
44
|
+
this.actions = actions;
|
|
45
45
|
this.name = "InsufficientBalanceError";
|
|
46
46
|
}
|
|
47
|
+
formatMessage() {
|
|
48
|
+
const lines = [];
|
|
49
|
+
if (this.plan === "pro") {
|
|
50
|
+
lines.push(
|
|
51
|
+
"Error: Your usage credit has been exhausted. Purchase additional credits to continue."
|
|
52
|
+
);
|
|
53
|
+
if (this.actions?.buyCredits) {
|
|
54
|
+
lines.push(` Buy credits: ${this.actions.buyCredits}`);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
lines.push(
|
|
58
|
+
"Error: Your free credit has been exhausted. Upgrade to Pro for unlimited requests with $5 included credit."
|
|
59
|
+
);
|
|
60
|
+
if (this.actions?.upgrade) {
|
|
61
|
+
lines.push(` Upgrade: ${this.actions.upgrade}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
47
66
|
};
|
|
48
67
|
var DailyLimitExceededError = class extends Error {
|
|
49
68
|
constructor(count, limit, resetsAt) {
|
|
@@ -60,6 +79,14 @@ var UnauthorizedError = class extends Error {
|
|
|
60
79
|
this.name = "UnauthorizedError";
|
|
61
80
|
}
|
|
62
81
|
};
|
|
82
|
+
var InvalidModelError = class extends Error {
|
|
83
|
+
constructor(model, allowedModels, message) {
|
|
84
|
+
super(message ?? `Model '${model}' is not supported.`);
|
|
85
|
+
this.model = model;
|
|
86
|
+
this.allowedModels = allowedModels;
|
|
87
|
+
this.name = "InvalidModelError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
63
90
|
async function getErrorText(response, error) {
|
|
64
91
|
if (error) {
|
|
65
92
|
try {
|
|
@@ -102,18 +129,34 @@ function parseSseEvents(buffer) {
|
|
|
102
129
|
return { events, remainder };
|
|
103
130
|
}
|
|
104
131
|
function handle402Error(error) {
|
|
105
|
-
const
|
|
106
|
-
if (typeof
|
|
132
|
+
const payload = error;
|
|
133
|
+
if (typeof payload?.balance === "number") {
|
|
107
134
|
log("generate error (402 insufficient_balance)", error);
|
|
108
|
-
|
|
135
|
+
const plan = payload.plan === "pro" || payload.plan === "free" ? payload.plan : "free";
|
|
136
|
+
throw new InsufficientBalanceError(
|
|
137
|
+
payload.balance,
|
|
138
|
+
plan,
|
|
139
|
+
payload.hint,
|
|
140
|
+
payload.actions
|
|
141
|
+
);
|
|
109
142
|
}
|
|
110
|
-
const payload = error;
|
|
111
143
|
const count = typeof payload?.count === "number" ? payload.count : 0;
|
|
112
144
|
const limit = typeof payload?.limit === "number" ? payload.limit : 0;
|
|
113
145
|
const resetsAt = payload?.resetsAt ?? "";
|
|
114
146
|
log("generate error (402 daily_limit)", error);
|
|
115
147
|
throw new DailyLimitExceededError(count, limit, resetsAt);
|
|
116
148
|
}
|
|
149
|
+
function throwInvalidModelError(error) {
|
|
150
|
+
const payload = error;
|
|
151
|
+
const message = payload?.message ?? "Model is not supported.";
|
|
152
|
+
const modelMatch = message.match(/Model '([^']+)' is not supported\./);
|
|
153
|
+
const model = payload?.model ?? modelMatch?.[1] ?? "unknown";
|
|
154
|
+
const allowedModels = Array.isArray(payload?.allowedModels) ? payload.allowedModels.filter(
|
|
155
|
+
(value) => typeof value === "string"
|
|
156
|
+
) : [];
|
|
157
|
+
log("generate error (400 invalid_model)", error);
|
|
158
|
+
throw new InvalidModelError(model, allowedModels, message);
|
|
159
|
+
}
|
|
117
160
|
function createApiClient(token) {
|
|
118
161
|
const headers = {
|
|
119
162
|
"Content-Type": "application/json"
|
|
@@ -150,6 +193,15 @@ function createApiClient(token) {
|
|
|
150
193
|
}
|
|
151
194
|
handle402Error(errorPayload);
|
|
152
195
|
}
|
|
196
|
+
if (res.status === 400) {
|
|
197
|
+
let errorPayload;
|
|
198
|
+
try {
|
|
199
|
+
errorPayload = await res.json();
|
|
200
|
+
} catch {
|
|
201
|
+
errorPayload = await getErrorText(res, null);
|
|
202
|
+
}
|
|
203
|
+
throwInvalidModelError(errorPayload);
|
|
204
|
+
}
|
|
153
205
|
if (!res.ok) {
|
|
154
206
|
const text = await getErrorText(res, null);
|
|
155
207
|
log("streamCommitMessage error", {
|
|
@@ -210,12 +262,8 @@ function createApiClient(token) {
|
|
|
210
262
|
throw new UnauthorizedError();
|
|
211
263
|
}
|
|
212
264
|
if (response.status === 402) {
|
|
213
|
-
const payload = error;
|
|
214
|
-
const count = typeof payload?.count === "number" ? payload.count : 0;
|
|
215
|
-
const limit = typeof payload?.limit === "number" ? payload.limit : 0;
|
|
216
|
-
const resetsAt = payload?.resetsAt ?? "";
|
|
217
265
|
log("command_execution error (402)", error);
|
|
218
|
-
|
|
266
|
+
handle402Error(error);
|
|
219
267
|
}
|
|
220
268
|
if (!response.ok) {
|
|
221
269
|
const text = await getErrorText(response, error);
|
|
@@ -244,6 +292,9 @@ function createApiClient(token) {
|
|
|
244
292
|
if (response.status === 402) {
|
|
245
293
|
handle402Error(error);
|
|
246
294
|
}
|
|
295
|
+
if (response.status === 400) {
|
|
296
|
+
throwInvalidModelError(error);
|
|
297
|
+
}
|
|
247
298
|
if (!response.ok) {
|
|
248
299
|
const text = await getErrorText(response, error);
|
|
249
300
|
log("generateCommitMessage error", { status: response.status, text });
|
|
@@ -295,6 +346,9 @@ function createApiClient(token) {
|
|
|
295
346
|
if (response.status === 402) {
|
|
296
347
|
handle402Error(error);
|
|
297
348
|
}
|
|
349
|
+
if (response.status === 400) {
|
|
350
|
+
throwInvalidModelError(error);
|
|
351
|
+
}
|
|
298
352
|
if (!response.ok) {
|
|
299
353
|
const text = await getErrorText(response, error);
|
|
300
354
|
log("generatePrTitleBody error", { status: response.status, text });
|
|
@@ -319,6 +373,9 @@ function createApiClient(token) {
|
|
|
319
373
|
if (response.status === 402) {
|
|
320
374
|
handle402Error(error);
|
|
321
375
|
}
|
|
376
|
+
if (response.status === 400) {
|
|
377
|
+
throwInvalidModelError(error);
|
|
378
|
+
}
|
|
322
379
|
if (!response.ok) {
|
|
323
380
|
const text = await getErrorText(response, error);
|
|
324
381
|
log("generatePrIntent error", { status: response.status, text });
|
|
@@ -568,7 +625,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
568
625
|
}
|
|
569
626
|
}
|
|
570
627
|
function promptChoice() {
|
|
571
|
-
return new Promise((
|
|
628
|
+
return new Promise((resolve3) => {
|
|
572
629
|
const fd = openSync("/dev/tty", "r");
|
|
573
630
|
const ttyInput = new tty.ReadStream(fd);
|
|
574
631
|
const rl = readline.createInterface({
|
|
@@ -591,17 +648,17 @@ function promptChoice() {
|
|
|
591
648
|
if (!key && !str) return;
|
|
592
649
|
if (str === "q" || str === "Q" || key?.name === "q" || key?.name === "c" && key.ctrl || key?.name === "escape") {
|
|
593
650
|
cleanup();
|
|
594
|
-
|
|
651
|
+
resolve3("q");
|
|
595
652
|
return;
|
|
596
653
|
}
|
|
597
654
|
if (str === "1" || key?.name === "1" || key?.sequence === "1") {
|
|
598
655
|
cleanup();
|
|
599
|
-
|
|
656
|
+
resolve3("1");
|
|
600
657
|
return;
|
|
601
658
|
}
|
|
602
659
|
if (str === "2" || key?.name === "2" || key?.sequence === "2") {
|
|
603
660
|
cleanup();
|
|
604
|
-
|
|
661
|
+
resolve3("2");
|
|
605
662
|
return;
|
|
606
663
|
}
|
|
607
664
|
};
|
|
@@ -618,7 +675,7 @@ function handleRetryLater() {
|
|
|
618
675
|
);
|
|
619
676
|
console.log(` ${ui.link("ultrahope jj describe")}`);
|
|
620
677
|
console.log("");
|
|
621
|
-
return new Promise((
|
|
678
|
+
return new Promise((resolve3) => {
|
|
622
679
|
const fd = openSync("/dev/tty", "r");
|
|
623
680
|
const ttyInput = new tty.ReadStream(fd);
|
|
624
681
|
const rl = readline.createInterface({
|
|
@@ -638,7 +695,7 @@ function handleRetryLater() {
|
|
|
638
695
|
if (!key && !str) return;
|
|
639
696
|
if (str === "\r" || str === "\n" || str === "q" || str === "Q" || key?.name === "return" || key?.name === "q" || key?.name === "c" && key.ctrl) {
|
|
640
697
|
cleanup();
|
|
641
|
-
|
|
698
|
+
resolve3();
|
|
642
699
|
}
|
|
643
700
|
};
|
|
644
701
|
ttyInput.on("keypress", handleKeypress);
|
|
@@ -702,11 +759,374 @@ async function handleCommandExecutionError(error, options) {
|
|
|
702
759
|
});
|
|
703
760
|
process.exit(1);
|
|
704
761
|
}
|
|
762
|
+
if (error instanceof InsufficientBalanceError) {
|
|
763
|
+
console.error(error.formatMessage());
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
705
766
|
const message = error instanceof Error ? error.message : String(error);
|
|
706
767
|
console.error(`Error: Failed to start command execution. ${message}`);
|
|
707
768
|
process.exit(1);
|
|
708
769
|
}
|
|
709
770
|
|
|
771
|
+
// lib/config.ts
|
|
772
|
+
import * as fs2 from "fs";
|
|
773
|
+
import * as os2 from "os";
|
|
774
|
+
import * as path2 from "path";
|
|
775
|
+
import { parse } from "smol-toml";
|
|
776
|
+
|
|
777
|
+
// ../shared/async-race.ts
|
|
778
|
+
async function* raceAsyncIterators({
|
|
779
|
+
iterators,
|
|
780
|
+
signal,
|
|
781
|
+
onError
|
|
782
|
+
}) {
|
|
783
|
+
if (iterators.length === 0) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (signal?.aborted) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const abortPromise = signal ? new Promise((resolve3) => {
|
|
790
|
+
signal.addEventListener("abort", () => resolve3(null), { once: true });
|
|
791
|
+
}) : null;
|
|
792
|
+
const pending = /* @__PURE__ */ new Map();
|
|
793
|
+
const startNext = (index) => {
|
|
794
|
+
const iterator = iterators[index];
|
|
795
|
+
if (!iterator) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
pending.set(
|
|
799
|
+
index,
|
|
800
|
+
iterator.next().then(
|
|
801
|
+
(result) => ({ kind: "result", index, result }),
|
|
802
|
+
(error) => ({ kind: "error", index, error })
|
|
803
|
+
)
|
|
804
|
+
);
|
|
805
|
+
};
|
|
806
|
+
const throwOrContinue = (error, index) => {
|
|
807
|
+
if (!onError) {
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
const decision = onError(index, error);
|
|
811
|
+
if (decision === "throw") {
|
|
812
|
+
throw error;
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
try {
|
|
816
|
+
for (let index = 0; index < iterators.length; index++) {
|
|
817
|
+
startNext(index);
|
|
818
|
+
}
|
|
819
|
+
while (pending.size > 0) {
|
|
820
|
+
const winner = abortPromise ? await Promise.race([
|
|
821
|
+
...pending.values(),
|
|
822
|
+
abortPromise.then(() => null)
|
|
823
|
+
]) : await Promise.race([...pending.values()]);
|
|
824
|
+
if (winner === null) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const { index } = winner;
|
|
828
|
+
pending.delete(index);
|
|
829
|
+
if (winner.kind === "error") {
|
|
830
|
+
throwOrContinue(winner.error, winner.index);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const { result } = winner;
|
|
834
|
+
if (!result.done) {
|
|
835
|
+
startNext(index);
|
|
836
|
+
yield { index, result };
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} finally {
|
|
840
|
+
for (const iterator of iterators) {
|
|
841
|
+
try {
|
|
842
|
+
await iterator.return?.();
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
for (const promise of pending.values()) {
|
|
847
|
+
promise.catch(() => void 0);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// lib/vcs-message-generator.ts
|
|
853
|
+
var DEFAULT_MODELS = [
|
|
854
|
+
// "mistral/ministral-3b",
|
|
855
|
+
// "cerebras/qwen-3-235b",
|
|
856
|
+
// "openai/gpt-5.1",
|
|
857
|
+
"mistral/ministral-3b",
|
|
858
|
+
"xai/grok-code-fast-1"
|
|
859
|
+
];
|
|
860
|
+
var isAbortError = (error) => error instanceof Error && error.name === "AbortError";
|
|
861
|
+
var isInvalidCliSessionIdError = (error) => error instanceof Error && error.message.includes("Invalid cliSessionId");
|
|
862
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
863
|
+
async function* generateCommitMessages(options) {
|
|
864
|
+
const {
|
|
865
|
+
diff,
|
|
866
|
+
models,
|
|
867
|
+
signal,
|
|
868
|
+
cliSessionId,
|
|
869
|
+
commandExecutionPromise,
|
|
870
|
+
useStream = false,
|
|
871
|
+
streamCaptureRecorder
|
|
872
|
+
} = options;
|
|
873
|
+
const resolvedCliSessionId = cliSessionId;
|
|
874
|
+
if (!resolvedCliSessionId) {
|
|
875
|
+
throw new Error("Missing cliSessionId for generate request.");
|
|
876
|
+
}
|
|
877
|
+
const requiredCliSessionId = resolvedCliSessionId;
|
|
878
|
+
const captureGenerationId = streamCaptureRecorder?.startGeneration({
|
|
879
|
+
cliSessionId: requiredCliSessionId,
|
|
880
|
+
models
|
|
881
|
+
});
|
|
882
|
+
const token = await getToken();
|
|
883
|
+
if (!token) {
|
|
884
|
+
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
const api = createApiClient(token);
|
|
888
|
+
const generateWithRetry = async function* (payload) {
|
|
889
|
+
const maxAttempts = 3;
|
|
890
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
891
|
+
const attemptNumber = attempt + 1;
|
|
892
|
+
try {
|
|
893
|
+
for await (const event of api.streamCommitMessage(payload, {
|
|
894
|
+
signal
|
|
895
|
+
})) {
|
|
896
|
+
if (captureGenerationId != null) {
|
|
897
|
+
streamCaptureRecorder?.recordEvent(captureGenerationId, {
|
|
898
|
+
model: payload.model,
|
|
899
|
+
attempt: attemptNumber,
|
|
900
|
+
event
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
log("generate", event);
|
|
904
|
+
yield event;
|
|
905
|
+
}
|
|
906
|
+
return;
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
909
|
+
if (isInvalidCliSessionIdError(error)) {
|
|
910
|
+
if (commandExecutionPromise) {
|
|
911
|
+
try {
|
|
912
|
+
await commandExecutionPromise;
|
|
913
|
+
continue;
|
|
914
|
+
} catch {
|
|
915
|
+
const abortError = new Error("Aborted");
|
|
916
|
+
abortError.name = "AbortError";
|
|
917
|
+
throw abortError;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (attempt < maxAttempts - 1) {
|
|
921
|
+
await delay(80 * (attempt + 1));
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
throw new Error("Failed to generate after retries.");
|
|
929
|
+
};
|
|
930
|
+
async function* generateForModel(model, slotIndex) {
|
|
931
|
+
try {
|
|
932
|
+
if (signal?.aborted) return;
|
|
933
|
+
let lastCommitMessage = "";
|
|
934
|
+
let providerMetadata;
|
|
935
|
+
for await (const event of generateWithRetry({
|
|
936
|
+
cliSessionId: requiredCliSessionId,
|
|
937
|
+
input: diff,
|
|
938
|
+
model,
|
|
939
|
+
guide: options.guide
|
|
940
|
+
})) {
|
|
941
|
+
if (event.type === "commit-message") {
|
|
942
|
+
lastCommitMessage = event.commitMessage;
|
|
943
|
+
if (useStream) {
|
|
944
|
+
yield {
|
|
945
|
+
content: lastCommitMessage,
|
|
946
|
+
slotId: model,
|
|
947
|
+
model,
|
|
948
|
+
isPartial: true,
|
|
949
|
+
slotIndex
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
} else if (event.type === "provider-metadata") {
|
|
953
|
+
providerMetadata = event.providerMetadata;
|
|
954
|
+
} else if (event.type === "error") {
|
|
955
|
+
throw new Error(event.message);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (lastCommitMessage) {
|
|
959
|
+
const { generationId, cost } = extractGatewayMetadata(providerMetadata);
|
|
960
|
+
yield {
|
|
961
|
+
content: lastCommitMessage,
|
|
962
|
+
slotId: model,
|
|
963
|
+
model,
|
|
964
|
+
cost,
|
|
965
|
+
generationId,
|
|
966
|
+
...useStream ? { isPartial: false } : {},
|
|
967
|
+
slotIndex
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (signal?.aborted || isAbortError(error)) return;
|
|
972
|
+
if (error instanceof InvalidModelError) throw error;
|
|
973
|
+
if (error instanceof InsufficientBalanceError) throw error;
|
|
974
|
+
if (isInvalidCliSessionIdError(error)) throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const iterators = models.map(
|
|
978
|
+
(model, index) => generateForModel(model, index)[Symbol.asyncIterator]()
|
|
979
|
+
);
|
|
980
|
+
try {
|
|
981
|
+
for await (const { result } of raceAsyncIterators({
|
|
982
|
+
iterators,
|
|
983
|
+
signal,
|
|
984
|
+
onError(index, error) {
|
|
985
|
+
if (isAbortError(error) || signal?.aborted) {
|
|
986
|
+
return "continue";
|
|
987
|
+
}
|
|
988
|
+
void index;
|
|
989
|
+
return "throw";
|
|
990
|
+
}
|
|
991
|
+
})) {
|
|
992
|
+
yield result.value;
|
|
993
|
+
}
|
|
994
|
+
} catch (error) {
|
|
995
|
+
if (error instanceof InvalidModelError) {
|
|
996
|
+
throw error;
|
|
997
|
+
}
|
|
998
|
+
if (error instanceof InsufficientBalanceError) {
|
|
999
|
+
console.error(error.formatMessage());
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
throw error;
|
|
1003
|
+
} finally {
|
|
1004
|
+
if (captureGenerationId != null) {
|
|
1005
|
+
streamCaptureRecorder?.finishGeneration(captureGenerationId);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// lib/config.ts
|
|
1011
|
+
var PROJECT_CONFIG_FILENAMES = [".ultrahope.toml", "ultrahope.toml"];
|
|
1012
|
+
function fail(message) {
|
|
1013
|
+
console.error(`Error: ${message}`);
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
1016
|
+
function validateModels(models, sourcePath) {
|
|
1017
|
+
if (!Array.isArray(models)) {
|
|
1018
|
+
fail(
|
|
1019
|
+
`Invalid config in ${sourcePath}: models must be an array of strings.`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
if (models.length === 0) {
|
|
1023
|
+
fail(`Invalid config in ${sourcePath}: models must not be empty.`);
|
|
1024
|
+
}
|
|
1025
|
+
const normalized = models.map((model, index) => {
|
|
1026
|
+
if (typeof model !== "string") {
|
|
1027
|
+
fail(
|
|
1028
|
+
`Invalid config in ${sourcePath}: models[${index}] must be a string.`
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
const trimmed = model.trim();
|
|
1032
|
+
if (!trimmed) {
|
|
1033
|
+
fail(
|
|
1034
|
+
`Invalid config in ${sourcePath}: models[${index}] must not be empty.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
return trimmed;
|
|
1038
|
+
});
|
|
1039
|
+
return normalized;
|
|
1040
|
+
}
|
|
1041
|
+
function parseModelsArg(value) {
|
|
1042
|
+
const rawModels = value.split(",");
|
|
1043
|
+
const models = rawModels.map((m) => m.trim());
|
|
1044
|
+
if (models.length === 0 || models.every((m) => m.length === 0)) {
|
|
1045
|
+
fail("--models requires at least one non-empty model.");
|
|
1046
|
+
}
|
|
1047
|
+
const emptyIndex = models.findIndex((m) => m.length === 0);
|
|
1048
|
+
if (emptyIndex !== -1) {
|
|
1049
|
+
fail(`--models contains an empty value at position ${emptyIndex + 1}.`);
|
|
1050
|
+
}
|
|
1051
|
+
return models;
|
|
1052
|
+
}
|
|
1053
|
+
function getGlobalConfigPath() {
|
|
1054
|
+
const configDir = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
1055
|
+
return path2.join(configDir, "ultrahope", "config.toml");
|
|
1056
|
+
}
|
|
1057
|
+
function findNearestProjectConfig(cwd) {
|
|
1058
|
+
let current = path2.resolve(cwd);
|
|
1059
|
+
while (true) {
|
|
1060
|
+
for (const filename of PROJECT_CONFIG_FILENAMES) {
|
|
1061
|
+
const candidate = path2.join(current, filename);
|
|
1062
|
+
if (fs2.existsSync(candidate)) {
|
|
1063
|
+
return candidate;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const parent = path2.dirname(current);
|
|
1067
|
+
if (parent === current) {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
current = parent;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function readConfigModels(configPath) {
|
|
1074
|
+
let raw = "";
|
|
1075
|
+
try {
|
|
1076
|
+
raw = fs2.readFileSync(configPath, "utf-8");
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1079
|
+
fail(`Failed to read config file ${configPath}: ${message}`);
|
|
1080
|
+
}
|
|
1081
|
+
let parsed;
|
|
1082
|
+
try {
|
|
1083
|
+
parsed = parse(raw);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1086
|
+
fail(`Failed to parse TOML config ${configPath}: ${message}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (parsed.models === void 0) {
|
|
1089
|
+
return void 0;
|
|
1090
|
+
}
|
|
1091
|
+
return validateModels(parsed.models, configPath);
|
|
1092
|
+
}
|
|
1093
|
+
function resolveModels(cliModels) {
|
|
1094
|
+
if (cliModels && cliModels.length > 0) {
|
|
1095
|
+
return cliModels;
|
|
1096
|
+
}
|
|
1097
|
+
const projectConfigPath = findNearestProjectConfig(process.cwd());
|
|
1098
|
+
if (projectConfigPath) {
|
|
1099
|
+
const projectModels = readConfigModels(projectConfigPath);
|
|
1100
|
+
if (projectModels) {
|
|
1101
|
+
return projectModels;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
1105
|
+
if (fs2.existsSync(globalConfigPath)) {
|
|
1106
|
+
const globalModels = readConfigModels(globalConfigPath);
|
|
1107
|
+
if (globalModels) {
|
|
1108
|
+
return globalModels;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return DEFAULT_MODELS;
|
|
1112
|
+
}
|
|
1113
|
+
async function ensureGlobalConfigFile() {
|
|
1114
|
+
const configPath = getGlobalConfigPath();
|
|
1115
|
+
if (fs2.existsSync(configPath)) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const dir = path2.dirname(configPath);
|
|
1119
|
+
await fs2.promises.mkdir(dir, { recursive: true });
|
|
1120
|
+
const content = `# Ultrahope CLI configuration
|
|
1121
|
+
# https://github.com/toyamarinyon/ultrahope
|
|
1122
|
+
|
|
1123
|
+
# Models to use for generation (each model produces one candidate)
|
|
1124
|
+
# See available models: https://ultrahope.dev/models
|
|
1125
|
+
models = ["mistral/ministral-3b", "xai/grok-code-fast-1"]
|
|
1126
|
+
`;
|
|
1127
|
+
await fs2.promises.writeFile(configPath, content, { mode: 420, flag: "wx" });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
710
1130
|
// lib/diff-stats.ts
|
|
711
1131
|
import { execSync } from "child_process";
|
|
712
1132
|
function getJjDiffStats(revision) {
|
|
@@ -745,39 +1165,85 @@ import {
|
|
|
745
1165
|
constants as constants2,
|
|
746
1166
|
mkdtempSync,
|
|
747
1167
|
openSync as openSync2,
|
|
748
|
-
readFileSync,
|
|
1168
|
+
readFileSync as readFileSync2,
|
|
749
1169
|
unlinkSync,
|
|
750
1170
|
writeFileSync
|
|
751
1171
|
} from "fs";
|
|
752
1172
|
import { tmpdir } from "os";
|
|
753
|
-
import { join as
|
|
1173
|
+
import { join as join4 } from "path";
|
|
754
1174
|
import * as readline3 from "readline";
|
|
755
1175
|
import * as tty2 from "tty";
|
|
756
1176
|
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
"\u2819",
|
|
762
|
-
"\u2839",
|
|
763
|
-
"\u2838",
|
|
764
|
-
"\u283C",
|
|
765
|
-
"\u2834",
|
|
766
|
-
"\u2826",
|
|
767
|
-
"\u2827",
|
|
768
|
-
"\u2807",
|
|
769
|
-
"\u280F"
|
|
770
|
-
];
|
|
771
|
-
function isTTY(output) {
|
|
772
|
-
return output.isTTY === true;
|
|
1177
|
+
// ../shared/terminal-selector-helpers.ts
|
|
1178
|
+
function formatModelName(model) {
|
|
1179
|
+
const parts = model.split("/");
|
|
1180
|
+
return parts.length > 1 ? parts[1] : model;
|
|
773
1181
|
}
|
|
774
|
-
function
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1182
|
+
function formatCost(cost) {
|
|
1183
|
+
return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
1184
|
+
}
|
|
1185
|
+
function formatTotalCostLabel(cost) {
|
|
1186
|
+
return `$${cost.toFixed(6)}`;
|
|
1187
|
+
}
|
|
1188
|
+
function getReadyCount(slots) {
|
|
1189
|
+
return slots.filter((slot) => slot.status === "ready").length;
|
|
1190
|
+
}
|
|
1191
|
+
function getTotalCost(slots) {
|
|
1192
|
+
return slots.reduce((sum, slot) => {
|
|
1193
|
+
if (slot.status === "ready" && slot.candidate.cost != null) {
|
|
1194
|
+
return sum + slot.candidate.cost;
|
|
1195
|
+
}
|
|
1196
|
+
return sum;
|
|
1197
|
+
}, 0);
|
|
1198
|
+
}
|
|
1199
|
+
function getLatestQuota(slots) {
|
|
1200
|
+
for (const slot of slots) {
|
|
1201
|
+
if (slot.status === "ready" && slot.candidate.quota) {
|
|
1202
|
+
return slot.candidate.quota;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return void 0;
|
|
1206
|
+
}
|
|
1207
|
+
function hasReadySlot(slots) {
|
|
1208
|
+
return getReadyCount(slots) > 0;
|
|
1209
|
+
}
|
|
1210
|
+
function selectNearestReady(slots, startIndex, direction) {
|
|
1211
|
+
for (let index = startIndex + direction; index >= 0 && index < slots.length; index += direction) {
|
|
1212
|
+
if (slots[index]?.status === "ready") {
|
|
1213
|
+
return index;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return startIndex;
|
|
1217
|
+
}
|
|
1218
|
+
function getSelectedCandidate(slots, selectedIndex) {
|
|
1219
|
+
const slot = slots[selectedIndex];
|
|
1220
|
+
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// lib/renderer.ts
|
|
1224
|
+
import * as readline2 from "readline";
|
|
1225
|
+
var SPINNER_FRAMES = [
|
|
1226
|
+
"\u280B",
|
|
1227
|
+
"\u2819",
|
|
1228
|
+
"\u2839",
|
|
1229
|
+
"\u2838",
|
|
1230
|
+
"\u283C",
|
|
1231
|
+
"\u2834",
|
|
1232
|
+
"\u2826",
|
|
1233
|
+
"\u2827",
|
|
1234
|
+
"\u2807",
|
|
1235
|
+
"\u280F"
|
|
1236
|
+
];
|
|
1237
|
+
function isTTY(output) {
|
|
1238
|
+
return output.isTTY === true;
|
|
1239
|
+
}
|
|
1240
|
+
function createRenderer(output) {
|
|
1241
|
+
let pendingHeight = 0;
|
|
1242
|
+
let committedHeight = 0;
|
|
1243
|
+
const render = (content) => {
|
|
1244
|
+
if (!isTTY(output)) {
|
|
1245
|
+
output.write(content);
|
|
1246
|
+
return;
|
|
781
1247
|
}
|
|
782
1248
|
if (pendingHeight > 0) {
|
|
783
1249
|
readline2.moveCursor(output, 0, -pendingHeight);
|
|
@@ -813,47 +1279,6 @@ function createRenderer(output) {
|
|
|
813
1279
|
|
|
814
1280
|
// lib/selector.ts
|
|
815
1281
|
var TTY_PATH = "/dev/tty";
|
|
816
|
-
function formatModelName(model) {
|
|
817
|
-
const parts = model.split("/");
|
|
818
|
-
return parts.length > 1 ? parts[1] : model;
|
|
819
|
-
}
|
|
820
|
-
function formatCost(cost) {
|
|
821
|
-
return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
822
|
-
}
|
|
823
|
-
function getReadyCount(slots) {
|
|
824
|
-
return slots.filter((s) => s.status === "ready").length;
|
|
825
|
-
}
|
|
826
|
-
function getTotalCost(slots) {
|
|
827
|
-
return slots.reduce((sum, slot) => {
|
|
828
|
-
if (slot.status === "ready" && slot.candidate.cost != null) {
|
|
829
|
-
return sum + slot.candidate.cost;
|
|
830
|
-
}
|
|
831
|
-
return sum;
|
|
832
|
-
}, 0);
|
|
833
|
-
}
|
|
834
|
-
function getLatestQuota(slots) {
|
|
835
|
-
for (const slot of slots) {
|
|
836
|
-
if (slot.status === "ready" && slot.candidate.quota) {
|
|
837
|
-
return slot.candidate.quota;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return void 0;
|
|
841
|
-
}
|
|
842
|
-
function hasReadySlot(slots) {
|
|
843
|
-
return slots.some((s) => s.status === "ready");
|
|
844
|
-
}
|
|
845
|
-
function getSelectedCandidate(slots, selectedIndex) {
|
|
846
|
-
const slot = slots[selectedIndex];
|
|
847
|
-
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
848
|
-
}
|
|
849
|
-
function selectNearestReady(slots, startIndex, direction) {
|
|
850
|
-
for (let i = startIndex + direction; i >= 0 && i < slots.length; i += direction) {
|
|
851
|
-
if (slots[i]?.status === "ready") {
|
|
852
|
-
return i;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
return startIndex;
|
|
856
|
-
}
|
|
857
1282
|
function collapseToReady(slots) {
|
|
858
1283
|
const readySlots = slots.filter((s) => s.status === "ready");
|
|
859
1284
|
slots.length = 0;
|
|
@@ -868,6 +1293,11 @@ function formatSlot(slot, selected) {
|
|
|
868
1293
|
const meta2 = slot.model ? `${theme.dim} ${formatModelName(slot.model)}${theme.reset}` : "";
|
|
869
1294
|
return meta2 ? [line2, meta2] : [line2];
|
|
870
1295
|
}
|
|
1296
|
+
if (slot.status === "error") {
|
|
1297
|
+
const radio2 = "\u25CB";
|
|
1298
|
+
const line2 = `${theme.dim} ${radio2} ${slot.content}${theme.reset}`;
|
|
1299
|
+
return [line2];
|
|
1300
|
+
}
|
|
871
1301
|
const candidate = slot.candidate;
|
|
872
1302
|
const title = candidate.content.split("\n")[0]?.trim() || "";
|
|
873
1303
|
const modelInfo = candidate.model ? candidate.cost ? `${formatModelName(candidate.model)} ${formatCost(candidate.cost)}` : formatModelName(candidate.model) : "";
|
|
@@ -882,9 +1312,6 @@ function formatSlot(slot, selected) {
|
|
|
882
1312
|
const meta = modelInfo ? `${theme.dim} ${modelInfo}${theme.reset}` : "";
|
|
883
1313
|
return meta ? [line, meta] : [line];
|
|
884
1314
|
}
|
|
885
|
-
function formatTotalCostLabel(cost) {
|
|
886
|
-
return `$${cost.toFixed(6)}`;
|
|
887
|
-
}
|
|
888
1315
|
function renderSelector(state, nowMs, renderer) {
|
|
889
1316
|
const { slots, selectedIndex, isGenerating, totalSlots } = state;
|
|
890
1317
|
const lines = [];
|
|
@@ -936,10 +1363,10 @@ function renderError(error, slots, totalSlots, output) {
|
|
|
936
1363
|
}
|
|
937
1364
|
}
|
|
938
1365
|
function openEditor(content) {
|
|
939
|
-
return new Promise((
|
|
1366
|
+
return new Promise((resolve3, reject) => {
|
|
940
1367
|
const editor = process.env.GIT_EDITOR || process.env.EDITOR || "vi";
|
|
941
|
-
const tmpDir = mkdtempSync(
|
|
942
|
-
const tmpFile =
|
|
1368
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "ultrahope-"));
|
|
1369
|
+
const tmpFile = join4(tmpDir, "EDIT_MESSAGE");
|
|
943
1370
|
writeFileSync(tmpFile, content);
|
|
944
1371
|
const child = spawn(editor, [tmpFile], { stdio: "inherit" });
|
|
945
1372
|
child.on("close", (code) => {
|
|
@@ -948,9 +1375,9 @@ function openEditor(content) {
|
|
|
948
1375
|
reject(new Error(`Editor exited with code ${code}`));
|
|
949
1376
|
return;
|
|
950
1377
|
}
|
|
951
|
-
const result =
|
|
1378
|
+
const result = readFileSync2(tmpFile, "utf-8").trim();
|
|
952
1379
|
unlinkSync(tmpFile);
|
|
953
|
-
|
|
1380
|
+
resolve3(result);
|
|
954
1381
|
});
|
|
955
1382
|
child.on("error", (err) => {
|
|
956
1383
|
try {
|
|
@@ -1010,12 +1437,12 @@ async function selectCandidate(options) {
|
|
|
1010
1437
|
);
|
|
1011
1438
|
}
|
|
1012
1439
|
async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
1013
|
-
return new Promise((
|
|
1440
|
+
return new Promise((resolve3) => {
|
|
1014
1441
|
let resolved = false;
|
|
1015
1442
|
const resolveOnce = (result) => {
|
|
1016
1443
|
if (resolved) return;
|
|
1017
1444
|
resolved = true;
|
|
1018
|
-
|
|
1445
|
+
resolve3(result);
|
|
1019
1446
|
};
|
|
1020
1447
|
const slots = [...initialSlots];
|
|
1021
1448
|
const state = {
|
|
@@ -1075,10 +1502,10 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1075
1502
|
};
|
|
1076
1503
|
const nextCandidate = async (iterator) => {
|
|
1077
1504
|
const abortPromise = new Promise(
|
|
1078
|
-
(
|
|
1505
|
+
(resolve4) => {
|
|
1079
1506
|
asyncCtx?.abortController.signal.addEventListener(
|
|
1080
1507
|
"abort",
|
|
1081
|
-
() =>
|
|
1508
|
+
() => resolve4({ done: true, value: void 0 }),
|
|
1082
1509
|
{ once: true }
|
|
1083
1510
|
);
|
|
1084
1511
|
}
|
|
@@ -1103,9 +1530,12 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1103
1530
|
const result = await nextCandidate(iterator);
|
|
1104
1531
|
if (result.done || cleanedUp) break;
|
|
1105
1532
|
const candidate = result.value;
|
|
1106
|
-
const targetIndex = slots.findIndex(
|
|
1107
|
-
(slot
|
|
1108
|
-
|
|
1533
|
+
const targetIndex = slots.findIndex((slot) => {
|
|
1534
|
+
if (slot.status === "ready") {
|
|
1535
|
+
return slot.candidate.slotId === candidate.slotId;
|
|
1536
|
+
}
|
|
1537
|
+
return slot.status === "pending" && slot.slotId === candidate.slotId;
|
|
1538
|
+
});
|
|
1109
1539
|
if (targetIndex >= 0 && targetIndex < slots.length) {
|
|
1110
1540
|
const isNewSlot = slots[targetIndex].status === "pending";
|
|
1111
1541
|
updateState((draft) => {
|
|
@@ -1124,10 +1554,15 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1124
1554
|
}
|
|
1125
1555
|
if (!cleanedUp) {
|
|
1126
1556
|
cancelGeneration();
|
|
1557
|
+
if (err instanceof InvalidModelError) {
|
|
1558
|
+
cleanup();
|
|
1559
|
+
resolveOnce({ action: "abort", error: err });
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1127
1562
|
renderer.clearAll();
|
|
1128
1563
|
renderError(err, slots, state.totalSlots, ttyOutput);
|
|
1129
1564
|
cleanup(false);
|
|
1130
|
-
resolveOnce({ action: "abort" });
|
|
1565
|
+
resolveOnce({ action: "abort", error: err });
|
|
1131
1566
|
}
|
|
1132
1567
|
} finally {
|
|
1133
1568
|
iterator.return?.();
|
|
@@ -1241,173 +1676,163 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1241
1676
|
});
|
|
1242
1677
|
}
|
|
1243
1678
|
|
|
1244
|
-
// lib/
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
return
|
|
1679
|
+
// lib/stream-capture.ts
|
|
1680
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1681
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
1682
|
+
var noopRecorder = {
|
|
1683
|
+
enabled: false,
|
|
1684
|
+
startGeneration: () => 0,
|
|
1685
|
+
recordEvent: () => {
|
|
1686
|
+
},
|
|
1687
|
+
finishGeneration: () => {
|
|
1688
|
+
},
|
|
1689
|
+
flush: () => null
|
|
1690
|
+
};
|
|
1691
|
+
function sanitizeEvent(event) {
|
|
1692
|
+
if (event.type === "provider-metadata") {
|
|
1693
|
+
return {
|
|
1694
|
+
type: "provider-metadata",
|
|
1695
|
+
providerMetadata: event.providerMetadata
|
|
1696
|
+
};
|
|
1259
1697
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
useStream = false
|
|
1270
|
-
} = options;
|
|
1271
|
-
const resolvedCliSessionId = cliSessionId;
|
|
1272
|
-
if (!resolvedCliSessionId) {
|
|
1273
|
-
throw new Error("Missing cliSessionId for generate request.");
|
|
1698
|
+
if (event.type === "usage") {
|
|
1699
|
+
return {
|
|
1700
|
+
type: "usage",
|
|
1701
|
+
usage: {
|
|
1702
|
+
inputTokens: event.usage.inputTokens,
|
|
1703
|
+
outputTokens: event.usage.outputTokens,
|
|
1704
|
+
totalTokens: event.usage.totalTokens
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1274
1707
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1708
|
+
if (event.type === "commit-message") {
|
|
1709
|
+
return {
|
|
1710
|
+
type: "commit-message",
|
|
1711
|
+
commitMessage: event.commitMessage
|
|
1712
|
+
};
|
|
1280
1713
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1714
|
+
return {
|
|
1715
|
+
type: "error",
|
|
1716
|
+
message: event.message
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function toSerializableCapture(capture) {
|
|
1720
|
+
return {
|
|
1721
|
+
...capture,
|
|
1722
|
+
runs: capture.runs.map((run) => ({
|
|
1723
|
+
...run,
|
|
1724
|
+
args: [...run.args],
|
|
1725
|
+
generations: run.generations.map((generation) => ({
|
|
1726
|
+
...generation,
|
|
1727
|
+
models: [...generation.models],
|
|
1728
|
+
events: generation.events.map((event) => ({
|
|
1729
|
+
atMs: event.atMs,
|
|
1730
|
+
model: event.model,
|
|
1731
|
+
attempt: event.attempt,
|
|
1732
|
+
event: sanitizeEvent(event.event)
|
|
1733
|
+
}))
|
|
1734
|
+
}))
|
|
1735
|
+
}))
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function createStreamCaptureRecorder(options) {
|
|
1739
|
+
if (!options.path) {
|
|
1740
|
+
return noopRecorder;
|
|
1741
|
+
}
|
|
1742
|
+
const outputPath = resolve2(process.cwd(), options.path);
|
|
1743
|
+
const runStartedAtMs = Date.now();
|
|
1744
|
+
const run = {
|
|
1745
|
+
id: "run-1",
|
|
1746
|
+
command: options.command,
|
|
1747
|
+
args: options.args,
|
|
1748
|
+
apiPath: options.apiPath,
|
|
1749
|
+
startedAt: new Date(runStartedAtMs).toISOString(),
|
|
1750
|
+
generations: []
|
|
1751
|
+
};
|
|
1752
|
+
const capture = {
|
|
1753
|
+
version: 2,
|
|
1754
|
+
source: "ultrahope-commit-message-stream",
|
|
1755
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1756
|
+
runs: [run]
|
|
1757
|
+
};
|
|
1758
|
+
let generationSequence = 0;
|
|
1759
|
+
let runFinished = false;
|
|
1760
|
+
const activeGenerations = /* @__PURE__ */ new Map();
|
|
1761
|
+
const writeCapture = () => {
|
|
1762
|
+
capture.capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1763
|
+
mkdirSync2(dirname3(outputPath), { recursive: true });
|
|
1764
|
+
writeFileSync2(
|
|
1765
|
+
outputPath,
|
|
1766
|
+
`${JSON.stringify(toSerializableCapture(capture), null, 2)}
|
|
1767
|
+
`,
|
|
1768
|
+
"utf-8"
|
|
1769
|
+
);
|
|
1770
|
+
};
|
|
1771
|
+
const ensureRunFinished = () => {
|
|
1772
|
+
if (runFinished) {
|
|
1773
|
+
return;
|
|
1313
1774
|
}
|
|
1314
|
-
|
|
1775
|
+
runFinished = true;
|
|
1776
|
+
run.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1315
1777
|
};
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1337
|
-
} else if (event.type === "provider-metadata") {
|
|
1338
|
-
providerMetadata = event.providerMetadata;
|
|
1339
|
-
} else if (event.type === "error") {
|
|
1340
|
-
throw new Error(event.message);
|
|
1341
|
-
}
|
|
1778
|
+
return {
|
|
1779
|
+
enabled: true,
|
|
1780
|
+
startGeneration: ({ cliSessionId, models }) => {
|
|
1781
|
+
generationSequence += 1;
|
|
1782
|
+
const startedAtMs = Date.now();
|
|
1783
|
+
const generation = {
|
|
1784
|
+
id: `generation-${generationSequence}`,
|
|
1785
|
+
cliSessionId,
|
|
1786
|
+
models: [...models],
|
|
1787
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1788
|
+
events: []
|
|
1789
|
+
};
|
|
1790
|
+
run.generations.push(generation);
|
|
1791
|
+
activeGenerations.set(generationSequence, { generation, startedAtMs });
|
|
1792
|
+
return generationSequence;
|
|
1793
|
+
},
|
|
1794
|
+
recordEvent: (generationId, record) => {
|
|
1795
|
+
const activeGeneration = activeGenerations.get(generationId);
|
|
1796
|
+
if (!activeGeneration) {
|
|
1797
|
+
return;
|
|
1342
1798
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1799
|
+
const eventAtMs = typeof record.event.atMs === "number" && Number.isFinite(record.event.atMs) ? record.event.atMs : Date.now() - activeGeneration.startedAtMs;
|
|
1800
|
+
activeGeneration.generation.events.push({
|
|
1801
|
+
atMs: Math.max(0, Math.round(eventAtMs)),
|
|
1802
|
+
model: record.model,
|
|
1803
|
+
attempt: record.attempt,
|
|
1804
|
+
event: sanitizeEvent(record.event)
|
|
1805
|
+
});
|
|
1806
|
+
},
|
|
1807
|
+
finishGeneration: (generationId) => {
|
|
1808
|
+
const activeGeneration = activeGenerations.get(generationId);
|
|
1809
|
+
if (!activeGeneration) {
|
|
1810
|
+
return;
|
|
1354
1811
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
index
|
|
1364
|
-
}));
|
|
1365
|
-
const pending = /* @__PURE__ */ new Map();
|
|
1366
|
-
const startNext = (it) => {
|
|
1367
|
-
const promise = it.iterator.next().then((result) => ({
|
|
1368
|
-
result,
|
|
1369
|
-
index: it.index
|
|
1370
|
-
}));
|
|
1371
|
-
pending.set(it.index, {
|
|
1372
|
-
iterator: it.iterator,
|
|
1373
|
-
promise,
|
|
1374
|
-
index: it.index
|
|
1375
|
-
});
|
|
1376
|
-
};
|
|
1377
|
-
for (const it of iterators) {
|
|
1378
|
-
startNext(it);
|
|
1379
|
-
}
|
|
1380
|
-
const abortPromise = createAbortPromise(signal);
|
|
1381
|
-
try {
|
|
1382
|
-
while (pending.size > 0) {
|
|
1383
|
-
if (signal?.aborted) break;
|
|
1384
|
-
const next = Promise.race(
|
|
1385
|
-
Array.from(pending.values()).map((p) => p.promise)
|
|
1386
|
-
);
|
|
1387
|
-
const winner = abortPromise ? await Promise.race([next, abortPromise]) : await next;
|
|
1388
|
-
if (!winner || signal?.aborted) break;
|
|
1389
|
-
const { result, index } = winner;
|
|
1390
|
-
const entry = pending.get(index);
|
|
1391
|
-
if (!entry) continue;
|
|
1392
|
-
if (result.done) {
|
|
1393
|
-
pending.delete(index);
|
|
1394
|
-
} else {
|
|
1395
|
-
yield result.value;
|
|
1396
|
-
startNext({ iterator: entry.iterator, index });
|
|
1812
|
+
activeGeneration.generation.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1813
|
+
activeGenerations.delete(generationId);
|
|
1814
|
+
writeCapture();
|
|
1815
|
+
},
|
|
1816
|
+
flush: () => {
|
|
1817
|
+
for (const [generationId, activeGeneration] of activeGenerations) {
|
|
1818
|
+
activeGeneration.generation.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1819
|
+
activeGenerations.delete(generationId);
|
|
1397
1820
|
}
|
|
1821
|
+
ensureRunFinished();
|
|
1822
|
+
writeCapture();
|
|
1823
|
+
return outputPath;
|
|
1398
1824
|
}
|
|
1399
|
-
}
|
|
1400
|
-
if (error instanceof InsufficientBalanceError) {
|
|
1401
|
-
console.error(
|
|
1402
|
-
"Error: Token balance exhausted. Upgrade your plan at https://ultrahope.dev/pricing"
|
|
1403
|
-
);
|
|
1404
|
-
process.exit(1);
|
|
1405
|
-
}
|
|
1406
|
-
throw error;
|
|
1407
|
-
}
|
|
1825
|
+
};
|
|
1408
1826
|
}
|
|
1409
1827
|
|
|
1410
1828
|
// commands/jj.ts
|
|
1829
|
+
function exitWithInvalidModelError(error) {
|
|
1830
|
+
console.error(`Error: Model '${error.model}' is not supported.`);
|
|
1831
|
+
if (error.allowedModels.length > 0) {
|
|
1832
|
+
console.error(`Available models: ${error.allowedModels.join(", ")}`);
|
|
1833
|
+
}
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1411
1836
|
function showQuotaInfo(quota) {
|
|
1412
1837
|
const { relative, local } = formatResetTime(quota.resetsAt);
|
|
1413
1838
|
console.log("");
|
|
@@ -1417,24 +1842,48 @@ function showQuotaInfo(quota) {
|
|
|
1417
1842
|
)
|
|
1418
1843
|
);
|
|
1419
1844
|
}
|
|
1845
|
+
function normalizeGuide(value) {
|
|
1846
|
+
if (!value) return void 0;
|
|
1847
|
+
const trimmed = value.trim();
|
|
1848
|
+
if (!trimmed) return void 0;
|
|
1849
|
+
return trimmed.length > 1024 ? trimmed.slice(0, 1024) : trimmed;
|
|
1850
|
+
}
|
|
1420
1851
|
function parseDescribeArgs(args2) {
|
|
1421
1852
|
let revision = "@";
|
|
1422
1853
|
let interactive = true;
|
|
1423
|
-
let
|
|
1854
|
+
let cliModels;
|
|
1855
|
+
let captureStreamPath;
|
|
1856
|
+
let guide;
|
|
1424
1857
|
for (let i = 0; i < args2.length; i++) {
|
|
1425
1858
|
const arg = args2[i];
|
|
1426
1859
|
if (arg === "-r") {
|
|
1427
1860
|
revision = args2[++i] || "@";
|
|
1428
1861
|
} else if (arg === "--no-interactive") {
|
|
1429
1862
|
interactive = false;
|
|
1430
|
-
} else if (arg === "--models"
|
|
1431
|
-
|
|
1863
|
+
} else if (arg === "--models") {
|
|
1864
|
+
const value = args2[++i];
|
|
1865
|
+
if (!value) {
|
|
1866
|
+
console.error("Error: --models requires a value");
|
|
1867
|
+
process.exit(1);
|
|
1868
|
+
}
|
|
1869
|
+
cliModels = parseModelsArg(value);
|
|
1870
|
+
} else if (arg === "--capture-stream") {
|
|
1871
|
+
const value = args2[++i];
|
|
1872
|
+
if (!value) {
|
|
1873
|
+
console.error("Error: --capture-stream requires a file path");
|
|
1874
|
+
process.exit(1);
|
|
1875
|
+
}
|
|
1876
|
+
captureStreamPath = value;
|
|
1877
|
+
} else if (arg === "--guide") {
|
|
1878
|
+
const value = args2[++i];
|
|
1879
|
+
if (!value) {
|
|
1880
|
+
console.error("Error: --guide requires a text value.");
|
|
1881
|
+
process.exit(1);
|
|
1882
|
+
}
|
|
1883
|
+
guide = normalizeGuide(value);
|
|
1432
1884
|
}
|
|
1433
1885
|
}
|
|
1434
|
-
|
|
1435
|
-
models = DEFAULT_MODELS;
|
|
1436
|
-
}
|
|
1437
|
-
return { revision, interactive, models };
|
|
1886
|
+
return { revision, interactive, cliModels, captureStreamPath, guide };
|
|
1438
1887
|
}
|
|
1439
1888
|
function getJjDiff(revision) {
|
|
1440
1889
|
try {
|
|
@@ -1466,7 +1915,7 @@ function assertDiffAvailable(revision, diff) {
|
|
|
1466
1915
|
process.exit(1);
|
|
1467
1916
|
}
|
|
1468
1917
|
}
|
|
1469
|
-
async function initCommandExecutionContext(args2,
|
|
1918
|
+
async function initCommandExecutionContext(args2, models, diff, guide) {
|
|
1470
1919
|
const token = await getToken();
|
|
1471
1920
|
if (!token) {
|
|
1472
1921
|
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
@@ -1481,13 +1930,14 @@ async function initCommandExecutionContext(args2, options, diff) {
|
|
|
1481
1930
|
requestPayload: {
|
|
1482
1931
|
input: diff,
|
|
1483
1932
|
target: "vcs-commit-message",
|
|
1484
|
-
models
|
|
1933
|
+
models,
|
|
1934
|
+
...guide ? { guide } : {}
|
|
1485
1935
|
}
|
|
1486
1936
|
});
|
|
1487
1937
|
commandExecutionPromise.catch(async (error) => {
|
|
1488
1938
|
abortController.abort(abortReasonForError(error));
|
|
1489
1939
|
await handleCommandExecutionError(error, {
|
|
1490
|
-
progress: { ready: 0, total:
|
|
1940
|
+
progress: { ready: 0, total: models.length }
|
|
1491
1941
|
});
|
|
1492
1942
|
});
|
|
1493
1943
|
return {
|
|
@@ -1512,41 +1962,53 @@ async function recordSelection(apiClient, generationId) {
|
|
|
1512
1962
|
console.error(`Warning: Failed to record selection. ${message}`);
|
|
1513
1963
|
}
|
|
1514
1964
|
}
|
|
1515
|
-
function createCandidateFactory(diff,
|
|
1965
|
+
function createCandidateFactory(diff, models, context, captureRecorder, guide) {
|
|
1516
1966
|
return (signal) => generateCommitMessages({
|
|
1517
1967
|
diff,
|
|
1518
|
-
models
|
|
1968
|
+
models,
|
|
1969
|
+
guide,
|
|
1519
1970
|
signal: mergeAbortSignals(signal, context.commandExecutionSignal),
|
|
1520
1971
|
cliSessionId: context.cliSessionId,
|
|
1521
1972
|
commandExecutionPromise: context.commandExecutionPromise,
|
|
1522
|
-
useStream: true
|
|
1973
|
+
useStream: true,
|
|
1974
|
+
streamCaptureRecorder: captureRecorder
|
|
1523
1975
|
});
|
|
1524
1976
|
}
|
|
1525
|
-
async function runNonInteractiveDescribe(
|
|
1977
|
+
async function runNonInteractiveDescribe(revision, models, diff, context, captureRecorder, guide) {
|
|
1526
1978
|
const gen = generateCommitMessages({
|
|
1527
1979
|
diff,
|
|
1528
|
-
models:
|
|
1980
|
+
models: models.slice(0, 1),
|
|
1981
|
+
guide,
|
|
1529
1982
|
signal: context.commandExecutionSignal,
|
|
1530
1983
|
cliSessionId: context.cliSessionId,
|
|
1531
1984
|
commandExecutionPromise: context.commandExecutionPromise,
|
|
1532
|
-
useStream: true
|
|
1985
|
+
useStream: true,
|
|
1986
|
+
streamCaptureRecorder: captureRecorder
|
|
1987
|
+
});
|
|
1988
|
+
const first = await gen.next().catch((error) => {
|
|
1989
|
+
if (error instanceof InvalidModelError) {
|
|
1990
|
+
exitWithInvalidModelError(error);
|
|
1991
|
+
}
|
|
1992
|
+
throw error;
|
|
1533
1993
|
});
|
|
1534
|
-
const first = await gen.next();
|
|
1535
1994
|
await recordSelection(context.apiClient, first.value?.generationId);
|
|
1536
1995
|
const message = first.value?.content ?? "";
|
|
1537
|
-
describeRevision(
|
|
1996
|
+
describeRevision(revision, message);
|
|
1538
1997
|
}
|
|
1539
|
-
async function runInteractiveDescribe(options, createCandidates, context) {
|
|
1998
|
+
async function runInteractiveDescribe(options, models, createCandidates, context) {
|
|
1540
1999
|
const stats = getJjDiffStats(options.revision);
|
|
1541
2000
|
console.log(ui.success(`Found ${formatDiffStats(stats)}`));
|
|
1542
2001
|
while (true) {
|
|
1543
2002
|
const result = await selectCandidate({
|
|
1544
2003
|
createCandidates,
|
|
1545
|
-
maxSlots:
|
|
2004
|
+
maxSlots: models.length,
|
|
1546
2005
|
abortSignal: context.commandExecutionSignal,
|
|
1547
|
-
models
|
|
2006
|
+
models
|
|
1548
2007
|
});
|
|
1549
2008
|
if (result.action === "abort") {
|
|
2009
|
+
if (result.error instanceof InvalidModelError) {
|
|
2010
|
+
exitWithInvalidModelError(result.error);
|
|
2011
|
+
}
|
|
1550
2012
|
if (isCommandExecutionAbort(context.commandExecutionSignal)) {
|
|
1551
2013
|
return;
|
|
1552
2014
|
}
|
|
@@ -1577,29 +2039,100 @@ async function runInteractiveDescribe(options, createCandidates, context) {
|
|
|
1577
2039
|
}
|
|
1578
2040
|
async function describe(args2) {
|
|
1579
2041
|
const options = parseDescribeArgs(args2);
|
|
2042
|
+
const models = resolveModels(options.cliModels);
|
|
1580
2043
|
const diff = getJjDiff(options.revision);
|
|
1581
2044
|
assertDiffAvailable(options.revision, diff);
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2045
|
+
const captureRecorder = createStreamCaptureRecorder({
|
|
2046
|
+
path: options.captureStreamPath,
|
|
2047
|
+
command: "ultrahope jj describe",
|
|
2048
|
+
args: ["describe", ...args2],
|
|
2049
|
+
apiPath: "/v1/commit-message/stream"
|
|
2050
|
+
});
|
|
2051
|
+
try {
|
|
2052
|
+
const context = await initCommandExecutionContext(
|
|
2053
|
+
args2,
|
|
2054
|
+
models,
|
|
2055
|
+
diff,
|
|
2056
|
+
options.guide
|
|
2057
|
+
);
|
|
2058
|
+
const createCandidates = createCandidateFactory(
|
|
2059
|
+
diff,
|
|
2060
|
+
models,
|
|
2061
|
+
context,
|
|
2062
|
+
captureRecorder,
|
|
2063
|
+
options.guide
|
|
2064
|
+
);
|
|
2065
|
+
if (!options.interactive) {
|
|
2066
|
+
await runNonInteractiveDescribe(
|
|
2067
|
+
options.revision,
|
|
2068
|
+
models,
|
|
2069
|
+
diff,
|
|
2070
|
+
context,
|
|
2071
|
+
captureRecorder,
|
|
2072
|
+
options.guide
|
|
2073
|
+
);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
await runInteractiveDescribe(options, models, createCandidates, context);
|
|
2077
|
+
} finally {
|
|
2078
|
+
const capturePath = captureRecorder.flush();
|
|
2079
|
+
if (capturePath) {
|
|
2080
|
+
console.log(ui.hint(`Captured stream replay to ${capturePath}`));
|
|
2081
|
+
}
|
|
1587
2082
|
}
|
|
1588
|
-
|
|
2083
|
+
}
|
|
2084
|
+
function setup() {
|
|
2085
|
+
try {
|
|
2086
|
+
const existing = execFileSync(
|
|
2087
|
+
"jj",
|
|
2088
|
+
["config", "get", "aliases.ultrahope"],
|
|
2089
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2090
|
+
).trim();
|
|
2091
|
+
if (existing) {
|
|
2092
|
+
console.log(ui.success("jj alias 'ultrahope' is already configured."));
|
|
2093
|
+
console.log(ui.hint(" Run `jj ultrahope describe` to use it."));
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
execFileSync(
|
|
2099
|
+
"jj",
|
|
2100
|
+
[
|
|
2101
|
+
"config",
|
|
2102
|
+
"set",
|
|
2103
|
+
"--user",
|
|
2104
|
+
"aliases.ultrahope",
|
|
2105
|
+
'["util", "exec", "--", "ultrahope", "jj"]'
|
|
2106
|
+
],
|
|
2107
|
+
{ stdio: "inherit" }
|
|
2108
|
+
);
|
|
2109
|
+
console.log(ui.success("Added jj alias 'ultrahope'."));
|
|
2110
|
+
console.log(
|
|
2111
|
+
ui.hint(
|
|
2112
|
+
" You can now run `jj ultrahope describe` instead of `ultrahope jj describe`."
|
|
2113
|
+
)
|
|
2114
|
+
);
|
|
1589
2115
|
}
|
|
1590
2116
|
function printHelp() {
|
|
1591
2117
|
console.log(`Usage: ultrahope jj <command>
|
|
1592
2118
|
|
|
1593
2119
|
Commands:
|
|
1594
2120
|
describe Generate commit description from changes
|
|
2121
|
+
setup Register 'ultrahope' as a jj alias
|
|
1595
2122
|
|
|
1596
2123
|
Describe options:
|
|
1597
2124
|
-r <revset> Revision to describe (default: @)
|
|
1598
2125
|
--no-interactive Single candidate, no selection
|
|
2126
|
+
--guide <text> Additional context to guide message generation
|
|
2127
|
+
--models <list> Comma-separated model list (overrides config)
|
|
2128
|
+
--capture-stream <path> Save candidate stream as replay JSON
|
|
1599
2129
|
|
|
1600
2130
|
Examples:
|
|
1601
2131
|
ultrahope jj describe # interactive mode
|
|
1602
|
-
ultrahope jj describe -r @- # for parent revision
|
|
2132
|
+
ultrahope jj describe -r @- # for parent revision
|
|
2133
|
+
ultrahope jj describe --guide "GHSA-gq3j-xvxp-8hrf: override reason"
|
|
2134
|
+
ultrahope jj describe --capture-stream packages/web/lib/demo/commit-message-stream.capture.json
|
|
2135
|
+
ultrahope jj setup # enable \`jj ultrahope\` alias`);
|
|
1603
2136
|
}
|
|
1604
2137
|
async function jj(args2) {
|
|
1605
2138
|
const [command2, ...rest] = args2;
|
|
@@ -1607,6 +2140,9 @@ async function jj(args2) {
|
|
|
1607
2140
|
case "describe":
|
|
1608
2141
|
await describe(rest);
|
|
1609
2142
|
break;
|
|
2143
|
+
case "setup":
|
|
2144
|
+
setup();
|
|
2145
|
+
break;
|
|
1610
2146
|
case "--help":
|
|
1611
2147
|
case "-h":
|
|
1612
2148
|
case void 0:
|
|
@@ -1636,6 +2172,7 @@ async function login(_args) {
|
|
|
1636
2172
|
deviceCode.expires_in
|
|
1637
2173
|
);
|
|
1638
2174
|
await saveToken(token);
|
|
2175
|
+
await ensureGlobalConfigFile();
|
|
1639
2176
|
console.log("Successfully authenticated!");
|
|
1640
2177
|
}
|
|
1641
2178
|
async function pollForToken(api, deviceCode, interval, expiresIn) {
|
|
@@ -1653,7 +2190,7 @@ async function pollForToken(api, deviceCode, interval, expiresIn) {
|
|
|
1653
2190
|
throw new Error("Authentication timed out");
|
|
1654
2191
|
}
|
|
1655
2192
|
function sleep(ms) {
|
|
1656
|
-
return new Promise((
|
|
2193
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1657
2194
|
}
|
|
1658
2195
|
|
|
1659
2196
|
// lib/stdin.ts
|
|
@@ -1676,6 +2213,13 @@ var TARGET_TO_API_PATH = {
|
|
|
1676
2213
|
"pr-title-body": "/v1/pr-title-body",
|
|
1677
2214
|
"pr-intent": "/v1/pr-intent"
|
|
1678
2215
|
};
|
|
2216
|
+
function exitWithInvalidModelError2(error) {
|
|
2217
|
+
console.error(`Error: Model '${error.model}' is not supported.`);
|
|
2218
|
+
if (error.allowedModels.length > 0) {
|
|
2219
|
+
console.error(`Available models: ${error.allowedModels.join(", ")}`);
|
|
2220
|
+
}
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
}
|
|
1679
2223
|
async function translate(args2) {
|
|
1680
2224
|
const options = parseArgs(args2);
|
|
1681
2225
|
const input = await stdin();
|
|
@@ -1685,6 +2229,12 @@ async function translate(args2) {
|
|
|
1685
2229
|
);
|
|
1686
2230
|
process.exit(1);
|
|
1687
2231
|
}
|
|
2232
|
+
if (options.captureStreamPath && options.target !== "vcs-commit-message") {
|
|
2233
|
+
console.error(
|
|
2234
|
+
"Error: --capture-stream is only supported with --target vcs-commit-message."
|
|
2235
|
+
);
|
|
2236
|
+
process.exit(1);
|
|
2237
|
+
}
|
|
1688
2238
|
if (options.target === "vcs-commit-message") {
|
|
1689
2239
|
await handleVcsCommitMessage(input, options, args2);
|
|
1690
2240
|
return;
|
|
@@ -1692,90 +2242,113 @@ async function translate(args2) {
|
|
|
1692
2242
|
await handleGenericTarget(input, options, args2);
|
|
1693
2243
|
}
|
|
1694
2244
|
async function handleVcsCommitMessage(input, options, args2) {
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
1699
|
-
process.exit(1);
|
|
1700
|
-
}
|
|
1701
|
-
const api = createApiClient(token);
|
|
1702
|
-
const {
|
|
1703
|
-
commandExecutionPromise: promise,
|
|
1704
|
-
abortController,
|
|
1705
|
-
cliSessionId: id
|
|
1706
|
-
} = startCommandExecution({
|
|
1707
|
-
api,
|
|
1708
|
-
command: "translate",
|
|
2245
|
+
const captureRecorder = createStreamCaptureRecorder({
|
|
2246
|
+
path: options.captureStreamPath,
|
|
2247
|
+
command: "ultrahope translate",
|
|
1709
2248
|
args: args2,
|
|
1710
|
-
apiPath:
|
|
1711
|
-
requestPayload: models.length === 1 ? { input, target: "vcs-commit-message", model: models[0] } : { input, target: "vcs-commit-message", models }
|
|
2249
|
+
apiPath: "/v1/commit-message/stream"
|
|
1712
2250
|
});
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2251
|
+
const models = resolveModels(options.cliModels);
|
|
2252
|
+
try {
|
|
2253
|
+
const token = await getToken();
|
|
2254
|
+
if (!token) {
|
|
2255
|
+
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
2256
|
+
process.exit(1);
|
|
2257
|
+
}
|
|
2258
|
+
const api = createApiClient(token);
|
|
2259
|
+
const {
|
|
2260
|
+
commandExecutionPromise: promise,
|
|
2261
|
+
abortController,
|
|
2262
|
+
cliSessionId: id
|
|
2263
|
+
} = startCommandExecution({
|
|
2264
|
+
api,
|
|
2265
|
+
command: "translate",
|
|
2266
|
+
args: args2,
|
|
2267
|
+
apiPath: TARGET_TO_API_PATH[options.target],
|
|
2268
|
+
requestPayload: models.length === 1 ? { input, target: "vcs-commit-message", model: models[0] } : { input, target: "vcs-commit-message", models }
|
|
1721
2269
|
});
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
2270
|
+
const cliSessionId = id;
|
|
2271
|
+
const commandExecutionSignal = abortController.signal;
|
|
2272
|
+
const commandExecutionPromise = promise;
|
|
2273
|
+
const apiClient = api;
|
|
2274
|
+
commandExecutionPromise.catch(async (error) => {
|
|
2275
|
+
abortController.abort(abortReasonForError(error));
|
|
2276
|
+
await handleCommandExecutionError(error, {
|
|
2277
|
+
progress: { ready: 0, total: models.length }
|
|
1729
2278
|
});
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
if (
|
|
1733
|
-
|
|
2279
|
+
});
|
|
2280
|
+
const recordSelection2 = async (generationId) => {
|
|
2281
|
+
if (!generationId || !apiClient) return;
|
|
2282
|
+
try {
|
|
2283
|
+
await apiClient.recordGenerationScore({
|
|
2284
|
+
generationId,
|
|
2285
|
+
value: 1
|
|
2286
|
+
});
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2289
|
+
if (message.includes("Generation not found")) {
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
console.error(`Warning: Failed to record selection. ${message}`);
|
|
1734
2293
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
};
|
|
1738
|
-
const createCandidates = (signal) => generateCommitMessages({
|
|
1739
|
-
diff: input,
|
|
1740
|
-
models,
|
|
1741
|
-
signal: mergeAbortSignals(signal, commandExecutionSignal),
|
|
1742
|
-
cliSessionId,
|
|
1743
|
-
commandExecutionPromise
|
|
1744
|
-
});
|
|
1745
|
-
if (!options.interactive) {
|
|
1746
|
-
const gen = generateCommitMessages({
|
|
2294
|
+
};
|
|
2295
|
+
const createCandidates = (signal) => generateCommitMessages({
|
|
1747
2296
|
diff: input,
|
|
1748
|
-
models
|
|
1749
|
-
signal: commandExecutionSignal,
|
|
2297
|
+
models,
|
|
2298
|
+
signal: mergeAbortSignals(signal, commandExecutionSignal),
|
|
1750
2299
|
cliSessionId,
|
|
1751
|
-
commandExecutionPromise
|
|
1752
|
-
|
|
1753
|
-
const first = await gen.next();
|
|
1754
|
-
await recordSelection2(first.value?.generationId);
|
|
1755
|
-
console.log(first.value?.content ?? "");
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
while (true) {
|
|
1759
|
-
const result = await selectCandidate({
|
|
1760
|
-
createCandidates,
|
|
1761
|
-
maxSlots: models.length,
|
|
1762
|
-
abortSignal: commandExecutionSignal,
|
|
1763
|
-
models
|
|
2300
|
+
commandExecutionPromise,
|
|
2301
|
+
streamCaptureRecorder: captureRecorder
|
|
1764
2302
|
});
|
|
1765
|
-
if (
|
|
1766
|
-
|
|
2303
|
+
if (!options.interactive) {
|
|
2304
|
+
const gen = generateCommitMessages({
|
|
2305
|
+
diff: input,
|
|
2306
|
+
models: models.slice(0, 1),
|
|
2307
|
+
signal: commandExecutionSignal,
|
|
2308
|
+
cliSessionId,
|
|
2309
|
+
commandExecutionPromise,
|
|
2310
|
+
streamCaptureRecorder: captureRecorder
|
|
2311
|
+
});
|
|
2312
|
+
const first = await gen.next().catch((error) => {
|
|
2313
|
+
if (error instanceof InvalidModelError) {
|
|
2314
|
+
exitWithInvalidModelError2(error);
|
|
2315
|
+
}
|
|
2316
|
+
throw error;
|
|
2317
|
+
});
|
|
2318
|
+
await recordSelection2(first.value?.generationId);
|
|
2319
|
+
console.log(first.value?.content ?? "");
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
while (true) {
|
|
2323
|
+
const result = await selectCandidate({
|
|
2324
|
+
createCandidates,
|
|
2325
|
+
maxSlots: models.length,
|
|
2326
|
+
abortSignal: commandExecutionSignal,
|
|
2327
|
+
models
|
|
2328
|
+
});
|
|
2329
|
+
if (result.action === "abort") {
|
|
2330
|
+
if (result.error instanceof InvalidModelError) {
|
|
2331
|
+
exitWithInvalidModelError2(result.error);
|
|
2332
|
+
}
|
|
2333
|
+
if (isCommandExecutionAbort(commandExecutionSignal)) {
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
console.error("Aborted.");
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
}
|
|
2339
|
+
if (result.action === "reroll") {
|
|
2340
|
+
continue;
|
|
2341
|
+
}
|
|
2342
|
+
if (result.action === "confirm" && result.selected) {
|
|
2343
|
+
await recordSelection2(result.selectedCandidate?.generationId);
|
|
2344
|
+
console.log(result.selected);
|
|
1767
2345
|
return;
|
|
1768
2346
|
}
|
|
1769
|
-
console.error("Aborted.");
|
|
1770
|
-
process.exit(1);
|
|
1771
2347
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
await recordSelection2(result.selectedCandidate?.generationId);
|
|
1777
|
-
console.log(result.selected);
|
|
1778
|
-
return;
|
|
2348
|
+
} finally {
|
|
2349
|
+
const capturePath = captureRecorder.flush();
|
|
2350
|
+
if (capturePath) {
|
|
2351
|
+
console.log(`Captured stream replay to ${capturePath}`);
|
|
1779
2352
|
}
|
|
1780
2353
|
}
|
|
1781
2354
|
}
|
|
@@ -1786,7 +2359,7 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
1786
2359
|
process.exit(1);
|
|
1787
2360
|
}
|
|
1788
2361
|
const api = createApiClient(token);
|
|
1789
|
-
const models = options.
|
|
2362
|
+
const models = resolveModels(options.cliModels);
|
|
1790
2363
|
const defaultModel = models[0];
|
|
1791
2364
|
const requestPayload = models.length === 1 ? { input, target: options.target, model: models[0] } : { input, target: options.target, models };
|
|
1792
2365
|
const {
|
|
@@ -1808,7 +2381,7 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
1808
2381
|
);
|
|
1809
2382
|
const isAbortError2 = (error) => error instanceof Error && error.name === "AbortError";
|
|
1810
2383
|
const isInvalidCliSessionIdError2 = (error) => error instanceof Error && error.message.includes("Invalid cliSessionId");
|
|
1811
|
-
const delay2 = (ms) => new Promise((
|
|
2384
|
+
const delay2 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1812
2385
|
const generateFn = (req) => {
|
|
1813
2386
|
if (options.target === "pr-title-body") {
|
|
1814
2387
|
return api.generatePrTitleBody(req, { signal: abortController.signal });
|
|
@@ -1858,10 +2431,11 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
1858
2431
|
if (isAbortError2(error) || abortController.signal.aborted) {
|
|
1859
2432
|
return candidates2;
|
|
1860
2433
|
}
|
|
2434
|
+
if (error instanceof InvalidModelError) {
|
|
2435
|
+
exitWithInvalidModelError2(error);
|
|
2436
|
+
}
|
|
1861
2437
|
if (error instanceof InsufficientBalanceError) {
|
|
1862
|
-
console.error(
|
|
1863
|
-
"Error: Token balance exhausted. Upgrade your plan at https://ultrahope.dev/pricing"
|
|
1864
|
-
);
|
|
2438
|
+
console.error(error.formatMessage());
|
|
1865
2439
|
process.exit(1);
|
|
1866
2440
|
}
|
|
1867
2441
|
throw error;
|
|
@@ -1878,10 +2452,11 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
1878
2452
|
if (isAbortError2(error) || abortController.signal.aborted) {
|
|
1879
2453
|
return null;
|
|
1880
2454
|
}
|
|
2455
|
+
if (error instanceof InvalidModelError) {
|
|
2456
|
+
exitWithInvalidModelError2(error);
|
|
2457
|
+
}
|
|
1881
2458
|
if (error instanceof InsufficientBalanceError) {
|
|
1882
|
-
console.error(
|
|
1883
|
-
"Error: Token balance exhausted. Upgrade your plan at https://ultrahope.dev/pricing"
|
|
1884
|
-
);
|
|
2459
|
+
console.error(error.formatMessage());
|
|
1885
2460
|
process.exit(1);
|
|
1886
2461
|
}
|
|
1887
2462
|
throw error;
|
|
@@ -1918,6 +2493,9 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
1918
2493
|
models: candidates.map((c) => c.model).filter(Boolean)
|
|
1919
2494
|
});
|
|
1920
2495
|
if (result.action === "abort") {
|
|
2496
|
+
if (result.error instanceof InvalidModelError) {
|
|
2497
|
+
exitWithInvalidModelError2(result.error);
|
|
2498
|
+
}
|
|
1921
2499
|
console.error("Aborted.");
|
|
1922
2500
|
process.exit(1);
|
|
1923
2501
|
}
|
|
@@ -1947,7 +2525,8 @@ async function handleGenericTarget(input, options, args2) {
|
|
|
1947
2525
|
function parseArgs(args2) {
|
|
1948
2526
|
let target;
|
|
1949
2527
|
let interactive = true;
|
|
1950
|
-
let
|
|
2528
|
+
let cliModels;
|
|
2529
|
+
let captureStreamPath;
|
|
1951
2530
|
for (let i = 0; i < args2.length; i++) {
|
|
1952
2531
|
const arg = args2[i];
|
|
1953
2532
|
if (arg === "--target" || arg === "-t") {
|
|
@@ -1960,13 +2539,23 @@ function parseArgs(args2) {
|
|
|
1960
2539
|
target = value;
|
|
1961
2540
|
} else if (arg === "--no-interactive") {
|
|
1962
2541
|
interactive = false;
|
|
2542
|
+
} else if (arg === "--models") {
|
|
2543
|
+
const value = args2[++i];
|
|
2544
|
+
if (!value) {
|
|
2545
|
+
console.error("Error: --models requires a value");
|
|
2546
|
+
process.exit(1);
|
|
2547
|
+
}
|
|
2548
|
+
cliModels = parseModelsArg(value);
|
|
1963
2549
|
} else if (arg === "--model") {
|
|
2550
|
+
console.error("Error: --model is no longer supported. Use --models.");
|
|
2551
|
+
process.exit(1);
|
|
2552
|
+
} else if (arg === "--capture-stream") {
|
|
1964
2553
|
const value = args2[++i];
|
|
1965
2554
|
if (!value) {
|
|
1966
|
-
console.error("Error: --
|
|
2555
|
+
console.error("Error: --capture-stream requires a file path");
|
|
1967
2556
|
process.exit(1);
|
|
1968
2557
|
}
|
|
1969
|
-
|
|
2558
|
+
captureStreamPath = value;
|
|
1970
2559
|
}
|
|
1971
2560
|
}
|
|
1972
2561
|
if (!target) {
|
|
@@ -1976,12 +2565,60 @@ function parseArgs(args2) {
|
|
|
1976
2565
|
);
|
|
1977
2566
|
process.exit(1);
|
|
1978
2567
|
}
|
|
1979
|
-
return { target, interactive,
|
|
2568
|
+
return { target, interactive, cliModels, captureStreamPath };
|
|
1980
2569
|
}
|
|
1981
2570
|
|
|
2571
|
+
// package.json
|
|
2572
|
+
var package_default = {
|
|
2573
|
+
name: "ultrahope",
|
|
2574
|
+
version: "0.1.4",
|
|
2575
|
+
description: "LLM-powered development workflow assistant",
|
|
2576
|
+
type: "module",
|
|
2577
|
+
license: "MIT",
|
|
2578
|
+
repository: {
|
|
2579
|
+
type: "git",
|
|
2580
|
+
url: "git+https://github.com/toyamarinyon/ultrahope.git"
|
|
2581
|
+
},
|
|
2582
|
+
keywords: [
|
|
2583
|
+
"cli",
|
|
2584
|
+
"llm",
|
|
2585
|
+
"ai",
|
|
2586
|
+
"git",
|
|
2587
|
+
"commit-message",
|
|
2588
|
+
"pull-request"
|
|
2589
|
+
],
|
|
2590
|
+
bin: {
|
|
2591
|
+
ultrahope: "dist/index.js",
|
|
2592
|
+
"git-ultrahope": "dist/git-ultrahope.js",
|
|
2593
|
+
"git-hope": "dist/git-ultrahope.js",
|
|
2594
|
+
"git-uh": "dist/git-ultrahope.js"
|
|
2595
|
+
},
|
|
2596
|
+
files: [
|
|
2597
|
+
"dist"
|
|
2598
|
+
],
|
|
2599
|
+
scripts: {
|
|
2600
|
+
build: "tsup",
|
|
2601
|
+
prebuild: "bun run generate:client",
|
|
2602
|
+
"generate:client": "tsx scripts/generate-client.ts",
|
|
2603
|
+
typecheck: "tsc --noEmit",
|
|
2604
|
+
prepublishOnly: "bun run build"
|
|
2605
|
+
},
|
|
2606
|
+
dependencies: {
|
|
2607
|
+
open: "^11.0.0",
|
|
2608
|
+
"openapi-fetch": "^0.15.0",
|
|
2609
|
+
"smol-toml": "^1.4.1"
|
|
2610
|
+
},
|
|
2611
|
+
devDependencies: {
|
|
2612
|
+
"@ultrahope/web": "workspace:*",
|
|
2613
|
+
"@types/node": "^22.15.29",
|
|
2614
|
+
"openapi-typescript": "^7.10.1",
|
|
2615
|
+
tsup: "8.5.0",
|
|
2616
|
+
typescript: "^5.8.3"
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
|
|
1982
2620
|
// index.ts
|
|
1983
|
-
var
|
|
1984
|
-
var { version } = require2("../package.json");
|
|
2621
|
+
var { version } = package_default;
|
|
1985
2622
|
var [command, ...args] = process.argv.slice(2);
|
|
1986
2623
|
async function main() {
|
|
1987
2624
|
switch (command) {
|