ultrahope 0.1.3 → 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 +12 -0
- package/dist/git-ultrahope.js +506 -260
- package/dist/index.js +594 -221
- package/package.json +1 -1
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 readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
7
|
-
import { join as join5 } 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) {
|
|
@@ -110,12 +129,17 @@ function parseSseEvents(buffer) {
|
|
|
110
129
|
return { events, remainder };
|
|
111
130
|
}
|
|
112
131
|
function handle402Error(error) {
|
|
113
|
-
const
|
|
114
|
-
if (typeof
|
|
132
|
+
const payload = error;
|
|
133
|
+
if (typeof payload?.balance === "number") {
|
|
115
134
|
log("generate error (402 insufficient_balance)", error);
|
|
116
|
-
|
|
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
|
+
);
|
|
117
142
|
}
|
|
118
|
-
const payload = error;
|
|
119
143
|
const count = typeof payload?.count === "number" ? payload.count : 0;
|
|
120
144
|
const limit = typeof payload?.limit === "number" ? payload.limit : 0;
|
|
121
145
|
const resetsAt = payload?.resetsAt ?? "";
|
|
@@ -238,12 +262,8 @@ function createApiClient(token) {
|
|
|
238
262
|
throw new UnauthorizedError();
|
|
239
263
|
}
|
|
240
264
|
if (response.status === 402) {
|
|
241
|
-
const payload = error;
|
|
242
|
-
const count = typeof payload?.count === "number" ? payload.count : 0;
|
|
243
|
-
const limit = typeof payload?.limit === "number" ? payload.limit : 0;
|
|
244
|
-
const resetsAt = payload?.resetsAt ?? "";
|
|
245
265
|
log("command_execution error (402)", error);
|
|
246
|
-
|
|
266
|
+
handle402Error(error);
|
|
247
267
|
}
|
|
248
268
|
if (!response.ok) {
|
|
249
269
|
const text = await getErrorText(response, error);
|
|
@@ -595,7 +615,7 @@ async function showDailyLimitPrompt(info) {
|
|
|
595
615
|
}
|
|
596
616
|
}
|
|
597
617
|
function promptChoice() {
|
|
598
|
-
return new Promise((
|
|
618
|
+
return new Promise((resolve3) => {
|
|
599
619
|
const fd = openSync("/dev/tty", "r");
|
|
600
620
|
const ttyInput = new tty.ReadStream(fd);
|
|
601
621
|
const rl = readline.createInterface({
|
|
@@ -618,17 +638,17 @@ function promptChoice() {
|
|
|
618
638
|
if (!key && !str) return;
|
|
619
639
|
if (str === "q" || str === "Q" || key?.name === "q" || key?.name === "c" && key.ctrl || key?.name === "escape") {
|
|
620
640
|
cleanup();
|
|
621
|
-
|
|
641
|
+
resolve3("q");
|
|
622
642
|
return;
|
|
623
643
|
}
|
|
624
644
|
if (str === "1" || key?.name === "1" || key?.sequence === "1") {
|
|
625
645
|
cleanup();
|
|
626
|
-
|
|
646
|
+
resolve3("1");
|
|
627
647
|
return;
|
|
628
648
|
}
|
|
629
649
|
if (str === "2" || key?.name === "2" || key?.sequence === "2") {
|
|
630
650
|
cleanup();
|
|
631
|
-
|
|
651
|
+
resolve3("2");
|
|
632
652
|
return;
|
|
633
653
|
}
|
|
634
654
|
};
|
|
@@ -645,7 +665,7 @@ function handleRetryLater() {
|
|
|
645
665
|
);
|
|
646
666
|
console.log(` ${ui.link("ultrahope jj describe")}`);
|
|
647
667
|
console.log("");
|
|
648
|
-
return new Promise((
|
|
668
|
+
return new Promise((resolve3) => {
|
|
649
669
|
const fd = openSync("/dev/tty", "r");
|
|
650
670
|
const ttyInput = new tty.ReadStream(fd);
|
|
651
671
|
const rl = readline.createInterface({
|
|
@@ -665,7 +685,7 @@ function handleRetryLater() {
|
|
|
665
685
|
if (!key && !str) return;
|
|
666
686
|
if (str === "\r" || str === "\n" || str === "q" || str === "Q" || key?.name === "return" || key?.name === "q" || key?.name === "c" && key.ctrl) {
|
|
667
687
|
cleanup();
|
|
668
|
-
|
|
688
|
+
resolve3();
|
|
669
689
|
}
|
|
670
690
|
};
|
|
671
691
|
ttyInput.on("keypress", handleKeypress);
|
|
@@ -729,6 +749,10 @@ async function handleCommandExecutionError(error, options) {
|
|
|
729
749
|
});
|
|
730
750
|
process.exit(1);
|
|
731
751
|
}
|
|
752
|
+
if (error instanceof InsufficientBalanceError) {
|
|
753
|
+
console.error(error.formatMessage());
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
732
756
|
const message = error instanceof Error ? error.message : String(error);
|
|
733
757
|
console.error(`Error: Failed to start command execution. ${message}`);
|
|
734
758
|
process.exit(1);
|
|
@@ -740,6 +764,81 @@ import * as os2 from "os";
|
|
|
740
764
|
import * as path2 from "path";
|
|
741
765
|
import { parse } from "smol-toml";
|
|
742
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
|
+
|
|
743
842
|
// lib/vcs-message-generator.ts
|
|
744
843
|
var DEFAULT_MODELS = [
|
|
745
844
|
// "mistral/ministral-3b",
|
|
@@ -750,14 +849,7 @@ var DEFAULT_MODELS = [
|
|
|
750
849
|
];
|
|
751
850
|
var isAbortError = (error) => error instanceof Error && error.name === "AbortError";
|
|
752
851
|
var isInvalidCliSessionIdError = (error) => error instanceof Error && error.message.includes("Invalid cliSessionId");
|
|
753
|
-
var delay = (ms) => new Promise((
|
|
754
|
-
var createAbortPromise = (signal) => signal ? new Promise((resolve2) => {
|
|
755
|
-
if (signal.aborted) {
|
|
756
|
-
resolve2(null);
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
signal.addEventListener("abort", () => resolve2(null), { once: true });
|
|
760
|
-
}) : null;
|
|
852
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
761
853
|
async function* generateCommitMessages(options) {
|
|
762
854
|
const {
|
|
763
855
|
diff,
|
|
@@ -765,13 +857,18 @@ async function* generateCommitMessages(options) {
|
|
|
765
857
|
signal,
|
|
766
858
|
cliSessionId,
|
|
767
859
|
commandExecutionPromise,
|
|
768
|
-
useStream = false
|
|
860
|
+
useStream = false,
|
|
861
|
+
streamCaptureRecorder
|
|
769
862
|
} = options;
|
|
770
863
|
const resolvedCliSessionId = cliSessionId;
|
|
771
864
|
if (!resolvedCliSessionId) {
|
|
772
865
|
throw new Error("Missing cliSessionId for generate request.");
|
|
773
866
|
}
|
|
774
867
|
const requiredCliSessionId = resolvedCliSessionId;
|
|
868
|
+
const captureGenerationId = streamCaptureRecorder?.startGeneration({
|
|
869
|
+
cliSessionId: requiredCliSessionId,
|
|
870
|
+
models
|
|
871
|
+
});
|
|
775
872
|
const token = await getToken();
|
|
776
873
|
if (!token) {
|
|
777
874
|
console.error("Error: Not authenticated. Run `ultrahope login` first.");
|
|
@@ -781,10 +878,18 @@ async function* generateCommitMessages(options) {
|
|
|
781
878
|
const generateWithRetry = async function* (payload) {
|
|
782
879
|
const maxAttempts = 3;
|
|
783
880
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
881
|
+
const attemptNumber = attempt + 1;
|
|
784
882
|
try {
|
|
785
883
|
for await (const event of api.streamCommitMessage(payload, {
|
|
786
884
|
signal
|
|
787
885
|
})) {
|
|
886
|
+
if (captureGenerationId != null) {
|
|
887
|
+
streamCaptureRecorder?.recordEvent(captureGenerationId, {
|
|
888
|
+
model: payload.model,
|
|
889
|
+
attempt: attemptNumber,
|
|
890
|
+
event
|
|
891
|
+
});
|
|
892
|
+
}
|
|
788
893
|
log("generate", event);
|
|
789
894
|
yield event;
|
|
790
895
|
}
|
|
@@ -820,7 +925,8 @@ async function* generateCommitMessages(options) {
|
|
|
820
925
|
for await (const event of generateWithRetry({
|
|
821
926
|
cliSessionId: requiredCliSessionId,
|
|
822
927
|
input: diff,
|
|
823
|
-
model
|
|
928
|
+
model,
|
|
929
|
+
guide: options.guide
|
|
824
930
|
})) {
|
|
825
931
|
if (event.type === "commit-message") {
|
|
826
932
|
lastCommitMessage = event.commitMessage;
|
|
@@ -858,55 +964,36 @@ async function* generateCommitMessages(options) {
|
|
|
858
964
|
if (isInvalidCliSessionIdError(error)) throw error;
|
|
859
965
|
}
|
|
860
966
|
}
|
|
861
|
-
const iterators = models.map(
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}));
|
|
865
|
-
const pending = /* @__PURE__ */ new Map();
|
|
866
|
-
const startNext = (it) => {
|
|
867
|
-
const promise = it.iterator.next().then((result) => ({
|
|
868
|
-
result,
|
|
869
|
-
index: it.index
|
|
870
|
-
}));
|
|
871
|
-
pending.set(it.index, {
|
|
872
|
-
iterator: it.iterator,
|
|
873
|
-
promise,
|
|
874
|
-
index: it.index
|
|
875
|
-
});
|
|
876
|
-
};
|
|
877
|
-
for (const it of iterators) {
|
|
878
|
-
startNext(it);
|
|
879
|
-
}
|
|
880
|
-
const abortPromise = createAbortPromise(signal);
|
|
967
|
+
const iterators = models.map(
|
|
968
|
+
(model, index) => generateForModel(model, index)[Symbol.asyncIterator]()
|
|
969
|
+
);
|
|
881
970
|
try {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (!entry) continue;
|
|
892
|
-
if (result.done) {
|
|
893
|
-
pending.delete(index);
|
|
894
|
-
} else {
|
|
895
|
-
yield result.value;
|
|
896
|
-
startNext({ iterator: entry.iterator, index });
|
|
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";
|
|
897
980
|
}
|
|
981
|
+
})) {
|
|
982
|
+
yield result.value;
|
|
898
983
|
}
|
|
899
984
|
} catch (error) {
|
|
900
985
|
if (error instanceof InvalidModelError) {
|
|
901
986
|
throw error;
|
|
902
987
|
}
|
|
903
988
|
if (error instanceof InsufficientBalanceError) {
|
|
904
|
-
console.error(
|
|
905
|
-
"Error: Token balance exhausted. Upgrade your plan at https://ultrahope.dev/pricing"
|
|
906
|
-
);
|
|
989
|
+
console.error(error.formatMessage());
|
|
907
990
|
process.exit(1);
|
|
908
991
|
}
|
|
909
992
|
throw error;
|
|
993
|
+
} finally {
|
|
994
|
+
if (captureGenerationId != null) {
|
|
995
|
+
streamCaptureRecorder?.finishGeneration(captureGenerationId);
|
|
996
|
+
}
|
|
910
997
|
}
|
|
911
998
|
}
|
|
912
999
|
|
|
@@ -1062,6 +1149,52 @@ import { join as join4 } from "path";
|
|
|
1062
1149
|
import * as readline3 from "readline";
|
|
1063
1150
|
import * as tty2 from "tty";
|
|
1064
1151
|
|
|
1152
|
+
// ../shared/terminal-selector-helpers.ts
|
|
1153
|
+
function formatModelName(model) {
|
|
1154
|
+
const parts = model.split("/");
|
|
1155
|
+
return parts.length > 1 ? parts[1] : model;
|
|
1156
|
+
}
|
|
1157
|
+
function formatCost(cost) {
|
|
1158
|
+
return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
1159
|
+
}
|
|
1160
|
+
function formatTotalCostLabel(cost) {
|
|
1161
|
+
return `$${cost.toFixed(6)}`;
|
|
1162
|
+
}
|
|
1163
|
+
function getReadyCount(slots) {
|
|
1164
|
+
return slots.filter((slot) => slot.status === "ready").length;
|
|
1165
|
+
}
|
|
1166
|
+
function getTotalCost(slots) {
|
|
1167
|
+
return slots.reduce((sum, slot) => {
|
|
1168
|
+
if (slot.status === "ready" && slot.candidate.cost != null) {
|
|
1169
|
+
return sum + slot.candidate.cost;
|
|
1170
|
+
}
|
|
1171
|
+
return sum;
|
|
1172
|
+
}, 0);
|
|
1173
|
+
}
|
|
1174
|
+
function getLatestQuota(slots) {
|
|
1175
|
+
for (const slot of slots) {
|
|
1176
|
+
if (slot.status === "ready" && slot.candidate.quota) {
|
|
1177
|
+
return slot.candidate.quota;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
function hasReadySlot(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;
|
|
1192
|
+
}
|
|
1193
|
+
function getSelectedCandidate(slots, selectedIndex) {
|
|
1194
|
+
const slot = slots[selectedIndex];
|
|
1195
|
+
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1065
1198
|
// lib/renderer.ts
|
|
1066
1199
|
import * as readline2 from "readline";
|
|
1067
1200
|
var SPINNER_FRAMES = [
|
|
@@ -1121,47 +1254,6 @@ function createRenderer(output) {
|
|
|
1121
1254
|
|
|
1122
1255
|
// lib/selector.ts
|
|
1123
1256
|
var TTY_PATH = "/dev/tty";
|
|
1124
|
-
function formatModelName(model) {
|
|
1125
|
-
const parts = model.split("/");
|
|
1126
|
-
return parts.length > 1 ? parts[1] : model;
|
|
1127
|
-
}
|
|
1128
|
-
function formatCost(cost) {
|
|
1129
|
-
return `$${cost.toFixed(7).replace(/0+$/, "").replace(/\.$/, "")}`;
|
|
1130
|
-
}
|
|
1131
|
-
function getReadyCount(slots) {
|
|
1132
|
-
return slots.filter((s) => s.status === "ready").length;
|
|
1133
|
-
}
|
|
1134
|
-
function getTotalCost(slots) {
|
|
1135
|
-
return slots.reduce((sum, slot) => {
|
|
1136
|
-
if (slot.status === "ready" && slot.candidate.cost != null) {
|
|
1137
|
-
return sum + slot.candidate.cost;
|
|
1138
|
-
}
|
|
1139
|
-
return sum;
|
|
1140
|
-
}, 0);
|
|
1141
|
-
}
|
|
1142
|
-
function getLatestQuota(slots) {
|
|
1143
|
-
for (const slot of slots) {
|
|
1144
|
-
if (slot.status === "ready" && slot.candidate.quota) {
|
|
1145
|
-
return slot.candidate.quota;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return void 0;
|
|
1149
|
-
}
|
|
1150
|
-
function hasReadySlot(slots) {
|
|
1151
|
-
return slots.some((s) => s.status === "ready");
|
|
1152
|
-
}
|
|
1153
|
-
function getSelectedCandidate(slots, selectedIndex) {
|
|
1154
|
-
const slot = slots[selectedIndex];
|
|
1155
|
-
return slot?.status === "ready" ? slot.candidate : void 0;
|
|
1156
|
-
}
|
|
1157
|
-
function selectNearestReady(slots, startIndex, direction) {
|
|
1158
|
-
for (let i = startIndex + direction; i >= 0 && i < slots.length; i += direction) {
|
|
1159
|
-
if (slots[i]?.status === "ready") {
|
|
1160
|
-
return i;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
return startIndex;
|
|
1164
|
-
}
|
|
1165
1257
|
function collapseToReady(slots) {
|
|
1166
1258
|
const readySlots = slots.filter((s) => s.status === "ready");
|
|
1167
1259
|
slots.length = 0;
|
|
@@ -1176,6 +1268,11 @@ function formatSlot(slot, selected) {
|
|
|
1176
1268
|
const meta2 = slot.model ? `${theme.dim} ${formatModelName(slot.model)}${theme.reset}` : "";
|
|
1177
1269
|
return meta2 ? [line2, meta2] : [line2];
|
|
1178
1270
|
}
|
|
1271
|
+
if (slot.status === "error") {
|
|
1272
|
+
const radio2 = "\u25CB";
|
|
1273
|
+
const line2 = `${theme.dim} ${radio2} ${slot.content}${theme.reset}`;
|
|
1274
|
+
return [line2];
|
|
1275
|
+
}
|
|
1179
1276
|
const candidate = slot.candidate;
|
|
1180
1277
|
const title = candidate.content.split("\n")[0]?.trim() || "";
|
|
1181
1278
|
const modelInfo = candidate.model ? candidate.cost ? `${formatModelName(candidate.model)} ${formatCost(candidate.cost)}` : formatModelName(candidate.model) : "";
|
|
@@ -1190,9 +1287,6 @@ function formatSlot(slot, selected) {
|
|
|
1190
1287
|
const meta = modelInfo ? `${theme.dim} ${modelInfo}${theme.reset}` : "";
|
|
1191
1288
|
return meta ? [line, meta] : [line];
|
|
1192
1289
|
}
|
|
1193
|
-
function formatTotalCostLabel(cost) {
|
|
1194
|
-
return `$${cost.toFixed(6)}`;
|
|
1195
|
-
}
|
|
1196
1290
|
function renderSelector(state, nowMs, renderer) {
|
|
1197
1291
|
const { slots, selectedIndex, isGenerating, totalSlots } = state;
|
|
1198
1292
|
const lines = [];
|
|
@@ -1244,7 +1338,7 @@ function renderError(error, slots, totalSlots, output) {
|
|
|
1244
1338
|
}
|
|
1245
1339
|
}
|
|
1246
1340
|
function openEditor(content) {
|
|
1247
|
-
return new Promise((
|
|
1341
|
+
return new Promise((resolve3, reject) => {
|
|
1248
1342
|
const editor = process.env.GIT_EDITOR || process.env.EDITOR || "vi";
|
|
1249
1343
|
const tmpDir = mkdtempSync(join4(tmpdir(), "ultrahope-"));
|
|
1250
1344
|
const tmpFile = join4(tmpDir, "EDIT_MESSAGE");
|
|
@@ -1258,7 +1352,7 @@ function openEditor(content) {
|
|
|
1258
1352
|
}
|
|
1259
1353
|
const result = readFileSync2(tmpFile, "utf-8").trim();
|
|
1260
1354
|
unlinkSync(tmpFile);
|
|
1261
|
-
|
|
1355
|
+
resolve3(result);
|
|
1262
1356
|
});
|
|
1263
1357
|
child.on("error", (err) => {
|
|
1264
1358
|
try {
|
|
@@ -1318,12 +1412,12 @@ async function selectCandidate(options) {
|
|
|
1318
1412
|
);
|
|
1319
1413
|
}
|
|
1320
1414
|
async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
1321
|
-
return new Promise((
|
|
1415
|
+
return new Promise((resolve3) => {
|
|
1322
1416
|
let resolved = false;
|
|
1323
1417
|
const resolveOnce = (result) => {
|
|
1324
1418
|
if (resolved) return;
|
|
1325
1419
|
resolved = true;
|
|
1326
|
-
|
|
1420
|
+
resolve3(result);
|
|
1327
1421
|
};
|
|
1328
1422
|
const slots = [...initialSlots];
|
|
1329
1423
|
const state = {
|
|
@@ -1383,10 +1477,10 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1383
1477
|
};
|
|
1384
1478
|
const nextCandidate = async (iterator) => {
|
|
1385
1479
|
const abortPromise = new Promise(
|
|
1386
|
-
(
|
|
1480
|
+
(resolve4) => {
|
|
1387
1481
|
asyncCtx?.abortController.signal.addEventListener(
|
|
1388
1482
|
"abort",
|
|
1389
|
-
() =>
|
|
1483
|
+
() => resolve4({ done: true, value: void 0 }),
|
|
1390
1484
|
{ once: true }
|
|
1391
1485
|
);
|
|
1392
1486
|
}
|
|
@@ -1411,9 +1505,12 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1411
1505
|
const result = await nextCandidate(iterator);
|
|
1412
1506
|
if (result.done || cleanedUp) break;
|
|
1413
1507
|
const candidate = result.value;
|
|
1414
|
-
const targetIndex = slots.findIndex(
|
|
1415
|
-
(slot
|
|
1416
|
-
|
|
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
|
+
});
|
|
1417
1514
|
if (targetIndex >= 0 && targetIndex < slots.length) {
|
|
1418
1515
|
const isNewSlot = slots[targetIndex].status === "pending";
|
|
1419
1516
|
updateState((draft) => {
|
|
@@ -1554,7 +1651,162 @@ async function selectFromSlots(initialSlots, asyncCtx, ttyIo) {
|
|
|
1554
1651
|
});
|
|
1555
1652
|
}
|
|
1556
1653
|
|
|
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
|
+
};
|
|
1672
|
+
}
|
|
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
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
if (event.type === "commit-message") {
|
|
1684
|
+
return {
|
|
1685
|
+
type: "commit-message",
|
|
1686
|
+
commitMessage: event.commitMessage
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
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;
|
|
1749
|
+
}
|
|
1750
|
+
runFinished = true;
|
|
1751
|
+
run.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1752
|
+
};
|
|
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;
|
|
1773
|
+
}
|
|
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;
|
|
1786
|
+
}
|
|
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);
|
|
1795
|
+
}
|
|
1796
|
+
ensureRunFinished();
|
|
1797
|
+
writeCapture();
|
|
1798
|
+
return outputPath;
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1557
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
|
+
}
|
|
1558
1810
|
function exitWithInvalidModelError(error) {
|
|
1559
1811
|
console.error(`Error: Model '${error.model}' is not supported.`);
|
|
1560
1812
|
if (error.allowedModels.length > 0) {
|
|
@@ -1573,6 +1825,8 @@ function showQuotaInfo(quota) {
|
|
|
1573
1825
|
}
|
|
1574
1826
|
function parseArgs(args2) {
|
|
1575
1827
|
let cliModels;
|
|
1828
|
+
let captureStreamPath;
|
|
1829
|
+
let guide;
|
|
1576
1830
|
for (let i = 0; i < args2.length; i++) {
|
|
1577
1831
|
const arg = args2[i];
|
|
1578
1832
|
if (arg === "--models") {
|
|
@@ -1583,12 +1837,29 @@ function parseArgs(args2) {
|
|
|
1583
1837
|
}
|
|
1584
1838
|
cliModels = parseModelsArg(value);
|
|
1585
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);
|
|
1855
|
+
i++;
|
|
1586
1856
|
}
|
|
1587
1857
|
}
|
|
1588
1858
|
return {
|
|
1589
|
-
message: args2.includes("-m") || args2.includes("--message"),
|
|
1590
1859
|
interactive: !args2.includes("--no-interactive"),
|
|
1591
|
-
cliModels
|
|
1860
|
+
cliModels,
|
|
1861
|
+
captureStreamPath,
|
|
1862
|
+
guide
|
|
1592
1863
|
};
|
|
1593
1864
|
}
|
|
1594
1865
|
function getStagedDiff() {
|
|
@@ -1601,31 +1872,6 @@ function getStagedDiff() {
|
|
|
1601
1872
|
process.exit(1);
|
|
1602
1873
|
}
|
|
1603
1874
|
}
|
|
1604
|
-
function openEditor2(initialMessage) {
|
|
1605
|
-
return new Promise((resolve2, reject) => {
|
|
1606
|
-
const editor = process.env.GIT_EDITOR || process.env.EDITOR || "vi";
|
|
1607
|
-
const tmpDir = mkdtempSync2(join5(tmpdir2(), "ultrahope-"));
|
|
1608
|
-
const tmpFile = join5(tmpDir, "COMMIT_EDITMSG");
|
|
1609
|
-
writeFileSync2(tmpFile, initialMessage);
|
|
1610
|
-
const child = spawn2(editor, [tmpFile], {
|
|
1611
|
-
stdio: "inherit"
|
|
1612
|
-
});
|
|
1613
|
-
child.on("close", (code) => {
|
|
1614
|
-
if (code !== 0) {
|
|
1615
|
-
unlinkSync2(tmpFile);
|
|
1616
|
-
reject(new Error(`Editor exited with code ${code}`));
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
const message = readFileSync3(tmpFile, "utf-8").trim();
|
|
1620
|
-
unlinkSync2(tmpFile);
|
|
1621
|
-
resolve2(message);
|
|
1622
|
-
});
|
|
1623
|
-
child.on("error", (err) => {
|
|
1624
|
-
unlinkSync2(tmpFile);
|
|
1625
|
-
reject(err);
|
|
1626
|
-
});
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1629
1875
|
function commitWithMessage(message) {
|
|
1630
1876
|
try {
|
|
1631
1877
|
execSync2(`git commit -m ${JSON.stringify(message)}`, { stdio: "inherit" });
|
|
@@ -1635,6 +1881,12 @@ function commitWithMessage(message) {
|
|
|
1635
1881
|
}
|
|
1636
1882
|
async function commit(args2) {
|
|
1637
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
|
+
});
|
|
1638
1890
|
const models = resolveModels(options.cliModels);
|
|
1639
1891
|
const diff = getStagedDiff();
|
|
1640
1892
|
if (!diff.trim()) {
|
|
@@ -1643,131 +1895,123 @@ async function commit(args2) {
|
|
|
1643
1895
|
);
|
|
1644
1896
|
process.exit(1);
|
|
1645
1897
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
const api = createApiClient(token);
|
|
1652
|
-
const {
|
|
1653
|
-
commandExecutionPromise: promise,
|
|
1654
|
-
abortController,
|
|
1655
|
-
cliSessionId: id
|
|
1656
|
-
} = startCommandExecution({
|
|
1657
|
-
api,
|
|
1658
|
-
command: "commit",
|
|
1659
|
-
args: args2,
|
|
1660
|
-
apiPath: "/v1/commit-message",
|
|
1661
|
-
requestPayload: {
|
|
1662
|
-
input: diff,
|
|
1663
|
-
target: "vcs-commit-message",
|
|
1664
|
-
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);
|
|
1665
1903
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
+
}
|
|
1675
1920
|
});
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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 }
|
|
1683
1929
|
});
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
if (
|
|
1687
|
-
|
|
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}`);
|
|
1688
1944
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
};
|
|
1692
|
-
const createCandidates = (signal) => generateCommitMessages({
|
|
1693
|
-
diff,
|
|
1694
|
-
models,
|
|
1695
|
-
signal: mergeAbortSignals(signal, commandExecutionSignal),
|
|
1696
|
-
cliSessionId,
|
|
1697
|
-
commandExecutionPromise
|
|
1698
|
-
});
|
|
1699
|
-
if (!options.interactive) {
|
|
1700
|
-
const gen = generateCommitMessages({
|
|
1945
|
+
};
|
|
1946
|
+
const createCandidates = (signal) => generateCommitMessages({
|
|
1701
1947
|
diff,
|
|
1702
|
-
models
|
|
1703
|
-
|
|
1948
|
+
models,
|
|
1949
|
+
guide: options.guide,
|
|
1950
|
+
signal: mergeAbortSignals(signal, commandExecutionSignal),
|
|
1704
1951
|
cliSessionId,
|
|
1705
|
-
commandExecutionPromise
|
|
1952
|
+
commandExecutionPromise,
|
|
1953
|
+
streamCaptureRecorder: captureRecorder
|
|
1706
1954
|
});
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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 ?? "";
|
|
1716
1973
|
commitWithMessage(message);
|
|
1717
1974
|
return;
|
|
1718
1975
|
}
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
if (result.error instanceof InvalidModelError) {
|
|
1738
|
-
exitWithInvalidModelError(result.error);
|
|
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);
|
|
1739
1994
|
}
|
|
1740
|
-
if (
|
|
1741
|
-
|
|
1995
|
+
if (result.action === "reroll") {
|
|
1996
|
+
continue;
|
|
1742
1997
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
continue;
|
|
1748
|
-
}
|
|
1749
|
-
if (result.action === "confirm" && result.selected) {
|
|
1750
|
-
await recordSelection(result.selectedCandidate?.generationId);
|
|
1751
|
-
const costLabel = result.totalCost != null ? ` (total: ${formatTotalCost(result.totalCost)})` : "";
|
|
1752
|
-
console.log(ui.success(`Message selected${costLabel}`));
|
|
1753
|
-
if (options.message) {
|
|
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}`));
|
|
1754
2002
|
console.log(`${ui.success("Running git commit")}
|
|
1755
2003
|
`);
|
|
1756
2004
|
commitWithMessage(result.selected);
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
if (!editedMessage) {
|
|
1760
|
-
console.error("Aborting commit due to empty message.");
|
|
1761
|
-
process.exit(1);
|
|
2005
|
+
if (result.quota) {
|
|
2006
|
+
showQuotaInfo(result.quota);
|
|
1762
2007
|
}
|
|
1763
|
-
|
|
1764
|
-
`);
|
|
1765
|
-
commitWithMessage(editedMessage);
|
|
1766
|
-
}
|
|
1767
|
-
if (result.quota) {
|
|
1768
|
-
showQuotaInfo(result.quota);
|
|
2008
|
+
return;
|
|
1769
2009
|
}
|
|
1770
|
-
|
|
2010
|
+
}
|
|
2011
|
+
} finally {
|
|
2012
|
+
const capturePath = captureRecorder.flush();
|
|
2013
|
+
if (capturePath) {
|
|
2014
|
+
console.log(ui.hint(`Captured stream replay to ${capturePath}`));
|
|
1771
2015
|
}
|
|
1772
2016
|
}
|
|
1773
2017
|
}
|
|
@@ -1797,14 +2041,16 @@ Commands:
|
|
|
1797
2041
|
commit Generate commit message from staged changes
|
|
1798
2042
|
|
|
1799
2043
|
Commit options:
|
|
1800
|
-
-
|
|
1801
|
-
--
|
|
2044
|
+
--no-interactive Single candidate, commit directly
|
|
2045
|
+
--guide <text> Additional context to guide message generation
|
|
1802
2046
|
--models <list> Comma-separated model list (overrides config)
|
|
2047
|
+
--capture-stream <path> Save commit-message stream as replay JSON
|
|
1803
2048
|
|
|
1804
2049
|
Examples:
|
|
1805
2050
|
git ultrahope commit # interactive selector (default)
|
|
1806
|
-
git ultrahope commit -
|
|
1807
|
-
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`);
|
|
1808
2054
|
}
|
|
1809
2055
|
main().catch((err) => {
|
|
1810
2056
|
console.error(err);
|