@heyditto/cli 2.0.1 → 2.2.0
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 +167 -1
- package/dist/agents/claude.d.ts +11 -0
- package/dist/agents/claude.js +48 -0
- package/dist/agents/claude.js.map +1 -0
- package/dist/agents/codex.d.ts +13 -0
- package/dist/agents/codex.js +48 -0
- package/dist/agents/codex.js.map +1 -0
- package/dist/agents/launch.d.ts +25 -0
- package/dist/agents/launch.js +376 -0
- package/dist/agents/launch.js.map +1 -0
- package/dist/agents/sessions.d.ts +28 -0
- package/dist/agents/sessions.js +63 -0
- package/dist/agents/sessions.js.map +1 -0
- package/dist/agents/types.d.ts +50 -0
- package/dist/agents/types.js +44 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/agents/worktree.d.ts +19 -0
- package/dist/agents/worktree.js +72 -0
- package/dist/agents/worktree.js.map +1 -0
- package/dist/api.d.ts +178 -0
- package/dist/api.js +163 -0
- package/dist/api.js.map +1 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +17 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.js +107 -24
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +77 -0
- package/dist/commands.js +638 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +18 -1
- package/dist/config.js.map +1 -1
- package/dist/device-login.d.ts +28 -0
- package/dist/device-login.js +43 -0
- package/dist/device-login.js.map +1 -0
- package/dist/endpoint-format.d.ts +9 -0
- package/dist/endpoint-format.js +21 -0
- package/dist/endpoint-format.js.map +1 -0
- package/dist/mcp-session.d.ts +40 -0
- package/dist/mcp-session.js +133 -0
- package/dist/mcp-session.js.map +1 -0
- package/dist/store.d.ts +26 -0
- package/dist/store.js +61 -0
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
package/dist/commands.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { Option } from "commander";
|
|
3
|
+
import { launchHarness, pickEndpoint } from "./agents/launch.js";
|
|
4
|
+
import { listSessions, removeSession } from "./agents/sessions.js";
|
|
5
|
+
import { HARNESSES, KEY_EXPIRIES } from "./agents/types.js";
|
|
6
|
+
import { createEndpoint, deleteEndpoint, findEndpoint, getEndpoint, isEndpointPending, listChatAgents, listEndpoints, listKeys, revokeKey, updateEndpoint, } from "./api.js";
|
|
7
|
+
import { openInBrowser } from "./browser.js";
|
|
8
|
+
import { endpointURL } from "./config.js";
|
|
9
|
+
import { activationLink, formatActivation } from "./endpoint-format.js";
|
|
10
|
+
import { SESSION_ENV, SESSION_ID_HEADER, endSession, readSessionHistory, resolveActiveSession, startSession, useSession, } from "./mcp-session.js";
|
|
11
|
+
import { readStoredAuth, updateStoredAuth } from "./store.js";
|
|
12
|
+
function pad(s, n) {
|
|
13
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
14
|
+
}
|
|
15
|
+
function spendColumn(e) {
|
|
16
|
+
const used = (e.spentTokens ?? 0).toLocaleString();
|
|
17
|
+
if (e.spendLimitTokens == null || e.spendLimitTokens < 0)
|
|
18
|
+
return `${used} / ∞`;
|
|
19
|
+
return `${used} / ${e.spendLimitTokens.toLocaleString()}${e.spendPeriod && e.spendPeriod !== "never" ? ` ${e.spendPeriod}` : ""}`;
|
|
20
|
+
}
|
|
21
|
+
function isJSON(options) {
|
|
22
|
+
return options.output === "json" || options.output === "raw";
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Endpoint controls spend the user's credits, so anything destructive asks
|
|
26
|
+
* the operator to type the slug back. `--yes` skips it for scripts; without a
|
|
27
|
+
* terminal and without `--yes` the command refuses.
|
|
28
|
+
*/
|
|
29
|
+
async function confirmElevated(action, slug, yes) {
|
|
30
|
+
if (yes)
|
|
31
|
+
return;
|
|
32
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
33
|
+
throw new Error(`refusing to ${action} "${slug}" without confirmation. Re-run with --yes to confirm.`);
|
|
34
|
+
}
|
|
35
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
36
|
+
try {
|
|
37
|
+
const typed = (await rl.question(`Type the endpoint slug (${slug}) to ${action}: `)).trim();
|
|
38
|
+
if (typed !== slug)
|
|
39
|
+
throw new Error(`aborted: "${typed}" did not match "${slug}"`);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rl.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Prints the backend's activation notice (with the claim token merged in) for inactive endpoints. */
|
|
46
|
+
async function noteActivation(endpoints) {
|
|
47
|
+
const pending = endpoints.filter(isEndpointPending);
|
|
48
|
+
if (pending.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
const stored = await readStoredAuth();
|
|
51
|
+
for (const e of pending) {
|
|
52
|
+
process.stderr.write(`\n! ${e.slug} is not active yet.\n${formatActivation(e, stored?.claimURL)}\n\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** JSON view of an endpoint with the activation link resolved for this install. */
|
|
56
|
+
async function endpointJSON(e) {
|
|
57
|
+
const stored = await readStoredAuth();
|
|
58
|
+
const link = activationLink(e, stored?.claimURL);
|
|
59
|
+
return {
|
|
60
|
+
...e,
|
|
61
|
+
...(e.activation ? { activation: { ...e.activation, ...(link ? { url: link } : {}) } } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function cmdEndpoints(options) {
|
|
65
|
+
if (options.setDefault && options.clearDefault)
|
|
66
|
+
throw new Error("use either --set-default or --clear-default");
|
|
67
|
+
const catalog = await listEndpoints();
|
|
68
|
+
if (options.clearDefault) {
|
|
69
|
+
await updateStoredAuth({ defaultEndpoint: undefined });
|
|
70
|
+
process.stderr.write("Cleared the default endpoint.\n");
|
|
71
|
+
}
|
|
72
|
+
if (options.setDefault) {
|
|
73
|
+
const wanted = options.setDefault.trim();
|
|
74
|
+
const match = findEndpoint(catalog.endpoints, wanted);
|
|
75
|
+
if (!match) {
|
|
76
|
+
throw new Error(`no endpoint named "${wanted}". Available: ${catalog.endpoints.map((e) => e.slug).join(", ") || "(none)"}`);
|
|
77
|
+
}
|
|
78
|
+
await updateStoredAuth({ defaultEndpoint: match.slug });
|
|
79
|
+
process.stderr.write(`Default endpoint set to ${match.slug}.\n`);
|
|
80
|
+
}
|
|
81
|
+
const defaultSlug = (await readStoredAuth())?.defaultEndpoint;
|
|
82
|
+
if (isJSON(options)) {
|
|
83
|
+
const endpoints = await Promise.all(catalog.endpoints.map(endpointJSON));
|
|
84
|
+
process.stdout.write(`${JSON.stringify({ ...catalog, endpoints, defaultEndpoint: defaultSlug ?? null }, null, 2)}\n`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (catalog.endpoints.length === 0) {
|
|
88
|
+
process.stdout.write(`No inference endpoints yet. Create one with \`heyditto endpoints create\`, or at ${endpointURL()}.\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const rows = catalog.endpoints.map((e) => [
|
|
92
|
+
e.slug === defaultSlug ? "*" : " ",
|
|
93
|
+
e.slug,
|
|
94
|
+
e.model,
|
|
95
|
+
spendColumn(e),
|
|
96
|
+
[isEndpointPending(e) ? "inactive" : "", e.recordTrace ? "traces" : ""].filter(Boolean).join(" "),
|
|
97
|
+
]);
|
|
98
|
+
const widths = [1, 0, 0, 0];
|
|
99
|
+
for (const r of rows)
|
|
100
|
+
for (let i = 1; i < 4; i++)
|
|
101
|
+
widths[i] = Math.max(widths[i], r[i].length);
|
|
102
|
+
process.stdout.write(` ${pad("SLUG", widths[1])} ${pad("MODEL", widths[2])} ${pad("SPEND (tokens)", widths[3])}\n`);
|
|
103
|
+
for (const r of rows) {
|
|
104
|
+
process.stdout.write(`${r[0]} ${pad(r[1], widths[1])} ${pad(r[2], widths[2])} ${pad(r[3], widths[3])} ${r[4]}\n`);
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write(`\nGateway: ${catalog.baseUrl}${defaultSlug ? ` (* = default)` : ""}\n`);
|
|
107
|
+
await noteActivation(catalog.endpoints);
|
|
108
|
+
}
|
|
109
|
+
export async function cmdEndpointCreate(options) {
|
|
110
|
+
const input = {};
|
|
111
|
+
if (options.name?.trim())
|
|
112
|
+
input.name = options.name.trim();
|
|
113
|
+
if (options.slug?.trim())
|
|
114
|
+
input.slug = options.slug.trim().toLowerCase();
|
|
115
|
+
if (options.model?.trim())
|
|
116
|
+
input.model = options.model.trim();
|
|
117
|
+
const created = await createEndpoint(input);
|
|
118
|
+
const stored = await readStoredAuth();
|
|
119
|
+
const makeDefault = options.default || !stored?.defaultEndpoint;
|
|
120
|
+
if (makeDefault)
|
|
121
|
+
await updateStoredAuth({ defaultEndpoint: created.slug });
|
|
122
|
+
if (isJSON(options)) {
|
|
123
|
+
process.stdout.write(`${JSON.stringify({ ...(await endpointJSON(created)), defaultEndpoint: makeDefault ? created.slug : (stored?.defaultEndpoint ?? null) }, null, 2)}\n`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
process.stdout.write(`Created endpoint ${created.slug} (model ${created.model})${makeDefault ? " — now the default" : ""}.\n`);
|
|
127
|
+
if (!isEndpointPending(created)) {
|
|
128
|
+
process.stdout.write(`Launch with: heyditto claude --endpoint ${created.slug}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
await noteActivation([created]);
|
|
132
|
+
}
|
|
133
|
+
export async function cmdEndpointShow(ref, options) {
|
|
134
|
+
const { endpoint, catalog } = await getEndpoint(ref);
|
|
135
|
+
const defaultSlug = (await readStoredAuth())?.defaultEndpoint;
|
|
136
|
+
if (isJSON(options)) {
|
|
137
|
+
process.stdout.write(`${JSON.stringify({ ...(await endpointJSON(endpoint)), baseUrl: catalog.baseUrl, isDefault: endpoint.slug === defaultSlug }, null, 2)}\n`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const lines = [
|
|
141
|
+
`slug: ${endpoint.slug}${endpoint.slug === defaultSlug ? " (default)" : ""}`,
|
|
142
|
+
`name: ${endpoint.name}`,
|
|
143
|
+
`id: ${endpoint.id}`,
|
|
144
|
+
`model: ${endpoint.model}${endpoint.modelMode ? ` (${endpoint.modelMode})` : ""}`,
|
|
145
|
+
`status: ${endpoint.status ?? "active"}`,
|
|
146
|
+
`spend: ${spendColumn(endpoint)}`,
|
|
147
|
+
`memory: recall ${endpoint.recallEnabled === false ? "off" : "on"}, record ${endpoint.recordEnabled === false ? "off" : "on"}${endpoint.memoryDepth !== undefined ? `, depth ${endpoint.memoryDepth}` : ""}`,
|
|
148
|
+
`traces: ${endpoint.recordTrace ? "on" : "off"}`,
|
|
149
|
+
`tools: ${(endpoint.tools ?? []).join(", ") || "(none)"}`,
|
|
150
|
+
`gateway: ${catalog.baseUrl}`,
|
|
151
|
+
`web: ${endpointURL(endpoint.id)}`,
|
|
152
|
+
];
|
|
153
|
+
if (endpoint.systemPrompt)
|
|
154
|
+
lines.push(`system prompt: ${endpoint.systemPrompt.length > 120 ? `${endpoint.systemPrompt.slice(0, 117)}…` : endpoint.systemPrompt}`);
|
|
155
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
156
|
+
await noteActivation([endpoint]);
|
|
157
|
+
}
|
|
158
|
+
export async function cmdEndpointUse(ref, options) {
|
|
159
|
+
const { endpoint } = await getEndpoint(ref);
|
|
160
|
+
await updateStoredAuth({ defaultEndpoint: endpoint.slug });
|
|
161
|
+
if (isJSON(options)) {
|
|
162
|
+
process.stdout.write(`${JSON.stringify({ defaultEndpoint: endpoint.slug }, null, 2)}\n`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(`Default endpoint set to ${endpoint.slug}.\n`);
|
|
166
|
+
await noteActivation([endpoint]);
|
|
167
|
+
}
|
|
168
|
+
export async function cmdEndpointPick(options) {
|
|
169
|
+
const catalog = await listEndpoints();
|
|
170
|
+
if (catalog.endpoints.length === 0) {
|
|
171
|
+
throw new Error("you have no inference endpoints yet. Create one with `heyditto endpoints create`.");
|
|
172
|
+
}
|
|
173
|
+
const stored = (await readStoredAuth())?.defaultEndpoint;
|
|
174
|
+
const picked = await pickEndpoint(catalog.endpoints, stored);
|
|
175
|
+
await updateStoredAuth({ defaultEndpoint: picked.slug });
|
|
176
|
+
if (isJSON(options)) {
|
|
177
|
+
process.stdout.write(`${JSON.stringify({ defaultEndpoint: picked.slug }, null, 2)}\n`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
process.stdout.write(`Default endpoint set to ${picked.slug}.\n`);
|
|
181
|
+
await noteActivation([picked]);
|
|
182
|
+
}
|
|
183
|
+
export async function cmdEndpointOpen(ref, options) {
|
|
184
|
+
// The developer console addresses endpoints by id; resolve the slug (or the
|
|
185
|
+
// stored default) through the catalog. No target → the endpoints list.
|
|
186
|
+
const target = ref ?? (await readStoredAuth())?.defaultEndpoint;
|
|
187
|
+
const id = target ? (await getEndpoint(target)).endpoint.id : undefined;
|
|
188
|
+
const url = endpointURL(id);
|
|
189
|
+
process.stdout.write(`${url}\n`);
|
|
190
|
+
if (!options.print) {
|
|
191
|
+
process.stderr.write("Opening in your browser…\n");
|
|
192
|
+
openInBrowser(url);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function onOff(flag, raw) {
|
|
196
|
+
if (raw === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
const v = raw.trim().toLowerCase();
|
|
199
|
+
if (v === "on" || v === "true" || v === "yes")
|
|
200
|
+
return true;
|
|
201
|
+
if (v === "off" || v === "false" || v === "no")
|
|
202
|
+
return false;
|
|
203
|
+
throw new Error(`${flag} must be on or off`);
|
|
204
|
+
}
|
|
205
|
+
export async function cmdEndpointSet(ref, options) {
|
|
206
|
+
const patch = {};
|
|
207
|
+
if (options.name !== undefined)
|
|
208
|
+
patch.name = options.name.trim();
|
|
209
|
+
if (options.model !== undefined)
|
|
210
|
+
patch.model = options.model.trim();
|
|
211
|
+
if (options.systemPrompt !== undefined)
|
|
212
|
+
patch.systemPrompt = options.systemPrompt;
|
|
213
|
+
if (options.spendLimit !== undefined) {
|
|
214
|
+
const raw = options.spendLimit.trim().toLowerCase();
|
|
215
|
+
if (raw === "none" || raw === "unlimited" || raw === "off") {
|
|
216
|
+
patch.spendLimitTokens = null;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const n = Number(raw.replace(/[_,]/g, ""));
|
|
220
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
221
|
+
throw new Error(`--spend-limit must be a positive integer token count or "none", got "${options.spendLimit}"`);
|
|
222
|
+
patch.spendLimitTokens = n;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (options.spendPeriod !== undefined)
|
|
226
|
+
patch.spendPeriod = options.spendPeriod;
|
|
227
|
+
const recordTrace = onOff("--record-trace", options.recordTrace);
|
|
228
|
+
if (recordTrace !== undefined)
|
|
229
|
+
patch.recordTrace = recordTrace;
|
|
230
|
+
const recall = onOff("--recall", options.recall);
|
|
231
|
+
if (recall !== undefined)
|
|
232
|
+
patch.recallEnabled = recall;
|
|
233
|
+
const record = onOff("--record", options.record);
|
|
234
|
+
if (record !== undefined)
|
|
235
|
+
patch.recordEnabled = record;
|
|
236
|
+
if (options.memoryDepth !== undefined) {
|
|
237
|
+
const n = Number(options.memoryDepth);
|
|
238
|
+
if (!Number.isInteger(n) || n < 0 || n > 25)
|
|
239
|
+
throw new Error("--memory-depth must be an integer from 0 to 25");
|
|
240
|
+
patch.memoryDepth = n;
|
|
241
|
+
}
|
|
242
|
+
if (Object.keys(patch).length === 0)
|
|
243
|
+
throw new Error("nothing to change; pass at least one --flag (see `heyditto endpoints set --help`)");
|
|
244
|
+
const { endpoint } = await getEndpoint(ref);
|
|
245
|
+
// Raising or removing a spend cap lets the endpoint spend more credits.
|
|
246
|
+
const raisesSpend = patch.spendLimitTokens === null ||
|
|
247
|
+
(typeof patch.spendLimitTokens === "number" &&
|
|
248
|
+
endpoint.spendLimitTokens != null &&
|
|
249
|
+
endpoint.spendLimitTokens >= 0 &&
|
|
250
|
+
patch.spendLimitTokens > endpoint.spendLimitTokens) ||
|
|
251
|
+
(patch.spendPeriod !== undefined && patch.spendPeriod !== endpoint.spendPeriod && patch.spendPeriod === "never");
|
|
252
|
+
if (raisesSpend)
|
|
253
|
+
await confirmElevated("raise the spend limit of", endpoint.slug, options.yes);
|
|
254
|
+
const updated = await updateEndpoint(endpoint.id, patch);
|
|
255
|
+
if (updated.slug !== endpoint.slug && (await readStoredAuth())?.defaultEndpoint === endpoint.slug) {
|
|
256
|
+
await updateStoredAuth({ defaultEndpoint: updated.slug });
|
|
257
|
+
}
|
|
258
|
+
if (isJSON(options)) {
|
|
259
|
+
process.stdout.write(`${JSON.stringify(await endpointJSON(updated), null, 2)}\n`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
process.stdout.write(`Updated ${updated.slug}: ${Object.keys(patch).join(", ")}.\n`);
|
|
263
|
+
await noteActivation([updated]);
|
|
264
|
+
}
|
|
265
|
+
export async function cmdEndpointDelete(ref, options) {
|
|
266
|
+
const { endpoint } = await getEndpoint(ref);
|
|
267
|
+
await confirmElevated("delete", endpoint.slug, options.yes);
|
|
268
|
+
await deleteEndpoint(endpoint.id);
|
|
269
|
+
const stored = await readStoredAuth();
|
|
270
|
+
if (stored?.defaultEndpoint === endpoint.slug || stored?.defaultEndpoint === endpoint.id) {
|
|
271
|
+
await updateStoredAuth({ defaultEndpoint: undefined });
|
|
272
|
+
}
|
|
273
|
+
if (isJSON(options)) {
|
|
274
|
+
process.stdout.write(`${JSON.stringify({ deleted: endpoint.id, slug: endpoint.slug }, null, 2)}\n`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
process.stdout.write(`Deleted endpoint ${endpoint.slug}. Its keys stop working immediately; threads and traces are kept.\n`);
|
|
278
|
+
}
|
|
279
|
+
export async function cmdEndpointKeys(ref, options) {
|
|
280
|
+
const { endpoint } = await getEndpoint(ref);
|
|
281
|
+
const keys = await listKeys(endpoint.id);
|
|
282
|
+
if (isJSON(options)) {
|
|
283
|
+
process.stdout.write(`${JSON.stringify({ endpoint: { id: endpoint.id, slug: endpoint.slug }, keys }, null, 2)}\n`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (keys.length === 0) {
|
|
287
|
+
process.stdout.write(`No keys on ${endpoint.slug}. \`heyditto claude\` mints a temporary one per session.\n`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const rows = keys.map((k) => [
|
|
291
|
+
k.id,
|
|
292
|
+
`…${k.keyHint}`,
|
|
293
|
+
k.name,
|
|
294
|
+
k.revokedAt ? "revoked" : k.expiresAt ? `expires ${k.expiresAt.slice(0, 10)}` : "no expiry",
|
|
295
|
+
k.lastUsedAt ? `used ${k.lastUsedAt.slice(0, 16).replace("T", " ")}` : "",
|
|
296
|
+
]);
|
|
297
|
+
const header = ["ID", "KEY", "NAME", "STATE", "LAST USED"];
|
|
298
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
299
|
+
const line = (r) => r.map((c, i) => (i === r.length - 1 ? c : pad(c, widths[i]))).join(" ");
|
|
300
|
+
process.stdout.write(`${line(header)}\n`);
|
|
301
|
+
for (const r of rows)
|
|
302
|
+
process.stdout.write(`${line(r)}\n`);
|
|
303
|
+
}
|
|
304
|
+
export async function cmdEndpointKeysRevoke(ref, keyId, options) {
|
|
305
|
+
const { endpoint } = await getEndpoint(ref);
|
|
306
|
+
await confirmElevated(`revoke key ${keyId} on`, endpoint.slug, options.yes);
|
|
307
|
+
await revokeKey(endpoint.id, keyId);
|
|
308
|
+
if (isJSON(options)) {
|
|
309
|
+
process.stdout.write(`${JSON.stringify({ revoked: keyId, endpoint: endpoint.slug }, null, 2)}\n`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
process.stdout.write(`Revoked key ${keyId} on ${endpoint.slug}.\n`);
|
|
313
|
+
}
|
|
314
|
+
/** Registers the `endpoints` group; bare `heyditto endpoints [flags]` still lists. */
|
|
315
|
+
export function registerEndpointCommands(program, addExamples, outputOption) {
|
|
316
|
+
const endpoints = program
|
|
317
|
+
.command("endpoints")
|
|
318
|
+
.description("manage the inference endpoints used by heyditto claude / codex")
|
|
319
|
+
.summary("manage inference endpoints")
|
|
320
|
+
.showHelpAfterError()
|
|
321
|
+
.addHelpText("after", `
|
|
322
|
+
Endpoint controls spend your Ditto credits, so delete, key revocation and
|
|
323
|
+
spend-limit increases ask you to type the slug back (or pass --yes).`);
|
|
324
|
+
addExamples(endpoints
|
|
325
|
+
.command("list", { isDefault: true })
|
|
326
|
+
.description("list your inference endpoints (* = default)")
|
|
327
|
+
.option("--set-default <slug>", "endpoint to use when --endpoint is omitted")
|
|
328
|
+
.option("--clear-default", "forget the default endpoint")
|
|
329
|
+
.addOption(outputOption())
|
|
330
|
+
.action(cmdEndpoints), ` heyditto endpoints
|
|
331
|
+
heyditto endpoints --set-default my-endpoint
|
|
332
|
+
heyditto endpoints list --output json`);
|
|
333
|
+
addExamples(endpoints
|
|
334
|
+
.command("create")
|
|
335
|
+
.description("create an endpoint (one click: name, slug and model are generated when omitted)")
|
|
336
|
+
.option("--name <name>", "display name")
|
|
337
|
+
.option("--slug <slug>", "url-safe slug (lowercase letters, digits, dashes)")
|
|
338
|
+
.option("--model <id>", "default model id (default: the gateway's default model)")
|
|
339
|
+
.option("--default", "make it the default for heyditto claude / codex (automatic when you have no default yet)")
|
|
340
|
+
.addOption(outputOption())
|
|
341
|
+
.action(cmdEndpointCreate), ` heyditto endpoints create
|
|
342
|
+
heyditto endpoints create --name "Work laptop" --model anthropic/claude-sonnet-5 --default`);
|
|
343
|
+
endpoints
|
|
344
|
+
.command("show")
|
|
345
|
+
.description("show one endpoint's settings")
|
|
346
|
+
.argument("<endpoint>", "endpoint slug or id")
|
|
347
|
+
.addOption(outputOption())
|
|
348
|
+
.action(cmdEndpointShow);
|
|
349
|
+
endpoints
|
|
350
|
+
.command("use")
|
|
351
|
+
.description("make an endpoint the default for heyditto claude / codex")
|
|
352
|
+
.argument("<endpoint>", "endpoint slug or id")
|
|
353
|
+
.addOption(outputOption())
|
|
354
|
+
.action(cmdEndpointUse);
|
|
355
|
+
endpoints
|
|
356
|
+
.command("pick")
|
|
357
|
+
.description("choose the default endpoint interactively")
|
|
358
|
+
.addOption(outputOption())
|
|
359
|
+
.action(cmdEndpointPick);
|
|
360
|
+
endpoints
|
|
361
|
+
.command("open")
|
|
362
|
+
.description("open the endpoint editor in the Ditto app (default endpoint when omitted)")
|
|
363
|
+
.argument("[endpoint]", "endpoint slug or id")
|
|
364
|
+
.option("--print", "print the URL without opening a browser")
|
|
365
|
+
.action(cmdEndpointOpen);
|
|
366
|
+
addExamples(endpoints
|
|
367
|
+
.command("set")
|
|
368
|
+
.description("change an endpoint's settings (mirror of the web editor)")
|
|
369
|
+
.argument("<endpoint>", "endpoint slug or id")
|
|
370
|
+
.option("--name <name>", "display name")
|
|
371
|
+
.option("--model <id>", "default model id")
|
|
372
|
+
.option("--system-prompt <text>", "system prompt prepended to every request")
|
|
373
|
+
.option("--spend-limit <tokens|none>", "spend cap in Ditto tokens, or none")
|
|
374
|
+
.addOption(new Option("--spend-period <period>", "window the spend cap resets on").choices(["daily", "weekly", "monthly", "yearly", "never"]))
|
|
375
|
+
.option("--record-trace <on|off>", "store raw request/response traces")
|
|
376
|
+
.option("--recall <on|off>", "recall memories into requests")
|
|
377
|
+
.option("--record <on|off>", "record new memories from requests")
|
|
378
|
+
.option("--memory-depth <n>", "memories recalled per request (0-25)")
|
|
379
|
+
.option("--yes", "skip the confirmation when raising a spend limit")
|
|
380
|
+
.addOption(outputOption())
|
|
381
|
+
.action(cmdEndpointSet), ` heyditto endpoints set my-endpoint --model openai/gpt-5.6-luna --record-trace on
|
|
382
|
+
heyditto endpoints set my-endpoint --spend-limit 5000000 --spend-period monthly`);
|
|
383
|
+
endpoints
|
|
384
|
+
.command("delete")
|
|
385
|
+
.description("delete an endpoint (keys stop working; threads and traces are kept)")
|
|
386
|
+
.argument("<endpoint>", "endpoint slug or id")
|
|
387
|
+
.option("--yes", "skip the confirmation prompt")
|
|
388
|
+
.addOption(outputOption())
|
|
389
|
+
.action(cmdEndpointDelete);
|
|
390
|
+
const keys = endpoints
|
|
391
|
+
.command("keys")
|
|
392
|
+
.description("list an endpoint's API keys")
|
|
393
|
+
.argument("<endpoint>", "endpoint slug or id")
|
|
394
|
+
.addOption(outputOption())
|
|
395
|
+
.action(cmdEndpointKeys);
|
|
396
|
+
keys
|
|
397
|
+
.command("revoke")
|
|
398
|
+
.description("revoke one key")
|
|
399
|
+
.argument("<endpoint>", "endpoint slug or id")
|
|
400
|
+
.argument("<keyId>", "key id (see `heyditto endpoints keys <endpoint>`)")
|
|
401
|
+
.option("--yes", "skip the confirmation prompt")
|
|
402
|
+
.addOption(outputOption())
|
|
403
|
+
.action(cmdEndpointKeysRevoke);
|
|
404
|
+
}
|
|
405
|
+
export async function cmdSessions(options) {
|
|
406
|
+
const records = await listSessions();
|
|
407
|
+
const shown = options.all ? records : records.slice(0, 20);
|
|
408
|
+
if (options.json) {
|
|
409
|
+
process.stdout.write(`${JSON.stringify(shown, null, 2)}\n`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (shown.length === 0) {
|
|
413
|
+
process.stdout.write("No coding-agent sessions yet. Start one with `heyditto claude` or `heyditto codex`.\n");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
for (const s of shown) {
|
|
417
|
+
const state = s.endedAt ? `exited ${s.exitCode ?? "?"}` : "running/unknown";
|
|
418
|
+
process.stdout.write(`${s.id} ${pad(s.harness, 6)} ${pad(s.endpointSlug, 16)} ${s.lastLaunchedAt.slice(0, 16).replace("T", " ")} ${state}\n`);
|
|
419
|
+
process.stdout.write(` ${s.worktree ?? s.cwd}${s.launches > 1 ? ` (${s.launches} launches)` : ""}\n`);
|
|
420
|
+
}
|
|
421
|
+
if (!options.all && records.length > shown.length) {
|
|
422
|
+
process.stdout.write(`\n…and ${records.length - shown.length} more (use --all)\n`);
|
|
423
|
+
}
|
|
424
|
+
process.stdout.write(`\nResume: heyditto <claude|codex> --resume <id>\n`);
|
|
425
|
+
}
|
|
426
|
+
export async function cmdSessionsRm(id) {
|
|
427
|
+
const removed = await removeSession(id);
|
|
428
|
+
if (!removed)
|
|
429
|
+
throw new Error(`no local session "${id}"`);
|
|
430
|
+
process.stdout.write(`Removed local session record ${id} (the Ditto thread and traces are kept).\n`);
|
|
431
|
+
}
|
|
432
|
+
/** Registers `claude` and `codex` on the program (which must have enablePositionalOptions()). */
|
|
433
|
+
export function registerHarnessCommands(program, addExamples) {
|
|
434
|
+
for (const harness of HARNESSES) {
|
|
435
|
+
const other = harness === "claude" ? "codex" : "claude";
|
|
436
|
+
const cmd = program
|
|
437
|
+
.command(`${harness} [args...]`)
|
|
438
|
+
.description(`launch ${harness === "claude" ? "Claude Code" : "Codex"} through a Ditto inference endpoint with a temporary key`)
|
|
439
|
+
.summary(`launch ${harness === "claude" ? "Claude Code" : "Codex"} through a Ditto endpoint`)
|
|
440
|
+
.option("-e, --endpoint <slug>", "inference endpoint slug or id (default: saved default, or a picker)")
|
|
441
|
+
.option("--budget <tokens>", "spend cap for this session's key, in Ditto tokens")
|
|
442
|
+
.addOption(new Option("--expires <duration>", "server-side safety expiry for the key")
|
|
443
|
+
.choices([...KEY_EXPIRIES])
|
|
444
|
+
.default("1d"))
|
|
445
|
+
.option("--keep-key", "do not revoke the key when the agent exits")
|
|
446
|
+
.option("--session <id>", "reuse a Ditto session id (X-Ditto-Session-Id) for the traces thread")
|
|
447
|
+
.option("--resume [id]", "resume a local session (default: the most recent one); mints a fresh key")
|
|
448
|
+
.option("-c, --continue", `continue the most recent ${harness} conversation in this directory`)
|
|
449
|
+
.option("--yolo", `bypass all permission prompts (${harness === "claude" ? "--dangerously-skip-permissions" : "--dangerously-bypass-approvals-and-sandbox"})`)
|
|
450
|
+
.option("--yellow", `auto-accept edits (${harness === "claude" ? "--permission-mode acceptEdits" : "-a on-request -s workspace-write"})`)
|
|
451
|
+
.option("-p, --prompt <text>", `headless run (${harness === "claude" ? "claude -p" : "codex exec"}); pair with --output-format etc.`)
|
|
452
|
+
.option("-m, --model <id>", harness === "codex" ? "model id (default: the endpoint slug)" : "model id (default: let the endpoint route Claude's ids)")
|
|
453
|
+
.option("-w, --worktree [name]", "run inside <repo>/.worktrees/<name> (created on a branch of the same name)")
|
|
454
|
+
.option("--name <label>", "key name shown in the Ditto app (default: cli:<harness>:<hostname>)")
|
|
455
|
+
.option("--dry-run", "print the command, args and env (key masked) without minting a key")
|
|
456
|
+
.allowUnknownOption()
|
|
457
|
+
.passThroughOptions();
|
|
458
|
+
if (harness === "claude")
|
|
459
|
+
cmd.option("--plan", "start in plan mode (--permission-mode plan)");
|
|
460
|
+
cmd.action(async (args, options) => {
|
|
461
|
+
await launchHarness(harness, args, options);
|
|
462
|
+
});
|
|
463
|
+
addExamples(cmd, ` heyditto ${harness} first run: sign in + pick an endpoint in the browser, then launch
|
|
464
|
+
heyditto ${harness} --endpoint my-endpoint --budget 500000
|
|
465
|
+
heyditto ${harness} --yellow --worktree feature-x
|
|
466
|
+
heyditto ${harness} -p "summarize this repo" --output-format json
|
|
467
|
+
heyditto ${harness} --resume reopen the last session in its thread
|
|
468
|
+
heyditto ${harness} -- --verbose forward flags to ${harness}
|
|
469
|
+
(see also: heyditto ${other})`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function jsonOut(options) {
|
|
473
|
+
return options.output === "json" || options.output === "raw";
|
|
474
|
+
}
|
|
475
|
+
function sessionOutputOption() {
|
|
476
|
+
return new Option("--output <format>", "output format").choices(["text", "json"]).default("text");
|
|
477
|
+
}
|
|
478
|
+
function relativeAge(iso) {
|
|
479
|
+
if (!iso)
|
|
480
|
+
return "";
|
|
481
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
482
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
483
|
+
return "just now";
|
|
484
|
+
const m = Math.round(ms / 60000);
|
|
485
|
+
if (m < 1)
|
|
486
|
+
return "just now";
|
|
487
|
+
if (m < 60)
|
|
488
|
+
return `${m}m ago`;
|
|
489
|
+
const h = Math.round(m / 60);
|
|
490
|
+
if (h < 48)
|
|
491
|
+
return `${h}h ago`;
|
|
492
|
+
return `${Math.round(h / 24)}d ago`;
|
|
493
|
+
}
|
|
494
|
+
export async function cmdSessionNew(nameParts, options) {
|
|
495
|
+
const name = nameParts.join(" ").trim() || undefined;
|
|
496
|
+
const record = await startSession(name, options.id);
|
|
497
|
+
if (jsonOut(options)) {
|
|
498
|
+
process.stdout.write(`${JSON.stringify({ active: true, ...record }, null, 2)}\n`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
process.stderr.write(`Started session ${record.id}${record.name ? ` (${record.name})` : ""}.\n` +
|
|
502
|
+
`Memory commands now send ${SESSION_ID_HEADER}; end it with \`heyditto session end\`.\n`);
|
|
503
|
+
process.stdout.write(`${record.id}\n`);
|
|
504
|
+
}
|
|
505
|
+
export async function cmdSessionList(options) {
|
|
506
|
+
const [history, active] = await Promise.all([readSessionHistory(), resolveActiveSession()]);
|
|
507
|
+
const rows = options.all ? history : history.slice(0, 20);
|
|
508
|
+
if (jsonOut(options)) {
|
|
509
|
+
process.stdout.write(`${JSON.stringify({ active: active ?? null, sessions: rows }, null, 2)}\n`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (rows.length === 0) {
|
|
513
|
+
process.stdout.write("No MCP sessions yet. Start one with `heyditto session new [name]`.\n");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
for (const r of rows) {
|
|
517
|
+
const mark = active?.id === r.id ? "*" : " ";
|
|
518
|
+
const state = r.endedAt ? "ended" : active?.id === r.id ? "active" : "idle";
|
|
519
|
+
process.stdout.write(`${mark} ${r.id} ${pad(state, 6)} ${pad(relativeAge(r.lastUsedAt ?? r.createdAt), 9)} ${r.name ?? ""}\n`);
|
|
520
|
+
}
|
|
521
|
+
if (active?.source === "env")
|
|
522
|
+
process.stdout.write(`\n${SESSION_ENV} pins session ${active.id} for this shell.\n`);
|
|
523
|
+
}
|
|
524
|
+
export async function cmdSessionUse(id, options) {
|
|
525
|
+
const record = await useSession(id);
|
|
526
|
+
if (jsonOut(options)) {
|
|
527
|
+
process.stdout.write(`${JSON.stringify({ active: true, ...record }, null, 2)}\n`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
process.stderr.write(`Using session ${record.id}${record.name ? ` (${record.name})` : ""}.\n`);
|
|
531
|
+
process.stdout.write(`${record.id}\n`);
|
|
532
|
+
}
|
|
533
|
+
export async function cmdSessionCurrent(options) {
|
|
534
|
+
const active = await resolveActiveSession();
|
|
535
|
+
if (jsonOut(options)) {
|
|
536
|
+
process.stdout.write(`${JSON.stringify(active ?? null, null, 2)}\n`);
|
|
537
|
+
if (!active)
|
|
538
|
+
process.exitCode = 1;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (!active) {
|
|
542
|
+
process.stderr.write("No active session. Start one with `heyditto session new [name]`.\n");
|
|
543
|
+
process.exitCode = 1;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
process.stdout.write(`${active.id}\n`);
|
|
547
|
+
if (active.name)
|
|
548
|
+
process.stderr.write(`name: ${active.name}\n`);
|
|
549
|
+
if (active.source === "env")
|
|
550
|
+
process.stderr.write(`(pinned by ${SESSION_ENV})\n`);
|
|
551
|
+
}
|
|
552
|
+
export async function cmdSessionEnd(options) {
|
|
553
|
+
const ended = await endSession();
|
|
554
|
+
if (jsonOut(options)) {
|
|
555
|
+
process.stdout.write(`${JSON.stringify(ended ?? null, null, 2)}\n`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!ended) {
|
|
559
|
+
process.stderr.write("No active session.\n");
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
process.stderr.write(`Ended session ${ended.id}. Memory commands go back to the implicit session.\n`);
|
|
563
|
+
}
|
|
564
|
+
export function registerSessionCommands(program, addExamples) {
|
|
565
|
+
const session = program
|
|
566
|
+
.command("session")
|
|
567
|
+
.description("explicit MCP sessions: group saves and searches into one thread")
|
|
568
|
+
.summary("manage the explicit MCP session")
|
|
569
|
+
.showHelpAfterError()
|
|
570
|
+
.addHelpText("after", `
|
|
571
|
+
Without a session, MCP saves fall into a time-based implicit session on the
|
|
572
|
+
server. 'session new' pins an explicit one: every request carries
|
|
573
|
+
${SESSION_ID_HEADER} (and the name once, as X-Ditto-Session-Name). Set
|
|
574
|
+
${SESSION_ENV} to pin a session for one shell or script.`);
|
|
575
|
+
addExamples(session
|
|
576
|
+
.command("new")
|
|
577
|
+
.description("start a new session and make it active")
|
|
578
|
+
.argument("[name...]", "optional name; becomes the thread title")
|
|
579
|
+
.option("--id <id>", "use this session id instead of a random uuid")
|
|
580
|
+
.addOption(sessionOutputOption())
|
|
581
|
+
.action(cmdSessionNew), ` heyditto session new "refactor auth module"
|
|
582
|
+
heyditto session new --output json | jq -r .id`);
|
|
583
|
+
session
|
|
584
|
+
.command("list")
|
|
585
|
+
.description("list local sessions (newest first; * = active)")
|
|
586
|
+
.option("--all", "show every record, not just the latest 20")
|
|
587
|
+
.addOption(sessionOutputOption())
|
|
588
|
+
.action(cmdSessionList);
|
|
589
|
+
session
|
|
590
|
+
.command("use")
|
|
591
|
+
.description("make an existing session active")
|
|
592
|
+
.argument("<id>", "session id (a unique prefix of at least 6 chars works)")
|
|
593
|
+
.addOption(sessionOutputOption())
|
|
594
|
+
.action(cmdSessionUse);
|
|
595
|
+
session
|
|
596
|
+
.command("current")
|
|
597
|
+
.description("print the active session id (exit 1 when none)")
|
|
598
|
+
.addOption(sessionOutputOption())
|
|
599
|
+
.action(cmdSessionCurrent);
|
|
600
|
+
session
|
|
601
|
+
.command("end")
|
|
602
|
+
.description("end the active session (history is kept)")
|
|
603
|
+
.addOption(sessionOutputOption())
|
|
604
|
+
.action(cmdSessionEnd);
|
|
605
|
+
}
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
// Chat agents: `heyditto agents`
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
function connectionsColumn(a) {
|
|
610
|
+
const live = (a.connections ?? []).filter((c) => !c.revokedAt);
|
|
611
|
+
return live.map((c) => `${c.kind}${c.name ? `:${c.name}` : ""}`).join(", ");
|
|
612
|
+
}
|
|
613
|
+
export async function cmdAgents(options) {
|
|
614
|
+
const agents = await listChatAgents();
|
|
615
|
+
if (jsonOut(options)) {
|
|
616
|
+
process.stdout.write(`${JSON.stringify({ agents }, null, 2)}\n`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (agents.length === 0) {
|
|
620
|
+
process.stdout.write("No agents yet.\n");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const rows = agents.map((a) => [
|
|
624
|
+
a.id,
|
|
625
|
+
a.kind,
|
|
626
|
+
a.name,
|
|
627
|
+
String(a.threadCount ?? ""),
|
|
628
|
+
relativeAge(a.lastActivityAt ?? a.updatedAt),
|
|
629
|
+
connectionsColumn(a),
|
|
630
|
+
]);
|
|
631
|
+
const header = ["ID", "KIND", "NAME", "THREADS", "LAST ACTIVITY", "CONNECTIONS"];
|
|
632
|
+
const widths = header.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
633
|
+
const line = (r) => r.map((c, i) => (i === r.length - 1 ? c : pad(c, widths[i]))).join(" ");
|
|
634
|
+
process.stdout.write(`${line(header)}\n`);
|
|
635
|
+
for (const r of rows)
|
|
636
|
+
process.stdout.write(`${line(r)}\n`);
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=commands.js.map
|