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/git-ultrahope.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// commands/commit.ts
|
|
4
|
-
import { execSync as execSync2
|
|
5
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
7
|
-
import { join as join4 } from "path";
|
|
4
|
+
import { execSync as execSync2 } 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 });
|
|
@@ -558,7 +615,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
558
615
|
}
|
|
559
616
|
}
|
|
560
617
|
function promptChoice() {
|
|
561
|
-
return new Promise((
|
|
618
|
+
return new Promise((resolve3) => {
|
|
562
619
|
const fd = openSync("/dev/tty", "r");
|
|
563
620
|
const ttyInput = new tty.ReadStream(fd);
|
|
564
621
|
const rl = readline.createInterface({
|
|
@@ -581,17 +638,17 @@ function promptChoice() {
|
|
|
581
638
|
if (!key && !str) return;
|
|
582
639
|
if (str === "q" || str === "Q" || key?.name === "q" || key?.name === "c" && key.ctrl || key?.name === "escape") {
|
|
583
640
|
cleanup();
|
|
584
|
-
|
|
641
|
+
resolve3("q");
|
|
585
642
|
return;
|
|
586
643
|
}
|
|
587
644
|
if (str === "1" || key?.name === "1" || key?.sequence === "1") {
|
|
588
645
|
cleanup();
|
|
589
|
-
|
|
646
|
+
resolve3("1");
|
|
590
647
|
return;
|
|
591
648
|
}
|
|
592
649
|
if (str === "2" || key?.name === "2" || key?.sequence === "2") {
|
|
593
650
|
cleanup();
|
|
594
|
-
|
|
651
|
+
resolve3("2");
|
|
595
652
|
return;
|
|
596
653
|
}
|
|
597
654
|
};
|
|
@@ -608,7 +665,7 @@ function handleRetryLater() {
|
|
|
608
665
|
);
|
|
609
666
|
console.log(` ${ui.link("ultrahope jj describe")}`);
|
|
610
667
|
console.log("");
|
|
611
|
-
return new Promise((
|
|
668
|
+
return new Promise((resolve3) => {
|
|
612
669
|
const fd = openSync("/dev/tty", "r");
|
|
613
670
|
const ttyInput = new tty.ReadStream(fd);
|
|
614
671
|
const rl = readline.createInterface({
|
|
@@ -628,7 +685,7 @@ function handleRetryLater() {
|
|
|
628
685
|
if (!key && !str) return;
|
|
629
686
|
if (str === "\r" || str === "\n" || str === "q" || str === "Q" || key?.name === "return" || key?.name === "q" || key?.name === "c" && key.ctrl) {
|
|
630
687
|
cleanup();
|
|
631
|
-
|
|
688
|
+
resolve3();
|
|
632
689
|
}
|
|
633
690
|
};
|
|
634
691
|
ttyInput.on("keypress", handleKeypress);
|
|
@@ -692,11 +749,358 @@ async function handleCommandExecutionError(error, options) {
|
|
|
692
749
|
});
|
|
693
750
|
process.exit(1);
|
|
694
751
|
}
|
|
752
|
+
if (error instanceof InsufficientBalanceError) {
|
|
753
|
+
console.error(error.formatMessage());
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
695
756
|
const message = error instanceof Error ? error.message : String(error);
|
|
696
757
|
console.error(`Error: Failed to start command execution. ${message}`);
|
|
697
758
|
process.exit(1);
|
|
698
759
|
}
|
|
699
760
|
|
|
761
|
+
// lib/config.ts
|
|
762
|
+
import * as fs2 from "fs";
|
|
763
|
+
import * as os2 from "os";
|
|
764
|
+
import * as path2 from "path";
|
|
765
|
+
import { parse } from "smol-toml";
|
|
766
|
+
|
|
767
|
+
// ../shared/async-race.ts
|
|
768
|
+
async function* raceAsyncIterators({
|
|
769
|
+
iterators,
|
|
770
|
+
signal,
|
|
771
|
+
onError
|
|
772
|
+
}) {
|
|
773
|
+
if (iterators.length === 0) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (signal?.aborted) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const abortPromise = signal ? new Promise((resolve3) => {
|
|
780
|
+
signal.addEventListener("abort", () => resolve3(null), { once: true });
|
|
781
|
+
}) : null;
|
|
782
|
+
const pending = /* @__PURE__ */ new Map();
|
|
783
|
+
const startNext = (index) => {
|
|
784
|
+
const iterator = iterators[index];
|
|
785
|
+
if (!iterator) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
pending.set(
|
|
789
|
+
index,
|
|
790
|
+
iterator.next().then(
|
|
791
|
+
(result) => ({ kind: "result", index, result }),
|
|
792
|
+
(error) => ({ kind: "error", index, error })
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
};
|
|
796
|
+
const throwOrContinue = (error, index) => {
|
|
797
|
+
if (!onError) {
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
const decision = onError(index, error);
|
|
801
|
+
if (decision === "throw") {
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
try {
|
|
806
|
+
for (let index = 0; index < iterators.length; index++) {
|
|
807
|
+
startNext(index);
|
|
808
|
+
}
|
|
809
|
+
while (pending.size > 0) {
|
|
810
|
+
const winner = abortPromise ? await Promise.race([
|
|
811
|
+
...pending.values(),
|
|
812
|
+
abortPromise.then(() => null)
|
|
813
|
+
]) : await Promise.race([...pending.values()]);
|
|
814
|
+
if (winner === null) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const { index } = winner;
|
|
818
|
+
pending.delete(index);
|
|
819
|
+
if (winner.kind === "error") {
|
|
820
|
+
throwOrContinue(winner.error, winner.index);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const { result } = winner;
|
|
824
|
+
if (!result.done) {
|
|
825
|
+
startNext(index);
|
|
826
|
+
yield { index, result };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} finally {
|
|
830
|
+
for (const iterator of iterators) {
|
|
831
|
+
try {
|
|
832
|
+
await iterator.return?.();
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
for (const promise of pending.values()) {
|
|
837
|
+
promise.catch(() => void 0);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// lib/vcs-message-generator.ts
|
|
843
|
+
var DEFAULT_MODELS = [
|
|
844
|
+
// "mistral/ministral-3b",
|
|
845
|
+
// "cerebras/qwen-3-235b",
|
|
846
|
+
// "openai/gpt-5.1",
|
|
847
|
+
"mistral/ministral-3b",
|
|
848
|
+
"xai/grok-code-fast-1"
|
|
849
|
+
];
|
|
850
|
+
var isAbortError = (error) => error instanceof Error && error.name === "AbortError";
|
|
851
|
+
var isInvalidCliSessionIdError = (error) => error instanceof Error && error.message.includes("Invalid cliSessionId");
|
|
852
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
853
|
+
async function* generateCommitMessages(options) {
|
|
854
|
+
const {
|
|
855
|
+
diff,
|
|
856
|
+
models,
|
|
857
|
+
signal,
|
|
858
|
+
cliSessionId,
|
|
859
|
+
commandExecutionPromise,
|
|
860
|
+
useStream = false,
|
|
861
|
+
streamCaptureRecorder
|
|
862
|
+
} = options;
|
|
863
|
+
const resolvedCliSessionId = cliSessionId;
|
|
864
|
+
if (!resolvedCliSessionId) {
|
|
865
|
+
throw new Error("Missing cliSessionId for generate request.");
|
|
866
|
+
}
|
|
867
|
+
const requiredCliSessionId = resolvedCliSessionId;
|
|
868
|
+
const captureGenerationId = streamCaptureRecorder?.startGeneration({
|
|
869
|
+
cliSessionId: requiredCliSessionId,
|
|
870
|
+
models
|
|
871
|
+
});
|
|
872
|
+
const token = await getToken();
|
|
873
|
+
if (!token) {
|
|
874
|
+
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
const api = createApiClient(token);
|
|
878
|
+
const generateWithRetry = async function* (payload) {
|
|
879
|
+
const maxAttempts = 3;
|
|
880
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
881
|
+
const attemptNumber = attempt + 1;
|
|
882
|
+
try {
|
|
883
|
+
for await (const event of api.streamCommitMessage(payload, {
|
|
884
|
+
signal
|
|
885
|
+
})) {
|
|
886
|
+
if (captureGenerationId != null) {
|
|
887
|
+
streamCaptureRecorder?.recordEvent(captureGenerationId, {
|
|
888
|
+
model: payload.model,
|
|
889
|
+
attempt: attemptNumber,
|
|
890
|
+
event
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
log("generate", event);
|
|
894
|
+
yield event;
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
899
|
+
if (isInvalidCliSessionIdError(error)) {
|
|
900
|
+
if (commandExecutionPromise) {
|
|
901
|
+
try {
|
|
902
|
+
await commandExecutionPromise;
|
|
903
|
+
continue;
|
|
904
|
+
} catch {
|
|
905
|
+
const abortError = new Error("Aborted");
|
|
906
|
+
abortError.name = "AbortError";
|
|
907
|
+
throw abortError;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (attempt < maxAttempts - 1) {
|
|
911
|
+
await delay(80 * (attempt + 1));
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
throw error;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
throw new Error("Failed to generate after retries.");
|
|
919
|
+
};
|
|
920
|
+
async function* generateForModel(model, slotIndex) {
|
|
921
|
+
try {
|
|
922
|
+
if (signal?.aborted) return;
|
|
923
|
+
let lastCommitMessage = "";
|
|
924
|
+
let providerMetadata;
|
|
925
|
+
for await (const event of generateWithRetry({
|
|
926
|
+
cliSessionId: requiredCliSessionId,
|
|
927
|
+
input: diff,
|
|
928
|
+
model,
|
|
929
|
+
guide: options.guide
|
|
930
|
+
})) {
|
|
931
|
+
if (event.type === "commit-message") {
|
|
932
|
+
lastCommitMessage = event.commitMessage;
|
|
933
|
+
if (useStream) {
|
|
934
|
+
yield {
|
|
935
|
+
content: lastCommitMessage,
|
|
936
|
+
slotId: model,
|
|
937
|
+
model,
|
|
938
|
+
isPartial: true,
|
|
939
|
+
slotIndex
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
} else if (event.type === "provider-metadata") {
|
|
943
|
+
providerMetadata = event.providerMetadata;
|
|
944
|
+
} else if (event.type === "error") {
|
|
945
|
+
throw new Error(event.message);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (lastCommitMessage) {
|
|
949
|
+
const { generationId, cost } = extractGatewayMetadata(providerMetadata);
|
|
950
|
+
yield {
|
|
951
|
+
content: lastCommitMessage,
|
|
952
|
+
slotId: model,
|
|
953
|
+
model,
|
|
954
|
+
cost,
|
|
955
|
+
generationId,
|
|
956
|
+
...useStream ? { isPartial: false } : {},
|
|
957
|
+
slotIndex
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (signal?.aborted || isAbortError(error)) return;
|
|
962
|
+
if (error instanceof InvalidModelError) throw error;
|
|
963
|
+
if (error instanceof InsufficientBalanceError) throw error;
|
|
964
|
+
if (isInvalidCliSessionIdError(error)) throw error;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const iterators = models.map(
|
|
968
|
+
(model, index) => generateForModel(model, index)[Symbol.asyncIterator]()
|
|
969
|
+
);
|
|
970
|
+
try {
|
|
971
|
+
for await (const { result } of raceAsyncIterators({
|
|
972
|
+
iterators,
|
|
973
|
+
signal,
|
|
974
|
+
onError(index, error) {
|
|
975
|
+
if (isAbortError(error) || signal?.aborted) {
|
|
976
|
+
return "continue";
|
|
977
|
+
}
|
|
978
|
+
void index;
|
|
979
|
+
return "throw";
|
|
980
|
+
}
|
|
981
|
+
})) {
|
|
982
|
+
yield result.value;
|
|
983
|
+
}
|
|
984
|
+
} catch (error) {
|
|
985
|
+
if (error instanceof InvalidModelError) {
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
if (error instanceof InsufficientBalanceError) {
|
|
989
|
+
console.error(error.formatMessage());
|
|
990
|
+
process.exit(1);
|
|
991
|
+
}
|
|
992
|
+
throw error;
|
|
993
|
+
} finally {
|
|
994
|
+
if (captureGenerationId != null) {
|
|
995
|
+
streamCaptureRecorder?.finishGeneration(captureGenerationId);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// lib/config.ts
|
|
1001
|
+
var PROJECT_CONFIG_FILENAMES = [".ultrahope.toml", "ultrahope.toml"];
|
|
1002
|
+
function fail(message) {
|
|
1003
|
+
console.error(`Error: ${message}`);
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
function validateModels(models, sourcePath) {
|
|
1007
|
+
if (!Array.isArray(models)) {
|
|
1008
|
+
fail(
|
|
1009
|
+
`Invalid config in ${sourcePath}: models must be an array of strings.`
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (models.length === 0) {
|
|
1013
|
+
fail(`Invalid config in ${sourcePath}: models must not be empty.`);
|
|
1014
|
+
}
|
|
1015
|
+
const normalized = models.map((model, index) => {
|
|
1016
|
+
if (typeof model !== "string") {
|
|
1017
|
+
fail(
|
|
1018
|
+
`Invalid config in ${sourcePath}: models[${index}] must be a string.`
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
const trimmed = model.trim();
|
|
1022
|
+
if (!trimmed) {
|
|
1023
|
+
fail(
|
|
1024
|
+
`Invalid config in ${sourcePath}: models[${index}] must not be empty.`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
return trimmed;
|
|
1028
|
+
});
|
|
1029
|
+
return normalized;
|
|
1030
|
+
}
|
|
1031
|
+
function parseModelsArg(value) {
|
|
1032
|
+
const rawModels = value.split(",");
|
|
1033
|
+
const models = rawModels.map((m) => m.trim());
|
|
1034
|
+
if (models.length === 0 || models.every((m) => m.length === 0)) {
|
|
1035
|
+
fail("--models requires at least one non-empty model.");
|
|
1036
|
+
}
|
|
1037
|
+
const emptyIndex = models.findIndex((m) => m.length === 0);
|
|
1038
|
+
if (emptyIndex !== -1) {
|
|
1039
|
+
fail(`--models contains an empty value at position ${emptyIndex + 1}.`);
|
|
1040
|
+
}
|
|
1041
|
+
return models;
|
|
1042
|
+
}
|
|
1043
|
+
function getGlobalConfigPath() {
|
|
1044
|
+
const configDir = process.env.XDG_CONFIG_HOME ?? path2.join(os2.homedir(), ".config");
|
|
1045
|
+
return path2.join(configDir, "ultrahope", "config.toml");
|
|
1046
|
+
}
|
|
1047
|
+
function findNearestProjectConfig(cwd) {
|
|
1048
|
+
let current = path2.resolve(cwd);
|
|
1049
|
+
while (true) {
|
|
1050
|
+
for (const filename of PROJECT_CONFIG_FILENAMES) {
|
|
1051
|
+
const candidate = path2.join(current, filename);
|
|
1052
|
+
if (fs2.existsSync(candidate)) {
|
|
1053
|
+
return candidate;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
const parent = path2.dirname(current);
|
|
1057
|
+
if (parent === current) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
current = parent;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function readConfigModels(configPath) {
|
|
1064
|
+
let raw = "";
|
|
1065
|
+
try {
|
|
1066
|
+
raw = fs2.readFileSync(configPath, "utf-8");
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1069
|
+
fail(`Failed to read config file ${configPath}: ${message}`);
|
|
1070
|
+
}
|
|
1071
|
+
let parsed;
|
|
1072
|
+
try {
|
|
1073
|
+
parsed = parse(raw);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1076
|
+
fail(`Failed to parse TOML config ${configPath}: ${message}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (parsed.models === void 0) {
|
|
1079
|
+
return void 0;
|
|
1080
|
+
}
|
|
1081
|
+
return validateModels(parsed.models, configPath);
|
|
1082
|
+
}
|
|
1083
|
+
function resolveModels(cliModels) {
|
|
1084
|
+
if (cliModels && cliModels.length > 0) {
|
|
1085
|
+
return cliModels;
|
|
1086
|
+
}
|
|
1087
|
+
const projectConfigPath = findNearestProjectConfig(process.cwd());
|
|
1088
|
+
if (projectConfigPath) {
|
|
1089
|
+
const projectModels = readConfigModels(projectConfigPath);
|
|
1090
|
+
if (projectModels) {
|
|
1091
|
+
return projectModels;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
1095
|
+
if (fs2.existsSync(globalConfigPath)) {
|
|
1096
|
+
const globalModels = readConfigModels(globalConfigPath);
|
|
1097
|
+
if (globalModels) {
|
|
1098
|
+
return globalModels;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return DEFAULT_MODELS;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
700
1104
|
// lib/diff-stats.ts
|
|
701
1105
|
import { execSync } from "child_process";
|
|
702
1106
|
function getGitStagedStats() {
|
|
@@ -736,83 +1140,28 @@ import {
|
|
|
736
1140
|
constants as constants2,
|
|
737
1141
|
mkdtempSync,
|
|
738
1142
|
openSync as openSync2,
|
|
739
|
-
readFileSync,
|
|
1143
|
+
readFileSync as readFileSync2,
|
|
740
1144
|
unlinkSync,
|
|
741
1145
|
writeFileSync
|
|
742
1146
|
} from "fs";
|
|
743
1147
|
import { tmpdir } from "os";
|
|
744
|
-
import { join as
|
|
1148
|
+
import { join as join4 } from "path";
|
|
745
1149
|
import * as readline3 from "readline";
|
|
746
1150
|
import * as tty2 from "tty";
|
|
747
1151
|
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
"\u2819",
|
|
753
|
-
"\u2839",
|
|
754
|
-
"\u2838",
|
|
755
|
-
"\u283C",
|
|
756
|
-
"\u2834",
|
|
757
|
-
"\u2826",
|
|
758
|
-
"\u2827",
|
|
759
|
-
"\u2807",
|
|
760
|
-
"\u280F"
|
|
761
|
-
];
|
|
762
|
-
function isTTY(output) {
|
|
763
|
-
return output.isTTY === true;
|
|
764
|
-
}
|
|
765
|
-
function createRenderer(output) {
|
|
766
|
-
let pendingHeight = 0;
|
|
767
|
-
let committedHeight = 0;
|
|
768
|
-
const render = (content) => {
|
|
769
|
-
if (!isTTY(output)) {
|
|
770
|
-
output.write(content);
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
if (pendingHeight > 0) {
|
|
774
|
-
readline2.moveCursor(output, 0, -pendingHeight);
|
|
775
|
-
readline2.cursorTo(output, 0);
|
|
776
|
-
readline2.clearScreenDown(output);
|
|
777
|
-
}
|
|
778
|
-
output.write(content);
|
|
779
|
-
pendingHeight = content.split("\n").length - 1;
|
|
780
|
-
};
|
|
781
|
-
const flush = () => {
|
|
782
|
-
committedHeight += pendingHeight;
|
|
783
|
-
pendingHeight = 0;
|
|
784
|
-
};
|
|
785
|
-
const clearAll = () => {
|
|
786
|
-
if (!isTTY(output)) {
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
const totalHeight = pendingHeight + committedHeight;
|
|
790
|
-
if (totalHeight > 0) {
|
|
791
|
-
readline2.moveCursor(output, 0, -totalHeight);
|
|
792
|
-
readline2.cursorTo(output, 0);
|
|
793
|
-
readline2.clearScreenDown(output);
|
|
794
|
-
}
|
|
795
|
-
pendingHeight = 0;
|
|
796
|
-
committedHeight = 0;
|
|
797
|
-
};
|
|
798
|
-
const reset = () => {
|
|
799
|
-
pendingHeight = 0;
|
|
800
|
-
committedHeight = 0;
|
|
801
|
-
};
|
|
802
|
-
return { render, flush, clearAll, reset };
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// lib/selector.ts
|
|
806
|
-
var TTY_PATH = "/dev/tty";
|
|
807
|
-
function formatModelName(model) {
|
|
808
|
-
const parts = model.split("/");
|
|
809
|
-
return parts.length > 1 ? parts[1] : model;
|
|
1152
|
+
// ../shared/terminal-selector-helpers.ts
|
|
1153
|
+
function formatModelName(model) {
|
|
1154
|
+
const parts = model.split("/");
|
|
1155
|
+
return parts.length > 1 ? parts[1] : model;
|
|
810
1156
|
}
|
|
811
1157
|
function formatCost(cost) {
|
|
812
1158
|
return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
813
1159
|
}
|
|
1160
|
+
function formatTotalCostLabel(cost) {
|
|
1161
|
+
return `$${cost.toFixed(6)}`;
|
|
1162
|
+
}
|
|
814
1163
|
function getReadyCount(slots) {
|
|
815
|
-
return slots.filter((
|
|
1164
|
+
return slots.filter((slot) => slot.status === "ready").length;
|
|
816
1165
|
}
|
|
817
1166
|
function getTotalCost(slots) {
|
|
818
1167
|
return slots.reduce((sum, slot) => {
|
|
@@ -831,20 +1180,80 @@ function getLatestQuota(slots) {
|
|
|
831
1180
|
return void 0;
|
|
832
1181
|
}
|
|
833
1182
|
function hasReadySlot(slots) {
|
|
834
|
-
return slots
|
|
1183
|
+
return getReadyCount(slots) > 0;
|
|
1184
|
+
}
|
|
1185
|
+
function selectNearestReady(slots, startIndex, direction) {
|
|
1186
|
+
for (let index = startIndex + direction; index >= 0 && index < slots.length; index += direction) {
|
|
1187
|
+
if (slots[index]?.status === "ready") {
|
|
1188
|
+
return index;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return startIndex;
|
|
835
1192
|
}
|
|
836
1193
|
function getSelectedCandidate(slots, selectedIndex) {
|
|
837
1194
|
const slot = slots[selectedIndex];
|
|
838
1195
|
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
839
1196
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1197
|
+
|
|
1198
|
+
// lib/renderer.ts
|
|
1199
|
+
import * as readline2 from "readline";
|
|
1200
|
+
var SPINNER_FRAMES = [
|
|
1201
|
+
"\u280B",
|
|
1202
|
+
"\u2819",
|
|
1203
|
+
"\u2839",
|
|
1204
|
+
"\u2838",
|
|
1205
|
+
"\u283C",
|
|
1206
|
+
"\u2834",
|
|
1207
|
+
"\u2826",
|
|
1208
|
+
"\u2827",
|
|
1209
|
+
"\u2807",
|
|
1210
|
+
"\u280F"
|
|
1211
|
+
];
|
|
1212
|
+
function isTTY(output) {
|
|
1213
|
+
return output.isTTY === true;
|
|
1214
|
+
}
|
|
1215
|
+
function createRenderer(output) {
|
|
1216
|
+
let pendingHeight = 0;
|
|
1217
|
+
let committedHeight = 0;
|
|
1218
|
+
const render = (content) => {
|
|
1219
|
+
if (!isTTY(output)) {
|
|
1220
|
+
output.write(content);
|
|
1221
|
+
return;
|
|
844
1222
|
}
|
|
845
|
-
|
|
846
|
-
|
|
1223
|
+
if (pendingHeight > 0) {
|
|
1224
|
+
readline2.moveCursor(output, 0, -pendingHeight);
|
|
1225
|
+
readline2.cursorTo(output, 0);
|
|
1226
|
+
readline2.clearScreenDown(output);
|
|
1227
|
+
}
|
|
1228
|
+
output.write(content);
|
|
1229
|
+
pendingHeight = content.split("\n").length - 1;
|
|
1230
|
+
};
|
|
1231
|
+
const flush = () => {
|
|
1232
|
+
committedHeight += pendingHeight;
|
|
1233
|
+
pendingHeight = 0;
|
|
1234
|
+
};
|
|
1235
|
+
const clearAll = () => {
|
|
1236
|
+
if (!isTTY(output)) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const totalHeight = pendingHeight + committedHeight;
|
|
1240
|
+
if (totalHeight > 0) {
|
|
1241
|
+
readline2.moveCursor(output, 0, -totalHeight);
|
|
1242
|
+
readline2.cursorTo(output, 0);
|
|
1243
|
+
readline2.clearScreenDown(output);
|
|
1244
|
+
}
|
|
1245
|
+
pendingHeight = 0;
|
|
1246
|
+
committedHeight = 0;
|
|
1247
|
+
};
|
|
1248
|
+
const reset = () => {
|
|
1249
|
+
pendingHeight = 0;
|
|
1250
|
+
committedHeight = 0;
|
|
1251
|
+
};
|
|
1252
|
+
return { render, flush, clearAll, reset };
|
|
847
1253
|
}
|
|
1254
|
+
|
|
1255
|
+
// lib/selector.ts
|
|
1256
|
+
var TTY_PATH = "/dev/tty";
|
|
848
1257
|
function collapseToReady(slots) {
|
|
849
1258
|
const readySlots = slots.filter((s) => s.status === "ready");
|
|
850
1259
|
slots.length = 0;
|
|
@@ -859,6 +1268,11 @@ function formatSlot(slot, selected) {
|
|
|
859
1268
|
const meta2 = slot.model ? `${theme.dim} ${formatModelName(slot.model)}${theme.reset}` : "";
|
|
860
1269
|
return meta2 ? [line2, meta2] : [line2];
|
|
861
1270
|
}
|
|
1271
|
+
if (slot.status === "error") {
|
|
1272
|
+
const radio2 = "\u25CB";
|
|
1273
|
+
const line2 = `${theme.dim} ${radio2} ${slot.content}${theme.reset}`;
|
|
1274
|
+
return [line2];
|
|
1275
|
+
}
|
|
862
1276
|
const candidate = slot.candidate;
|
|
863
1277
|
const title = candidate.content.split("\n")[0]?.trim() || "";
|
|
864
1278
|
const modelInfo = candidate.model ? candidate.cost ? `${formatModelName(candidate.model)} ${formatCost(candidate.cost)}` : formatModelName(candidate.model) : "";
|
|
@@ -873,9 +1287,6 @@ function formatSlot(slot, selected) {
|
|
|
873
1287
|
const meta = modelInfo ? `${theme.dim} ${modelInfo}${theme.reset}` : "";
|
|
874
1288
|
return meta ? [line, meta] : [line];
|
|
875
1289
|
}
|
|
876
|
-
function formatTotalCostLabel(cost) {
|
|
877
|
-
return `$${cost.toFixed(6)}`;
|
|
878
|
-
}
|
|
879
1290
|
function renderSelector(state, nowMs, renderer) {
|
|
880
1291
|
const { slots, selectedIndex, isGenerating, totalSlots } = state;
|
|
881
1292
|
const lines = [];
|
|
@@ -927,10 +1338,10 @@ function renderError(error, slots, totalSlots, output) {
|
|
|
927
1338
|
}
|
|
928
1339
|
}
|
|
929
1340
|
function openEditor(content) {
|
|
930
|
-
return new Promise((
|
|
1341
|
+
return new Promise((resolve3, reject) => {
|
|
931
1342
|
const editor = process.env.GIT_EDITOR || process.env.EDITOR || "vi";
|
|
932
|
-
const tmpDir = mkdtempSync(
|
|
933
|
-
const tmpFile =
|
|
1343
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "ultrahope-"));
|
|
1344
|
+
const tmpFile = join4(tmpDir, "EDIT_MESSAGE");
|
|
934
1345
|
writeFileSync(tmpFile, content);
|
|
935
1346
|
const child = spawn(editor, [tmpFile], { stdio: "inherit" });
|
|
936
1347
|
child.on("close", (code) => {
|
|
@@ -939,9 +1350,9 @@ function openEditor(content) {
|
|
|
939
1350
|
reject(new Error(`Editor exited with code ${code}`));
|
|
940
1351
|
return;
|
|
941
1352
|
}
|
|
942
|
-
const result =
|
|
1353
|
+
const result = readFileSync2(tmpFile, "utf-8").trim();
|
|
943
1354
|
unlinkSync(tmpFile);
|
|
944
|
-
|
|
1355
|
+
resolve3(result);
|
|
945
1356
|
});
|
|
946
1357
|
child.on("error", (err) => {
|
|
947
1358
|
try {
|
|
@@ -1001,12 +1412,12 @@ async function selectCandidate(options) {
|
|
|
1001
1412
|
);
|
|
1002
1413
|
}
|
|
1003
1414
|
async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
1004
|
-
return new Promise((
|
|
1415
|
+
return new Promise((resolve3) => {
|
|
1005
1416
|
let resolved = false;
|
|
1006
1417
|
const resolveOnce = (result) => {
|
|
1007
1418
|
if (resolved) return;
|
|
1008
1419
|
resolved = true;
|
|
1009
|
-
|
|
1420
|
+
resolve3(result);
|
|
1010
1421
|
};
|
|
1011
1422
|
const slots = [...initialSlots];
|
|
1012
1423
|
const state = {
|
|
@@ -1066,10 +1477,10 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1066
1477
|
};
|
|
1067
1478
|
const nextCandidate = async (iterator) => {
|
|
1068
1479
|
const abortPromise = new Promise(
|
|
1069
|
-
(
|
|
1480
|
+
(resolve4) => {
|
|
1070
1481
|
asyncCtx?.abortController.signal.addEventListener(
|
|
1071
1482
|
"abort",
|
|
1072
|
-
() =>
|
|
1483
|
+
() => resolve4({ done: true, value: void 0 }),
|
|
1073
1484
|
{ once: true }
|
|
1074
1485
|
);
|
|
1075
1486
|
}
|
|
@@ -1094,9 +1505,12 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1094
1505
|
const result = await nextCandidate(iterator);
|
|
1095
1506
|
if (result.done || cleanedUp) break;
|
|
1096
1507
|
const candidate = result.value;
|
|
1097
|
-
const targetIndex = slots.findIndex(
|
|
1098
|
-
(slot
|
|
1099
|
-
|
|
1508
|
+
const targetIndex = slots.findIndex((slot) => {
|
|
1509
|
+
if (slot.status === "ready") {
|
|
1510
|
+
return slot.candidate.slotId === candidate.slotId;
|
|
1511
|
+
}
|
|
1512
|
+
return slot.status === "pending" && slot.slotId === candidate.slotId;
|
|
1513
|
+
});
|
|
1100
1514
|
if (targetIndex >= 0 && targetIndex < slots.length) {
|
|
1101
1515
|
const isNewSlot = slots[targetIndex].status === "pending";
|
|
1102
1516
|
updateState((draft) => {
|
|
@@ -1115,10 +1529,15 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1115
1529
|
}
|
|
1116
1530
|
if (!cleanedUp) {
|
|
1117
1531
|
cancelGeneration();
|
|
1532
|
+
if (err instanceof InvalidModelError) {
|
|
1533
|
+
cleanup();
|
|
1534
|
+
resolveOnce({ action: "abort", error: err });
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1118
1537
|
renderer.clearAll();
|
|
1119
1538
|
renderError(err, slots, state.totalSlots, ttyOutput);
|
|
1120
1539
|
cleanup(false);
|
|
1121
|
-
resolveOnce({ action: "abort" });
|
|
1540
|
+
resolveOnce({ action: "abort", error: err });
|
|
1122
1541
|
}
|
|
1123
1542
|
} finally {
|
|
1124
1543
|
iterator.return?.();
|
|
@@ -1232,173 +1651,169 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1232
1651
|
});
|
|
1233
1652
|
}
|
|
1234
1653
|
|
|
1235
|
-
// lib/
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
return
|
|
1654
|
+
// lib/stream-capture.ts
|
|
1655
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1656
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
1657
|
+
var noopRecorder = {
|
|
1658
|
+
enabled: false,
|
|
1659
|
+
startGeneration: () => 0,
|
|
1660
|
+
recordEvent: () => {
|
|
1661
|
+
},
|
|
1662
|
+
finishGeneration: () => {
|
|
1663
|
+
},
|
|
1664
|
+
flush: () => null
|
|
1665
|
+
};
|
|
1666
|
+
function sanitizeEvent(event) {
|
|
1667
|
+
if (event.type === "provider-metadata") {
|
|
1668
|
+
return {
|
|
1669
|
+
type: "provider-metadata",
|
|
1670
|
+
providerMetadata: event.providerMetadata
|
|
1671
|
+
};
|
|
1250
1672
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
useStream = false
|
|
1261
|
-
} = options;
|
|
1262
|
-
const resolvedCliSessionId = cliSessionId;
|
|
1263
|
-
if (!resolvedCliSessionId) {
|
|
1264
|
-
throw new Error("Missing cliSessionId for generate request.");
|
|
1673
|
+
if (event.type === "usage") {
|
|
1674
|
+
return {
|
|
1675
|
+
type: "usage",
|
|
1676
|
+
usage: {
|
|
1677
|
+
inputTokens: event.usage.inputTokens,
|
|
1678
|
+
outputTokens: event.usage.outputTokens,
|
|
1679
|
+
totalTokens: event.usage.totalTokens
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1265
1682
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1683
|
+
if (event.type === "commit-message") {
|
|
1684
|
+
return {
|
|
1685
|
+
type: "commit-message",
|
|
1686
|
+
commitMessage: event.commitMessage
|
|
1687
|
+
};
|
|
1271
1688
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1689
|
+
return {
|
|
1690
|
+
type: "error",
|
|
1691
|
+
message: event.message
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function toSerializableCapture(capture) {
|
|
1695
|
+
return {
|
|
1696
|
+
...capture,
|
|
1697
|
+
runs: capture.runs.map((run) => ({
|
|
1698
|
+
...run,
|
|
1699
|
+
args: [...run.args],
|
|
1700
|
+
generations: run.generations.map((generation) => ({
|
|
1701
|
+
...generation,
|
|
1702
|
+
models: [...generation.models],
|
|
1703
|
+
events: generation.events.map((event) => ({
|
|
1704
|
+
atMs: event.atMs,
|
|
1705
|
+
model: event.model,
|
|
1706
|
+
attempt: event.attempt,
|
|
1707
|
+
event: sanitizeEvent(event.event)
|
|
1708
|
+
}))
|
|
1709
|
+
}))
|
|
1710
|
+
}))
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function createStreamCaptureRecorder(options) {
|
|
1714
|
+
if (!options.path) {
|
|
1715
|
+
return noopRecorder;
|
|
1716
|
+
}
|
|
1717
|
+
const outputPath = resolve2(process.cwd(), options.path);
|
|
1718
|
+
const runStartedAtMs = Date.now();
|
|
1719
|
+
const run = {
|
|
1720
|
+
id: "run-1",
|
|
1721
|
+
command: options.command,
|
|
1722
|
+
args: options.args,
|
|
1723
|
+
apiPath: options.apiPath,
|
|
1724
|
+
startedAt: new Date(runStartedAtMs).toISOString(),
|
|
1725
|
+
generations: []
|
|
1726
|
+
};
|
|
1727
|
+
const capture = {
|
|
1728
|
+
version: 2,
|
|
1729
|
+
source: "ultrahope-commit-message-stream",
|
|
1730
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1731
|
+
runs: [run]
|
|
1732
|
+
};
|
|
1733
|
+
let generationSequence = 0;
|
|
1734
|
+
let runFinished = false;
|
|
1735
|
+
const activeGenerations = /* @__PURE__ */ new Map();
|
|
1736
|
+
const writeCapture = () => {
|
|
1737
|
+
capture.capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1738
|
+
mkdirSync2(dirname3(outputPath), { recursive: true });
|
|
1739
|
+
writeFileSync2(
|
|
1740
|
+
outputPath,
|
|
1741
|
+
`${JSON.stringify(toSerializableCapture(capture), null, 2)}
|
|
1742
|
+
`,
|
|
1743
|
+
"utf-8"
|
|
1744
|
+
);
|
|
1745
|
+
};
|
|
1746
|
+
const ensureRunFinished = () => {
|
|
1747
|
+
if (runFinished) {
|
|
1748
|
+
return;
|
|
1304
1749
|
}
|
|
1305
|
-
|
|
1750
|
+
runFinished = true;
|
|
1751
|
+
run.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1306
1752
|
};
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
} else if (event.type === "provider-metadata") {
|
|
1329
|
-
providerMetadata = event.providerMetadata;
|
|
1330
|
-
} else if (event.type === "error") {
|
|
1331
|
-
throw new Error(event.message);
|
|
1332
|
-
}
|
|
1753
|
+
return {
|
|
1754
|
+
enabled: true,
|
|
1755
|
+
startGeneration: ({ cliSessionId, models }) => {
|
|
1756
|
+
generationSequence += 1;
|
|
1757
|
+
const startedAtMs = Date.now();
|
|
1758
|
+
const generation = {
|
|
1759
|
+
id: `generation-${generationSequence}`,
|
|
1760
|
+
cliSessionId,
|
|
1761
|
+
models: [...models],
|
|
1762
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
1763
|
+
events: []
|
|
1764
|
+
};
|
|
1765
|
+
run.generations.push(generation);
|
|
1766
|
+
activeGenerations.set(generationSequence, { generation, startedAtMs });
|
|
1767
|
+
return generationSequence;
|
|
1768
|
+
},
|
|
1769
|
+
recordEvent: (generationId, record) => {
|
|
1770
|
+
const activeGeneration = activeGenerations.get(generationId);
|
|
1771
|
+
if (!activeGeneration) {
|
|
1772
|
+
return;
|
|
1333
1773
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1774
|
+
const eventAtMs = typeof record.event.atMs === "number" && Number.isFinite(record.event.atMs) ? record.event.atMs : Date.now() - activeGeneration.startedAtMs;
|
|
1775
|
+
activeGeneration.generation.events.push({
|
|
1776
|
+
atMs: Math.max(0, Math.round(eventAtMs)),
|
|
1777
|
+
model: record.model,
|
|
1778
|
+
attempt: record.attempt,
|
|
1779
|
+
event: sanitizeEvent(record.event)
|
|
1780
|
+
});
|
|
1781
|
+
},
|
|
1782
|
+
finishGeneration: (generationId) => {
|
|
1783
|
+
const activeGeneration = activeGenerations.get(generationId);
|
|
1784
|
+
if (!activeGeneration) {
|
|
1785
|
+
return;
|
|
1345
1786
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
index
|
|
1355
|
-
}));
|
|
1356
|
-
const pending = /* @__PURE__ */ new Map();
|
|
1357
|
-
const startNext = (it) => {
|
|
1358
|
-
const promise = it.iterator.next().then((result) => ({
|
|
1359
|
-
result,
|
|
1360
|
-
index: it.index
|
|
1361
|
-
}));
|
|
1362
|
-
pending.set(it.index, {
|
|
1363
|
-
iterator: it.iterator,
|
|
1364
|
-
promise,
|
|
1365
|
-
index: it.index
|
|
1366
|
-
});
|
|
1367
|
-
};
|
|
1368
|
-
for (const it of iterators) {
|
|
1369
|
-
startNext(it);
|
|
1370
|
-
}
|
|
1371
|
-
const abortPromise = createAbortPromise(signal);
|
|
1372
|
-
try {
|
|
1373
|
-
while (pending.size > 0) {
|
|
1374
|
-
if (signal?.aborted) break;
|
|
1375
|
-
const next = Promise.race(
|
|
1376
|
-
Array.from(pending.values()).map((p) => p.promise)
|
|
1377
|
-
);
|
|
1378
|
-
const winner = abortPromise ? await Promise.race([next, abortPromise]) : await next;
|
|
1379
|
-
if (!winner || signal?.aborted) break;
|
|
1380
|
-
const { result, index } = winner;
|
|
1381
|
-
const entry = pending.get(index);
|
|
1382
|
-
if (!entry) continue;
|
|
1383
|
-
if (result.done) {
|
|
1384
|
-
pending.delete(index);
|
|
1385
|
-
} else {
|
|
1386
|
-
yield result.value;
|
|
1387
|
-
startNext({ iterator: entry.iterator, index });
|
|
1787
|
+
activeGeneration.generation.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1788
|
+
activeGenerations.delete(generationId);
|
|
1789
|
+
writeCapture();
|
|
1790
|
+
},
|
|
1791
|
+
flush: () => {
|
|
1792
|
+
for (const [generationId, activeGeneration] of activeGenerations) {
|
|
1793
|
+
activeGeneration.generation.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1794
|
+
activeGenerations.delete(generationId);
|
|
1388
1795
|
}
|
|
1796
|
+
ensureRunFinished();
|
|
1797
|
+
writeCapture();
|
|
1798
|
+
return outputPath;
|
|
1389
1799
|
}
|
|
1390
|
-
}
|
|
1391
|
-
if (error instanceof InsufficientBalanceError) {
|
|
1392
|
-
console.error(
|
|
1393
|
-
"Error: Token balance exhausted. Upgrade your plan at https://ultrahope.dev/pricing"
|
|
1394
|
-
);
|
|
1395
|
-
process.exit(1);
|
|
1396
|
-
}
|
|
1397
|
-
throw error;
|
|
1398
|
-
}
|
|
1800
|
+
};
|
|
1399
1801
|
}
|
|
1400
1802
|
|
|
1401
1803
|
// commands/commit.ts
|
|
1804
|
+
function normalizeGuide(value) {
|
|
1805
|
+
if (!value) return void 0;
|
|
1806
|
+
const trimmed = value.trim();
|
|
1807
|
+
if (!trimmed) return void 0;
|
|
1808
|
+
return trimmed.length > 1024 ? trimmed.slice(0, 1024) : trimmed;
|
|
1809
|
+
}
|
|
1810
|
+
function exitWithInvalidModelError(error) {
|
|
1811
|
+
console.error(`Error: Model '${error.model}' is not supported.`);
|
|
1812
|
+
if (error.allowedModels.length > 0) {
|
|
1813
|
+
console.error(`Available models: ${error.allowedModels.join(", ")}`);
|
|
1814
|
+
}
|
|
1815
|
+
process.exit(1);
|
|
1816
|
+
}
|
|
1402
1817
|
function showQuotaInfo(quota) {
|
|
1403
1818
|
const { relative, local } = formatResetTime(quota.resetsAt);
|
|
1404
1819
|
console.log("");
|
|
@@ -1409,21 +1824,42 @@ function showQuotaInfo(quota) {
|
|
|
1409
1824
|
);
|
|
1410
1825
|
}
|
|
1411
1826
|
function parseArgs(args2) {
|
|
1412
|
-
let
|
|
1827
|
+
let cliModels;
|
|
1828
|
+
let captureStreamPath;
|
|
1829
|
+
let guide;
|
|
1413
1830
|
for (let i = 0; i < args2.length; i++) {
|
|
1414
1831
|
const arg = args2[i];
|
|
1415
|
-
if (arg === "--models"
|
|
1416
|
-
|
|
1832
|
+
if (arg === "--models") {
|
|
1833
|
+
const value = args2[i + 1];
|
|
1834
|
+
if (!value) {
|
|
1835
|
+
console.error("Error: --models requires a value");
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
}
|
|
1838
|
+
cliModels = parseModelsArg(value);
|
|
1839
|
+
i++;
|
|
1840
|
+
} else if (arg === "--capture-stream") {
|
|
1841
|
+
const value = args2[i + 1];
|
|
1842
|
+
if (!value) {
|
|
1843
|
+
console.error("Error: --capture-stream requires a file path");
|
|
1844
|
+
process.exit(1);
|
|
1845
|
+
}
|
|
1846
|
+
captureStreamPath = value;
|
|
1847
|
+
i++;
|
|
1848
|
+
} else if (arg === "--guide") {
|
|
1849
|
+
const value = args2[i + 1];
|
|
1850
|
+
if (!value) {
|
|
1851
|
+
console.error("Error: --guide requires a text value.");
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
guide = normalizeGuide(value);
|
|
1417
1855
|
i++;
|
|
1418
1856
|
}
|
|
1419
1857
|
}
|
|
1420
|
-
if (models.length === 0) {
|
|
1421
|
-
models = DEFAULT_MODELS;
|
|
1422
|
-
}
|
|
1423
1858
|
return {
|
|
1424
|
-
message: args2.includes("-m") || args2.includes("--message"),
|
|
1425
1859
|
interactive: !args2.includes("--no-interactive"),
|
|
1426
|
-
|
|
1860
|
+
cliModels,
|
|
1861
|
+
captureStreamPath,
|
|
1862
|
+
guide
|
|
1427
1863
|
};
|
|
1428
1864
|
}
|
|
1429
1865
|
function getStagedDiff() {
|
|
@@ -1436,31 +1872,6 @@ function getStagedDiff() {
|
|
|
1436
1872
|
process.exit(1);
|
|
1437
1873
|
}
|
|
1438
1874
|
}
|
|
1439
|
-
function openEditor2(initialMessage) {
|
|
1440
|
-
return new Promise((resolve, reject) => {
|
|
1441
|
-
const editor = process.env.GIT_EDITOR || process.env.EDITOR || "vi";
|
|
1442
|
-
const tmpDir = mkdtempSync2(join4(tmpdir2(), "ultrahope-"));
|
|
1443
|
-
const tmpFile = join4(tmpDir, "COMMIT_EDITMSG");
|
|
1444
|
-
writeFileSync2(tmpFile, initialMessage);
|
|
1445
|
-
const child = spawn2(editor, [tmpFile], {
|
|
1446
|
-
stdio: "inherit"
|
|
1447
|
-
});
|
|
1448
|
-
child.on("close", (code) => {
|
|
1449
|
-
if (code !== 0) {
|
|
1450
|
-
unlinkSync2(tmpFile);
|
|
1451
|
-
reject(new Error(`Editor exited with code ${code}`));
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
const message = readFileSync2(tmpFile, "utf-8").trim();
|
|
1455
|
-
unlinkSync2(tmpFile);
|
|
1456
|
-
resolve(message);
|
|
1457
|
-
});
|
|
1458
|
-
child.on("error", (err) => {
|
|
1459
|
-
unlinkSync2(tmpFile);
|
|
1460
|
-
reject(err);
|
|
1461
|
-
});
|
|
1462
|
-
});
|
|
1463
|
-
}
|
|
1464
1875
|
function commitWithMessage(message) {
|
|
1465
1876
|
try {
|
|
1466
1877
|
execSync2(`git commit -m ${JSON.stringify(message)}`, { stdio: "inherit" });
|
|
@@ -1470,6 +1881,13 @@ function commitWithMessage(message) {
|
|
|
1470
1881
|
}
|
|
1471
1882
|
async function commit(args2) {
|
|
1472
1883
|
const options = parseArgs(args2);
|
|
1884
|
+
const captureRecorder = createStreamCaptureRecorder({
|
|
1885
|
+
path: options.captureStreamPath,
|
|
1886
|
+
command: "git ultrahope commit",
|
|
1887
|
+
args: ["commit", ...args2],
|
|
1888
|
+
apiPath: "/v1/commit-message/stream"
|
|
1889
|
+
});
|
|
1890
|
+
const models = resolveModels(options.cliModels);
|
|
1473
1891
|
const diff = getStagedDiff();
|
|
1474
1892
|
if (!diff.trim()) {
|
|
1475
1893
|
console.error(
|
|
@@ -1477,123 +1895,123 @@ async function commit(args2) {
|
|
|
1477
1895
|
);
|
|
1478
1896
|
process.exit(1);
|
|
1479
1897
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
const api = createApiClient(token);
|
|
1486
|
-
const {
|
|
1487
|
-
commandExecutionPromise: promise,
|
|
1488
|
-
abortController,
|
|
1489
|
-
cliSessionId: id
|
|
1490
|
-
} = startCommandExecution({
|
|
1491
|
-
api,
|
|
1492
|
-
command: "commit",
|
|
1493
|
-
args: args2,
|
|
1494
|
-
apiPath: "/v1/commit-message",
|
|
1495
|
-
requestPayload: {
|
|
1496
|
-
input: diff,
|
|
1497
|
-
target: "vcs-commit-message",
|
|
1498
|
-
models: options.models
|
|
1898
|
+
try {
|
|
1899
|
+
const token = await getToken();
|
|
1900
|
+
if (!token) {
|
|
1901
|
+
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
1902
|
+
process.exit(1);
|
|
1499
1903
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1904
|
+
const api = createApiClient(token);
|
|
1905
|
+
const {
|
|
1906
|
+
commandExecutionPromise: promise,
|
|
1907
|
+
abortController,
|
|
1908
|
+
cliSessionId: id
|
|
1909
|
+
} = startCommandExecution({
|
|
1910
|
+
api,
|
|
1911
|
+
command: "commit",
|
|
1912
|
+
args: args2,
|
|
1913
|
+
apiPath: "/v1/commit-message",
|
|
1914
|
+
requestPayload: {
|
|
1915
|
+
input: diff,
|
|
1916
|
+
target: "vcs-commit-message",
|
|
1917
|
+
model: models[0],
|
|
1918
|
+
...options.guide ? { guide: options.guide } : {}
|
|
1919
|
+
}
|
|
1509
1920
|
});
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1921
|
+
const cliSessionId = id;
|
|
1922
|
+
const commandExecutionSignal = abortController.signal;
|
|
1923
|
+
const commandExecutionPromise = promise;
|
|
1924
|
+
const apiClient = api;
|
|
1925
|
+
commandExecutionPromise.catch(async (error) => {
|
|
1926
|
+
abortController.abort(abortReasonForError(error));
|
|
1927
|
+
await handleCommandExecutionError(error, {
|
|
1928
|
+
progress: { ready: 0, total: models.length }
|
|
1517
1929
|
});
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
if (
|
|
1521
|
-
|
|
1930
|
+
});
|
|
1931
|
+
const recordSelection = async (generationId) => {
|
|
1932
|
+
if (!generationId || !apiClient) return;
|
|
1933
|
+
try {
|
|
1934
|
+
await apiClient.recordGenerationScore({
|
|
1935
|
+
generationId,
|
|
1936
|
+
value: 1
|
|
1937
|
+
});
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1940
|
+
if (message.includes("Generation not found")) {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
console.error(`Warning: Failed to record selection. ${message}`);
|
|
1522
1944
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
};
|
|
1526
|
-
const createCandidates = (signal) => generateCommitMessages({
|
|
1527
|
-
diff,
|
|
1528
|
-
models: options.models,
|
|
1529
|
-
signal: mergeAbortSignals(signal, commandExecutionSignal),
|
|
1530
|
-
cliSessionId,
|
|
1531
|
-
commandExecutionPromise
|
|
1532
|
-
});
|
|
1533
|
-
if (!options.interactive) {
|
|
1534
|
-
const gen = generateCommitMessages({
|
|
1945
|
+
};
|
|
1946
|
+
const createCandidates = (signal) => generateCommitMessages({
|
|
1535
1947
|
diff,
|
|
1536
|
-
models
|
|
1537
|
-
|
|
1948
|
+
models,
|
|
1949
|
+
guide: options.guide,
|
|
1950
|
+
signal: mergeAbortSignals(signal, commandExecutionSignal),
|
|
1538
1951
|
cliSessionId,
|
|
1539
|
-
commandExecutionPromise
|
|
1952
|
+
commandExecutionPromise,
|
|
1953
|
+
streamCaptureRecorder: captureRecorder
|
|
1540
1954
|
});
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1955
|
+
if (!options.interactive) {
|
|
1956
|
+
const gen = generateCommitMessages({
|
|
1957
|
+
diff,
|
|
1958
|
+
models: models.slice(0, 1),
|
|
1959
|
+
guide: options.guide,
|
|
1960
|
+
signal: commandExecutionSignal,
|
|
1961
|
+
cliSessionId,
|
|
1962
|
+
commandExecutionPromise,
|
|
1963
|
+
streamCaptureRecorder: captureRecorder
|
|
1964
|
+
});
|
|
1965
|
+
const first = await gen.next().catch((error) => {
|
|
1966
|
+
if (error instanceof InvalidModelError) {
|
|
1967
|
+
exitWithInvalidModelError(error);
|
|
1968
|
+
}
|
|
1969
|
+
throw error;
|
|
1970
|
+
});
|
|
1971
|
+
await recordSelection(first.value?.generationId);
|
|
1972
|
+
const message = first.value?.content ?? "";
|
|
1545
1973
|
commitWithMessage(message);
|
|
1546
1974
|
return;
|
|
1547
1975
|
}
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
if (isCommandExecutionAbort(commandExecutionSignal)) {
|
|
1567
|
-
return;
|
|
1976
|
+
const stats = getGitStagedStats();
|
|
1977
|
+
console.log(ui.success(`Found ${formatDiffStats(stats)}`));
|
|
1978
|
+
while (true) {
|
|
1979
|
+
const result = await selectCandidate({
|
|
1980
|
+
createCandidates,
|
|
1981
|
+
maxSlots: models.length,
|
|
1982
|
+
abortSignal: commandExecutionSignal,
|
|
1983
|
+
models
|
|
1984
|
+
});
|
|
1985
|
+
if (result.action === "abort") {
|
|
1986
|
+
if (result.error instanceof InvalidModelError) {
|
|
1987
|
+
exitWithInvalidModelError(result.error);
|
|
1988
|
+
}
|
|
1989
|
+
if (isCommandExecutionAbort(commandExecutionSignal)) {
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
console.error("Aborting commit.");
|
|
1993
|
+
process.exit(1);
|
|
1568
1994
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
await recordSelection(result.selectedCandidate?.generationId);
|
|
1577
|
-
const costLabel = result.totalCost != null ? ` (total: ${formatTotalCost(result.totalCost)})` : "";
|
|
1578
|
-
console.log(ui.success(`Message selected${costLabel}`));
|
|
1579
|
-
if (options.message) {
|
|
1995
|
+
if (result.action === "reroll") {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (result.action === "confirm" && result.selected) {
|
|
1999
|
+
await recordSelection(result.selectedCandidate?.generationId);
|
|
2000
|
+
const costLabel = result.totalCost != null ? ` (total: ${formatTotalCost(result.totalCost)})` : "";
|
|
2001
|
+
console.log(ui.success(`Message selected${costLabel}`));
|
|
1580
2002
|
console.log(`${ui.success("Running git commit")}
|
|
1581
2003
|
`);
|
|
1582
2004
|
commitWithMessage(result.selected);
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if (!editedMessage) {
|
|
1586
|
-
console.error("Aborting commit due to empty message.");
|
|
1587
|
-
process.exit(1);
|
|
2005
|
+
if (result.quota) {
|
|
2006
|
+
showQuotaInfo(result.quota);
|
|
1588
2007
|
}
|
|
1589
|
-
|
|
1590
|
-
`);
|
|
1591
|
-
commitWithMessage(editedMessage);
|
|
1592
|
-
}
|
|
1593
|
-
if (result.quota) {
|
|
1594
|
-
showQuotaInfo(result.quota);
|
|
2008
|
+
return;
|
|
1595
2009
|
}
|
|
1596
|
-
|
|
2010
|
+
}
|
|
2011
|
+
} finally {
|
|
2012
|
+
const capturePath = captureRecorder.flush();
|
|
2013
|
+
if (capturePath) {
|
|
2014
|
+
console.log(ui.hint(`Captured stream replay to ${capturePath}`));
|
|
1597
2015
|
}
|
|
1598
2016
|
}
|
|
1599
2017
|
}
|
|
@@ -1623,13 +2041,16 @@ Commands:
|
|
|
1623
2041
|
commit Generate commit message from staged changes
|
|
1624
2042
|
|
|
1625
2043
|
Commit options:
|
|
1626
|
-
-
|
|
1627
|
-
--
|
|
2044
|
+
--no-interactive Single candidate, commit directly
|
|
2045
|
+
--guide <text> Additional context to guide message generation
|
|
2046
|
+
--models <list> Comma-separated model list (overrides config)
|
|
2047
|
+
--capture-stream <path> Save commit-message stream as replay JSON
|
|
1628
2048
|
|
|
1629
2049
|
Examples:
|
|
1630
2050
|
git ultrahope commit # interactive selector (default)
|
|
1631
|
-
git ultrahope commit -
|
|
1632
|
-
git ultrahope commit --
|
|
2051
|
+
git ultrahope commit --guide "GHSA-gq3j-xvxp-8hrf: override reason"
|
|
2052
|
+
git ultrahope commit --capture-stream /tmp/git-commit-stream.capture.json
|
|
2053
|
+
git ultrahope commit --no-interactive # single candidate, commit directly`);
|
|
1633
2054
|
}
|
|
1634
2055
|
main().catch((err) => {
|
|
1635
2056
|
console.error(err);
|