ada-agent 0.1.0 → 0.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 +14 -7
- package/bench/README.md +88 -88
- package/bench/swebench.mjs +242 -242
- package/docs/architecture.md +163 -139
- package/docs/architecture.svg +73 -73
- package/docs/cloudflare.md +81 -0
- package/docs/connectors.md +49 -48
- package/docs/integrations.md +62 -59
- package/package.json +65 -64
- package/src/client/catalog.json +1 -0
- package/src/client/cli.ts +1262 -1253
- package/src/client/models-dev.ts +106 -52
- package/src/selfcheck.ts +26 -0
- package/src/server/config.ts +65 -58
- package/src/server/providers/openai-compat.ts +78 -76
- package/src/server/providers/registry.ts +32 -31
- package/src/server/router.ts +33 -29
- package/src/shared/types.ts +21 -20
package/src/client/cli.ts
CHANGED
|
@@ -1,1253 +1,1262 @@
|
|
|
1
|
-
// ada client REPL. Talks only to the ada backend.
|
|
2
|
-
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { stdin, stdout } from "node:process";
|
|
9
|
-
import OpenAI from "openai";
|
|
10
|
-
import { Agent, type ApprovalDecision, type OnApprove } from "./agent.ts";
|
|
11
|
-
import { expandPrompt, loadPrompts } from "./prompts.ts";
|
|
12
|
-
import { Session, list, type SessionMeta } from "./session.ts";
|
|
13
|
-
import { deleteCredential, getCredential, listCredentials } from "../server/credentials.ts";
|
|
14
|
-
import { deviceLogin, oauthConfig } from "../server/oauth.ts";
|
|
15
|
-
import { addTrust, isTrusted, loadSettings, setActiveAgentPermissions, type Settings } from "./settings.ts";
|
|
16
|
-
import { getCommands, loadExtensions } from "./extensions.ts";
|
|
17
|
-
import { registerTool, setAsker } from "./tools.ts";
|
|
18
|
-
import { addRemoteSkill, loadSkills, registerSkillTool } from "./skills.ts";
|
|
19
|
-
import { addConnector, listConnectors, loadMcpServers, removeConnector } from "./mcp.ts";
|
|
20
|
-
import { addExtension, selfUpdate } from "./pkg.ts";
|
|
21
|
-
import { runTui } from "./tui-mode.ts";
|
|
22
|
-
import { loadImage } from "./image.ts";
|
|
23
|
-
import { notify, readClipboard, readClipboardImage } from "./platform.ts";
|
|
24
|
-
import { undoAll } from "./checkpoint.ts";
|
|
25
|
-
import { restore as restoreSnapshot, snapshot } from "./snapshot.ts";
|
|
26
|
-
import { prefetch } from "./models-dev.ts";
|
|
27
|
-
import { renderJobs, startJob } from "./background.ts";
|
|
28
|
-
import { renderTodos } from "./todos.ts";
|
|
29
|
-
import { track } from "./telemetry.ts";
|
|
30
|
-
|
|
31
|
-
type Msg = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
32
|
-
type RL = ReturnType<typeof createInterface>;
|
|
33
|
-
|
|
34
|
-
const BACKEND = process.env.ADA_BACKEND_URL ?? "http://localhost:8787/v1";
|
|
35
|
-
|
|
36
|
-
/** A stored GitHub/Google login token, sent as the bearer so the backend can identify us. */
|
|
37
|
-
function identityToken(): string | undefined {
|
|
38
|
-
for (const p of ["github", "google"]) {
|
|
39
|
-
const c = getCredential(p);
|
|
40
|
-
if (c?.type === "oauth" && c.access) return c.access;
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function clientKey(): string {
|
|
46
|
-
return process.env.ADA_CLIENT_KEY ?? identityToken() ?? "dev";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface Flags {
|
|
50
|
-
model?: string;
|
|
51
|
-
listModels?: boolean;
|
|
52
|
-
cont?: boolean;
|
|
53
|
-
resume?: boolean;
|
|
54
|
-
yolo?: boolean;
|
|
55
|
-
print?: string;
|
|
56
|
-
reasoning?: "low" | "medium" | "high";
|
|
57
|
-
models?: string[];
|
|
58
|
-
json?: boolean;
|
|
59
|
-
rpc?: boolean;
|
|
60
|
-
tui?: boolean;
|
|
61
|
-
strategy?: string;
|
|
62
|
-
agent?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function escapeHtml(s: string): string {
|
|
66
|
-
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c] ?? c);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Render a session transcript as a self-contained HTML page (for `ada share`). */
|
|
70
|
-
function renderTranscript(title: string, messages: Array<{ role?: string; content?: unknown }>): string {
|
|
71
|
-
const body = messages
|
|
72
|
-
.map((m) => {
|
|
73
|
-
const c = m.content;
|
|
74
|
-
const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => (p as { text?: string }).text ?? "[image]").join("") : c == null ? "" : JSON.stringify(c);
|
|
75
|
-
if (!text.trim()) return "";
|
|
76
|
-
return `<div class="msg ${m.role ?? ""}"><div class="role">${escapeHtml(m.role ?? "")}</div><pre>${escapeHtml(text)}</pre></div>`;
|
|
77
|
-
})
|
|
78
|
-
.join("\n");
|
|
79
|
-
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)} · ada</title><style>
|
|
80
|
-
body{background:#0d0f12;color:#e6e9ee;font:14px/1.6 ui-sans-serif,system-ui,sans-serif;max-width:820px;margin:0 auto;padding:32px}
|
|
81
|
-
h1{color:#ffaf00;font-size:18px}.msg{margin:16px 0;border-left:3px solid #262b33;padding-left:14px}
|
|
82
|
-
.msg.user{border-color:#ffaf00}.msg.assistant{border-color:#3fb950}.msg.tool{border-color:#82aaff}
|
|
83
|
-
.role{font:600 11px ui-monospace,monospace;color:#9aa3af;text-transform:uppercase;margin-bottom:4px}
|
|
84
|
-
pre{margin:0;white-space:pre-wrap;font:13px/1.6 ui-monospace,monospace;color:#c5cdd6}
|
|
85
|
-
</style></head><body><h1>◆ ${escapeHtml(title)}</h1>${body}</body></html>`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Activate a named agent profile (prompt + permission rules) from settings. Returns false if unknown. */
|
|
89
|
-
function switchAgent(agent: Agent, name: string, settings: Settings): boolean {
|
|
90
|
-
const profile = settings.agents?.[name];
|
|
91
|
-
if (!profile) return false;
|
|
92
|
-
setActiveAgentPermissions(profile.permissions ?? null);
|
|
93
|
-
if (profile.prompt) agent.pushSystem(`You are now acting as the "${name}" agent. ${profile.prompt}`);
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parseArgs(argv: string[]): Flags {
|
|
98
|
-
const f: Flags = {};
|
|
99
|
-
for (let i = 0; i < argv.length; i++) {
|
|
100
|
-
const a = argv[i];
|
|
101
|
-
if (a === "--model") f.model = argv[++i];
|
|
102
|
-
else if (a === "--list-models") f.listModels = true;
|
|
103
|
-
else if (a === "--continue") f.cont = true;
|
|
104
|
-
else if (a === "--resume") f.resume = true;
|
|
105
|
-
else if (a === "--yolo") f.yolo = true;
|
|
106
|
-
else if (a === "-p" || a === "--print") f.print = argv[++i] ?? "";
|
|
107
|
-
else if (a === "--reasoning") {
|
|
108
|
-
const v = argv[++i];
|
|
109
|
-
if (v === "low" || v === "medium" || v === "high") f.reasoning = v;
|
|
110
|
-
} else if (a === "--models") {
|
|
111
|
-
f.models = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
112
|
-
} else if (a === "--json") {
|
|
113
|
-
f.json = true;
|
|
114
|
-
} else if (a === "--rpc") {
|
|
115
|
-
f.rpc = true;
|
|
116
|
-
} else if (a === "--tui") {
|
|
117
|
-
f.tui = true;
|
|
118
|
-
} else if (a === "--strategy") {
|
|
119
|
-
f.strategy = argv[++i];
|
|
120
|
-
} else if (a === "--agent") {
|
|
121
|
-
f.agent = argv[++i];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return f;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function fuzzyPick(query: string, ids: string[]): string | null {
|
|
128
|
-
const q = query.toLowerCase();
|
|
129
|
-
const exact = ids.find((id) => id.toLowerCase() === q);
|
|
130
|
-
if (exact) return exact;
|
|
131
|
-
const subs = ids.filter((id) => id.toLowerCase().includes(q));
|
|
132
|
-
if (subs.length) return subs.sort((a, b) => a.length - b.length)[0]!;
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function fetchModelIds(client: OpenAI): Promise<string[]> {
|
|
137
|
-
const ids: string[] = [];
|
|
138
|
-
const res = await client.models.list();
|
|
139
|
-
for await (const m of res) ids.push(m.id);
|
|
140
|
-
return ids.sort();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function reportModelsError(e: unknown): void {
|
|
144
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
145
|
-
console.error(`Could not reach the ada backend at ${BACKEND}: ${msg}`);
|
|
146
|
-
console.error("Is the backend running? Start it in another terminal: npm run server");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function printModels(client: OpenAI): Promise<void> {
|
|
150
|
-
try {
|
|
151
|
-
const ids = await fetchModelIds(client);
|
|
152
|
-
console.log(ids.join("\n"));
|
|
153
|
-
console.log(`\n${ids.length} models`);
|
|
154
|
-
} catch (e) {
|
|
155
|
-
reportModelsError(e);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function pickModel(client: OpenAI, rl: RL): Promise<string> {
|
|
160
|
-
console.log("Fetching available models…");
|
|
161
|
-
let ids: string[] = [];
|
|
162
|
-
try {
|
|
163
|
-
ids = await fetchModelIds(client);
|
|
164
|
-
} catch (e) {
|
|
165
|
-
reportModelsError(e);
|
|
166
|
-
}
|
|
167
|
-
if (!ids.length) return (await rl.question("Enter a model id: ")).trim();
|
|
168
|
-
const shown = ids.slice(0, 40);
|
|
169
|
-
if (stdin.isTTY) {
|
|
170
|
-
const choice = await select(rl, "Select a model (↑/↓ · Enter · Esc to type an id):", shown);
|
|
171
|
-
if (choice !== null) return shown[choice]!;
|
|
172
|
-
}
|
|
173
|
-
shown.forEach((id, i) => console.log(`${String(i + 1).padStart(2)}. ${id}`));
|
|
174
|
-
if (ids.length > 40) console.log(`… and ${ids.length - 40} more (type an id directly)`);
|
|
175
|
-
const a = (await rl.question("pick # or type a model id: ")).trim();
|
|
176
|
-
const n = Number(a);
|
|
177
|
-
if (Number.isInteger(n) && n >= 1 && n <= ids.length) return ids[n - 1]!;
|
|
178
|
-
return fuzzyPick(a, ids) ?? a;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function pickSession(rl: RL): Promise<string | null> {
|
|
182
|
-
const metas = list();
|
|
183
|
-
if (!metas.length) {
|
|
184
|
-
console.log("No saved sessions.");
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
const top = metas.slice(0, 20);
|
|
188
|
-
if (stdin.isTTY) {
|
|
189
|
-
const choice = await select(rl, "Resume which session? (↑/↓ · Enter · Esc to cancel):", top.map((m) => m.title));
|
|
190
|
-
if (choice !== null) return top[choice]?.file ?? null;
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
top.forEach((m, i) => console.log(`${String(i + 1).padStart(2)}. ${m.title}`));
|
|
194
|
-
const a = (await rl.question("resume #: ")).trim();
|
|
195
|
-
const idx = Number(a) - 1;
|
|
196
|
-
return top[idx]?.file ?? null;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function rawOn(rl: RL, onData: (b: Buffer) => void): void {
|
|
200
|
-
rl.pause();
|
|
201
|
-
if (stdin.isTTY) stdin.setRawMode(true);
|
|
202
|
-
stdin.on("data", onData);
|
|
203
|
-
stdin.resume();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function rawOff(rl: RL, onData: (b: Buffer) => void): void {
|
|
207
|
-
stdin.off("data", onData);
|
|
208
|
-
if (stdin.isTTY) stdin.setRawMode(false);
|
|
209
|
-
rl.resume();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Decode a raw stdin chunk to a logical key: arrows (or j/k/Tab), enter, esc, ctrl-c, or the literal char. */
|
|
213
|
-
function decodeKey(s: string): string {
|
|
214
|
-
if (s === "\x1b[A" || s === "k") return "up";
|
|
215
|
-
if (s === "\x1b[B" || s === "j" || s === "\t") return "down";
|
|
216
|
-
if (s === "\r" || s === "\n") return "enter";
|
|
217
|
-
if (s === "\x03") return "ctrl-c";
|
|
218
|
-
if (s === "\x1b") return "esc";
|
|
219
|
-
return s;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Read decoded keypresses in raw mode until a handler calls done(). Two robustness points the
|
|
224
|
-
* naive version got wrong: (1) a bare ESC may be the head of a split "\x1b[A" arrow sequence on
|
|
225
|
-
* Windows/slow ptys, so it's held ~50ms and re-joined with the next chunk before being treated as
|
|
226
|
-
* Esc; (2) teardown (listener removal + raw-mode restore + rl.resume) is guaranteed exactly once.
|
|
227
|
-
*/
|
|
228
|
-
function readKeys(rl: RL, onKey: (key: string, done: () => void) => void): void {
|
|
229
|
-
let settled = false;
|
|
230
|
-
let escTimer: ReturnType<typeof setTimeout> | null = null;
|
|
231
|
-
const done = (): void => {
|
|
232
|
-
if (settled) return;
|
|
233
|
-
settled = true;
|
|
234
|
-
if (escTimer) clearTimeout(escTimer);
|
|
235
|
-
stdin.off("data", handler);
|
|
236
|
-
if (stdin.isTTY) stdin.setRawMode(false);
|
|
237
|
-
rl.resume();
|
|
238
|
-
};
|
|
239
|
-
const emit = (s: string): void => onKey(decodeKey(s), done);
|
|
240
|
-
const handler = (buf: Buffer): void => {
|
|
241
|
-
let s = buf.toString("utf8");
|
|
242
|
-
if (escTimer) {
|
|
243
|
-
clearTimeout(escTimer);
|
|
244
|
-
escTimer = null;
|
|
245
|
-
s = `\x1b${s}`; // re-join the ESC we were holding with this follow-up chunk (arrow keys)
|
|
246
|
-
}
|
|
247
|
-
if (s === "\x1b") {
|
|
248
|
-
escTimer = setTimeout(() => {
|
|
249
|
-
escTimer = null;
|
|
250
|
-
emit("\x1b");
|
|
251
|
-
}, 50);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
emit(s);
|
|
255
|
-
};
|
|
256
|
-
rl.pause();
|
|
257
|
-
if (stdin.isTTY) stdin.setRawMode(true);
|
|
258
|
-
stdin.on("data", handler);
|
|
259
|
-
stdin.resume();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** Arrow-key list selector. Returns the chosen index, or null on Esc / non-TTY (caller falls back). */
|
|
263
|
-
async function select(rl: RL, title: string, items: string[]): Promise<number | null> {
|
|
264
|
-
if (!stdin.isTTY || !items.length) return null;
|
|
265
|
-
let idx = 0;
|
|
266
|
-
const draw = (first: boolean): void => {
|
|
267
|
-
if (!first) stdout.write(`\x1b[${items.length}A`); // jump back up to redraw in place
|
|
268
|
-
for (let i = 0; i < items.length; i++) {
|
|
269
|
-
stdout.write(i === idx ? `\x1b[2K\x1b[38;5;214m❯ ${items[i]}\x1b[0m\n` : `\x1b[2K ${items[i]}\n`);
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
stdout.write(`${title}\n`);
|
|
273
|
-
draw(true);
|
|
274
|
-
return await new Promise<number | null>((res) => {
|
|
275
|
-
readKeys(rl, (key, done) => {
|
|
276
|
-
if (key === "up") {
|
|
277
|
-
idx = (idx - 1 + items.length) % items.length;
|
|
278
|
-
draw(false);
|
|
279
|
-
} else if (key === "down") {
|
|
280
|
-
idx = (idx + 1) % items.length;
|
|
281
|
-
draw(false);
|
|
282
|
-
} else if (key === "enter") {
|
|
283
|
-
done();
|
|
284
|
-
res(idx);
|
|
285
|
-
} else if (key === "esc" || key === "ctrl-c") {
|
|
286
|
-
done();
|
|
287
|
-
res(null);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
type PermMode = "ask" | "plan" | "auto";
|
|
294
|
-
type ApproveChoice = "yes" | "auto" | "plan" | "no";
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Tool-approval prompt. A fixed single-key prompt (no scrolling redraw — that fought the streaming
|
|
298
|
-
* transcript and glitched): it states in plain words what permission is being requested + the actual
|
|
299
|
-
* target, then reads one key — [y]es · [a]uto (run the rest without asking) · [p]lan (switch to plan
|
|
300
|
-
* mode, skip this) · [n]o/Esc. `summary` is "<permission phrase>\n<detail>" from the agent.
|
|
301
|
-
*/
|
|
302
|
-
async function approvePrompt(rl: RL, name: string, summary: string): Promise<ApproveChoice> {
|
|
303
|
-
const nl = summary.indexOf("\n");
|
|
304
|
-
const risk = (nl >= 0 ? summary.slice(0, nl) : summary) || `run the ${name} tool`;
|
|
305
|
-
const detail = nl >= 0 ? summary.slice(nl + 1).trim() : "";
|
|
306
|
-
const danger = risk.startsWith("⚠");
|
|
307
|
-
if (!stdin.isTTY) {
|
|
308
|
-
const ans = (await rl.question(`\x1b[33m? ${risk} [y]es / [a]uto / [p]lan / [N]o \x1b[0m`)).trim().toLowerCase();
|
|
309
|
-
return ans[0] === "y" ? "yes" : ans[0] === "a" ? "auto" : ans[0] === "p" ? "plan" : "no";
|
|
310
|
-
}
|
|
311
|
-
const cols = (stdout.columns || 80) - 2;
|
|
312
|
-
const head = `${danger ? "\x1b[31m" : "\x1b[33m"}ada wants to ${risk.replace(/^⚠ /, "")}\x1b[0m`;
|
|
313
|
-
const det = detail ? ` \x1b[2m${detail.length > cols ? `${detail.slice(0, cols - 1)}…` : detail}\x1b[0m\n` : "";
|
|
314
|
-
stdout.write(`\n${danger ? "\x1b[31m⚠\x1b[0m " : ""}${head}\n${det}\x1b[2m[\x1b[0my\x1b[2m]es [\x1b[0ma\x1b[2m]uto [\x1b[0mp\x1b[2m]lan [\x1b[0mn\x1b[2m]o ›\x1b[0m `);
|
|
315
|
-
return await new Promise<ApproveChoice>((res) => {
|
|
316
|
-
readKeys(rl, (key, done) => {
|
|
317
|
-
const k = key.length === 1 ? key.toLowerCase() : key;
|
|
318
|
-
const val: ApproveChoice | null = k === "y" || key === "enter" ? "yes" : k === "a" ? "auto" : k === "p" ? "plan" : k === "n" || key === "esc" || key === "ctrl-c" ? "no" : null;
|
|
319
|
-
if (!val) return;
|
|
320
|
-
done();
|
|
321
|
-
stdout.write(`\r\x1b[2K${val === "no" ? "\x1b[31m✗\x1b[0m skipped" : `\x1b[32m✓\x1b[0m ${val === "auto" ? "auto (won't ask again)" : val === "plan" ? "→ plan mode" : "ran"}`}\n`);
|
|
322
|
-
res(val);
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function printTree(currentFile: string): void {
|
|
328
|
-
const metas = list();
|
|
329
|
-
if (!metas.length) {
|
|
330
|
-
console.log("No sessions.");
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
const children = new Map<string, SessionMeta[]>();
|
|
334
|
-
for (const m of metas) {
|
|
335
|
-
if (m.parent) {
|
|
336
|
-
const arr = children.get(m.parent) ?? [];
|
|
337
|
-
arr.push(m);
|
|
338
|
-
children.set(m.parent, arr);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const rec = (m: SessionMeta, depth: number): void => {
|
|
342
|
-
const mark = m.file === currentFile ? "\x1b[38;5;214m●\x1b[0m" : "○";
|
|
343
|
-
console.log(`${" ".repeat(depth)}${mark} ${basename(m.file)} \x1b[2m${m.title}\x1b[0m`);
|
|
344
|
-
for (const c of children.get(m.file) ?? []) rec(c, depth + 1);
|
|
345
|
-
};
|
|
346
|
-
for (const m of metas.filter((x) => !x.parent || !metas.some((y) => y.file === x.parent))) rec(m, 0);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function makeClient(): OpenAI {
|
|
350
|
-
return new OpenAI({ baseURL: BACKEND, apiKey: clientKey(), maxRetries: 0 });
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Run the device flow for `provider`; returns true on success (token stored). */
|
|
354
|
-
async function loginFlow(provider: string): Promise<boolean> {
|
|
355
|
-
const cfg = oauthConfig(provider);
|
|
356
|
-
if (!cfg) {
|
|
357
|
-
console.log(`No OAuth config for ${provider}. Set ADA_OAUTH_${provider.toUpperCase()}_{CLIENT_ID,DEVICE_URL,TOKEN_URL}.`);
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
360
|
-
try {
|
|
361
|
-
await deviceLogin(provider, cfg, (s) => console.log(s));
|
|
362
|
-
return true;
|
|
363
|
-
} catch (e) {
|
|
364
|
-
console.error(`login failed: ${e instanceof Error ? e.message : e}`);
|
|
365
|
-
return false;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/** Startup login check: probe the backend; if it says 401, offer to sign in and rebuild the client. */
|
|
370
|
-
async function ensureAuth(rl: RL, client: OpenAI): Promise<OpenAI> {
|
|
371
|
-
let status: number;
|
|
372
|
-
try {
|
|
373
|
-
const r = await fetch(`${BACKEND}/whoami`, { headers: { authorization: `Bearer ${clientKey()}` } });
|
|
374
|
-
status = r.status;
|
|
375
|
-
} catch {
|
|
376
|
-
return client; // backend unreachable — the model fetch will report it
|
|
377
|
-
}
|
|
378
|
-
if (status !== 401) return client; // 200 = already authorized, or backend is open (dev)
|
|
379
|
-
const provider = ["github", "google"].find((p) => oauthConfig(p));
|
|
380
|
-
if (!provider) {
|
|
381
|
-
console.log("\x1b[33mthis backend requires login, but no OAuth provider is configured (set ADA_OAUTH_*).\x1b[0m");
|
|
382
|
-
return client;
|
|
383
|
-
}
|
|
384
|
-
const ans = (await rl.question(`\x1b[33mnot logged in — sign in with ${provider}? [Y/n] \x1b[0m`)).trim().toLowerCase();
|
|
385
|
-
if (ans === "n" || ans === "no") return client;
|
|
386
|
-
return (await loginFlow(provider)) ? makeClient() : client;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async function authCommand(sub: string, provider?: string): Promise<void> {
|
|
390
|
-
if (!provider) {
|
|
391
|
-
console.error(`usage: ada ${sub} <provider>`);
|
|
392
|
-
console.log(listCredentials().length ? `logged in: ${listCredentials().join(", ")}` : "no stored credentials");
|
|
393
|
-
process.exit(1);
|
|
394
|
-
}
|
|
395
|
-
if (sub === "logout") {
|
|
396
|
-
await deleteCredential(provider);
|
|
397
|
-
console.log(`logged out ${provider}`);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (!(await loginFlow(provider))) process.exit(1);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/** Gate project-level files (.ada/prompts, AGENTS.md, project settings) behind explicit trust. */
|
|
404
|
-
async function ensureTrust(rl: RL): Promise<boolean> {
|
|
405
|
-
const cwd = process.cwd();
|
|
406
|
-
if (isTrusted(cwd)) return true;
|
|
407
|
-
if (!stdin.isTTY) return false; // headless: never load untrusted project files
|
|
408
|
-
const ans = (await rl.question(`Trust ${cwd} and load its .ada config (prompts, AGENTS.md, settings)? [y/N] `)).trim().toLowerCase();
|
|
409
|
-
if (ans === "y" || ans === "yes") {
|
|
410
|
-
addTrust(cwd);
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// ANSI-Shadow "ada", rendered as a truecolor splash. █ = gradient body, box glyphs = drop shadow.
|
|
417
|
-
const ADA_ART = [
|
|
418
|
-
" █████╗ ██████╗ █████╗ ",
|
|
419
|
-
"██╔══██╗██╔══██╗██╔══██╗",
|
|
420
|
-
"███████║██║ ██║███████║",
|
|
421
|
-
"██╔══██║██║ ██║██╔══██║",
|
|
422
|
-
"██║ ██║██████╔╝██║ ██║",
|
|
423
|
-
"╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝",
|
|
424
|
-
];
|
|
425
|
-
const GRADIENT: [number, number, number][] = [
|
|
426
|
-
[255, 214, 92], // gold
|
|
427
|
-
[255, 122, 41], // orange
|
|
428
|
-
[214, 51, 132], // magenta
|
|
429
|
-
];
|
|
430
|
-
|
|
431
|
-
/** Interpolate the gradient stops at t∈[0,1]. */
|
|
432
|
-
function gradientAt(t: number): [number, number, number] {
|
|
433
|
-
const seg = Math.max(0, Math.min(1, t)) * (GRADIENT.length - 1);
|
|
434
|
-
const i = Math.min(Math.floor(seg), GRADIENT.length - 2);
|
|
435
|
-
const f = seg - i;
|
|
436
|
-
const [a, b] = [GRADIENT[i]!, GRADIENT[i + 1]!];
|
|
437
|
-
return [0, 1, 2].map((k) => Math.round(a[k]! + (b[k]! - a[k]!) * f)) as [number, number, number];
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function adaVersion(): string {
|
|
441
|
-
try {
|
|
442
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
443
|
-
return (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string }).version ?? "0.0.0";
|
|
444
|
-
} catch {
|
|
445
|
-
return "0.0.0";
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const TAG = "a coding agent from zero";
|
|
450
|
-
const W = Math.max(...ADA_ART.map((l) => l.length));
|
|
451
|
-
const H = ADA_ART.length;
|
|
452
|
-
const EDGE = 6; // width of the bright leading edge of the light sweep
|
|
453
|
-
|
|
454
|
-
/** One logo row at light-sweep position `sweep`. Unlit ahead of the edge, white-hot at it, settled gradient behind. */
|
|
455
|
-
function logoRow(line: string, y: number, sweep: number): string {
|
|
456
|
-
let s = "\x1b[2K "; // clear line, then indent
|
|
457
|
-
[...line].forEach((ch, x) => {
|
|
458
|
-
if (ch === " ") return void (s += " ");
|
|
459
|
-
if (ch !== "█") return void (s += `\x1b[0m\x1b[38;2;92;72;82m${ch}`); // outline = drop shadow
|
|
460
|
-
if (x > sweep) return void (s += `\x1b[0m\x1b[38;2;70;55;62m█`); // not yet lit
|
|
461
|
-
const [r, g, b] = gradientAt((x / W + y / H) / 2);
|
|
462
|
-
const d = sweep - x;
|
|
463
|
-
if (d < EDGE) {
|
|
464
|
-
const t = 1 - d / EDGE; // 1 at the edge, fading to 0 as it settles
|
|
465
|
-
const mix = (c: number): number => Math.round(c + (255 - c) * t * 0.85);
|
|
466
|
-
return void (s += `\x1b[1m\x1b[38;2;${mix(r)};${mix(g)};${mix(b)}m█`);
|
|
467
|
-
}
|
|
468
|
-
return void (s += `\x1b[1m\x1b[38;2;${r};${g};${b}m█`);
|
|
469
|
-
});
|
|
470
|
-
return s + "\x1b[0m";
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
474
|
-
|
|
475
|
-
/** Startup splash: a ~400ms left-to-right light sweep over the logo on a TTY; static plain text otherwise. */
|
|
476
|
-
async function printBanner(): Promise<void> {
|
|
477
|
-
const fancy = stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
478
|
-
if (!fancy) {
|
|
479
|
-
const body = ADA_ART.map((l) => ` ${l}`).join("\n");
|
|
480
|
-
stdout.write(`\n${body}\n ${TAG} v${adaVersion()}\n\n`);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
const frames = 18;
|
|
484
|
-
stdout.write("\x1b[?25l\n"); // hide cursor, leading blank line
|
|
485
|
-
try {
|
|
486
|
-
for (let f = 0; f <= frames; f++) {
|
|
487
|
-
const sweep = (f / frames) * (W + EDGE);
|
|
488
|
-
if (f > 0) stdout.write(`\x1b[${H}A`); // jump back up to redraw in place
|
|
489
|
-
stdout.write(`${ADA_ART.map((line, y) => logoRow(line, y, sweep)).join("\n")}\n`);
|
|
490
|
-
if (f < frames) await sleep(400 / frames);
|
|
491
|
-
}
|
|
492
|
-
} finally {
|
|
493
|
-
stdout.write("\x1b[?25h"); // always restore the cursor, even if interrupted
|
|
494
|
-
}
|
|
495
|
-
stdout.write(` \x1b[2m${TAG}\x1b[0m \x1b[38;2;214;51;132mv${adaVersion()}\x1b[0m\n\n`);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
async function main(): Promise<void> {
|
|
499
|
-
const sub = process.argv[2];
|
|
500
|
-
if (sub === "login" || sub === "logout") {
|
|
501
|
-
await authCommand(sub, process.argv[3]);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
if (sub === "add") {
|
|
505
|
-
const spec = process.argv[3];
|
|
506
|
-
if (!spec) {
|
|
507
|
-
console.error("usage: ada add <git-url | npm-package>");
|
|
508
|
-
process.exit(1);
|
|
509
|
-
}
|
|
510
|
-
try {
|
|
511
|
-
addExtension(spec);
|
|
512
|
-
} catch (e) {
|
|
513
|
-
console.error(e instanceof Error ? e.message : e);
|
|
514
|
-
process.exit(1);
|
|
515
|
-
}
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
if (sub === "update") {
|
|
519
|
-
selfUpdate();
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (sub === "mcp") {
|
|
523
|
-
const action = process.argv[3] ?? "list";
|
|
524
|
-
const name = process.argv[4];
|
|
525
|
-
if (action === "list" || action === "ls") {
|
|
526
|
-
console.log("Connector catalog (● configured · ○ available):\n");
|
|
527
|
-
for (const c of listConnectors()) {
|
|
528
|
-
const dot = c.configured ? "\x1b[38;5;214m●\x1b[0m" : "○";
|
|
529
|
-
const env = c.needsEnv.length ? ` \x1b[2m(set: ${c.needsEnv.join(", ")})\x1b[0m` : "";
|
|
530
|
-
console.log(` ${dot} ${c.name.padEnd(14)} ${c.description}${env}`);
|
|
531
|
-
}
|
|
532
|
-
console.log("\n ada mcp add <name> · ada mcp remove <name>");
|
|
533
|
-
console.log(" custom server: edit .ada/mcp.json — a { command,args } (stdio) or { url } (http) entry");
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (action === "add") {
|
|
537
|
-
if (!name) {
|
|
538
|
-
console.error("usage: ada mcp add <name>");
|
|
539
|
-
process.exit(1);
|
|
540
|
-
}
|
|
541
|
-
const r = addConnector(name);
|
|
542
|
-
if (!r.ok) {
|
|
543
|
-
console.error(r.error);
|
|
544
|
-
process.exit(1);
|
|
545
|
-
}
|
|
546
|
-
console.log(`\x1b[38;5;214m✓\x1b[0m added "${name}" to .ada/mcp.json`);
|
|
547
|
-
if (r.envVars.length) console.log(` set before use: ${r.envVars.join(", ")}`);
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
if (action === "remove" || action === "rm") {
|
|
551
|
-
if (!name) {
|
|
552
|
-
console.error("usage: ada mcp remove <name>");
|
|
553
|
-
process.exit(1);
|
|
554
|
-
}
|
|
555
|
-
console.log(removeConnector(name) ? `removed "${name}" from .ada/mcp.json` : `"${name}" was not configured`);
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
console.error("usage: ada mcp [list | add <name> | remove <name>]");
|
|
559
|
-
process.exit(1);
|
|
560
|
-
}
|
|
561
|
-
if (sub === "worktree" || sub === "wt") {
|
|
562
|
-
const action = process.argv[3] ?? "list";
|
|
563
|
-
const git = (...a: string[]): { status: number | null; out: string } => {
|
|
564
|
-
const r = spawnSync("git", a, { encoding: "utf8", cwd: process.cwd() });
|
|
565
|
-
return { status: r.status, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
566
|
-
};
|
|
567
|
-
if (action === "list" || action === "ls") {
|
|
568
|
-
const r = git("worktree", "list");
|
|
569
|
-
console.log(r.status === 0 ? r.out : "(not a git repo or no worktrees)");
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
if (action === "add" || action === "new") {
|
|
573
|
-
const name = process.argv[4];
|
|
574
|
-
if (!name) {
|
|
575
|
-
console.error("usage: ada worktree add <name>");
|
|
576
|
-
process.exit(1);
|
|
577
|
-
}
|
|
578
|
-
const branch = `ada/${name}`;
|
|
579
|
-
const dir = resolve(process.cwd(), "..", `${basename(process.cwd())}-${name}`);
|
|
580
|
-
const r = git("worktree", "add", "-b", branch, dir);
|
|
581
|
-
if (r.status !== 0) {
|
|
582
|
-
console.error(r.out || "git worktree add failed");
|
|
583
|
-
process.exit(1);
|
|
584
|
-
}
|
|
585
|
-
console.log(`\x1b[38;5;214m✓\x1b[0m worktree ${dir}\n branch ${branch} — cd "${dir}" && ada`);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (action === "remove" || action === "rm") {
|
|
589
|
-
const name = process.argv[4];
|
|
590
|
-
if (!name) {
|
|
591
|
-
console.error("usage: ada worktree remove <name>");
|
|
592
|
-
process.exit(1);
|
|
593
|
-
}
|
|
594
|
-
const dir = resolve(process.cwd(), "..", `${basename(process.cwd())}-${name}`);
|
|
595
|
-
const r = git("worktree", "remove", dir);
|
|
596
|
-
console.log(r.status === 0 ? `removed ${dir}` : r.out);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
console.error("usage: ada worktree [list | add <name> | remove <name>]");
|
|
600
|
-
process.exit(1);
|
|
601
|
-
}
|
|
602
|
-
if (sub === "skill") {
|
|
603
|
-
const action = process.argv[3] ?? "list";
|
|
604
|
-
if (action === "add") {
|
|
605
|
-
const url = process.argv[4];
|
|
606
|
-
if (!url) {
|
|
607
|
-
console.error("usage: ada skill add <url> (a SKILL.md, or a JSON index of them)");
|
|
608
|
-
process.exit(1);
|
|
609
|
-
}
|
|
610
|
-
try {
|
|
611
|
-
const added = await addRemoteSkill(url);
|
|
612
|
-
console.log(added.length ? `\x1b[38;5;214m✓\x1b[0m installed: ${added.join(", ")} → ~/.ada/skills/` : "no skills found at that URL");
|
|
613
|
-
} catch (e) {
|
|
614
|
-
console.error(e instanceof Error ? e.message : e);
|
|
615
|
-
process.exit(1);
|
|
616
|
-
}
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
if (action === "list" || action === "ls") {
|
|
620
|
-
for (const s of loadSkills(true)) console.log(` ${s.name.padEnd(22)} ${s.description}`);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
console.error("usage: ada skill [list | add <url>]");
|
|
624
|
-
process.exit(1);
|
|
625
|
-
}
|
|
626
|
-
if (sub === "
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
model
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
session =
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
session = Session.
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (options?.length
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
setMode
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
console.log("\x1b[
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
if (line === "/
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
console.log(
|
|
1065
|
-
continue;
|
|
1066
|
-
}
|
|
1067
|
-
if (line === "/
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
model =
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
else
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
}
|
|
1253
|
-
);
|
|
1
|
+
// ada client REPL. Talks only to the ada backend.
|
|
2
|
+
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { stdin, stdout } from "node:process";
|
|
9
|
+
import OpenAI from "openai";
|
|
10
|
+
import { Agent, type ApprovalDecision, type OnApprove } from "./agent.ts";
|
|
11
|
+
import { expandPrompt, loadPrompts } from "./prompts.ts";
|
|
12
|
+
import { Session, list, type SessionMeta } from "./session.ts";
|
|
13
|
+
import { deleteCredential, getCredential, listCredentials } from "../server/credentials.ts";
|
|
14
|
+
import { deviceLogin, oauthConfig } from "../server/oauth.ts";
|
|
15
|
+
import { addTrust, isTrusted, loadSettings, setActiveAgentPermissions, type Settings } from "./settings.ts";
|
|
16
|
+
import { getCommands, loadExtensions } from "./extensions.ts";
|
|
17
|
+
import { registerTool, setAsker } from "./tools.ts";
|
|
18
|
+
import { addRemoteSkill, loadSkills, registerSkillTool } from "./skills.ts";
|
|
19
|
+
import { addConnector, listConnectors, loadMcpServers, removeConnector } from "./mcp.ts";
|
|
20
|
+
import { addExtension, selfUpdate } from "./pkg.ts";
|
|
21
|
+
import { runTui } from "./tui-mode.ts";
|
|
22
|
+
import { loadImage } from "./image.ts";
|
|
23
|
+
import { notify, readClipboard, readClipboardImage } from "./platform.ts";
|
|
24
|
+
import { undoAll } from "./checkpoint.ts";
|
|
25
|
+
import { restore as restoreSnapshot, snapshot } from "./snapshot.ts";
|
|
26
|
+
import { catalogText, prefetch } from "./models-dev.ts";
|
|
27
|
+
import { renderJobs, startJob } from "./background.ts";
|
|
28
|
+
import { renderTodos } from "./todos.ts";
|
|
29
|
+
import { track } from "./telemetry.ts";
|
|
30
|
+
|
|
31
|
+
type Msg = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
32
|
+
type RL = ReturnType<typeof createInterface>;
|
|
33
|
+
|
|
34
|
+
const BACKEND = process.env.ADA_BACKEND_URL ?? "http://localhost:8787/v1";
|
|
35
|
+
|
|
36
|
+
/** A stored GitHub/Google login token, sent as the bearer so the backend can identify us. */
|
|
37
|
+
function identityToken(): string | undefined {
|
|
38
|
+
for (const p of ["github", "google"]) {
|
|
39
|
+
const c = getCredential(p);
|
|
40
|
+
if (c?.type === "oauth" && c.access) return c.access;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clientKey(): string {
|
|
46
|
+
return process.env.ADA_CLIENT_KEY ?? identityToken() ?? "dev";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Flags {
|
|
50
|
+
model?: string;
|
|
51
|
+
listModels?: boolean;
|
|
52
|
+
cont?: boolean;
|
|
53
|
+
resume?: boolean;
|
|
54
|
+
yolo?: boolean;
|
|
55
|
+
print?: string;
|
|
56
|
+
reasoning?: "low" | "medium" | "high";
|
|
57
|
+
models?: string[];
|
|
58
|
+
json?: boolean;
|
|
59
|
+
rpc?: boolean;
|
|
60
|
+
tui?: boolean;
|
|
61
|
+
strategy?: string;
|
|
62
|
+
agent?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function escapeHtml(s: string): string {
|
|
66
|
+
return s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c] ?? c);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Render a session transcript as a self-contained HTML page (for `ada share`). */
|
|
70
|
+
function renderTranscript(title: string, messages: Array<{ role?: string; content?: unknown }>): string {
|
|
71
|
+
const body = messages
|
|
72
|
+
.map((m) => {
|
|
73
|
+
const c = m.content;
|
|
74
|
+
const text = typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => (p as { text?: string }).text ?? "[image]").join("") : c == null ? "" : JSON.stringify(c);
|
|
75
|
+
if (!text.trim()) return "";
|
|
76
|
+
return `<div class="msg ${m.role ?? ""}"><div class="role">${escapeHtml(m.role ?? "")}</div><pre>${escapeHtml(text)}</pre></div>`;
|
|
77
|
+
})
|
|
78
|
+
.join("\n");
|
|
79
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)} · ada</title><style>
|
|
80
|
+
body{background:#0d0f12;color:#e6e9ee;font:14px/1.6 ui-sans-serif,system-ui,sans-serif;max-width:820px;margin:0 auto;padding:32px}
|
|
81
|
+
h1{color:#ffaf00;font-size:18px}.msg{margin:16px 0;border-left:3px solid #262b33;padding-left:14px}
|
|
82
|
+
.msg.user{border-color:#ffaf00}.msg.assistant{border-color:#3fb950}.msg.tool{border-color:#82aaff}
|
|
83
|
+
.role{font:600 11px ui-monospace,monospace;color:#9aa3af;text-transform:uppercase;margin-bottom:4px}
|
|
84
|
+
pre{margin:0;white-space:pre-wrap;font:13px/1.6 ui-monospace,monospace;color:#c5cdd6}
|
|
85
|
+
</style></head><body><h1>◆ ${escapeHtml(title)}</h1>${body}</body></html>`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Activate a named agent profile (prompt + permission rules) from settings. Returns false if unknown. */
|
|
89
|
+
function switchAgent(agent: Agent, name: string, settings: Settings): boolean {
|
|
90
|
+
const profile = settings.agents?.[name];
|
|
91
|
+
if (!profile) return false;
|
|
92
|
+
setActiveAgentPermissions(profile.permissions ?? null);
|
|
93
|
+
if (profile.prompt) agent.pushSystem(`You are now acting as the "${name}" agent. ${profile.prompt}`);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseArgs(argv: string[]): Flags {
|
|
98
|
+
const f: Flags = {};
|
|
99
|
+
for (let i = 0; i < argv.length; i++) {
|
|
100
|
+
const a = argv[i];
|
|
101
|
+
if (a === "--model") f.model = argv[++i];
|
|
102
|
+
else if (a === "--list-models") f.listModels = true;
|
|
103
|
+
else if (a === "--continue") f.cont = true;
|
|
104
|
+
else if (a === "--resume") f.resume = true;
|
|
105
|
+
else if (a === "--yolo") f.yolo = true;
|
|
106
|
+
else if (a === "-p" || a === "--print") f.print = argv[++i] ?? "";
|
|
107
|
+
else if (a === "--reasoning") {
|
|
108
|
+
const v = argv[++i];
|
|
109
|
+
if (v === "low" || v === "medium" || v === "high") f.reasoning = v;
|
|
110
|
+
} else if (a === "--models") {
|
|
111
|
+
f.models = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
112
|
+
} else if (a === "--json") {
|
|
113
|
+
f.json = true;
|
|
114
|
+
} else if (a === "--rpc") {
|
|
115
|
+
f.rpc = true;
|
|
116
|
+
} else if (a === "--tui") {
|
|
117
|
+
f.tui = true;
|
|
118
|
+
} else if (a === "--strategy") {
|
|
119
|
+
f.strategy = argv[++i];
|
|
120
|
+
} else if (a === "--agent") {
|
|
121
|
+
f.agent = argv[++i];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return f;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fuzzyPick(query: string, ids: string[]): string | null {
|
|
128
|
+
const q = query.toLowerCase();
|
|
129
|
+
const exact = ids.find((id) => id.toLowerCase() === q);
|
|
130
|
+
if (exact) return exact;
|
|
131
|
+
const subs = ids.filter((id) => id.toLowerCase().includes(q));
|
|
132
|
+
if (subs.length) return subs.sort((a, b) => a.length - b.length)[0]!;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function fetchModelIds(client: OpenAI): Promise<string[]> {
|
|
137
|
+
const ids: string[] = [];
|
|
138
|
+
const res = await client.models.list();
|
|
139
|
+
for await (const m of res) ids.push(m.id);
|
|
140
|
+
return ids.sort();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reportModelsError(e: unknown): void {
|
|
144
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
145
|
+
console.error(`Could not reach the ada backend at ${BACKEND}: ${msg}`);
|
|
146
|
+
console.error("Is the backend running? Start it in another terminal: npm run server");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function printModels(client: OpenAI): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
const ids = await fetchModelIds(client);
|
|
152
|
+
console.log(ids.join("\n"));
|
|
153
|
+
console.log(`\n${ids.length} models`);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
reportModelsError(e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function pickModel(client: OpenAI, rl: RL): Promise<string> {
|
|
160
|
+
console.log("Fetching available models…");
|
|
161
|
+
let ids: string[] = [];
|
|
162
|
+
try {
|
|
163
|
+
ids = await fetchModelIds(client);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
reportModelsError(e);
|
|
166
|
+
}
|
|
167
|
+
if (!ids.length) return (await rl.question("Enter a model id: ")).trim();
|
|
168
|
+
const shown = ids.slice(0, 40);
|
|
169
|
+
if (stdin.isTTY) {
|
|
170
|
+
const choice = await select(rl, "Select a model (↑/↓ · Enter · Esc to type an id):", shown);
|
|
171
|
+
if (choice !== null) return shown[choice]!;
|
|
172
|
+
}
|
|
173
|
+
shown.forEach((id, i) => console.log(`${String(i + 1).padStart(2)}. ${id}`));
|
|
174
|
+
if (ids.length > 40) console.log(`… and ${ids.length - 40} more (type an id directly)`);
|
|
175
|
+
const a = (await rl.question("pick # or type a model id: ")).trim();
|
|
176
|
+
const n = Number(a);
|
|
177
|
+
if (Number.isInteger(n) && n >= 1 && n <= ids.length) return ids[n - 1]!;
|
|
178
|
+
return fuzzyPick(a, ids) ?? a;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function pickSession(rl: RL): Promise<string | null> {
|
|
182
|
+
const metas = list();
|
|
183
|
+
if (!metas.length) {
|
|
184
|
+
console.log("No saved sessions.");
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const top = metas.slice(0, 20);
|
|
188
|
+
if (stdin.isTTY) {
|
|
189
|
+
const choice = await select(rl, "Resume which session? (↑/↓ · Enter · Esc to cancel):", top.map((m) => m.title));
|
|
190
|
+
if (choice !== null) return top[choice]?.file ?? null;
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
top.forEach((m, i) => console.log(`${String(i + 1).padStart(2)}. ${m.title}`));
|
|
194
|
+
const a = (await rl.question("resume #: ")).trim();
|
|
195
|
+
const idx = Number(a) - 1;
|
|
196
|
+
return top[idx]?.file ?? null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rawOn(rl: RL, onData: (b: Buffer) => void): void {
|
|
200
|
+
rl.pause();
|
|
201
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
202
|
+
stdin.on("data", onData);
|
|
203
|
+
stdin.resume();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function rawOff(rl: RL, onData: (b: Buffer) => void): void {
|
|
207
|
+
stdin.off("data", onData);
|
|
208
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
209
|
+
rl.resume();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Decode a raw stdin chunk to a logical key: arrows (or j/k/Tab), enter, esc, ctrl-c, or the literal char. */
|
|
213
|
+
function decodeKey(s: string): string {
|
|
214
|
+
if (s === "\x1b[A" || s === "k") return "up";
|
|
215
|
+
if (s === "\x1b[B" || s === "j" || s === "\t") return "down";
|
|
216
|
+
if (s === "\r" || s === "\n") return "enter";
|
|
217
|
+
if (s === "\x03") return "ctrl-c";
|
|
218
|
+
if (s === "\x1b") return "esc";
|
|
219
|
+
return s;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read decoded keypresses in raw mode until a handler calls done(). Two robustness points the
|
|
224
|
+
* naive version got wrong: (1) a bare ESC may be the head of a split "\x1b[A" arrow sequence on
|
|
225
|
+
* Windows/slow ptys, so it's held ~50ms and re-joined with the next chunk before being treated as
|
|
226
|
+
* Esc; (2) teardown (listener removal + raw-mode restore + rl.resume) is guaranteed exactly once.
|
|
227
|
+
*/
|
|
228
|
+
function readKeys(rl: RL, onKey: (key: string, done: () => void) => void): void {
|
|
229
|
+
let settled = false;
|
|
230
|
+
let escTimer: ReturnType<typeof setTimeout> | null = null;
|
|
231
|
+
const done = (): void => {
|
|
232
|
+
if (settled) return;
|
|
233
|
+
settled = true;
|
|
234
|
+
if (escTimer) clearTimeout(escTimer);
|
|
235
|
+
stdin.off("data", handler);
|
|
236
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
237
|
+
rl.resume();
|
|
238
|
+
};
|
|
239
|
+
const emit = (s: string): void => onKey(decodeKey(s), done);
|
|
240
|
+
const handler = (buf: Buffer): void => {
|
|
241
|
+
let s = buf.toString("utf8");
|
|
242
|
+
if (escTimer) {
|
|
243
|
+
clearTimeout(escTimer);
|
|
244
|
+
escTimer = null;
|
|
245
|
+
s = `\x1b${s}`; // re-join the ESC we were holding with this follow-up chunk (arrow keys)
|
|
246
|
+
}
|
|
247
|
+
if (s === "\x1b") {
|
|
248
|
+
escTimer = setTimeout(() => {
|
|
249
|
+
escTimer = null;
|
|
250
|
+
emit("\x1b");
|
|
251
|
+
}, 50);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
emit(s);
|
|
255
|
+
};
|
|
256
|
+
rl.pause();
|
|
257
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
258
|
+
stdin.on("data", handler);
|
|
259
|
+
stdin.resume();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Arrow-key list selector. Returns the chosen index, or null on Esc / non-TTY (caller falls back). */
|
|
263
|
+
async function select(rl: RL, title: string, items: string[]): Promise<number | null> {
|
|
264
|
+
if (!stdin.isTTY || !items.length) return null;
|
|
265
|
+
let idx = 0;
|
|
266
|
+
const draw = (first: boolean): void => {
|
|
267
|
+
if (!first) stdout.write(`\x1b[${items.length}A`); // jump back up to redraw in place
|
|
268
|
+
for (let i = 0; i < items.length; i++) {
|
|
269
|
+
stdout.write(i === idx ? `\x1b[2K\x1b[38;5;214m❯ ${items[i]}\x1b[0m\n` : `\x1b[2K ${items[i]}\n`);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
stdout.write(`${title}\n`);
|
|
273
|
+
draw(true);
|
|
274
|
+
return await new Promise<number | null>((res) => {
|
|
275
|
+
readKeys(rl, (key, done) => {
|
|
276
|
+
if (key === "up") {
|
|
277
|
+
idx = (idx - 1 + items.length) % items.length;
|
|
278
|
+
draw(false);
|
|
279
|
+
} else if (key === "down") {
|
|
280
|
+
idx = (idx + 1) % items.length;
|
|
281
|
+
draw(false);
|
|
282
|
+
} else if (key === "enter") {
|
|
283
|
+
done();
|
|
284
|
+
res(idx);
|
|
285
|
+
} else if (key === "esc" || key === "ctrl-c") {
|
|
286
|
+
done();
|
|
287
|
+
res(null);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type PermMode = "ask" | "plan" | "auto";
|
|
294
|
+
type ApproveChoice = "yes" | "auto" | "plan" | "no";
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Tool-approval prompt. A fixed single-key prompt (no scrolling redraw — that fought the streaming
|
|
298
|
+
* transcript and glitched): it states in plain words what permission is being requested + the actual
|
|
299
|
+
* target, then reads one key — [y]es · [a]uto (run the rest without asking) · [p]lan (switch to plan
|
|
300
|
+
* mode, skip this) · [n]o/Esc. `summary` is "<permission phrase>\n<detail>" from the agent.
|
|
301
|
+
*/
|
|
302
|
+
async function approvePrompt(rl: RL, name: string, summary: string): Promise<ApproveChoice> {
|
|
303
|
+
const nl = summary.indexOf("\n");
|
|
304
|
+
const risk = (nl >= 0 ? summary.slice(0, nl) : summary) || `run the ${name} tool`;
|
|
305
|
+
const detail = nl >= 0 ? summary.slice(nl + 1).trim() : "";
|
|
306
|
+
const danger = risk.startsWith("⚠");
|
|
307
|
+
if (!stdin.isTTY) {
|
|
308
|
+
const ans = (await rl.question(`\x1b[33m? ${risk} [y]es / [a]uto / [p]lan / [N]o \x1b[0m`)).trim().toLowerCase();
|
|
309
|
+
return ans[0] === "y" ? "yes" : ans[0] === "a" ? "auto" : ans[0] === "p" ? "plan" : "no";
|
|
310
|
+
}
|
|
311
|
+
const cols = (stdout.columns || 80) - 2;
|
|
312
|
+
const head = `${danger ? "\x1b[31m" : "\x1b[33m"}ada wants to ${risk.replace(/^⚠ /, "")}\x1b[0m`;
|
|
313
|
+
const det = detail ? ` \x1b[2m${detail.length > cols ? `${detail.slice(0, cols - 1)}…` : detail}\x1b[0m\n` : "";
|
|
314
|
+
stdout.write(`\n${danger ? "\x1b[31m⚠\x1b[0m " : ""}${head}\n${det}\x1b[2m[\x1b[0my\x1b[2m]es [\x1b[0ma\x1b[2m]uto [\x1b[0mp\x1b[2m]lan [\x1b[0mn\x1b[2m]o ›\x1b[0m `);
|
|
315
|
+
return await new Promise<ApproveChoice>((res) => {
|
|
316
|
+
readKeys(rl, (key, done) => {
|
|
317
|
+
const k = key.length === 1 ? key.toLowerCase() : key;
|
|
318
|
+
const val: ApproveChoice | null = k === "y" || key === "enter" ? "yes" : k === "a" ? "auto" : k === "p" ? "plan" : k === "n" || key === "esc" || key === "ctrl-c" ? "no" : null;
|
|
319
|
+
if (!val) return;
|
|
320
|
+
done();
|
|
321
|
+
stdout.write(`\r\x1b[2K${val === "no" ? "\x1b[31m✗\x1b[0m skipped" : `\x1b[32m✓\x1b[0m ${val === "auto" ? "auto (won't ask again)" : val === "plan" ? "→ plan mode" : "ran"}`}\n`);
|
|
322
|
+
res(val);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function printTree(currentFile: string): void {
|
|
328
|
+
const metas = list();
|
|
329
|
+
if (!metas.length) {
|
|
330
|
+
console.log("No sessions.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const children = new Map<string, SessionMeta[]>();
|
|
334
|
+
for (const m of metas) {
|
|
335
|
+
if (m.parent) {
|
|
336
|
+
const arr = children.get(m.parent) ?? [];
|
|
337
|
+
arr.push(m);
|
|
338
|
+
children.set(m.parent, arr);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const rec = (m: SessionMeta, depth: number): void => {
|
|
342
|
+
const mark = m.file === currentFile ? "\x1b[38;5;214m●\x1b[0m" : "○";
|
|
343
|
+
console.log(`${" ".repeat(depth)}${mark} ${basename(m.file)} \x1b[2m${m.title}\x1b[0m`);
|
|
344
|
+
for (const c of children.get(m.file) ?? []) rec(c, depth + 1);
|
|
345
|
+
};
|
|
346
|
+
for (const m of metas.filter((x) => !x.parent || !metas.some((y) => y.file === x.parent))) rec(m, 0);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function makeClient(): OpenAI {
|
|
350
|
+
return new OpenAI({ baseURL: BACKEND, apiKey: clientKey(), maxRetries: 0 });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Run the device flow for `provider`; returns true on success (token stored). */
|
|
354
|
+
async function loginFlow(provider: string): Promise<boolean> {
|
|
355
|
+
const cfg = oauthConfig(provider);
|
|
356
|
+
if (!cfg) {
|
|
357
|
+
console.log(`No OAuth config for ${provider}. Set ADA_OAUTH_${provider.toUpperCase()}_{CLIENT_ID,DEVICE_URL,TOKEN_URL}.`);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await deviceLogin(provider, cfg, (s) => console.log(s));
|
|
362
|
+
return true;
|
|
363
|
+
} catch (e) {
|
|
364
|
+
console.error(`login failed: ${e instanceof Error ? e.message : e}`);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Startup login check: probe the backend; if it says 401, offer to sign in and rebuild the client. */
|
|
370
|
+
async function ensureAuth(rl: RL, client: OpenAI): Promise<OpenAI> {
|
|
371
|
+
let status: number;
|
|
372
|
+
try {
|
|
373
|
+
const r = await fetch(`${BACKEND}/whoami`, { headers: { authorization: `Bearer ${clientKey()}` } });
|
|
374
|
+
status = r.status;
|
|
375
|
+
} catch {
|
|
376
|
+
return client; // backend unreachable — the model fetch will report it
|
|
377
|
+
}
|
|
378
|
+
if (status !== 401) return client; // 200 = already authorized, or backend is open (dev)
|
|
379
|
+
const provider = ["github", "google"].find((p) => oauthConfig(p));
|
|
380
|
+
if (!provider) {
|
|
381
|
+
console.log("\x1b[33mthis backend requires login, but no OAuth provider is configured (set ADA_OAUTH_*).\x1b[0m");
|
|
382
|
+
return client;
|
|
383
|
+
}
|
|
384
|
+
const ans = (await rl.question(`\x1b[33mnot logged in — sign in with ${provider}? [Y/n] \x1b[0m`)).trim().toLowerCase();
|
|
385
|
+
if (ans === "n" || ans === "no") return client;
|
|
386
|
+
return (await loginFlow(provider)) ? makeClient() : client;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function authCommand(sub: string, provider?: string): Promise<void> {
|
|
390
|
+
if (!provider) {
|
|
391
|
+
console.error(`usage: ada ${sub} <provider>`);
|
|
392
|
+
console.log(listCredentials().length ? `logged in: ${listCredentials().join(", ")}` : "no stored credentials");
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
if (sub === "logout") {
|
|
396
|
+
await deleteCredential(provider);
|
|
397
|
+
console.log(`logged out ${provider}`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!(await loginFlow(provider))) process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Gate project-level files (.ada/prompts, AGENTS.md, project settings) behind explicit trust. */
|
|
404
|
+
async function ensureTrust(rl: RL): Promise<boolean> {
|
|
405
|
+
const cwd = process.cwd();
|
|
406
|
+
if (isTrusted(cwd)) return true;
|
|
407
|
+
if (!stdin.isTTY) return false; // headless: never load untrusted project files
|
|
408
|
+
const ans = (await rl.question(`Trust ${cwd} and load its .ada config (prompts, AGENTS.md, settings)? [y/N] `)).trim().toLowerCase();
|
|
409
|
+
if (ans === "y" || ans === "yes") {
|
|
410
|
+
addTrust(cwd);
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ANSI-Shadow "ada", rendered as a truecolor splash. █ = gradient body, box glyphs = drop shadow.
|
|
417
|
+
const ADA_ART = [
|
|
418
|
+
" █████╗ ██████╗ █████╗ ",
|
|
419
|
+
"██╔══██╗██╔══██╗██╔══██╗",
|
|
420
|
+
"███████║██║ ██║███████║",
|
|
421
|
+
"██╔══██║██║ ██║██╔══██║",
|
|
422
|
+
"██║ ██║██████╔╝██║ ██║",
|
|
423
|
+
"╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝",
|
|
424
|
+
];
|
|
425
|
+
const GRADIENT: [number, number, number][] = [
|
|
426
|
+
[255, 214, 92], // gold
|
|
427
|
+
[255, 122, 41], // orange
|
|
428
|
+
[214, 51, 132], // magenta
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
/** Interpolate the gradient stops at t∈[0,1]. */
|
|
432
|
+
function gradientAt(t: number): [number, number, number] {
|
|
433
|
+
const seg = Math.max(0, Math.min(1, t)) * (GRADIENT.length - 1);
|
|
434
|
+
const i = Math.min(Math.floor(seg), GRADIENT.length - 2);
|
|
435
|
+
const f = seg - i;
|
|
436
|
+
const [a, b] = [GRADIENT[i]!, GRADIENT[i + 1]!];
|
|
437
|
+
return [0, 1, 2].map((k) => Math.round(a[k]! + (b[k]! - a[k]!) * f)) as [number, number, number];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function adaVersion(): string {
|
|
441
|
+
try {
|
|
442
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
443
|
+
return (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string }).version ?? "0.0.0";
|
|
444
|
+
} catch {
|
|
445
|
+
return "0.0.0";
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const TAG = "a coding agent from zero";
|
|
450
|
+
const W = Math.max(...ADA_ART.map((l) => l.length));
|
|
451
|
+
const H = ADA_ART.length;
|
|
452
|
+
const EDGE = 6; // width of the bright leading edge of the light sweep
|
|
453
|
+
|
|
454
|
+
/** One logo row at light-sweep position `sweep`. Unlit ahead of the edge, white-hot at it, settled gradient behind. */
|
|
455
|
+
function logoRow(line: string, y: number, sweep: number): string {
|
|
456
|
+
let s = "\x1b[2K "; // clear line, then indent
|
|
457
|
+
[...line].forEach((ch, x) => {
|
|
458
|
+
if (ch === " ") return void (s += " ");
|
|
459
|
+
if (ch !== "█") return void (s += `\x1b[0m\x1b[38;2;92;72;82m${ch}`); // outline = drop shadow
|
|
460
|
+
if (x > sweep) return void (s += `\x1b[0m\x1b[38;2;70;55;62m█`); // not yet lit
|
|
461
|
+
const [r, g, b] = gradientAt((x / W + y / H) / 2);
|
|
462
|
+
const d = sweep - x;
|
|
463
|
+
if (d < EDGE) {
|
|
464
|
+
const t = 1 - d / EDGE; // 1 at the edge, fading to 0 as it settles
|
|
465
|
+
const mix = (c: number): number => Math.round(c + (255 - c) * t * 0.85);
|
|
466
|
+
return void (s += `\x1b[1m\x1b[38;2;${mix(r)};${mix(g)};${mix(b)}m█`);
|
|
467
|
+
}
|
|
468
|
+
return void (s += `\x1b[1m\x1b[38;2;${r};${g};${b}m█`);
|
|
469
|
+
});
|
|
470
|
+
return s + "\x1b[0m";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
|
474
|
+
|
|
475
|
+
/** Startup splash: a ~400ms left-to-right light sweep over the logo on a TTY; static plain text otherwise. */
|
|
476
|
+
async function printBanner(): Promise<void> {
|
|
477
|
+
const fancy = stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
478
|
+
if (!fancy) {
|
|
479
|
+
const body = ADA_ART.map((l) => ` ${l}`).join("\n");
|
|
480
|
+
stdout.write(`\n${body}\n ${TAG} v${adaVersion()}\n\n`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const frames = 18;
|
|
484
|
+
stdout.write("\x1b[?25l\n"); // hide cursor, leading blank line
|
|
485
|
+
try {
|
|
486
|
+
for (let f = 0; f <= frames; f++) {
|
|
487
|
+
const sweep = (f / frames) * (W + EDGE);
|
|
488
|
+
if (f > 0) stdout.write(`\x1b[${H}A`); // jump back up to redraw in place
|
|
489
|
+
stdout.write(`${ADA_ART.map((line, y) => logoRow(line, y, sweep)).join("\n")}\n`);
|
|
490
|
+
if (f < frames) await sleep(400 / frames);
|
|
491
|
+
}
|
|
492
|
+
} finally {
|
|
493
|
+
stdout.write("\x1b[?25h"); // always restore the cursor, even if interrupted
|
|
494
|
+
}
|
|
495
|
+
stdout.write(` \x1b[2m${TAG}\x1b[0m \x1b[38;2;214;51;132mv${adaVersion()}\x1b[0m\n\n`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function main(): Promise<void> {
|
|
499
|
+
const sub = process.argv[2];
|
|
500
|
+
if (sub === "login" || sub === "logout") {
|
|
501
|
+
await authCommand(sub, process.argv[3]);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (sub === "add") {
|
|
505
|
+
const spec = process.argv[3];
|
|
506
|
+
if (!spec) {
|
|
507
|
+
console.error("usage: ada add <git-url | npm-package>");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
addExtension(spec);
|
|
512
|
+
} catch (e) {
|
|
513
|
+
console.error(e instanceof Error ? e.message : e);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (sub === "update") {
|
|
519
|
+
selfUpdate();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (sub === "mcp") {
|
|
523
|
+
const action = process.argv[3] ?? "list";
|
|
524
|
+
const name = process.argv[4];
|
|
525
|
+
if (action === "list" || action === "ls") {
|
|
526
|
+
console.log("Connector catalog (● configured · ○ available):\n");
|
|
527
|
+
for (const c of listConnectors()) {
|
|
528
|
+
const dot = c.configured ? "\x1b[38;5;214m●\x1b[0m" : "○";
|
|
529
|
+
const env = c.needsEnv.length ? ` \x1b[2m(set: ${c.needsEnv.join(", ")})\x1b[0m` : "";
|
|
530
|
+
console.log(` ${dot} ${c.name.padEnd(14)} ${c.description}${env}`);
|
|
531
|
+
}
|
|
532
|
+
console.log("\n ada mcp add <name> · ada mcp remove <name>");
|
|
533
|
+
console.log(" custom server: edit .ada/mcp.json — a { command,args } (stdio) or { url } (http) entry");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (action === "add") {
|
|
537
|
+
if (!name) {
|
|
538
|
+
console.error("usage: ada mcp add <name>");
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
const r = addConnector(name);
|
|
542
|
+
if (!r.ok) {
|
|
543
|
+
console.error(r.error);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
console.log(`\x1b[38;5;214m✓\x1b[0m added "${name}" to .ada/mcp.json`);
|
|
547
|
+
if (r.envVars.length) console.log(` set before use: ${r.envVars.join(", ")}`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (action === "remove" || action === "rm") {
|
|
551
|
+
if (!name) {
|
|
552
|
+
console.error("usage: ada mcp remove <name>");
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
console.log(removeConnector(name) ? `removed "${name}" from .ada/mcp.json` : `"${name}" was not configured`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
console.error("usage: ada mcp [list | add <name> | remove <name>]");
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
if (sub === "worktree" || sub === "wt") {
|
|
562
|
+
const action = process.argv[3] ?? "list";
|
|
563
|
+
const git = (...a: string[]): { status: number | null; out: string } => {
|
|
564
|
+
const r = spawnSync("git", a, { encoding: "utf8", cwd: process.cwd() });
|
|
565
|
+
return { status: r.status, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
|
|
566
|
+
};
|
|
567
|
+
if (action === "list" || action === "ls") {
|
|
568
|
+
const r = git("worktree", "list");
|
|
569
|
+
console.log(r.status === 0 ? r.out : "(not a git repo or no worktrees)");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (action === "add" || action === "new") {
|
|
573
|
+
const name = process.argv[4];
|
|
574
|
+
if (!name) {
|
|
575
|
+
console.error("usage: ada worktree add <name>");
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
const branch = `ada/${name}`;
|
|
579
|
+
const dir = resolve(process.cwd(), "..", `${basename(process.cwd())}-${name}`);
|
|
580
|
+
const r = git("worktree", "add", "-b", branch, dir);
|
|
581
|
+
if (r.status !== 0) {
|
|
582
|
+
console.error(r.out || "git worktree add failed");
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
console.log(`\x1b[38;5;214m✓\x1b[0m worktree ${dir}\n branch ${branch} — cd "${dir}" && ada`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (action === "remove" || action === "rm") {
|
|
589
|
+
const name = process.argv[4];
|
|
590
|
+
if (!name) {
|
|
591
|
+
console.error("usage: ada worktree remove <name>");
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
const dir = resolve(process.cwd(), "..", `${basename(process.cwd())}-${name}`);
|
|
595
|
+
const r = git("worktree", "remove", dir);
|
|
596
|
+
console.log(r.status === 0 ? `removed ${dir}` : r.out);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
console.error("usage: ada worktree [list | add <name> | remove <name>]");
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
if (sub === "skill") {
|
|
603
|
+
const action = process.argv[3] ?? "list";
|
|
604
|
+
if (action === "add") {
|
|
605
|
+
const url = process.argv[4];
|
|
606
|
+
if (!url) {
|
|
607
|
+
console.error("usage: ada skill add <url> (a SKILL.md, or a JSON index of them)");
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const added = await addRemoteSkill(url);
|
|
612
|
+
console.log(added.length ? `\x1b[38;5;214m✓\x1b[0m installed: ${added.join(", ")} → ~/.ada/skills/` : "no skills found at that URL");
|
|
613
|
+
} catch (e) {
|
|
614
|
+
console.error(e instanceof Error ? e.message : e);
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (action === "list" || action === "ls") {
|
|
620
|
+
for (const s of loadSkills(true)) console.log(` ${s.name.padEnd(22)} ${s.description}`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
console.error("usage: ada skill [list | add <url>]");
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
if (sub === "catalog") {
|
|
627
|
+
// Offline model catalog (curated popular providers) — context limits + pricing, no backend/network.
|
|
628
|
+
console.log(catalogText(process.argv[3]));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (sub === "acp") {
|
|
632
|
+
// Minimal Agent Client Protocol bridge over stdio (JSON-RPC 2.0, newline-delimited). Scaffold:
|
|
633
|
+
// handles initialize + prompt so an ACP-aware editor can drive ada. Extend method names/framing
|
|
634
|
+
// to match your client's ACP version.
|
|
635
|
+
const trusted = isTrusted(process.cwd());
|
|
636
|
+
const settings = loadSettings(trusted);
|
|
637
|
+
await loadExtensions(trusted);
|
|
638
|
+
registerSkillTool(loadSkills(trusted));
|
|
639
|
+
await loadMcpServers(trusted);
|
|
640
|
+
const client = makeClient();
|
|
641
|
+
let model = process.env.ADA_MODEL || settings.model || "";
|
|
642
|
+
if (!model) {
|
|
643
|
+
try {
|
|
644
|
+
model = (await fetchModelIds(client))[0] ?? "";
|
|
645
|
+
} catch {
|
|
646
|
+
/* offline */
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const agent = new Agent({ client, model, session: Session.create(), onApprove: async (): Promise<ApprovalDecision> => "yes", autoApprove: true, project: trusted, compactAt: settings.compactAt });
|
|
650
|
+
const send = (msg: object): void => void stdout.write(`${JSON.stringify(msg)}\n`);
|
|
651
|
+
let buf = "";
|
|
652
|
+
stdin.on("data", async (d) => {
|
|
653
|
+
buf += d.toString("utf8");
|
|
654
|
+
let nl: number;
|
|
655
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
656
|
+
const line = buf.slice(0, nl).trim();
|
|
657
|
+
buf = buf.slice(nl + 1);
|
|
658
|
+
if (!line) continue;
|
|
659
|
+
let msg: { id?: number; method?: string; params?: Record<string, unknown> };
|
|
660
|
+
try {
|
|
661
|
+
msg = JSON.parse(line);
|
|
662
|
+
} catch {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (msg.method === "initialize") send({ jsonrpc: "2.0", id: msg.id, result: { protocolVersion: 1, agentCapabilities: { promptCapabilities: {} } } });
|
|
666
|
+
else if (msg.method === "session/new" || msg.method === "newSession") send({ jsonrpc: "2.0", id: msg.id, result: { sessionId: "ada" } });
|
|
667
|
+
else if (msg.method === "session/prompt" || msg.method === "prompt") {
|
|
668
|
+
const p = msg.params ?? {};
|
|
669
|
+
const blocks = (p.prompt ?? p.text) as unknown;
|
|
670
|
+
const text = Array.isArray(blocks) ? blocks.map((b) => (b as { text?: string }).text ?? "").join("") : String(blocks ?? "");
|
|
671
|
+
try {
|
|
672
|
+
const out = await agent.send(text, { quiet: true });
|
|
673
|
+
send({ jsonrpc: "2.0", id: msg.id, result: { stopReason: "end_turn", content: [{ type: "text", text: out }] } });
|
|
674
|
+
} catch (e) {
|
|
675
|
+
send({ jsonrpc: "2.0", id: msg.id, error: { code: -32000, message: e instanceof Error ? e.message : String(e) } });
|
|
676
|
+
}
|
|
677
|
+
} else if (msg.id != null) send({ jsonrpc: "2.0", id: msg.id, result: {} });
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
await new Promise(() => {});
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (sub === "share") {
|
|
684
|
+
const arg = process.argv[3];
|
|
685
|
+
const metas = list();
|
|
686
|
+
const meta = arg ? metas.find((m) => m.file.includes(arg) || m.title.toLowerCase().includes(arg.toLowerCase())) : metas[0];
|
|
687
|
+
if (!meta) {
|
|
688
|
+
console.error(arg ? `no session matching "${arg}"` : "no sessions yet");
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
const messages = Session.open(meta.file).load() as Array<{ role?: string; content?: unknown }>;
|
|
692
|
+
const html = renderTranscript(meta.title, messages);
|
|
693
|
+
const port = Number(process.env.ADA_SHARE_PORT) || 8790;
|
|
694
|
+
const { createServer } = await import("node:http");
|
|
695
|
+
createServer((_req, res) => res.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(html)).listen(port, () =>
|
|
696
|
+
console.log(`\x1b[38;5;214m◆\x1b[0m session "${meta.title}" → http://localhost:${port} (local, read-only — Ctrl+C to stop)`),
|
|
697
|
+
);
|
|
698
|
+
await new Promise(() => {});
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (sub === "serve") {
|
|
702
|
+
const trusted = isTrusted(process.cwd());
|
|
703
|
+
const settings = loadSettings(trusted);
|
|
704
|
+
await loadExtensions(trusted);
|
|
705
|
+
registerSkillTool(loadSkills(trusted));
|
|
706
|
+
await loadMcpServers(trusted);
|
|
707
|
+
const client = makeClient();
|
|
708
|
+
let model = (process.argv[3] && !process.argv[3].startsWith("--") ? process.argv[3] : "") || process.env.ADA_MODEL || settings.model || "";
|
|
709
|
+
if (!model) {
|
|
710
|
+
try {
|
|
711
|
+
model = (await fetchModelIds(client))[0] ?? "";
|
|
712
|
+
} catch {
|
|
713
|
+
/* offline */
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const port = Number(process.env.ADA_HTTP_PORT) || 8788;
|
|
717
|
+
const { createServer } = await import("node:http");
|
|
718
|
+
createServer((req, res) => {
|
|
719
|
+
if (req.method === "GET" && (req.url === "/health" || req.url === "/")) {
|
|
720
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ ok: true, model }));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (req.method === "POST" && req.url === "/v1/prompt") {
|
|
724
|
+
let body = "";
|
|
725
|
+
req.on("data", (c) => (body += c));
|
|
726
|
+
req.on("end", async () => {
|
|
727
|
+
try {
|
|
728
|
+
const j = JSON.parse(body || "{}") as { text?: string; model?: string };
|
|
729
|
+
const agent = new Agent({ client, model: j.model || model, session: Session.create(), onApprove: async (): Promise<ApprovalDecision> => "yes", autoApprove: true, project: trusted, compactAt: settings.compactAt });
|
|
730
|
+
const text = await agent.send(String(j.text ?? ""), { quiet: true });
|
|
731
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ text, usage: agent.usageReport() }));
|
|
732
|
+
} catch (e) {
|
|
733
|
+
res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
res.writeHead(404).end();
|
|
739
|
+
}).listen(port, () => console.log(`ada HTTP API on http://localhost:${port} · POST /v1/prompt {"text":"…"} · model ${model || "(none — set one)"}`));
|
|
740
|
+
await new Promise(() => {}); // keep the process alive for the server
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
744
|
+
void prefetch(); // warm the models.dev catalog (pricing/limits) in the background
|
|
745
|
+
let client = makeClient();
|
|
746
|
+
|
|
747
|
+
if (flags.listModels) {
|
|
748
|
+
await printModels(client);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const scoped = flags.models ?? [];
|
|
753
|
+
|
|
754
|
+
// Headless RPC mode: newline-delimited JSON over stdio. One {"type":"prompt","text":…} per line in.
|
|
755
|
+
if (flags.rpc) {
|
|
756
|
+
const trusted = isTrusted(process.cwd());
|
|
757
|
+
const settings = loadSettings(trusted);
|
|
758
|
+
let rm = flags.model ?? process.env.ADA_MODEL ?? settings.model ?? scoped[0] ?? "";
|
|
759
|
+
if (!rm) {
|
|
760
|
+
try {
|
|
761
|
+
rm = (await fetchModelIds(client))[0] ?? "";
|
|
762
|
+
} catch {
|
|
763
|
+
/* ignore */
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (!rm) {
|
|
767
|
+
process.stdout.write(`${JSON.stringify({ type: "error", error: "no model available" })}\n`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
await loadExtensions(trusted);
|
|
771
|
+
registerSkillTool(loadSkills(trusted));
|
|
772
|
+
await loadMcpServers(trusted);
|
|
773
|
+
const agent = new Agent({
|
|
774
|
+
client,
|
|
775
|
+
model: rm,
|
|
776
|
+
session: Session.create(),
|
|
777
|
+
onApprove: async (): Promise<ApprovalDecision> => "yes",
|
|
778
|
+
autoApprove: true,
|
|
779
|
+
reasoning: flags.reasoning ?? settings.reasoning,
|
|
780
|
+
project: trusted,
|
|
781
|
+
compactAt: settings.compactAt,
|
|
782
|
+
});
|
|
783
|
+
process.stdout.write(`${JSON.stringify({ type: "ready", model: rm })}\n`);
|
|
784
|
+
for await (const line of createInterface({ input: stdin })) {
|
|
785
|
+
const t = line.trim();
|
|
786
|
+
if (!t) continue;
|
|
787
|
+
let prompt = t;
|
|
788
|
+
try {
|
|
789
|
+
const obj = JSON.parse(t) as { text?: string; prompt?: string };
|
|
790
|
+
prompt = obj.text ?? obj.prompt ?? "";
|
|
791
|
+
} catch {
|
|
792
|
+
/* treat the raw line as the prompt */
|
|
793
|
+
}
|
|
794
|
+
if (!prompt) continue;
|
|
795
|
+
try {
|
|
796
|
+
const text = await agent.send(prompt, { quiet: true });
|
|
797
|
+
process.stdout.write(`${JSON.stringify({ type: "result", text, usage: agent.usageReport() })}\n`);
|
|
798
|
+
} catch (e) {
|
|
799
|
+
process.stdout.write(`${JSON.stringify({ type: "error", error: e instanceof Error ? e.message : String(e) })}\n`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Headless print mode: run one prompt non-interactively and exit.
|
|
806
|
+
if (flags.print !== undefined) {
|
|
807
|
+
const trusted = isTrusted(process.cwd());
|
|
808
|
+
const settings = loadSettings(trusted);
|
|
809
|
+
let pm = flags.model ?? process.env.ADA_MODEL ?? settings.model ?? scoped[0] ?? "";
|
|
810
|
+
if (!pm) {
|
|
811
|
+
try {
|
|
812
|
+
pm = (await fetchModelIds(client))[0] ?? "";
|
|
813
|
+
} catch {
|
|
814
|
+
/* ignore */
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (!pm) {
|
|
818
|
+
console.error("No model available. Pass --model <id> or set ADA_MODEL.");
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
const agent = new Agent({
|
|
822
|
+
client,
|
|
823
|
+
model: pm,
|
|
824
|
+
session: Session.create(),
|
|
825
|
+
onApprove: async (): Promise<ApprovalDecision> => "yes",
|
|
826
|
+
autoApprove: true,
|
|
827
|
+
reasoning: flags.reasoning ?? settings.reasoning,
|
|
828
|
+
project: trusted,
|
|
829
|
+
compactAt: settings.compactAt,
|
|
830
|
+
});
|
|
831
|
+
if (flags.strategy) agent.setStrategy(flags.strategy);
|
|
832
|
+
const text = await agent.send(flags.print, { quiet: !!flags.json });
|
|
833
|
+
if (flags.json) console.log(JSON.stringify({ model: pm, text, usage: agent.usageReport() }));
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
838
|
+
await printBanner();
|
|
839
|
+
// While a turn runs we listen for raw keys (interrupt/steer); onApprove pauses this to read a line.
|
|
840
|
+
let turn: { onData: (b: Buffer) => void } | null = null;
|
|
841
|
+
|
|
842
|
+
const includeProject = await ensureTrust(rl);
|
|
843
|
+
const settings = loadSettings(includeProject);
|
|
844
|
+
const prompts = loadPrompts(includeProject);
|
|
845
|
+
const kbInterrupt = settings.keybindings?.interrupt;
|
|
846
|
+
const exts = await loadExtensions(includeProject);
|
|
847
|
+
const skills = loadSkills(includeProject);
|
|
848
|
+
registerSkillTool(skills);
|
|
849
|
+
const mcp = await loadMcpServers(includeProject);
|
|
850
|
+
|
|
851
|
+
client = await ensureAuth(rl, client); // always check login at startup; prompt if the backend says 401
|
|
852
|
+
|
|
853
|
+
let session: Session;
|
|
854
|
+
let history: Msg[] = [];
|
|
855
|
+
if (flags.cont) {
|
|
856
|
+
const s = Session.latest();
|
|
857
|
+
if (s) {
|
|
858
|
+
session = s;
|
|
859
|
+
history = s.load() as unknown as Msg[];
|
|
860
|
+
console.log(`Resuming ${s.file} (${history.length} messages)`);
|
|
861
|
+
} else {
|
|
862
|
+
console.log("No session to continue; starting fresh.");
|
|
863
|
+
session = Session.create();
|
|
864
|
+
}
|
|
865
|
+
} else if (flags.resume) {
|
|
866
|
+
const file = await pickSession(rl);
|
|
867
|
+
if (file) {
|
|
868
|
+
session = Session.open(file);
|
|
869
|
+
history = session.load() as unknown as Msg[];
|
|
870
|
+
console.log(`Resuming (${history.length} messages)`);
|
|
871
|
+
} else {
|
|
872
|
+
session = Session.create();
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
session = Session.create();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
let model = flags.model ?? process.env.ADA_MODEL ?? settings.model ?? scoped[0] ?? "";
|
|
879
|
+
if (!model) {
|
|
880
|
+
model = await pickModel(client, rl);
|
|
881
|
+
if (!model) {
|
|
882
|
+
rl.close();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const autoApprove = !!flags.yolo || process.env.ADA_AUTO_APPROVE === "1" || !!settings.autoApprove;
|
|
888
|
+
// Permission mode: ask = confirm each tool, plan = read-only (plan, don't run), auto = run freely.
|
|
889
|
+
let mode = "ask" as PermMode; // `as` keeps the CFA type PermMode (it's mutated via setMode, a closure)
|
|
890
|
+
let setMode = (_m: PermMode): void => {}; // reassigned once `agent` exists
|
|
891
|
+
const onApprove: OnApprove = async (name, summary): Promise<ApprovalDecision> => {
|
|
892
|
+
if (mode === "auto") return "yes";
|
|
893
|
+
if (turn && stdin.isTTY) rawOff(rl, turn.onData); // detach the turn's raw key listener first
|
|
894
|
+
try {
|
|
895
|
+
const choice = await approvePrompt(rl, name, summary);
|
|
896
|
+
if (choice === "auto") {
|
|
897
|
+
setMode("auto");
|
|
898
|
+
return "all";
|
|
899
|
+
}
|
|
900
|
+
if (choice === "plan") {
|
|
901
|
+
setMode("plan");
|
|
902
|
+
return "no";
|
|
903
|
+
}
|
|
904
|
+
return choice; // "yes" | "no"
|
|
905
|
+
} finally {
|
|
906
|
+
if (turn && stdin.isTTY) rawOn(rl, turn.onData);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
setAsker(async (question, options) => {
|
|
911
|
+
if (turn && stdin.isTTY) rawOff(rl, turn.onData);
|
|
912
|
+
try {
|
|
913
|
+
// Multiple-choice → arrow-key selector; free-text → a plain line.
|
|
914
|
+
if (options?.length && stdin.isTTY) {
|
|
915
|
+
const i = await select(rl, `\x1b[36m? ${question}\x1b[0m`, options);
|
|
916
|
+
return i == null ? "" : options[i]!;
|
|
917
|
+
}
|
|
918
|
+
let prompt = `\x1b[36m? ${question}\x1b[0m`;
|
|
919
|
+
if (options?.length) prompt += `\n${options.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}\n› `;
|
|
920
|
+
else prompt += " ";
|
|
921
|
+
const ans = (await rl.question(prompt)).trim();
|
|
922
|
+
if (options?.length) {
|
|
923
|
+
const n = Number(ans);
|
|
924
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) return options[n - 1]!;
|
|
925
|
+
}
|
|
926
|
+
return ans;
|
|
927
|
+
} finally {
|
|
928
|
+
if (turn && stdin.isTTY) rawOn(rl, turn.onData);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// Subagent: delegate an isolated subtask to a fresh ada agent (registered before the agent
|
|
933
|
+
// snapshots its tool list, so it appears in the registry).
|
|
934
|
+
registerTool({
|
|
935
|
+
name: "spawn_agent",
|
|
936
|
+
description: "Delegate a self-contained subtask to a fresh ada sub-agent; returns its final summary. Use for isolated research or a chunk of work handled independently.",
|
|
937
|
+
parameters: {
|
|
938
|
+
type: "object",
|
|
939
|
+
properties: { task: { type: "string", description: "The subtask, with all the context the sub-agent needs." } },
|
|
940
|
+
required: ["task"],
|
|
941
|
+
additionalProperties: false,
|
|
942
|
+
},
|
|
943
|
+
needsApproval: false,
|
|
944
|
+
async run(args) {
|
|
945
|
+
const sub = new Agent({
|
|
946
|
+
client,
|
|
947
|
+
model,
|
|
948
|
+
session: Session.create(),
|
|
949
|
+
onApprove,
|
|
950
|
+
autoApprove,
|
|
951
|
+
reasoning: flags.reasoning ?? settings.reasoning,
|
|
952
|
+
project: includeProject,
|
|
953
|
+
compactAt: settings.compactAt,
|
|
954
|
+
});
|
|
955
|
+
try {
|
|
956
|
+
const text = await sub.send(String(args.task ?? ""), { quiet: true });
|
|
957
|
+
return { output: text || "(sub-agent returned no text)" };
|
|
958
|
+
} catch (e) {
|
|
959
|
+
return { output: String(e instanceof Error ? e.message : e), isError: true };
|
|
960
|
+
}
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
registerTool({
|
|
965
|
+
name: "background_task",
|
|
966
|
+
description: "Start a self-contained subtask in the background and return its job id immediately — don't wait for it. Use for long, independent work. The user checks results with /jobs.",
|
|
967
|
+
parameters: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: { task: { type: "string", description: "The subtask, with all the context the sub-agent needs." } },
|
|
970
|
+
required: ["task"],
|
|
971
|
+
additionalProperties: false,
|
|
972
|
+
},
|
|
973
|
+
needsApproval: false,
|
|
974
|
+
async run(args) {
|
|
975
|
+
const task = String(args.task ?? "");
|
|
976
|
+
const id = startJob(task, async () => {
|
|
977
|
+
const sub = new Agent({ client, model, session: Session.create(), onApprove, autoApprove: true, project: includeProject, compactAt: settings.compactAt });
|
|
978
|
+
return sub.send(task, { quiet: true });
|
|
979
|
+
});
|
|
980
|
+
return { output: `Started background job ${id}. Check results with /jobs (don't wait on it).` };
|
|
981
|
+
},
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
const agent = new Agent({
|
|
985
|
+
client,
|
|
986
|
+
model,
|
|
987
|
+
session,
|
|
988
|
+
onApprove,
|
|
989
|
+
autoApprove,
|
|
990
|
+
reasoning: flags.reasoning ?? settings.reasoning,
|
|
991
|
+
project: includeProject,
|
|
992
|
+
compactAt: settings.compactAt,
|
|
993
|
+
history,
|
|
994
|
+
});
|
|
995
|
+
if (flags.strategy) agent.setStrategy(flags.strategy);
|
|
996
|
+
if (flags.agent && !switchAgent(agent, flags.agent, settings)) console.error(`unknown agent: ${flags.agent} (configure in .ada/settings.json)`);
|
|
997
|
+
|
|
998
|
+
setMode = (m: PermMode): void => {
|
|
999
|
+
mode = m;
|
|
1000
|
+
agent.setPlanMode(m === "plan");
|
|
1001
|
+
agent.setAutoApprove(m === "auto");
|
|
1002
|
+
};
|
|
1003
|
+
setMode(autoApprove ? "auto" : "ask"); // apply the initial mode (e.g. --yolo → auto) consistently
|
|
1004
|
+
|
|
1005
|
+
if (flags.tui && stdin.isTTY) {
|
|
1006
|
+
rl.close(); // hand stdin to the TUI so readline doesn't echo keystrokes too
|
|
1007
|
+
await runTui(agent, model);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
console.log(`\nada — model ${model} via ${BACKEND}`);
|
|
1012
|
+
console.log("commands: /model [id] /models /next /reasoning [low|medium|high|off] /compact /context /exit");
|
|
1013
|
+
console.log(" \x1b[1mmode:\x1b[0m /ask /plan /auto (or /mode to cycle) · /run /fork /tree /rewind /undo /todos /cost /image /paste");
|
|
1014
|
+
if (prompts.size) console.log(`prompt templates: ${[...prompts.keys()].map((k) => `/${k}`).join(" ")}`);
|
|
1015
|
+
if (exts.length) console.log(`extensions: ${exts.join(" ")}`);
|
|
1016
|
+
if (skills.length) console.log(`skills: ${skills.map((s) => s.name).join(" ")}`);
|
|
1017
|
+
if (mcp.length) console.log(`mcp: ${mcp.join(" ")}`);
|
|
1018
|
+
console.log("\x1b[2mduring a turn: Esc/Ctrl+C = interrupt · type + Enter = steer\x1b[0m\n");
|
|
1019
|
+
|
|
1020
|
+
const pendingImages: string[] = []; // images attached via /image or /paste, sent with the next message
|
|
1021
|
+
for (;;) {
|
|
1022
|
+
if (stdin.isTTY) {
|
|
1023
|
+
const modeTag = mode === "plan" ? " · \x1b[33mplan\x1b[0m\x1b[2m" : mode === "auto" ? " · \x1b[31mauto\x1b[0m\x1b[2m" : " · ask";
|
|
1024
|
+
process.stdout.write(`\x1b[2m${model}${modeTag} · ~${agent.contextTokens()} tok\x1b[0m\n`);
|
|
1025
|
+
}
|
|
1026
|
+
let line: string;
|
|
1027
|
+
try {
|
|
1028
|
+
line = (await rl.question("\x1b[38;5;214m›\x1b[0m ")).trim();
|
|
1029
|
+
} catch {
|
|
1030
|
+
break; // stdin closed (Ctrl+D / EOF)
|
|
1031
|
+
}
|
|
1032
|
+
if (!line) continue;
|
|
1033
|
+
if (line === "/exit" || line === "/quit") break;
|
|
1034
|
+
if (line === "/compact") {
|
|
1035
|
+
try {
|
|
1036
|
+
console.log(await agent.compactNow());
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
console.error(`[error] ${e instanceof Error ? e.message : e}`);
|
|
1039
|
+
}
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
if (line === "/context") {
|
|
1043
|
+
console.log(`~${agent.contextTokens()} est. tokens in context`);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (line === "/tree") {
|
|
1047
|
+
printTree(session.file);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (line === "/fork") {
|
|
1051
|
+
session = Session.open(agent.fork());
|
|
1052
|
+
console.log(`\x1b[2mforked → new branch ${basename(session.file)}\x1b[0m`);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
if (line === "/rewind") {
|
|
1056
|
+
console.log(agent.rewind());
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
if (line === "/cost") {
|
|
1060
|
+
console.log(agent.usageReport());
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (line === "/undo") {
|
|
1064
|
+
console.log(undoAll());
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (line === "/snapshot") {
|
|
1068
|
+
const t = snapshot();
|
|
1069
|
+
console.log(t ? `\x1b[38;5;214m✓\x1b[0m snapshot saved (${t.slice(0, 8)}) — /restore to roll back the whole tree` : "snapshot failed (not a git repo?)");
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (line === "/restore") {
|
|
1073
|
+
console.log(restoreSnapshot() ? "\x1b[38;5;214m✓\x1b[0m restored the working tree to the last snapshot" : "nothing to restore (take a /snapshot first)");
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (line === "/jobs") {
|
|
1077
|
+
console.log(renderJobs());
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (line === "/todos") {
|
|
1081
|
+
console.log(renderTodos());
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (line === "/ask" || line === "/auto" || line === "/plan" || line === "/mode") {
|
|
1085
|
+
const next: PermMode = line === "/mode" ? (mode === "ask" ? "plan" : mode === "plan" ? "auto" : "ask") : (line.slice(1) as PermMode);
|
|
1086
|
+
setMode(next);
|
|
1087
|
+
const blurb = { ask: "confirm each tool before it runs", plan: "ada plans but won't edit — /run to execute", auto: "run tools without asking (destructive bash still confirms)" }[next];
|
|
1088
|
+
console.log(`mode → \x1b[1m${next}\x1b[0m \x1b[2m(${blurb})\x1b[0m`);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
if (line === "/run") {
|
|
1092
|
+
if (mode !== "plan") {
|
|
1093
|
+
console.log("not in plan mode.");
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
setMode("ask");
|
|
1097
|
+
console.log("\x1b[2mplan approved — executing…\x1b[0m");
|
|
1098
|
+
line = "Proceed and implement the plan above.";
|
|
1099
|
+
}
|
|
1100
|
+
if (line === "/models") {
|
|
1101
|
+
await printModels(client);
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
if (line === "/catalog" || line.startsWith("/catalog ")) {
|
|
1105
|
+
console.log(catalogText(line.slice("/catalog".length).trim() || undefined));
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (line === "/model" || line.startsWith("/model ")) {
|
|
1109
|
+
const id = line.slice("/model".length).trim();
|
|
1110
|
+
if (id) {
|
|
1111
|
+
agent.setModel(id);
|
|
1112
|
+
model = id;
|
|
1113
|
+
console.log(`model → ${id}`);
|
|
1114
|
+
} else {
|
|
1115
|
+
console.log(`current model: ${model}`);
|
|
1116
|
+
}
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
if (line === "/next") {
|
|
1120
|
+
if (scoped.length) {
|
|
1121
|
+
model = scoped[(scoped.indexOf(model) + 1) % scoped.length]!;
|
|
1122
|
+
agent.setModel(model);
|
|
1123
|
+
console.log(`model → ${model}`);
|
|
1124
|
+
} else {
|
|
1125
|
+
console.log("no --models scope set (start with --models a,b,c)");
|
|
1126
|
+
}
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
if (line === "/reasoning" || line.startsWith("/reasoning ")) {
|
|
1130
|
+
const v = line.slice("/reasoning".length).trim();
|
|
1131
|
+
if (v === "low" || v === "medium" || v === "high") {
|
|
1132
|
+
agent.setReasoning(v);
|
|
1133
|
+
console.log(`reasoning → ${v}`);
|
|
1134
|
+
} else if (v === "off" || v === "none") {
|
|
1135
|
+
agent.setReasoning(undefined);
|
|
1136
|
+
console.log("reasoning → off");
|
|
1137
|
+
} else {
|
|
1138
|
+
console.log(`reasoning: ${agent.reasoning ?? "off"} (set: low | medium | high | off)`);
|
|
1139
|
+
}
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (line === "/strategy" || line.startsWith("/strategy ")) {
|
|
1143
|
+
const v = line.slice("/strategy".length).trim();
|
|
1144
|
+
if (v) {
|
|
1145
|
+
agent.setStrategy(v);
|
|
1146
|
+
console.log(`strategy → ${v}`);
|
|
1147
|
+
} else {
|
|
1148
|
+
console.log(`strategy: ${agent.getStrategy()} (react | single | plan | multi | toolsmith)`);
|
|
1149
|
+
}
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (line === "/agent" || line.startsWith("/agent ")) {
|
|
1153
|
+
const name = line.slice("/agent".length).trim();
|
|
1154
|
+
if (!name) console.log(`agents: ${Object.keys(settings.agents ?? {}).join(", ") || "(none — configure in .ada/settings.json)"}`);
|
|
1155
|
+
else if (switchAgent(agent, name, settings)) console.log(`agent → ${name}`);
|
|
1156
|
+
else console.log(`unknown agent: ${name}`);
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (line === "/image" || line.startsWith("/image ")) {
|
|
1160
|
+
const p = line.slice("/image".length).trim();
|
|
1161
|
+
if (!p) {
|
|
1162
|
+
console.log("usage: /image <path> (attaches an image to your next message)");
|
|
1163
|
+
} else {
|
|
1164
|
+
const img = loadImage(p);
|
|
1165
|
+
if (!img) console.log(`could not read image: ${p} (need .png/.jpg/.gif/.webp/.bmp)`);
|
|
1166
|
+
else {
|
|
1167
|
+
pendingImages.push(img.dataUrl);
|
|
1168
|
+
console.log(`\x1b[2m📎 ${img.name} (${Math.round(img.bytes / 1024)} KB) attached — ${pendingImages.length} image(s) queued; now type your question\x1b[0m`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
if (line === "/paste") {
|
|
1174
|
+
const clipImg = readClipboardImage();
|
|
1175
|
+
if (clipImg) {
|
|
1176
|
+
pendingImages.push(clipImg);
|
|
1177
|
+
console.log(`\x1b[2m📎 image attached from clipboard — ${pendingImages.length} queued; now type your question\x1b[0m`);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const clip = readClipboard();
|
|
1181
|
+
if (!clip) {
|
|
1182
|
+
console.log("clipboard empty or unavailable");
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
console.log(`\x1b[2m(pasted ${clip.length} chars from clipboard)\x1b[0m`);
|
|
1186
|
+
line = clip;
|
|
1187
|
+
}
|
|
1188
|
+
if (line.startsWith("/")) {
|
|
1189
|
+
const cn = line.slice(1).split(/\s+/)[0]!;
|
|
1190
|
+
const cmd = getCommands().get(cn);
|
|
1191
|
+
if (cmd) {
|
|
1192
|
+
try {
|
|
1193
|
+
const out = await cmd.run(line.slice(1 + cn.length).trim());
|
|
1194
|
+
if (out) console.log(out);
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
console.error(`[command ${cn}] ${e instanceof Error ? e.message : e}`);
|
|
1197
|
+
}
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
let toSend = line;
|
|
1202
|
+
if (line.startsWith("/")) {
|
|
1203
|
+
const expanded = expandPrompt(prompts, line);
|
|
1204
|
+
if (expanded === null) {
|
|
1205
|
+
console.log(`unknown command: ${line.split(/\s+/)[0]} (chat without the leading /, or add .ada/prompts/<name>.md)`);
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
toSend = expanded;
|
|
1209
|
+
}
|
|
1210
|
+
const abort = new AbortController();
|
|
1211
|
+
const steer: string[] = [];
|
|
1212
|
+
let lineBuf = "";
|
|
1213
|
+
const onData = (buf: Buffer): void => {
|
|
1214
|
+
const s = buf.toString("utf8");
|
|
1215
|
+
if (s === "\x03" || s === "\x1b" || (kbInterrupt !== undefined && s === kbInterrupt)) {
|
|
1216
|
+
abort.abort(); // Ctrl+C / Esc / configured key → interrupt this turn
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (s.startsWith("\x1b")) return; // ignore other escape sequences (arrow keys, etc.)
|
|
1220
|
+
for (const ch of s) {
|
|
1221
|
+
if (ch === "\r" || ch === "\n") {
|
|
1222
|
+
const m = lineBuf.trim();
|
|
1223
|
+
lineBuf = "";
|
|
1224
|
+
if (m) {
|
|
1225
|
+
steer.push(m);
|
|
1226
|
+
process.stdout.write(`\x1b[2m ↳ queued (steers after this turn): ${m}\x1b[0m\n`);
|
|
1227
|
+
}
|
|
1228
|
+
} else if (ch === "\x7f") {
|
|
1229
|
+
lineBuf = lineBuf.slice(0, -1);
|
|
1230
|
+
} else if (ch >= " ") {
|
|
1231
|
+
lineBuf += ch;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
turn = { onData };
|
|
1236
|
+
if (stdin.isTTY) rawOn(rl, onData);
|
|
1237
|
+
const turnStart = Date.now();
|
|
1238
|
+
track("turn", { model });
|
|
1239
|
+
const imgs = pendingImages.length ? pendingImages.slice() : undefined;
|
|
1240
|
+
pendingImages.length = 0;
|
|
1241
|
+
process.stdout.write("\n\x1b[38;5;214m◆\x1b[0m \x1b[1mada\x1b[0m\n");
|
|
1242
|
+
try {
|
|
1243
|
+
await agent.send(toSend, { signal: abort.signal, steer, images: imgs });
|
|
1244
|
+
if (!abort.signal.aborted && Date.now() - turnStart > 8000) notify("ada", "task complete");
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
track("error", { message: e instanceof Error ? e.message : String(e) });
|
|
1247
|
+
console.error(`\n[error] ${e instanceof Error ? e.message : e}`);
|
|
1248
|
+
} finally {
|
|
1249
|
+
if (stdin.isTTY) rawOff(rl, onData);
|
|
1250
|
+
turn = null;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
rl.close();
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
main().then(
|
|
1257
|
+
() => process.exit(0), // explicit exit: node-pty (bash) and stdin can keep the loop alive otherwise
|
|
1258
|
+
(e) => {
|
|
1259
|
+
console.error(e instanceof Error ? e.message : e);
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
},
|
|
1262
|
+
);
|