@orangecollective/oc 0.1.1
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 +103 -0
- package/dist/oc.js +1416 -0
- package/package.json +64 -0
package/dist/oc.js
ADDED
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/oc.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
|
|
6
|
+
// src/auth.ts
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
9
|
+
import { createClient } from "@supabase/supabase-js";
|
|
10
|
+
import open from "open";
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
var VERSION = true ? "0.1.1" : "0.0.0-dev";
|
|
17
|
+
function isOlderThan(version, floor) {
|
|
18
|
+
const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
19
|
+
const [a, b] = [parse(version), parse(floor)];
|
|
20
|
+
for (let i = 0; i < 3; i++) {
|
|
21
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) < (b[i] ?? 0);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
var DEFAULT_HOST = "https://www.orangecollective.vc";
|
|
26
|
+
var MODELS = [
|
|
27
|
+
{ provider: "anthropic", id: "claude-fable-5-1", label: "Claude Fable 5.1", short: "Fable 5.1" },
|
|
28
|
+
{ provider: "anthropic", id: "claude-opus-5-5", label: "Claude Opus 5.5", short: "Opus 5.5" },
|
|
29
|
+
{ provider: "anthropic", id: "claude-sonnet-5", label: "Claude Sonnet 5", short: "Sonnet 5" },
|
|
30
|
+
{ provider: "openai", id: "gpt-6-astra", label: "GPT-6 Astra", short: "GPT-6 Astra" },
|
|
31
|
+
{ provider: "openai", id: "gpt-6-sol", label: "GPT-6 Sol", short: "GPT-6 Sol" },
|
|
32
|
+
{ provider: "openai", id: "gpt-6-luna", label: "GPT-6 Luna", short: "GPT-6 Luna" }
|
|
33
|
+
];
|
|
34
|
+
var DEFAULT_MODEL_ID = "gpt-6-sol";
|
|
35
|
+
var configDir = () => process.env.OC_CONFIG_DIR ?? path.join(homedir(), ".oc");
|
|
36
|
+
var credentialsPath = () => path.join(configDir(), "credentials.json");
|
|
37
|
+
var statePath = () => path.join(configDir(), "state.json");
|
|
38
|
+
async function readJson(file) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function writeSecret(file, value) {
|
|
46
|
+
await mkdir(configDir(), { recursive: true, mode: 448 });
|
|
47
|
+
await writeFile(file, `${JSON.stringify(value, null, 2)}
|
|
48
|
+
`, { mode: 384 });
|
|
49
|
+
await chmod(file, 384);
|
|
50
|
+
}
|
|
51
|
+
var readCredentials = () => readJson(credentialsPath());
|
|
52
|
+
var writeCredentials = (c) => writeSecret(credentialsPath(), c);
|
|
53
|
+
var clearCredentials = () => rm(credentialsPath(), { force: true });
|
|
54
|
+
var readState = () => readJson(statePath());
|
|
55
|
+
async function patchState(patch) {
|
|
56
|
+
const next = { ...await readState() ?? {}, ...patch };
|
|
57
|
+
await writeSecret(statePath(), next);
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
function resolveHost(flagHost, stored) {
|
|
61
|
+
const host = flagHost ?? process.env.OC_HOST ?? stored ?? DEFAULT_HOST;
|
|
62
|
+
return host.replace(/\/+$/, "");
|
|
63
|
+
}
|
|
64
|
+
function modelLabel(modelId) {
|
|
65
|
+
return MODELS.find((m) => m.id === modelId)?.short ?? modelId;
|
|
66
|
+
}
|
|
67
|
+
function qualifiedModelId(modelId) {
|
|
68
|
+
const model = MODELS.find((m) => m.id === modelId);
|
|
69
|
+
return model ? `${model.provider}/${model.id}` : `openai/${DEFAULT_MODEL_ID}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/auth.ts
|
|
73
|
+
var LOGIN_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
74
|
+
var REFRESH_SKEW_SECONDS = 120;
|
|
75
|
+
async function fetchCliConfig(host) {
|
|
76
|
+
const res = await fetch(`${host}/api/cli/config`);
|
|
77
|
+
if (!res.ok) throw new Error(`Couldn't reach ${host} (HTTP ${res.status})`);
|
|
78
|
+
const json = await res.json();
|
|
79
|
+
if (!json.supabaseUrl || !json.supabaseAnonKey) {
|
|
80
|
+
throw new Error(`${host} didn't return a usable CLI config`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
supabaseUrl: json.supabaseUrl,
|
|
84
|
+
supabaseAnonKey: json.supabaseAnonKey,
|
|
85
|
+
host: new URL(res.url).origin,
|
|
86
|
+
minCliVersion: json.minCliVersion
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function anonClient(config) {
|
|
90
|
+
return createClient(config.supabaseUrl, config.supabaseAnonKey, {
|
|
91
|
+
auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function constantTimeEquals(a, b) {
|
|
95
|
+
const left = Buffer.from(a);
|
|
96
|
+
const right = Buffer.from(b);
|
|
97
|
+
if (left.length !== right.length) return false;
|
|
98
|
+
return timingSafeEqual(left, right);
|
|
99
|
+
}
|
|
100
|
+
var DONE_PAGE = `<!doctype html><meta charset="utf-8"><title>Orange Collective CLI</title>
|
|
101
|
+
<body style="font:15px/1.6 -apple-system,system-ui,sans-serif;display:grid;place-items:center;height:100vh;margin:0;color:#111">
|
|
102
|
+
<div style="text-align:center"><h1 style="font-size:17px;margin:0 0 8px">You're signed in</h1>
|
|
103
|
+
<p style="margin:0;color:#666">Close this tab and head back to your terminal.</p></div>`;
|
|
104
|
+
function awaitHandoff(expectedCode) {
|
|
105
|
+
return new Promise((resolveServer, rejectServer) => {
|
|
106
|
+
let settle;
|
|
107
|
+
let fail2;
|
|
108
|
+
const handoff = new Promise((res, rej) => {
|
|
109
|
+
settle = res;
|
|
110
|
+
fail2 = rej;
|
|
111
|
+
});
|
|
112
|
+
const server = createServer((req, res) => {
|
|
113
|
+
if (req.method !== "POST" || !req.url?.startsWith("/callback")) {
|
|
114
|
+
res.writeHead(404).end();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let body = "";
|
|
118
|
+
req.on("data", (chunk) => {
|
|
119
|
+
body += chunk;
|
|
120
|
+
if (body.length > 8192) req.destroy();
|
|
121
|
+
});
|
|
122
|
+
req.on("end", () => {
|
|
123
|
+
const form = new URLSearchParams(body);
|
|
124
|
+
const code = form.get("code") ?? "";
|
|
125
|
+
const tokenHash = form.get("tokenHash") ?? "";
|
|
126
|
+
const email = form.get("email") ?? "";
|
|
127
|
+
if (!constantTimeEquals(code, expectedCode) || !tokenHash || !email) {
|
|
128
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end("Unexpected login code");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(DONE_PAGE);
|
|
132
|
+
settle({ tokenHash, email });
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
fail2(new Error("Timed out waiting for the browser. Run `oc login` again."));
|
|
137
|
+
}, LOGIN_TIMEOUT_MS);
|
|
138
|
+
handoff.finally(() => {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
server.close();
|
|
141
|
+
});
|
|
142
|
+
server.on("error", rejectServer);
|
|
143
|
+
server.listen(0, "127.0.0.1", () => {
|
|
144
|
+
resolveServer({ port: server.address().port, handoff });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function login(requestedHost, events) {
|
|
149
|
+
const config = await fetchCliConfig(requestedHost);
|
|
150
|
+
assertVersionSupported(config.minCliVersion);
|
|
151
|
+
const host = config.host;
|
|
152
|
+
const code = randomBytes(32).toString("hex");
|
|
153
|
+
const { port, handoff } = await awaitHandoff(code);
|
|
154
|
+
const url = `${host}/auth/cli/${port}/${code}`;
|
|
155
|
+
events.onUrl(url, code.slice(0, 8));
|
|
156
|
+
void open(url).catch(() => {
|
|
157
|
+
});
|
|
158
|
+
const { tokenHash, email } = await handoff;
|
|
159
|
+
const { data, error } = await anonClient(config).auth.verifyOtp({
|
|
160
|
+
token_hash: tokenHash,
|
|
161
|
+
type: "magiclink"
|
|
162
|
+
});
|
|
163
|
+
if (error || !data.session) {
|
|
164
|
+
throw new Error(error?.message ?? "The login code was rejected. Try `oc login` again.");
|
|
165
|
+
}
|
|
166
|
+
const credentials = {
|
|
167
|
+
host,
|
|
168
|
+
supabaseUrl: config.supabaseUrl,
|
|
169
|
+
supabaseAnonKey: config.supabaseAnonKey,
|
|
170
|
+
accessToken: data.session.access_token,
|
|
171
|
+
refreshToken: data.session.refresh_token,
|
|
172
|
+
expiresAt: data.session.expires_at ?? Math.floor(Date.now() / 1e3) + 3600,
|
|
173
|
+
userId: data.session.user.id,
|
|
174
|
+
email: data.session.user.email ?? email
|
|
175
|
+
};
|
|
176
|
+
await writeCredentials(credentials);
|
|
177
|
+
return credentials;
|
|
178
|
+
}
|
|
179
|
+
function assertVersionSupported(minVersion) {
|
|
180
|
+
if (!minVersion || !isOlderThan(VERSION, minVersion)) return;
|
|
181
|
+
throw new Error(
|
|
182
|
+
`This is oc ${VERSION}, but the app now needs ${minVersion} or newer.
|
|
183
|
+
Upgrade with: npm i -g @orangecollective/oc@latest`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
async function logout() {
|
|
187
|
+
await clearCredentials();
|
|
188
|
+
}
|
|
189
|
+
var NotLoggedInError = class extends Error {
|
|
190
|
+
constructor() {
|
|
191
|
+
super("Not signed in. Run `oc login` first.");
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
async function createTokenProvider(expectedHost) {
|
|
195
|
+
const stored = await readCredentials();
|
|
196
|
+
if (!stored) throw new NotLoggedInError();
|
|
197
|
+
if (stored.host !== expectedHost) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Signed in to ${stored.host}, not ${expectedHost}. Run \`oc login --host ${expectedHost}\`.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
let current = stored;
|
|
203
|
+
let inFlight = null;
|
|
204
|
+
const refresh = async () => {
|
|
205
|
+
const { data, error } = await anonClient(current).auth.refreshSession({
|
|
206
|
+
refresh_token: current.refreshToken
|
|
207
|
+
});
|
|
208
|
+
if (error || !data.session) {
|
|
209
|
+
await clearCredentials();
|
|
210
|
+
throw new Error("Your session expired. Run `oc login` again.");
|
|
211
|
+
}
|
|
212
|
+
current = {
|
|
213
|
+
...current,
|
|
214
|
+
accessToken: data.session.access_token,
|
|
215
|
+
refreshToken: data.session.refresh_token,
|
|
216
|
+
expiresAt: data.session.expires_at ?? Math.floor(Date.now() / 1e3) + 3600
|
|
217
|
+
};
|
|
218
|
+
await writeCredentials(current);
|
|
219
|
+
return current.accessToken;
|
|
220
|
+
};
|
|
221
|
+
const getToken = async () => {
|
|
222
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
223
|
+
if (current.expiresAt - REFRESH_SKEW_SECONDS > now) return current.accessToken;
|
|
224
|
+
inFlight ??= refresh().finally(() => {
|
|
225
|
+
inFlight = null;
|
|
226
|
+
});
|
|
227
|
+
return inFlight;
|
|
228
|
+
};
|
|
229
|
+
return { credentials: current, getToken };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/api.ts
|
|
233
|
+
import { DefaultChatTransport } from "ai";
|
|
234
|
+
|
|
235
|
+
// ../lib/entity-linkify.ts
|
|
236
|
+
var WORD_CHAR = /[A-Za-z0-9]/;
|
|
237
|
+
function createEntityLinkifier(targets) {
|
|
238
|
+
const sorted = targets.filter((t) => t.name && t.name.trim().length >= 2 && !/[[\]()`]/.test(t.name)).sort((a, b) => b.name.length - a.name.length);
|
|
239
|
+
if (sorted.length === 0) return (text) => text;
|
|
240
|
+
const byFirstChar = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const t of sorted) {
|
|
242
|
+
const ch = t.name[0];
|
|
243
|
+
const bucket = byFirstChar.get(ch);
|
|
244
|
+
if (bucket) bucket.push(t);
|
|
245
|
+
else byFirstChar.set(ch, [t]);
|
|
246
|
+
}
|
|
247
|
+
return (text) => {
|
|
248
|
+
if (!text) return text;
|
|
249
|
+
let out = "";
|
|
250
|
+
let i = 0;
|
|
251
|
+
const n = text.length;
|
|
252
|
+
while (i < n) {
|
|
253
|
+
if (text.startsWith("```", i)) {
|
|
254
|
+
const end = text.indexOf("```", i + 3);
|
|
255
|
+
if (end === -1) return out + text.slice(i);
|
|
256
|
+
out += text.slice(i, end + 3);
|
|
257
|
+
i = end + 3;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (text[i] === "`") {
|
|
261
|
+
const end = text.indexOf("`", i + 1);
|
|
262
|
+
if (end === -1) return out + text.slice(i);
|
|
263
|
+
out += text.slice(i, end + 1);
|
|
264
|
+
i = end + 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (text[i] === "[") {
|
|
268
|
+
const close = text.indexOf("]", i);
|
|
269
|
+
if (close !== -1 && text[close + 1] === "(") {
|
|
270
|
+
const paren = text.indexOf(")", close);
|
|
271
|
+
if (paren !== -1) {
|
|
272
|
+
out += text.slice(i, paren + 1);
|
|
273
|
+
i = paren + 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const prev = i === 0 ? "" : text[i - 1];
|
|
279
|
+
let matched = false;
|
|
280
|
+
if (!WORD_CHAR.test(prev)) {
|
|
281
|
+
const bucket = byFirstChar.get(text[i]);
|
|
282
|
+
if (bucket) {
|
|
283
|
+
for (const t of bucket) {
|
|
284
|
+
if (text.startsWith(t.name, i)) {
|
|
285
|
+
const after = text[i + t.name.length] ?? "";
|
|
286
|
+
if (!WORD_CHAR.test(after)) {
|
|
287
|
+
out += `[${t.name}](${t.token})`;
|
|
288
|
+
i += t.name.length;
|
|
289
|
+
matched = true;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!matched) {
|
|
297
|
+
out += text[i];
|
|
298
|
+
i++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function linkifyEntityTokens(text, targets) {
|
|
305
|
+
return createEntityLinkifier(targets)(text);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ../lib/batch-activity-summary.ts
|
|
309
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
310
|
+
var ENTITIES_SENTINEL = "\n__OC_ENTITIES_END__\n";
|
|
311
|
+
function linkifyEntities(text, entities) {
|
|
312
|
+
return linkifyEntityTokens(text, [
|
|
313
|
+
...entities.founders.map((f) => ({ name: f.name, token: `#oc-f-${f.id}` })),
|
|
314
|
+
...entities.companies.map((c) => ({ name: c.name, token: `#oc-c-${c.id}` }))
|
|
315
|
+
]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/api.ts
|
|
319
|
+
var OcClient = class {
|
|
320
|
+
constructor(host, getToken) {
|
|
321
|
+
this.host = host;
|
|
322
|
+
this.getToken = getToken;
|
|
323
|
+
}
|
|
324
|
+
host;
|
|
325
|
+
getToken;
|
|
326
|
+
async authHeaders() {
|
|
327
|
+
return {
|
|
328
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
329
|
+
// Lets the server attribute odd requests to a specific release.
|
|
330
|
+
"x-oc-cli-version": VERSION
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async request(pathname, init) {
|
|
334
|
+
const res = await fetch(`${this.host}${pathname}`, {
|
|
335
|
+
...init,
|
|
336
|
+
headers: { ...init?.headers, ...await this.authHeaders() }
|
|
337
|
+
});
|
|
338
|
+
if (res.status === 401) throw new Error("Session rejected. Run `oc login` again.");
|
|
339
|
+
return res;
|
|
340
|
+
}
|
|
341
|
+
async bootstrap() {
|
|
342
|
+
const res = await this.request("/api/cli/bootstrap");
|
|
343
|
+
if (!res.ok) throw new Error(`Couldn't load your account (HTTP ${res.status})`);
|
|
344
|
+
return await res.json();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Stream the batch activity digest. Yields the server's greeting and body as
|
|
348
|
+
* it arrives — the wire format is an entity-catalog JSON prefix, the shared
|
|
349
|
+
* sentinel, then markdown (see app/api/batch-activity-summary/route.ts).
|
|
350
|
+
*/
|
|
351
|
+
async *digest(args) {
|
|
352
|
+
const now = /* @__PURE__ */ new Date();
|
|
353
|
+
const hour = now.getHours();
|
|
354
|
+
const res = await this.request("/api/batch-activity-summary", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { "Content-Type": "application/json" },
|
|
357
|
+
signal: args.signal,
|
|
358
|
+
body: JSON.stringify({
|
|
359
|
+
batch: args.batch,
|
|
360
|
+
teamId: args.teamId,
|
|
361
|
+
// The server builds "Good afternoon Ryan," from these — same local-clock
|
|
362
|
+
// buckets the web component uses.
|
|
363
|
+
weekday: now.toLocaleDateString(void 0, { weekday: "long" }),
|
|
364
|
+
partOfDay: hour < 12 ? "morning" : hour < 18 ? "afternoon" : "evening",
|
|
365
|
+
refresh: args.refresh === true
|
|
366
|
+
})
|
|
367
|
+
});
|
|
368
|
+
if (!res.ok || !res.body) throw new Error(`Couldn't load the digest (HTTP ${res.status})`);
|
|
369
|
+
const decoder = new TextDecoder();
|
|
370
|
+
const reader = res.body.getReader();
|
|
371
|
+
let buffer = "";
|
|
372
|
+
let entities = null;
|
|
373
|
+
let offset = 0;
|
|
374
|
+
let sentinelSeen = false;
|
|
375
|
+
for (; ; ) {
|
|
376
|
+
const { done, value } = await reader.read();
|
|
377
|
+
if (done) break;
|
|
378
|
+
buffer += decoder.decode(value, { stream: true });
|
|
379
|
+
if (!sentinelSeen) {
|
|
380
|
+
const index = buffer.indexOf(ENTITIES_SENTINEL);
|
|
381
|
+
if (index < 0) continue;
|
|
382
|
+
try {
|
|
383
|
+
entities = JSON.parse(buffer.slice(0, index));
|
|
384
|
+
} catch {
|
|
385
|
+
entities = { companies: [], founders: [] };
|
|
386
|
+
}
|
|
387
|
+
offset = index + ENTITIES_SENTINEL.length;
|
|
388
|
+
sentinelSeen = true;
|
|
389
|
+
}
|
|
390
|
+
yield { text: buffer.slice(offset), entities };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async searchContext(args) {
|
|
394
|
+
const params = new URLSearchParams({
|
|
395
|
+
q: args.query,
|
|
396
|
+
companies: "6",
|
|
397
|
+
founders: "6",
|
|
398
|
+
skills: "4",
|
|
399
|
+
sections: "batches,companies,founders,skills"
|
|
400
|
+
});
|
|
401
|
+
if (args.teamId != null) params.set("teamId", String(args.teamId));
|
|
402
|
+
const res = await this.request(`/api/search/context?${params}`, { signal: args.signal });
|
|
403
|
+
if (!res.ok) return [];
|
|
404
|
+
return flattenContextResults(await res.json());
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Transport for POST /api/chat. Sends exactly the body the web chat panel's
|
|
408
|
+
* transport sends, so the route needs no CLI-specific branch.
|
|
409
|
+
*/
|
|
410
|
+
chatTransport(getBody) {
|
|
411
|
+
return new DefaultChatTransport({
|
|
412
|
+
api: `${this.host}/api/chat`,
|
|
413
|
+
headers: () => this.authHeaders(),
|
|
414
|
+
body: () => {
|
|
415
|
+
const body = getBody();
|
|
416
|
+
return { ...body, modelId: qualifiedModelId(body.modelId) };
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
function emphasiseEntities(text, entities) {
|
|
422
|
+
if (!entities) return text;
|
|
423
|
+
return linkifyEntities(text, entities).replace(/\[([^\]]+)\]\(#oc-[cf]-[^)]+\)/g, "**$1**");
|
|
424
|
+
}
|
|
425
|
+
function flattenContextResults(raw) {
|
|
426
|
+
const data = raw ?? {};
|
|
427
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
428
|
+
return [
|
|
429
|
+
...(data.batches ?? []).map((b) => ({
|
|
430
|
+
type: "batch",
|
|
431
|
+
rawId: b.id,
|
|
432
|
+
label: b.nickname,
|
|
433
|
+
subtitle: `${capitalize(b.season)} ${b.year}`
|
|
434
|
+
})),
|
|
435
|
+
...(data.companies ?? []).map((c) => ({
|
|
436
|
+
type: "company",
|
|
437
|
+
rawId: String(c.id),
|
|
438
|
+
label: c.name,
|
|
439
|
+
subtitle: c.batch ?? void 0,
|
|
440
|
+
iconUrl: c.logoUrl ?? void 0
|
|
441
|
+
})),
|
|
442
|
+
...(data.founders ?? []).map((f) => ({
|
|
443
|
+
type: "founder",
|
|
444
|
+
rawId: f.id,
|
|
445
|
+
label: f.name,
|
|
446
|
+
iconUrl: f.avatarUrl ?? void 0
|
|
447
|
+
})),
|
|
448
|
+
...(data.skills ?? []).map((s) => ({
|
|
449
|
+
type: "skill",
|
|
450
|
+
rawId: s.slug,
|
|
451
|
+
label: s.name
|
|
452
|
+
}))
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
function buildContextPrefix(items) {
|
|
456
|
+
if (items.length === 0) return "";
|
|
457
|
+
const rendered = items.map((i) => `${i.type}:${i.rawId}:${i.label}:${[i.subtitle ?? "", i.iconUrl ?? ""].join(":")}`).join(", ");
|
|
458
|
+
return `[Context: ${rendered}]
|
|
459
|
+
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/ui/App.tsx
|
|
464
|
+
import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
465
|
+
import { Box as Box5, Static, Text as Text5, useApp, useInput } from "ink";
|
|
466
|
+
import { generateId, readUIMessageStream } from "ai";
|
|
467
|
+
|
|
468
|
+
// src/markdown.ts
|
|
469
|
+
import { Marked } from "marked";
|
|
470
|
+
import { markedTerminal } from "marked-terminal";
|
|
471
|
+
var marked = new Marked();
|
|
472
|
+
marked.use(
|
|
473
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
474
|
+
markedTerminal({ reflowText: true, width: Math.min(process.stdout.columns || 80, 100) })
|
|
475
|
+
);
|
|
476
|
+
function stripContextLeakage(text) {
|
|
477
|
+
return text.replace(/^\[Context: [^\]]+\]\s*/, "").replace(/<enriched_context>[\s\S]*?<\/enriched_context>\n*/g, "").replace(/^<enriched_context>[\s\S]*$/, "").replace(/^\[Context[\s\S]*$/, "").trimStart();
|
|
478
|
+
}
|
|
479
|
+
function renderMarkdown(text) {
|
|
480
|
+
try {
|
|
481
|
+
const out = marked.parse(text, { async: false });
|
|
482
|
+
return typeof out === "string" ? out.trimEnd() : text.trimEnd();
|
|
483
|
+
} catch {
|
|
484
|
+
return text.trimEnd();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/ui/Input.tsx
|
|
489
|
+
import { Box, Text } from "ink";
|
|
490
|
+
|
|
491
|
+
// src/ui/theme.ts
|
|
492
|
+
var ORANGE = {
|
|
493
|
+
tint: "#ffe2c4",
|
|
494
|
+
light: "#ffbe86",
|
|
495
|
+
mid: "#ff9d4d",
|
|
496
|
+
base: "#f97316",
|
|
497
|
+
deep: "#e2560b",
|
|
498
|
+
shadow: "#b8420a"
|
|
499
|
+
};
|
|
500
|
+
var LOGO_RAMP = [
|
|
501
|
+
ORANGE.tint,
|
|
502
|
+
ORANGE.light,
|
|
503
|
+
ORANGE.mid,
|
|
504
|
+
ORANGE.base,
|
|
505
|
+
ORANGE.deep,
|
|
506
|
+
ORANGE.shadow
|
|
507
|
+
];
|
|
508
|
+
var ACCENT = ORANGE.base;
|
|
509
|
+
|
|
510
|
+
// src/ui/Input.tsx
|
|
511
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
512
|
+
var PLACEHOLDER = "ask anything \u2014 @ for context, / for commands";
|
|
513
|
+
function Input({
|
|
514
|
+
value,
|
|
515
|
+
cursor,
|
|
516
|
+
context,
|
|
517
|
+
busy,
|
|
518
|
+
status,
|
|
519
|
+
hint
|
|
520
|
+
}) {
|
|
521
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
522
|
+
context.length > 0 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
|
|
523
|
+
/* @__PURE__ */ jsx(Text, { color: ACCENT, children: "\u23F5 " }),
|
|
524
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: context.map((c) => c.label).join(", ") })
|
|
525
|
+
] }) : null,
|
|
526
|
+
/* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: busy ? "gray" : ACCENT, paddingX: 1, children: [
|
|
527
|
+
/* @__PURE__ */ jsx(Text, { color: busy ? "gray" : ACCENT, children: "> " }),
|
|
528
|
+
value.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: PLACEHOLDER }) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
529
|
+
value.slice(0, cursor),
|
|
530
|
+
/* @__PURE__ */ jsx(Text, { inverse: true, children: value[cursor] ?? " " }),
|
|
531
|
+
value.slice(cursor + 1)
|
|
532
|
+
] })
|
|
533
|
+
] }),
|
|
534
|
+
/* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
|
|
535
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: status }),
|
|
536
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: hint ? ` ${hint}` : "" })
|
|
537
|
+
] })
|
|
538
|
+
] });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/ui/Logo.tsx
|
|
542
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
543
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
544
|
+
var LOGO_ROWS = [
|
|
545
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
|
|
546
|
+
"\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
547
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
548
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
549
|
+
"\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551",
|
|
550
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D"
|
|
551
|
+
];
|
|
552
|
+
var LOGO_WIDTH = 43;
|
|
553
|
+
function CompactLogo() {
|
|
554
|
+
return /* @__PURE__ */ jsx2(Text2, { bold: true, color: ORANGE.base, children: "oc-cli" });
|
|
555
|
+
}
|
|
556
|
+
function Logo() {
|
|
557
|
+
const columns = process.stdout.columns || 80;
|
|
558
|
+
if (columns < LOGO_WIDTH + 2) return /* @__PURE__ */ jsx2(CompactLogo, {});
|
|
559
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: LOGO_ROWS.map((row, index) => /* @__PURE__ */ jsx2(Text2, { color: LOGO_RAMP[index], children: row }, row)) });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/ui/Picker.tsx
|
|
563
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
564
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
565
|
+
var MAX_VISIBLE = 10;
|
|
566
|
+
var MAX_LABEL_PAD = 28;
|
|
567
|
+
function Picker({
|
|
568
|
+
rows,
|
|
569
|
+
selected,
|
|
570
|
+
status
|
|
571
|
+
}) {
|
|
572
|
+
const start = Math.max(0, Math.min(selected - MAX_VISIBLE + 1, rows.length - MAX_VISIBLE));
|
|
573
|
+
const visible = rows.slice(Math.max(0, start), Math.max(0, start) + MAX_VISIBLE);
|
|
574
|
+
const labelPad = Math.min(
|
|
575
|
+
MAX_LABEL_PAD,
|
|
576
|
+
visible.reduce((width, row) => Math.max(width, row.label.length), 0)
|
|
577
|
+
);
|
|
578
|
+
if (rows.length === 0) {
|
|
579
|
+
return status ? /* @__PURE__ */ jsx3(Box3, { paddingLeft: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: status }) }) : null;
|
|
580
|
+
}
|
|
581
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: visible.map((row) => {
|
|
582
|
+
const isSelected = rows[selected]?.key === row.key;
|
|
583
|
+
return /* @__PURE__ */ jsxs2(Box3, { paddingLeft: 1, children: [
|
|
584
|
+
/* @__PURE__ */ jsx3(Text3, { color: isSelected ? ACCENT : void 0, children: isSelected ? "\u276F " : " " }),
|
|
585
|
+
/* @__PURE__ */ jsx3(Text3, { color: isSelected ? ACCENT : void 0, bold: isSelected, children: row.label }),
|
|
586
|
+
row.meta ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `${" ".repeat(Math.max(1, labelPad - row.label.length))} ${row.meta}` }) : null
|
|
587
|
+
] }, row.key);
|
|
588
|
+
}) });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/ui/Turn.tsx
|
|
592
|
+
import { useEffect, useState } from "react";
|
|
593
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
594
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
595
|
+
var SPINNER = ["\u2722", "\u2733", "\u2736", "\u273B", "\u273D", "\u273B", "\u2736", "\u2733"];
|
|
596
|
+
function useSpinner(active) {
|
|
597
|
+
const [frame, setFrame] = useState(0);
|
|
598
|
+
useEffect(() => {
|
|
599
|
+
if (!active) return;
|
|
600
|
+
const timer = setInterval(() => setFrame((f) => f + 1), 120);
|
|
601
|
+
return () => clearInterval(timer);
|
|
602
|
+
}, [active]);
|
|
603
|
+
return SPINNER[frame % SPINNER.length];
|
|
604
|
+
}
|
|
605
|
+
function ToolRows({ tools, live }) {
|
|
606
|
+
if (tools.length === 0) return null;
|
|
607
|
+
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: tools.map((tool) => /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
|
|
608
|
+
/* @__PURE__ */ jsxs3(Box4, { children: [
|
|
609
|
+
/* @__PURE__ */ jsxs3(Text4, { color: tool.state === "error" ? "red" : tool.state === "running" ? ACCENT : "gray", children: [
|
|
610
|
+
"\u25CF",
|
|
611
|
+
" "
|
|
612
|
+
] }),
|
|
613
|
+
/* @__PURE__ */ jsx4(Text4, { bold: tool.state === "running" && live, children: tool.name })
|
|
614
|
+
] }),
|
|
615
|
+
/* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
616
|
+
" \u23BF ",
|
|
617
|
+
tool.state === "running" ? "running\u2026" : tool.state === "error" ? "failed" : "done"
|
|
618
|
+
] })
|
|
619
|
+
] }, tool.id)) });
|
|
620
|
+
}
|
|
621
|
+
function Sources({ sources }) {
|
|
622
|
+
if (sources.length === 0) return null;
|
|
623
|
+
return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginTop: 1, children: sources.map((source, index) => /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
624
|
+
` ${index + 1}. `,
|
|
625
|
+
source.title ? `${source.title} \u2014 ` : "",
|
|
626
|
+
source.url
|
|
627
|
+
] }, source.id)) });
|
|
628
|
+
}
|
|
629
|
+
function AssistantTurn({
|
|
630
|
+
text,
|
|
631
|
+
tools,
|
|
632
|
+
sources,
|
|
633
|
+
thoughtSeconds
|
|
634
|
+
}) {
|
|
635
|
+
const body = renderMarkdown(stripContextLeakage(text));
|
|
636
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
637
|
+
/* @__PURE__ */ jsx4(ToolRows, { tools, live: false }),
|
|
638
|
+
thoughtSeconds != null ? /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
639
|
+
"\u273B thought for ",
|
|
640
|
+
thoughtSeconds,
|
|
641
|
+
"s"
|
|
642
|
+
] }) : null,
|
|
643
|
+
body ? /* @__PURE__ */ jsx4(Box4, { marginTop: tools.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx4(Text4, { children: body }) }) : null,
|
|
644
|
+
/* @__PURE__ */ jsx4(Sources, { sources })
|
|
645
|
+
] });
|
|
646
|
+
}
|
|
647
|
+
function StreamingTurn({ turn }) {
|
|
648
|
+
const body = stripContextLeakage(turn.text);
|
|
649
|
+
const reasoningTail = turn.reasoning.trim().split("\n").at(-1) ?? "";
|
|
650
|
+
return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
651
|
+
/* @__PURE__ */ jsx4(ToolRows, { tools: turn.tools, live: true }),
|
|
652
|
+
!body && reasoningTail ? /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
653
|
+
" ",
|
|
654
|
+
truncate(reasoningTail, (process.stdout.columns || 80) - 4)
|
|
655
|
+
] }) : null,
|
|
656
|
+
body ? /* @__PURE__ */ jsx4(Text4, { children: body }) : null
|
|
657
|
+
] });
|
|
658
|
+
}
|
|
659
|
+
function StatusLine({ turn }) {
|
|
660
|
+
const spinner = useSpinner(true);
|
|
661
|
+
const [now, setNow] = useState(Date.now());
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
const timer = setInterval(() => setNow(Date.now()), 500);
|
|
664
|
+
return () => clearInterval(timer);
|
|
665
|
+
}, []);
|
|
666
|
+
const elapsed = Math.max(0, Math.round((now - turn.startedAt) / 1e3));
|
|
667
|
+
const running = turn.tools.find((t) => t.state === "running");
|
|
668
|
+
const verb = running ? running.name : turn.text ? "Writing" : "Thinking";
|
|
669
|
+
return /* @__PURE__ */ jsxs3(Box4, { children: [
|
|
670
|
+
/* @__PURE__ */ jsxs3(Text4, { color: ACCENT, children: [
|
|
671
|
+
spinner,
|
|
672
|
+
" "
|
|
673
|
+
] }),
|
|
674
|
+
/* @__PURE__ */ jsxs3(Text4, { children: [
|
|
675
|
+
verb,
|
|
676
|
+
"\u2026 "
|
|
677
|
+
] }),
|
|
678
|
+
/* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
|
|
679
|
+
"(",
|
|
680
|
+
elapsed,
|
|
681
|
+
"s \xB7 esc to interrupt)"
|
|
682
|
+
] })
|
|
683
|
+
] });
|
|
684
|
+
}
|
|
685
|
+
function truncate(value, width) {
|
|
686
|
+
if (width <= 1 || value.length <= width) return value;
|
|
687
|
+
return `${value.slice(0, Math.max(1, width - 1))}\u2026`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/ui/types.ts
|
|
691
|
+
var EMPTY_TURN = {
|
|
692
|
+
text: "",
|
|
693
|
+
reasoning: "",
|
|
694
|
+
tools: [],
|
|
695
|
+
sources: [],
|
|
696
|
+
startedAt: 0
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
// src/ui/App.tsx
|
|
700
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
701
|
+
var FLUSH_INTERVAL_MS = 60;
|
|
702
|
+
var MENTION_DEBOUNCE_MS = 200;
|
|
703
|
+
var EXIT_CONFIRM_MS = 3e3;
|
|
704
|
+
var COMMANDS = [
|
|
705
|
+
{ name: "model", args: "[name]", blurb: "switch model" },
|
|
706
|
+
{ name: "web", blurb: "toggle web search" },
|
|
707
|
+
{ name: "team", args: "[slug]", blurb: "switch active team" },
|
|
708
|
+
{ name: "batch", args: "[W26]", blurb: "set the batch for the digest" },
|
|
709
|
+
{ name: "digest", args: "[-r]", blurb: "re-run the activity digest" },
|
|
710
|
+
{ name: "new", blurb: "start a fresh thread" },
|
|
711
|
+
{ name: "clear", blurb: "clear the screen" },
|
|
712
|
+
{ name: "help", blurb: "show shortcuts" },
|
|
713
|
+
{ name: "quit", blurb: "exit" }
|
|
714
|
+
];
|
|
715
|
+
var SHORTCUTS = ` @name attach a company, founder, batch or skill
|
|
716
|
+
/ commands
|
|
717
|
+
\u2191 \u2193 history, or move through a list
|
|
718
|
+
tab accept a suggestion
|
|
719
|
+
esc interrupt \xB7 close a list \xB7 drop context
|
|
720
|
+
ctrl-a / ctrl-e start / end of line
|
|
721
|
+
ctrl-w / ctrl-u delete word / line
|
|
722
|
+
ctrl-c \xD72 exit`;
|
|
723
|
+
function App({ client, bootstrap, host, state }) {
|
|
724
|
+
const { exit } = useApp();
|
|
725
|
+
const [transcript, setTranscript] = useState2([]);
|
|
726
|
+
const [live, setLive] = useState2(null);
|
|
727
|
+
const [digestText, setDigestText] = useState2(null);
|
|
728
|
+
const [busy, setBusy] = useState2(false);
|
|
729
|
+
const [exitArmed, setExitArmed] = useState2(false);
|
|
730
|
+
const [showShortcuts, setShowShortcuts] = useState2(false);
|
|
731
|
+
const [value, setValue] = useState2("");
|
|
732
|
+
const [cursor, setCursor] = useState2(0);
|
|
733
|
+
const [history, setHistory] = useState2([]);
|
|
734
|
+
const [historyIndex, setHistoryIndex] = useState2(null);
|
|
735
|
+
const [context, setContext] = useState2([]);
|
|
736
|
+
const [mentionQuery, setMentionQuery] = useState2(null);
|
|
737
|
+
const [mentionRecords, setMentionRecords] = useState2([]);
|
|
738
|
+
const [mentionLoading, setMentionLoading] = useState2(false);
|
|
739
|
+
const [mentionSearchedFor, setMentionSearchedFor] = useState2(null);
|
|
740
|
+
const [selected, setSelected] = useState2(0);
|
|
741
|
+
const [teamId, setTeamId] = useState2(
|
|
742
|
+
state.activeTeamId ?? bootstrap.activeTeamId
|
|
743
|
+
);
|
|
744
|
+
const [batch, setBatch] = useState2(state.batch ?? bootstrap.currentBatch);
|
|
745
|
+
const [modelId, setModelId] = useState2(state.modelId ?? DEFAULT_MODEL_ID);
|
|
746
|
+
const [webSearch, setWebSearch] = useState2(state.webSearchEnabled ?? false);
|
|
747
|
+
const threadIdRef = useRef(state.threadId ?? generateId());
|
|
748
|
+
const messagesRef = useRef([]);
|
|
749
|
+
const turnAbortRef = useRef(null);
|
|
750
|
+
const digestAbortRef = useRef(null);
|
|
751
|
+
const team = useMemo(
|
|
752
|
+
() => bootstrap.teams.find((t) => t.teamId === teamId) ?? bootstrap.teams[0] ?? null,
|
|
753
|
+
[bootstrap.teams, teamId]
|
|
754
|
+
);
|
|
755
|
+
const push = useCallback((item) => {
|
|
756
|
+
setTranscript((prev) => [...prev, { ...item, id: item.id ?? generateId() }]);
|
|
757
|
+
}, []);
|
|
758
|
+
const notice = useCallback(
|
|
759
|
+
(text, tone = "info") => push({ kind: "notice", text, tone }),
|
|
760
|
+
[push]
|
|
761
|
+
);
|
|
762
|
+
const bodyRef = useRef({ modelId, webSearch, teamId });
|
|
763
|
+
bodyRef.current = { modelId, webSearch, teamId };
|
|
764
|
+
const transport = useMemo(
|
|
765
|
+
() => client.chatTransport(() => ({
|
|
766
|
+
threadId: threadIdRef.current,
|
|
767
|
+
modelId: bodyRef.current.modelId,
|
|
768
|
+
webSearchEnabled: bodyRef.current.webSearch,
|
|
769
|
+
activeTeamId: bodyRef.current.teamId
|
|
770
|
+
})),
|
|
771
|
+
[client]
|
|
772
|
+
);
|
|
773
|
+
const runDigest = useCallback(
|
|
774
|
+
async (refresh) => {
|
|
775
|
+
if (!batch || teamId == null) {
|
|
776
|
+
notice("No current batch for this team \u2014 nothing to summarize.", "error");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
setDigestText("");
|
|
780
|
+
digestAbortRef.current?.abort();
|
|
781
|
+
const controller = new AbortController();
|
|
782
|
+
digestAbortRef.current = controller;
|
|
783
|
+
let lastFlush = 0;
|
|
784
|
+
let text = "";
|
|
785
|
+
let rendered = "";
|
|
786
|
+
try {
|
|
787
|
+
for await (const chunk of client.digest({
|
|
788
|
+
batch,
|
|
789
|
+
teamId,
|
|
790
|
+
refresh,
|
|
791
|
+
signal: controller.signal
|
|
792
|
+
})) {
|
|
793
|
+
text = chunk.text;
|
|
794
|
+
rendered = emphasiseEntities(text, chunk.entities);
|
|
795
|
+
const now = Date.now();
|
|
796
|
+
if (now - lastFlush > FLUSH_INTERVAL_MS) {
|
|
797
|
+
lastFlush = now;
|
|
798
|
+
setDigestText(text);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
push({ kind: "digest", text: renderMarkdown(rendered || text) });
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (!controller.signal.aborted) notice(describeError(error), "error");
|
|
804
|
+
} finally {
|
|
805
|
+
setDigestText(null);
|
|
806
|
+
digestAbortRef.current = null;
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
[batch, client, notice, push, teamId]
|
|
810
|
+
);
|
|
811
|
+
const bootedRef = useRef(false);
|
|
812
|
+
useEffect2(() => {
|
|
813
|
+
if (bootedRef.current) return;
|
|
814
|
+
bootedRef.current = true;
|
|
815
|
+
push({
|
|
816
|
+
kind: "banner",
|
|
817
|
+
host,
|
|
818
|
+
email: bootstrap.user.email ?? "",
|
|
819
|
+
team: team?.name ?? "\u2014",
|
|
820
|
+
batch
|
|
821
|
+
});
|
|
822
|
+
void runDigest(false);
|
|
823
|
+
}, []);
|
|
824
|
+
const slashQuery = useMemo(() => {
|
|
825
|
+
const match = /^\/(\S*)$/.exec(value);
|
|
826
|
+
return match ? match[1] : null;
|
|
827
|
+
}, [value]);
|
|
828
|
+
const commandRows = useMemo(() => {
|
|
829
|
+
if (slashQuery === null) return [];
|
|
830
|
+
const needle = slashQuery.toLowerCase();
|
|
831
|
+
return COMMANDS.filter((c) => c.name.startsWith(needle)).map((c) => ({
|
|
832
|
+
key: c.name,
|
|
833
|
+
label: `/${c.name}${c.args ? ` ${c.args}` : ""}`,
|
|
834
|
+
meta: c.blurb
|
|
835
|
+
}));
|
|
836
|
+
}, [slashQuery]);
|
|
837
|
+
const mentionRows = useMemo(
|
|
838
|
+
() => mentionRecords.map((record) => ({
|
|
839
|
+
key: `${record.type}:${record.rawId}`,
|
|
840
|
+
label: record.label,
|
|
841
|
+
meta: [record.type, record.subtitle].filter(Boolean).join(" \xB7 ")
|
|
842
|
+
})),
|
|
843
|
+
[mentionRecords]
|
|
844
|
+
);
|
|
845
|
+
const pickerRows = slashQuery !== null ? commandRows : mentionRows;
|
|
846
|
+
const pickerOpen = slashQuery !== null || mentionQuery !== null;
|
|
847
|
+
useEffect2(() => setSelected(0), [slashQuery, mentionRecords]);
|
|
848
|
+
useEffect2(() => {
|
|
849
|
+
if (mentionQuery === null) {
|
|
850
|
+
setMentionRecords([]);
|
|
851
|
+
setMentionLoading(false);
|
|
852
|
+
setMentionSearchedFor(null);
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const controller = new AbortController();
|
|
856
|
+
setMentionLoading(true);
|
|
857
|
+
const timer = setTimeout(() => {
|
|
858
|
+
client.searchContext({ query: mentionQuery, teamId, signal: controller.signal }).then((records) => {
|
|
859
|
+
setMentionRecords(records);
|
|
860
|
+
setMentionSearchedFor(mentionQuery);
|
|
861
|
+
}).catch(() => {
|
|
862
|
+
}).finally(() => setMentionLoading(false));
|
|
863
|
+
}, MENTION_DEBOUNCE_MS);
|
|
864
|
+
return () => {
|
|
865
|
+
clearTimeout(timer);
|
|
866
|
+
controller.abort();
|
|
867
|
+
};
|
|
868
|
+
}, [client, mentionQuery, teamId]);
|
|
869
|
+
const send = useCallback(
|
|
870
|
+
async (raw) => {
|
|
871
|
+
const attached = context;
|
|
872
|
+
const userMessage = {
|
|
873
|
+
id: generateId(),
|
|
874
|
+
role: "user",
|
|
875
|
+
parts: [{ type: "text", text: buildContextPrefix(attached) + raw }]
|
|
876
|
+
};
|
|
877
|
+
const outgoing = [...messagesRef.current, userMessage];
|
|
878
|
+
messagesRef.current = outgoing;
|
|
879
|
+
push({ kind: "user", text: raw, context: attached });
|
|
880
|
+
setContext([]);
|
|
881
|
+
setBusy(true);
|
|
882
|
+
const controller = new AbortController();
|
|
883
|
+
turnAbortRef.current = controller;
|
|
884
|
+
const startedAt = Date.now();
|
|
885
|
+
setLive({ ...EMPTY_TURN, startedAt });
|
|
886
|
+
let lastFlush = 0;
|
|
887
|
+
let final = null;
|
|
888
|
+
let streamError = null;
|
|
889
|
+
try {
|
|
890
|
+
const stream = await transport.sendMessages({
|
|
891
|
+
trigger: "submit-message",
|
|
892
|
+
chatId: threadIdRef.current,
|
|
893
|
+
messageId: void 0,
|
|
894
|
+
messages: outgoing,
|
|
895
|
+
abortSignal: controller.signal
|
|
896
|
+
});
|
|
897
|
+
for await (const snapshot of readUIMessageStream({
|
|
898
|
+
stream,
|
|
899
|
+
onError: (error) => {
|
|
900
|
+
streamError = error;
|
|
901
|
+
}
|
|
902
|
+
})) {
|
|
903
|
+
final = snapshot;
|
|
904
|
+
const now = Date.now();
|
|
905
|
+
if (now - lastFlush > FLUSH_INTERVAL_MS) {
|
|
906
|
+
lastFlush = now;
|
|
907
|
+
setLive({ ...readTurn(snapshot), startedAt });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const parsed = final ? readTurn(final) : null;
|
|
911
|
+
const hasContent = !!parsed && (parsed.text.trim().length > 0 || parsed.tools.length > 0);
|
|
912
|
+
if (final && parsed && hasContent) {
|
|
913
|
+
messagesRef.current = [...outgoing, final];
|
|
914
|
+
push({
|
|
915
|
+
kind: "assistant",
|
|
916
|
+
text: parsed.text,
|
|
917
|
+
tools: parsed.tools.map((t) => ({
|
|
918
|
+
...t,
|
|
919
|
+
state: t.state === "running" ? "done" : t.state
|
|
920
|
+
})),
|
|
921
|
+
sources: parsed.sources,
|
|
922
|
+
thoughtSeconds: parsed.reasoning.trim() ? Math.max(1, Math.round((Date.now() - startedAt) / 1e3)) : null
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (streamError) {
|
|
926
|
+
notice(describeError(streamError), "error");
|
|
927
|
+
} else if (!hasContent) {
|
|
928
|
+
notice("The model returned nothing. Try again, or /model to switch.", "error");
|
|
929
|
+
}
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (controller.signal.aborted) notice("Interrupted.");
|
|
932
|
+
else notice(describeError(error), "error");
|
|
933
|
+
} finally {
|
|
934
|
+
setLive(null);
|
|
935
|
+
setBusy(false);
|
|
936
|
+
turnAbortRef.current = null;
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
[context, notice, push, transport]
|
|
940
|
+
);
|
|
941
|
+
const runCommand = useCallback(
|
|
942
|
+
(raw) => {
|
|
943
|
+
const [command, ...rest] = raw.trim().slice(1).split(/\s+/);
|
|
944
|
+
const argument = rest.join(" ").trim();
|
|
945
|
+
switch (command) {
|
|
946
|
+
case "help":
|
|
947
|
+
setShowShortcuts((v) => !v);
|
|
948
|
+
return;
|
|
949
|
+
case "quit":
|
|
950
|
+
case "exit":
|
|
951
|
+
exit();
|
|
952
|
+
return;
|
|
953
|
+
case "clear":
|
|
954
|
+
setTranscript([]);
|
|
955
|
+
return;
|
|
956
|
+
case "new":
|
|
957
|
+
threadIdRef.current = generateId();
|
|
958
|
+
messagesRef.current = [];
|
|
959
|
+
void patchState({ threadId: threadIdRef.current });
|
|
960
|
+
notice("Started a new thread.");
|
|
961
|
+
return;
|
|
962
|
+
case "web": {
|
|
963
|
+
const next = !webSearch;
|
|
964
|
+
setWebSearch(next);
|
|
965
|
+
void patchState({ webSearchEnabled: next });
|
|
966
|
+
notice(`Web search ${next ? "on" : "off"}.`);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
case "model": {
|
|
970
|
+
if (!argument) {
|
|
971
|
+
notice(
|
|
972
|
+
MODELS.map((m) => ` ${m.id === modelId ? "\u276F" : " "} ${m.id} ${m.label}`).join("\n")
|
|
973
|
+
);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const needle = argument.toLowerCase();
|
|
977
|
+
const match = MODELS.find((m) => m.id.toLowerCase() === needle) ?? MODELS.find((m) => m.id.toLowerCase().includes(needle)) ?? MODELS.find((m) => m.short.toLowerCase().includes(needle));
|
|
978
|
+
if (!match) {
|
|
979
|
+
notice(`No model matches \u201C${argument}\u201D. Try /model with no argument.`, "error");
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
setModelId(match.id);
|
|
983
|
+
void patchState({ modelId: match.id });
|
|
984
|
+
notice(`Model: ${match.label}`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
case "team": {
|
|
988
|
+
if (!argument) {
|
|
989
|
+
notice(
|
|
990
|
+
bootstrap.teams.map((t) => ` ${t.teamId === teamId ? "\u276F" : " "} ${t.slug ?? t.teamId} ${t.name}`).join("\n")
|
|
991
|
+
);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const needle = argument.toLowerCase();
|
|
995
|
+
const match = bootstrap.teams.find((t) => t.slug?.toLowerCase() === needle) ?? bootstrap.teams.find((t) => t.name.toLowerCase().includes(needle));
|
|
996
|
+
if (!match) {
|
|
997
|
+
notice(`You're not on a team matching \u201C${argument}\u201D.`, "error");
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
setTeamId(match.teamId);
|
|
1001
|
+
void patchState({ activeTeamId: match.teamId });
|
|
1002
|
+
notice(`Team: ${match.name}`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
case "batch": {
|
|
1006
|
+
if (!argument) {
|
|
1007
|
+
notice(`Batch: ${batch ?? "none"}`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const next = argument.toUpperCase();
|
|
1011
|
+
setBatch(next);
|
|
1012
|
+
void patchState({ batch: next });
|
|
1013
|
+
notice(`Batch: ${next}`);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
case "digest":
|
|
1017
|
+
void runDigest(/--refresh|-r\b/.test(argument));
|
|
1018
|
+
return;
|
|
1019
|
+
default:
|
|
1020
|
+
notice(`Unknown command \u201C/${command}\u201D. Type / to see them.`, "error");
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
[batch, bootstrap.teams, exit, modelId, notice, runDigest, teamId, webSearch]
|
|
1024
|
+
);
|
|
1025
|
+
const submit = useCallback(() => {
|
|
1026
|
+
const raw = value.trim();
|
|
1027
|
+
setValue("");
|
|
1028
|
+
setCursor(0);
|
|
1029
|
+
setHistoryIndex(null);
|
|
1030
|
+
setMentionQuery(null);
|
|
1031
|
+
if (!raw) return;
|
|
1032
|
+
setHistory((prev) => [...prev, raw]);
|
|
1033
|
+
if (raw.startsWith("/")) {
|
|
1034
|
+
runCommand(raw);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
void send(raw);
|
|
1038
|
+
}, [runCommand, send, value]);
|
|
1039
|
+
const syncMention = useCallback((text, at) => {
|
|
1040
|
+
const match = /(?:^|\s)@([^\s@]*)$/.exec(text.slice(0, at));
|
|
1041
|
+
setMentionQuery(match ? match[1] : null);
|
|
1042
|
+
}, []);
|
|
1043
|
+
const applyEdit = useCallback(
|
|
1044
|
+
(text, at) => {
|
|
1045
|
+
setValue(text);
|
|
1046
|
+
setCursor(at);
|
|
1047
|
+
syncMention(text, at);
|
|
1048
|
+
},
|
|
1049
|
+
[syncMention]
|
|
1050
|
+
);
|
|
1051
|
+
const acceptPick = useCallback(() => {
|
|
1052
|
+
if (slashQuery !== null) {
|
|
1053
|
+
const row = commandRows[selected];
|
|
1054
|
+
if (!row) return;
|
|
1055
|
+
const next = `/${row.key} `;
|
|
1056
|
+
setValue(next);
|
|
1057
|
+
setCursor(next.length);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const record = mentionRecords[selected];
|
|
1061
|
+
if (!record) return;
|
|
1062
|
+
const before = value.slice(0, cursor).replace(/@[^\s@]*$/, "");
|
|
1063
|
+
setContext(
|
|
1064
|
+
(prev) => prev.some((c) => c.type === record.type && c.rawId === record.rawId) ? prev : [...prev, record]
|
|
1065
|
+
);
|
|
1066
|
+
setValue(before + value.slice(cursor));
|
|
1067
|
+
setCursor(before.length);
|
|
1068
|
+
setMentionQuery(null);
|
|
1069
|
+
}, [commandRows, cursor, mentionRecords, selected, slashQuery, value]);
|
|
1070
|
+
useInput((input, key) => {
|
|
1071
|
+
if (key.ctrl && input === "c") {
|
|
1072
|
+
if (exitArmed) {
|
|
1073
|
+
exit();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
setExitArmed(true);
|
|
1077
|
+
setTimeout(() => setExitArmed(false), EXIT_CONFIRM_MS);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (key.ctrl && input === "d" && value.length === 0) {
|
|
1081
|
+
exit();
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (exitArmed) setExitArmed(false);
|
|
1085
|
+
if (key.escape) {
|
|
1086
|
+
if (pickerOpen) {
|
|
1087
|
+
setMentionQuery(null);
|
|
1088
|
+
if (slashQuery !== null) applyEdit("", 0);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (turnAbortRef.current) {
|
|
1092
|
+
turnAbortRef.current.abort();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (digestAbortRef.current) {
|
|
1096
|
+
digestAbortRef.current.abort();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (showShortcuts) {
|
|
1100
|
+
setShowShortcuts(false);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (context.length > 0) setContext([]);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (pickerOpen && pickerRows.length > 0) {
|
|
1107
|
+
if (key.upArrow) {
|
|
1108
|
+
setSelected((i) => (i - 1 + pickerRows.length) % pickerRows.length);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (key.downArrow) {
|
|
1112
|
+
setSelected((i) => (i + 1) % pickerRows.length);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (key.tab) {
|
|
1116
|
+
acceptPick();
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (key.return && mentionQuery !== null) {
|
|
1120
|
+
acceptPick();
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (key.return) {
|
|
1125
|
+
if (!busy) submit();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (key.leftArrow) {
|
|
1129
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (key.rightArrow) {
|
|
1133
|
+
setCursor((c) => Math.min(value.length, c + 1));
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (key.upArrow || key.downArrow) {
|
|
1137
|
+
if (history.length === 0) return;
|
|
1138
|
+
const nextIndex = key.upArrow ? Math.max(0, (historyIndex ?? history.length) - 1) : historyIndex == null ? null : Math.min(history.length, historyIndex + 1);
|
|
1139
|
+
if (nextIndex == null || nextIndex >= history.length) {
|
|
1140
|
+
setHistoryIndex(null);
|
|
1141
|
+
applyEdit("", 0);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
setHistoryIndex(nextIndex);
|
|
1145
|
+
applyEdit(history[nextIndex], history[nextIndex].length);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (key.backspace || key.delete) {
|
|
1149
|
+
if (cursor === 0) return;
|
|
1150
|
+
applyEdit(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (key.ctrl) {
|
|
1154
|
+
if (input === "a") setCursor(0);
|
|
1155
|
+
else if (input === "e") setCursor(value.length);
|
|
1156
|
+
else if (input === "u") applyEdit(value.slice(cursor), 0);
|
|
1157
|
+
else if (input === "k") applyEdit(value.slice(0, cursor), cursor);
|
|
1158
|
+
else if (input === "w") {
|
|
1159
|
+
const trimmed = value.slice(0, cursor).replace(/\s*\S+$/, "");
|
|
1160
|
+
applyEdit(trimmed + value.slice(cursor), trimmed.length);
|
|
1161
|
+
}
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (input === "?" && value.length === 0) {
|
|
1165
|
+
setShowShortcuts((v) => !v);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (input && !key.meta) {
|
|
1169
|
+
const typed = input.replace(/[\r\n]+/g, " ");
|
|
1170
|
+
if (!typed) return;
|
|
1171
|
+
applyEdit(value.slice(0, cursor) + typed + value.slice(cursor), cursor + typed.length);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
const status = [
|
|
1175
|
+
modelLabel(modelId),
|
|
1176
|
+
webSearch ? "web on" : "web off",
|
|
1177
|
+
team?.name ?? "no team",
|
|
1178
|
+
batch ?? "no batch"
|
|
1179
|
+
].join(" \xB7 ");
|
|
1180
|
+
const hint = exitArmed ? "press ctrl-c again to exit" : busy ? "" : showShortcuts ? "esc to close" : "? for shortcuts";
|
|
1181
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
|
|
1182
|
+
/* @__PURE__ */ jsx5(Static, { items: transcript, children: (item) => /* @__PURE__ */ jsx5(TranscriptBlock, { item }, item.id) }),
|
|
1183
|
+
digestText !== null ? /* @__PURE__ */ jsx5(DigestStream, { text: digestText }) : null,
|
|
1184
|
+
live ? /* @__PURE__ */ jsx5(StreamingTurn, { turn: live }) : null,
|
|
1185
|
+
live ? /* @__PURE__ */ jsx5(StatusLine, { turn: live }) : null,
|
|
1186
|
+
showShortcuts ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: SHORTCUTS }) }) : null,
|
|
1187
|
+
pickerOpen ? /* @__PURE__ */ jsx5(
|
|
1188
|
+
Picker,
|
|
1189
|
+
{
|
|
1190
|
+
rows: pickerRows,
|
|
1191
|
+
selected,
|
|
1192
|
+
status: mentionQuery !== null ? mentionLoading || mentionSearchedFor !== mentionQuery ? "searching\u2026" : `no records match \u201C${mentionQuery}\u201D` : void 0
|
|
1193
|
+
}
|
|
1194
|
+
) : null,
|
|
1195
|
+
/* @__PURE__ */ jsx5(
|
|
1196
|
+
Input,
|
|
1197
|
+
{
|
|
1198
|
+
value,
|
|
1199
|
+
cursor,
|
|
1200
|
+
context,
|
|
1201
|
+
busy,
|
|
1202
|
+
status,
|
|
1203
|
+
hint
|
|
1204
|
+
}
|
|
1205
|
+
)
|
|
1206
|
+
] });
|
|
1207
|
+
}
|
|
1208
|
+
function DigestStream({ text }) {
|
|
1209
|
+
const spinner = useSpinner(true);
|
|
1210
|
+
if (!text.trim()) {
|
|
1211
|
+
return /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
|
|
1212
|
+
/* @__PURE__ */ jsxs4(Text5, { color: ACCENT, children: [
|
|
1213
|
+
spinner,
|
|
1214
|
+
" "
|
|
1215
|
+
] }),
|
|
1216
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Summarizing the latest activity\u2026" })
|
|
1217
|
+
] });
|
|
1218
|
+
}
|
|
1219
|
+
return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { children: text }) });
|
|
1220
|
+
}
|
|
1221
|
+
function TranscriptBlock({ item }) {
|
|
1222
|
+
switch (item.kind) {
|
|
1223
|
+
case "banner":
|
|
1224
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
1225
|
+
/* @__PURE__ */ jsx5(Logo, {}),
|
|
1226
|
+
/* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
|
|
1227
|
+
/* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
|
|
1228
|
+
item.email,
|
|
1229
|
+
item.email ? " \xB7 " : "",
|
|
1230
|
+
item.host.replace(/^https?:\/\//, "")
|
|
1231
|
+
] }),
|
|
1232
|
+
/* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
|
|
1233
|
+
item.team,
|
|
1234
|
+
item.batch ? ` \xB7 ${item.batch}` : ""
|
|
1235
|
+
] })
|
|
1236
|
+
] })
|
|
1237
|
+
] });
|
|
1238
|
+
case "digest":
|
|
1239
|
+
return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { children: item.text }) });
|
|
1240
|
+
case "user":
|
|
1241
|
+
return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
|
|
1242
|
+
/* @__PURE__ */ jsxs4(Box5, { children: [
|
|
1243
|
+
/* @__PURE__ */ jsx5(Text5, { color: ACCENT, children: "> " }),
|
|
1244
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: item.text })
|
|
1245
|
+
] }),
|
|
1246
|
+
item.context.length > 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \u23F5 ${item.context.map((c) => c.label).join(", ")}` }) : null
|
|
1247
|
+
] });
|
|
1248
|
+
case "assistant":
|
|
1249
|
+
return /* @__PURE__ */ jsx5(
|
|
1250
|
+
AssistantTurn,
|
|
1251
|
+
{
|
|
1252
|
+
text: item.text,
|
|
1253
|
+
tools: item.tools,
|
|
1254
|
+
sources: item.sources,
|
|
1255
|
+
thoughtSeconds: item.thoughtSeconds
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
case "notice":
|
|
1259
|
+
return /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: item.tone === "error" ? "red" : void 0, dimColor: item.tone !== "error", children: item.text }) });
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function readTurn(message) {
|
|
1263
|
+
let text = "";
|
|
1264
|
+
let reasoning = "";
|
|
1265
|
+
const tools = [];
|
|
1266
|
+
const sources = [];
|
|
1267
|
+
for (const part of message.parts) {
|
|
1268
|
+
const type = part.type;
|
|
1269
|
+
if (type === "text") {
|
|
1270
|
+
text += part.text;
|
|
1271
|
+
} else if (type === "reasoning") {
|
|
1272
|
+
reasoning += part.text;
|
|
1273
|
+
} else if (type === "source-url") {
|
|
1274
|
+
const source = part;
|
|
1275
|
+
if (!sources.some((s) => s.url === source.url)) {
|
|
1276
|
+
sources.push({ id: source.sourceId ?? source.url, url: source.url, title: source.title });
|
|
1277
|
+
}
|
|
1278
|
+
} else if (type === "dynamic-tool" || type.startsWith("tool-")) {
|
|
1279
|
+
const tool = part;
|
|
1280
|
+
const name = tool.toolName ?? type.slice("tool-".length);
|
|
1281
|
+
tools.push({
|
|
1282
|
+
id: tool.toolCallId ?? `${name}:${tools.length}`,
|
|
1283
|
+
name,
|
|
1284
|
+
state: tool.state === "output-available" ? "done" : tool.state === "output-error" ? "error" : "running"
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return { text, reasoning, tools, sources };
|
|
1289
|
+
}
|
|
1290
|
+
var MAX_ERROR_CHARS = 300;
|
|
1291
|
+
function describeError(error) {
|
|
1292
|
+
return truncateError(extractMessage(error));
|
|
1293
|
+
}
|
|
1294
|
+
function extractMessage(error) {
|
|
1295
|
+
if (error instanceof Error) return unwrapJsonMessage(error.message);
|
|
1296
|
+
if (typeof error === "object" && error !== null) {
|
|
1297
|
+
const record = error;
|
|
1298
|
+
const message = record.message ?? record.errorText;
|
|
1299
|
+
if (typeof message === "string" && message) return unwrapJsonMessage(message);
|
|
1300
|
+
}
|
|
1301
|
+
if (typeof error === "string") return unwrapJsonMessage(error);
|
|
1302
|
+
return "Something went wrong.";
|
|
1303
|
+
}
|
|
1304
|
+
function unwrapJsonMessage(raw, depth = 0) {
|
|
1305
|
+
if (depth > 3) return raw;
|
|
1306
|
+
const trimmed = raw.trim();
|
|
1307
|
+
if (!trimmed.startsWith("{")) return raw;
|
|
1308
|
+
try {
|
|
1309
|
+
const parsed = JSON.parse(trimmed);
|
|
1310
|
+
const message = parsed.message ?? parsed.error?.message;
|
|
1311
|
+
if (typeof message === "string" && message) return unwrapJsonMessage(message, depth + 1);
|
|
1312
|
+
} catch {
|
|
1313
|
+
}
|
|
1314
|
+
return raw;
|
|
1315
|
+
}
|
|
1316
|
+
function truncateError(message) {
|
|
1317
|
+
const collapsed = message.replace(/\s+/g, " ").trim();
|
|
1318
|
+
return collapsed.length > MAX_ERROR_CHARS ? `${collapsed.slice(0, MAX_ERROR_CHARS)}\u2026` : collapsed;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/oc.tsx
|
|
1322
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1323
|
+
var USAGE = `oc \u2014 Orange Collective in your terminal
|
|
1324
|
+
|
|
1325
|
+
Usage
|
|
1326
|
+
oc [chat] open the research agent (default)
|
|
1327
|
+
oc login authorize this machine in your browser
|
|
1328
|
+
oc logout forget the stored session
|
|
1329
|
+
oc --help
|
|
1330
|
+
oc --version
|
|
1331
|
+
|
|
1332
|
+
Options
|
|
1333
|
+
--host <url> target a different deployment (or set OC_HOST)`;
|
|
1334
|
+
function parseArgv(argv) {
|
|
1335
|
+
let command = "chat";
|
|
1336
|
+
let host;
|
|
1337
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1338
|
+
const arg = argv[i];
|
|
1339
|
+
if (arg === "--host") host = argv[++i];
|
|
1340
|
+
else if (arg.startsWith("--host=")) host = arg.slice("--host=".length);
|
|
1341
|
+
else if (arg === "--help" || arg === "-h") command = "help";
|
|
1342
|
+
else if (arg === "--version" || arg === "-v") command = "version";
|
|
1343
|
+
else if (!arg.startsWith("-")) command = arg;
|
|
1344
|
+
}
|
|
1345
|
+
return { command, host };
|
|
1346
|
+
}
|
|
1347
|
+
function fail(message) {
|
|
1348
|
+
process.stderr.write(`${message}
|
|
1349
|
+
`);
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
}
|
|
1352
|
+
async function main() {
|
|
1353
|
+
const { command, host: hostFlag } = parseArgv(process.argv.slice(2));
|
|
1354
|
+
if (command === "help") {
|
|
1355
|
+
process.stdout.write(`${USAGE}
|
|
1356
|
+
`);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (command === "version") {
|
|
1360
|
+
process.stdout.write(`${VERSION}
|
|
1361
|
+
`);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (command === "logout") {
|
|
1365
|
+
await logout();
|
|
1366
|
+
process.stdout.write("Signed out.\n");
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const stored = await readCredentials();
|
|
1370
|
+
const host = resolveHost(hostFlag, stored?.host);
|
|
1371
|
+
if (command === "login") {
|
|
1372
|
+
const credentials = await login(host, {
|
|
1373
|
+
onUrl: (url, shortCode) => {
|
|
1374
|
+
process.stdout.write(
|
|
1375
|
+
`
|
|
1376
|
+
Opening your browser to authorize this machine.
|
|
1377
|
+
If it doesn't open, visit:
|
|
1378
|
+
${url}
|
|
1379
|
+
|
|
1380
|
+
Confirm this code matches the page: ${shortCode}
|
|
1381
|
+
|
|
1382
|
+
`
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
}).catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
|
1386
|
+
process.stdout.write(`Signed in as ${credentials.email}.
|
|
1387
|
+
`);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (command !== "chat") fail(`Unknown command \u201C${command}\u201D.
|
|
1391
|
+
|
|
1392
|
+
${USAGE}`);
|
|
1393
|
+
const provider = await createTokenProvider(host).catch((error) => {
|
|
1394
|
+
if (error instanceof NotLoggedInError) fail(error.message);
|
|
1395
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
1396
|
+
});
|
|
1397
|
+
const client = new OcClient(host, provider.getToken);
|
|
1398
|
+
const [bootstrap, state] = await Promise.all([
|
|
1399
|
+
client.bootstrap().catch((error) => fail(describe(error))),
|
|
1400
|
+
readState()
|
|
1401
|
+
]);
|
|
1402
|
+
assertVersionSupported(bootstrap.minCliVersion);
|
|
1403
|
+
if (bootstrap.teams.length === 0) {
|
|
1404
|
+
fail("Your account isn't on a team yet \u2014 ask an admin to add you.");
|
|
1405
|
+
}
|
|
1406
|
+
const instance = render(
|
|
1407
|
+
/* @__PURE__ */ jsx6(App, { client, bootstrap, host, state: state ?? {} }),
|
|
1408
|
+
// Ctrl-C is handled in App (press twice), so Ink must not exit on the first.
|
|
1409
|
+
{ exitOnCtrlC: false }
|
|
1410
|
+
);
|
|
1411
|
+
await instance.waitUntilExit();
|
|
1412
|
+
}
|
|
1413
|
+
function describe(error) {
|
|
1414
|
+
return error instanceof Error ? error.message : String(error);
|
|
1415
|
+
}
|
|
1416
|
+
main().catch((error) => fail(describe(error)));
|