@9thprotocol/cli 0.1.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/dist/repl.js ADDED
@@ -0,0 +1,266 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin, stdout } from "node:process";
3
+ import { AgentSession, McpManager, loadSkills, resolveVault, skillMessage, } from "@9thprotocol/agent-core";
4
+ import { cyan, dim, red, requireAuth, usageLine, delegationLine, yellow, DEFAULT_MODEL, AUTO_BIASES, fetchCatalog, getAutoBias, setAutoBias, } from "./shared.js";
5
+ const MODES = ["default", "accept-edits", "plan", "bypass"];
6
+ /** Drain non-TTY stdin before anything else can consume it. */
7
+ async function readAllStdin() {
8
+ let data = "";
9
+ stdin.setEncoding("utf8");
10
+ for await (const chunk of stdin)
11
+ data += chunk;
12
+ return data;
13
+ }
14
+ export async function runRepl(cwd) {
15
+ const auth = await requireAuth();
16
+ const platformGet = async (pathname) => {
17
+ const res = await fetch(`${auth.platform.baseUrl}${pathname}`, {
18
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
19
+ });
20
+ if (!res.ok)
21
+ throw new Error(`${res.status}: ${await res.text()}`);
22
+ return res.json();
23
+ };
24
+ const platformPost = async (pathname, body) => {
25
+ const res = await fetch(`${auth.platform.baseUrl}${pathname}`, {
26
+ method: "POST",
27
+ headers: { Authorization: `Bearer ${auth.apiKey}`, "Content-Type": "application/json" },
28
+ body: JSON.stringify(body),
29
+ });
30
+ if (!res.ok)
31
+ throw new Error(`${res.status}: ${await res.text()}`);
32
+ return res.json();
33
+ };
34
+ // Piped stdin is fully buffered up front. readline would otherwise emit the
35
+ // piped lines during the async startup below (catalog fetch, MCP connect)
36
+ // with nothing listening yet, and they'd be silently dropped.
37
+ const piped = !stdin.isTTY ? await readAllStdin() : null;
38
+ const rl = readline.createInterface({ input: stdin, output: stdout });
39
+ const mcp = await McpManager.fromCwd(cwd);
40
+ let skills = loadSkills(cwd);
41
+ const vault = resolveVault(cwd);
42
+ const catalog = await fetchCatalog(auth);
43
+ // Held by reference so `/bias` retunes the live session without rebuilding it.
44
+ const autoRouter = { bias: getAutoBias(), ...(catalog ? { catalog } : {}) };
45
+ const session = new AgentSession({
46
+ apiKey: auth.apiKey,
47
+ ...(auth.platform ? { platform: auth.platform } : {}),
48
+ mcp,
49
+ model: process.env.NINEP_MODEL ?? DEFAULT_MODEL,
50
+ autoRouter,
51
+ cwd,
52
+ mode: "default",
53
+ decide: async (req) => {
54
+ const answer = await rl.question(`${yellow("● permission")} ${req.summary}, allow? [y/N] `);
55
+ return /^y(es)?$/i.test(answer.trim());
56
+ },
57
+ askUser: async (q) => {
58
+ console.log(`\n${cyan("● question")} ${q.question}`);
59
+ if (q.options?.length) {
60
+ q.options.forEach((o, i) => console.log(dim(` ${i + 1}. ${o}`)));
61
+ const answer = (await rl.question("choose a number or type an answer: ")).trim();
62
+ const idx = Number(answer);
63
+ const pick = q.options[idx - 1];
64
+ return Number.isInteger(idx) && pick !== undefined ? pick : answer;
65
+ }
66
+ return (await rl.question("answer: ")).trim();
67
+ },
68
+ });
69
+ console.log(cyan("9th Protocol (9p) beta"));
70
+ console.log(dim(`model: ${session.isAuto ? `auto (${autoRouter.bias})` : session.model} · mode: ${session.mode} · auth: ${auth.platform ? "platform" : "byok"} · cwd: ${cwd}`));
71
+ if (vault)
72
+ console.log(dim(`vault: ${vault}`));
73
+ if (mcp.servers.length)
74
+ console.log(dim(`mcp: ${mcp.servers.join(", ")} (${mcp.tools.length} tools)`));
75
+ if (skills.length)
76
+ console.log(dim(`skills: ${skills.map((s) => "/" + s.name).join(" ")}`));
77
+ console.log(dim("/help for commands"));
78
+ async function showModels() {
79
+ if (!auth.platform) {
80
+ console.log(dim("model catalog requires platform auth; BYOK can use any OpenRouter id via /model <id>"));
81
+ return;
82
+ }
83
+ const data = (await platformGet("/models"));
84
+ for (const m of data.models) {
85
+ const marker = m.id === session.model ? cyan("→") : " ";
86
+ const lock = m.locked ? yellow(" 🔒 upgrade to use") : "";
87
+ console.log(`${marker} ${m.id.padEnd(34)} ${dim(m.tier.padEnd(9))} ${dim(`~${m.burn.heavySessionCredits} cr/heavy-session`)}${lock}`);
88
+ }
89
+ }
90
+ async function offerReset(code) {
91
+ if (!auth.platform)
92
+ return;
93
+ const scope = code === "weekly_limit" ? "week" : "window";
94
+ const answer = await rl.question(`${yellow("●")} buy a ${scope} reset to continue now? [y/N] `);
95
+ if (!/^y(es)?$/i.test(answer.trim()))
96
+ return;
97
+ try {
98
+ const result = (await platformPost("/billing/session-reset", { scope }));
99
+ console.log(dim(`reset applied ($${result.priceUsd}), ${result.resetsUsedThisWeek}/5 used this week. Re-send your message.`));
100
+ }
101
+ catch (err) {
102
+ console.log(red(`reset failed: ${err instanceof Error ? err.message : err}`));
103
+ }
104
+ }
105
+ async function run(message) {
106
+ let streaming = false;
107
+ const breakLine = () => {
108
+ if (streaming)
109
+ stdout.write("\n");
110
+ streaming = false;
111
+ };
112
+ for await (const ev of session.send(message)) {
113
+ switch (ev.type) {
114
+ case "text_delta":
115
+ if (!streaming)
116
+ stdout.write("\n");
117
+ streaming = true;
118
+ stdout.write(ev.text);
119
+ break;
120
+ case "model_selected":
121
+ console.log(dim(`◆ auto → ${ev.model} ${dim(`(${ev.reason})`)}`));
122
+ break;
123
+ case "compacted":
124
+ breakLine();
125
+ console.log(dim(`⊙ compacted context ~${ev.beforeTokens} → ~${ev.afterTokens} tokens`));
126
+ break;
127
+ case "tool_start":
128
+ breakLine();
129
+ console.log(dim(`⚙ ${ev.summary}`));
130
+ break;
131
+ case "tool_end": {
132
+ const first = ev.output.split("\n")[0] ?? "";
133
+ const preview = first.length > 120 ? first.slice(0, 120) + "…" : first;
134
+ console.log(ev.isError ? yellow(` ✗ ${preview}`) : dim(` ✓ ${preview} (${ev.durationMs}ms)`));
135
+ break;
136
+ }
137
+ case "permission_denied":
138
+ breakLine();
139
+ console.log(yellow(`✗ denied: ${ev.summary}`));
140
+ break;
141
+ case "turn_end": {
142
+ breakLine();
143
+ const saved = delegationLine(session.delegated);
144
+ console.log(dim(`⏺ ${usageLine(ev.usage)}${saved ? ` · ${saved}` : ""}`));
145
+ break;
146
+ }
147
+ case "error":
148
+ breakLine();
149
+ console.log(red(`error: ${ev.message}`));
150
+ if (ev.code === "window_limit" || ev.code === "weekly_limit")
151
+ await offerReset(ev.code);
152
+ if (ev.code === "credits_exhausted") {
153
+ console.log(yellow("Out of credits, top-ups and upgrades land with payments (MVP2)."));
154
+ }
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ async function handleSlash(line) {
160
+ const [cmd = "", ...rest] = line.slice(1).split(/\s+/);
161
+ const arg = rest.join(" ").trim();
162
+ switch (cmd) {
163
+ case "help":
164
+ console.log(dim("/models · /model [id|auto] · /bias [economy|balanced|quality] · /mode [default|accept-edits|plan|bypass] · /skills · /<skill> [args] · /compact · /clear · /usage · /exit\n" +
165
+ "subcommands: 9p init (memory/vault setup) · 9p map (codebase → vault graph)"));
166
+ return;
167
+ case "models":
168
+ await showModels().catch((err) => console.log(red(String(err))));
169
+ return;
170
+ case "model":
171
+ if (arg)
172
+ session.setModel(arg);
173
+ console.log(dim(session.isAuto
174
+ ? `model: auto (${autoRouter.bias}), last ran ${session.model}`
175
+ : `model: ${session.model}`));
176
+ return;
177
+ case "bias":
178
+ if (arg && AUTO_BIASES.includes(arg)) {
179
+ autoRouter.bias = arg;
180
+ setAutoBias(autoRouter.bias);
181
+ }
182
+ else if (arg) {
183
+ console.log(red(`unknown bias: ${arg} (${AUTO_BIASES.join(" | ")})`));
184
+ }
185
+ console.log(dim(`auto bias: ${autoRouter.bias}`));
186
+ return;
187
+ case "mode":
188
+ if (arg && MODES.includes(arg))
189
+ session.mode = arg;
190
+ else if (arg)
191
+ console.log(red(`unknown mode: ${arg}`));
192
+ console.log(dim(`mode: ${session.mode}`));
193
+ return;
194
+ case "skills":
195
+ skills = loadSkills(cwd);
196
+ if (!skills.length)
197
+ console.log(dim("no skills found (~/.9p/skills or .9p/skills)"));
198
+ for (const s of skills)
199
+ console.log(` /${s.name} ${dim(s.description || s.source)}`);
200
+ return;
201
+ case "clear":
202
+ session.clear();
203
+ console.log(dim("context cleared"));
204
+ return;
205
+ case "usage": {
206
+ const saved = delegationLine(session.delegated);
207
+ console.log(dim(`${usageLine(session.usage)} · context ~${session.contextTokens} tokens`));
208
+ if (saved)
209
+ console.log(dim(saved));
210
+ return;
211
+ }
212
+ case "compact": {
213
+ const result = await session.compact();
214
+ console.log(dim(result
215
+ ? `compacted ~${result.before} → ~${result.after} tokens`
216
+ : "nothing to compact yet"));
217
+ return;
218
+ }
219
+ case "exit":
220
+ case "quit":
221
+ await mcp.close();
222
+ rl.close();
223
+ process.exit(0);
224
+ // eslint-disable-next-line no-fallthrough
225
+ default: {
226
+ const skill = skills.find((s) => s.name === cmd);
227
+ if (skill) {
228
+ await run(skillMessage(skill, arg));
229
+ return;
230
+ }
231
+ console.log(red(`unknown command: /${cmd}`));
232
+ }
233
+ }
234
+ }
235
+ // Non-interactive: run each piped line as a message, then exit.
236
+ if (piped !== null) {
237
+ for (const line of piped.split("\n").map((l) => l.trim()).filter(Boolean)) {
238
+ console.log(cyan(`\n9p ❯ ${line}`));
239
+ if (line.startsWith("/"))
240
+ await handleSlash(line);
241
+ else
242
+ await run(line);
243
+ }
244
+ await mcp.close();
245
+ rl.close();
246
+ return;
247
+ }
248
+ while (true) {
249
+ let line;
250
+ try {
251
+ line = (await rl.question(cyan("\n9p ❯ "))).trim();
252
+ }
253
+ catch {
254
+ break; // Ctrl-D / closed stdin, exit quietly rather than throwing.
255
+ }
256
+ if (!line)
257
+ continue;
258
+ if (line.startsWith("/"))
259
+ await handleSlash(line);
260
+ else
261
+ await run(line);
262
+ }
263
+ await mcp.close();
264
+ rl.close();
265
+ }
266
+ //# sourceMappingURL=repl.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from "ink-testing-library";
3
+ import { App } from "../tui/app.js";
4
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
+ const usage = { inputTokens: 10, cachedTokens: 5, outputTokens: 3, requests: 1 };
6
+ // Non-zero so the statusline's delegation branch is actually rendered; a fixture
7
+ // of all zeros would compile against the widened AgentLike and test nothing.
8
+ const delegated = {
9
+ calls: 2,
10
+ contextTokensSaved: 6944,
11
+ workerUsage: { inputTokens: 6968, cachedTokens: 0, outputTokens: 2334, requests: 2 },
12
+ };
13
+ let bridge = null;
14
+ let permissionAsked = false;
15
+ const fake = {
16
+ model: "fake/test-model",
17
+ mode: "default",
18
+ usage,
19
+ delegated,
20
+ async *send(text) {
21
+ if (text.includes("dangerous")) {
22
+ permissionAsked = true;
23
+ const allowed = await bridge.requestPermission("bash: rm -i something");
24
+ yield { type: "text_delta", text: allowed ? "ran it" : "skipped it" };
25
+ }
26
+ else {
27
+ yield { type: "text_delta", text: `echo:${text}` };
28
+ }
29
+ yield { type: "turn_end", usage };
30
+ },
31
+ clear() { },
32
+ };
33
+ const { lastFrame, frames, stdin } = render(_jsx(App, { session: fake, meta: { cwd: "/tmp/x", authLabel: "byok", vault: "/tmp/x/vault", skills: [], mcpSummary: null }, register: (b) => (bridge = b) }));
34
+ function expect(cond, label) {
35
+ console.log(`${cond ? "ok" : "FAIL"} - ${label}`);
36
+ if (!cond)
37
+ process.exitCode = 1;
38
+ }
39
+ await sleep(50);
40
+ // the banner lives in <Static>. It appears in an early frame, not necessarily the last
41
+ expect(frames.join("\n").includes("9th Protocol"), "banner renders");
42
+ expect((lastFrame() ?? "").includes("fake/test-model"), "statusline shows model");
43
+ expect((lastFrame() ?? "").includes("6.9k saved"), "statusline shows delegation saving");
44
+ stdin.write("/usage");
45
+ await sleep(30);
46
+ stdin.write("\r");
47
+ await sleep(80);
48
+ expect(frames.join("\n").includes("6944 tokens kept out of context"), "/usage reports what delegation kept out of context");
49
+ stdin.write("hello");
50
+ await sleep(30);
51
+ stdin.write("\r");
52
+ await sleep(100);
53
+ expect((lastFrame() ?? "").includes("echo:hello"), "message round-trip renders response");
54
+ stdin.write("do the dangerous thing");
55
+ await sleep(30);
56
+ stdin.write("\r");
57
+ await sleep(100);
58
+ expect(permissionAsked, "permission prompt requested");
59
+ expect((lastFrame() ?? "").includes("allow? [y/n]"), "permission prompt renders");
60
+ stdin.write("y");
61
+ await sleep(100);
62
+ expect((lastFrame() ?? "").includes("ran it"), "permission allow resumes agent");
63
+ process.exit(process.exitCode ?? 0);
64
+ //# sourceMappingURL=tui-smoke.js.map
@@ -0,0 +1,8 @@
1
+ import { type AgentEvent } from "@9thprotocol/agent-core";
2
+ /**
3
+ * `9p serve`: the local web session. One session per server, bound to
4
+ * 127.0.0.1 only (this is a local UI, not a network service). Used directly in
5
+ * a browser and embedded by the VS Code extension.
6
+ */
7
+ export declare function runServe(cwd: string, port: number): Promise<void>;
8
+ export type { AgentEvent };
package/dist/serve.js ADDED
@@ -0,0 +1,225 @@
1
+ import http from "node:http";
2
+ import crypto from "node:crypto";
3
+ import { AgentSession, McpManager, loadSkills, resolveVault, skillMessage, } from "@9thprotocol/agent-core";
4
+ import { cyan, dim, requireAuth, DEFAULT_MODEL, AUTO_BIASES, fetchCatalog, getAutoBias, setAutoBias, } from "./shared.js";
5
+ import { chatHtml } from "./webui.js";
6
+ const MODES = ["default", "accept-edits", "plan", "bypass"];
7
+ function readJson(req) {
8
+ return new Promise((resolve, reject) => {
9
+ let body = "";
10
+ req.on("data", (c) => (body += c));
11
+ req.on("end", () => {
12
+ try {
13
+ resolve(body ? JSON.parse(body) : {});
14
+ }
15
+ catch {
16
+ reject(new Error("invalid JSON body"));
17
+ }
18
+ });
19
+ req.on("error", reject);
20
+ });
21
+ }
22
+ /**
23
+ * `9p serve`: the local web session. One session per server, bound to
24
+ * 127.0.0.1 only (this is a local UI, not a network service). Used directly in
25
+ * a browser and embedded by the VS Code extension.
26
+ */
27
+ export async function runServe(cwd, port) {
28
+ const auth = await requireAuth();
29
+ const mcp = await McpManager.fromCwd(cwd);
30
+ const skills = loadSkills(cwd);
31
+ const vault = resolveVault(cwd);
32
+ const clients = new Set();
33
+ const pendingPerms = new Map();
34
+ const pendingAsks = new Map();
35
+ let busy = false;
36
+ const broadcast = (ev) => {
37
+ const line = `data: ${JSON.stringify(ev)}\n\n`;
38
+ for (const client of clients)
39
+ client.write(line);
40
+ };
41
+ const catalog = await fetchCatalog(auth);
42
+ // Held by reference so /config can retune the live session's routing bias
43
+ // without rebuilding it.
44
+ const autoRouter = { bias: getAutoBias(), ...(catalog ? { catalog } : {}) };
45
+ const session = new AgentSession({
46
+ apiKey: auth.apiKey,
47
+ ...(auth.platform ? { platform: auth.platform } : {}),
48
+ mcp,
49
+ model: process.env.NINEP_MODEL ?? DEFAULT_MODEL,
50
+ autoRouter,
51
+ cwd,
52
+ mode: "default",
53
+ decide: (req) => new Promise((resolve) => {
54
+ const id = crypto.randomUUID();
55
+ pendingPerms.set(id, resolve);
56
+ broadcast({ type: "permission_request", id, summary: req.summary });
57
+ }),
58
+ askUser: (q) => new Promise((resolve) => {
59
+ const id = crypto.randomUUID();
60
+ pendingAsks.set(id, resolve);
61
+ broadcast({ type: "ask_request", id, question: q.question, options: q.options ?? [] });
62
+ }),
63
+ });
64
+ let modelIds = [];
65
+ if (auth.platform) {
66
+ try {
67
+ const res = await fetch(`${auth.platform.baseUrl}/models`, {
68
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
69
+ });
70
+ if (res.ok) {
71
+ const data = (await res.json());
72
+ modelIds = data.models.filter((m) => !m.locked).map((m) => m.id);
73
+ }
74
+ }
75
+ catch {
76
+ // catalog is a nicety; the session still works without it
77
+ }
78
+ }
79
+ if (!modelIds.includes(session.model))
80
+ modelIds.unshift(session.model);
81
+ async function run(message) {
82
+ busy = true;
83
+ broadcast({ type: "busy", value: true });
84
+ try {
85
+ for await (const ev of session.send(message)) {
86
+ broadcast(ev);
87
+ }
88
+ }
89
+ finally {
90
+ busy = false;
91
+ broadcast({ type: "busy", value: false });
92
+ }
93
+ }
94
+ const server = http.createServer(async (req, res) => {
95
+ const url = new URL(req.url ?? "/", "http://localhost");
96
+ try {
97
+ if (req.method === "GET" && url.pathname === "/") {
98
+ res.writeHead(200, { "Content-Type": "text/html" }).end(chatHtml());
99
+ return;
100
+ }
101
+ if (req.method === "GET" && url.pathname === "/events") {
102
+ res.writeHead(200, {
103
+ "Content-Type": "text/event-stream",
104
+ "Cache-Control": "no-cache",
105
+ Connection: "keep-alive",
106
+ });
107
+ res.write(": connected\n\n");
108
+ clients.add(res);
109
+ req.on("close", () => clients.delete(res));
110
+ return;
111
+ }
112
+ if (req.method === "GET" && url.pathname === "/state") {
113
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
114
+ model: session.model,
115
+ mode: session.mode,
116
+ usage: session.usage,
117
+ delegated: session.delegated,
118
+ contextTokens: session.contextTokens,
119
+ busy,
120
+ cwd,
121
+ vault,
122
+ models: modelIds,
123
+ bias: autoRouter.bias,
124
+ mcp: mcp.servers,
125
+ skills: skills.map((s) => s.name),
126
+ }));
127
+ return;
128
+ }
129
+ if (req.method === "POST" && url.pathname === "/send") {
130
+ if (busy) {
131
+ res.writeHead(409).end("busy");
132
+ return;
133
+ }
134
+ const body = await readJson(req);
135
+ let message = String(body.message ?? "").trim();
136
+ if (!message) {
137
+ res.writeHead(400).end("message required");
138
+ return;
139
+ }
140
+ // /<skill> [args] invocations work in the web session too
141
+ if (message.startsWith("/")) {
142
+ const [name = "", ...rest] = message.slice(1).split(/\s+/);
143
+ const skill = skills.find((s) => s.name === name);
144
+ if (skill)
145
+ message = skillMessage(skill, rest.join(" "));
146
+ }
147
+ void run(message);
148
+ res.writeHead(202).end("ok");
149
+ return;
150
+ }
151
+ if (req.method === "POST" && url.pathname === "/permission") {
152
+ const body = await readJson(req);
153
+ const resolve = pendingPerms.get(String(body.id));
154
+ if (resolve) {
155
+ pendingPerms.delete(String(body.id));
156
+ resolve(Boolean(body.allow));
157
+ }
158
+ res.writeHead(200).end("ok");
159
+ return;
160
+ }
161
+ if (req.method === "POST" && url.pathname === "/answer") {
162
+ const body = await readJson(req);
163
+ const resolve = pendingAsks.get(String(body.id));
164
+ if (resolve) {
165
+ pendingAsks.delete(String(body.id));
166
+ resolve(String(body.answer ?? ""));
167
+ }
168
+ res.writeHead(200).end("ok");
169
+ return;
170
+ }
171
+ if (req.method === "POST" && url.pathname === "/config") {
172
+ const body = await readJson(req);
173
+ // setModel, not `session.model =`: assigning the field would be
174
+ // overwritten by the router on the next turn in Auto mode.
175
+ if (typeof body.model === "string" && body.model)
176
+ session.setModel(body.model);
177
+ if (typeof body.mode === "string" && MODES.includes(body.mode)) {
178
+ session.mode = body.mode;
179
+ }
180
+ if (typeof body.bias === "string" && AUTO_BIASES.includes(body.bias)) {
181
+ autoRouter.bias = body.bias;
182
+ setAutoBias(autoRouter.bias);
183
+ }
184
+ res.writeHead(200).end("ok");
185
+ return;
186
+ }
187
+ /**
188
+ * Account and usage for the Settings and Usage panels.
189
+ *
190
+ * Proxied rather than called from the page: the platform token lives in
191
+ * this process, and handing it to the renderer would put a credential
192
+ * into a browser context for no benefit.
193
+ */
194
+ if (req.method === "GET" && (url.pathname === "/me" || url.pathname === "/usage")) {
195
+ if (!auth.platform) {
196
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ byok: true }));
197
+ return;
198
+ }
199
+ const target = url.pathname === "/me"
200
+ ? "/me"
201
+ : `/billing/usage?groupBy=${encodeURIComponent(url.searchParams.get("groupBy") ?? "model")}` +
202
+ `&days=${encodeURIComponent(url.searchParams.get("days") ?? "30")}`;
203
+ try {
204
+ const r = await fetch(`${auth.platform.baseUrl}${target}`, {
205
+ headers: { Authorization: `Bearer ${auth.apiKey}` },
206
+ });
207
+ res.writeHead(r.status, { "Content-Type": "application/json" }).end(await r.text());
208
+ }
209
+ catch (err) {
210
+ res.writeHead(502, { "Content-Type": "application/json" }).end(JSON.stringify({ error: String(err) }));
211
+ }
212
+ return;
213
+ }
214
+ res.writeHead(404).end("not found");
215
+ }
216
+ catch (err) {
217
+ res.writeHead(500).end(err instanceof Error ? err.message : "error");
218
+ }
219
+ });
220
+ server.listen(port, "127.0.0.1", () => {
221
+ console.log(`${cyan("9p serve")}, local session at http://127.0.0.1:${port}`);
222
+ console.log(dim(`cwd: ${cwd} · auth: ${auth.platform ? "platform" : "byok"}${vault ? ` · vault: ${vault}` : ""}`));
223
+ });
224
+ }
225
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1,60 @@
1
+ import { type RouterBias, type RouterCandidate, type DelegationTotals, type UsageTotals } from "@9thprotocol/agent-core";
2
+ export declare const dim: (s: string) => string;
3
+ export declare const cyan: (s: string) => string;
4
+ export declare const yellow: (s: string) => string;
5
+ export declare const red: (s: string) => string;
6
+ /** Auto routing is the product default (PLAN.md §5.5); `/model <id>` pins a model. */
7
+ export declare const DEFAULT_MODEL = "auto";
8
+ /** Auto bias from NINEP_AUTO_BIAS or ~/.9p/config.json; defaults to balanced. */
9
+ export declare function getAutoBias(): RouterBias;
10
+ export declare function setAutoBias(bias: RouterBias): void;
11
+ export declare const AUTO_BIASES: RouterBias[];
12
+ /**
13
+ * Catalog with plan locks for the Auto router. Platform-only: BYOK has no plan
14
+ * gating, so the router falls back to its built-in ladders.
15
+ */
16
+ export declare function fetchCatalog(auth: Auth): Promise<RouterCandidate[] | undefined>;
17
+ export interface Auth {
18
+ apiKey: string;
19
+ platform?: {
20
+ baseUrl: string;
21
+ };
22
+ }
23
+ /**
24
+ * Auth resolution order:
25
+ * 1. NINEP_API_URL + NINEP_TOKEN (platform, metered)
26
+ * 2. ~/.9p/auth.json {"apiUrl", "token"} (platform)
27
+ * 3. OPENROUTER_API_KEY or ~/.9p/auth.json {"openrouterApiKey"} (BYOK, direct)
28
+ */
29
+ export declare function getAuth(): Auth | null;
30
+ /**
31
+ * Resolve credentials, refreshing an expiring platform token first.
32
+ * Env-provided tokens are used verbatim. We don't own their lifecycle.
33
+ */
34
+ export type AuthFailure = "none" | "expired";
35
+ /**
36
+ * Resolve credentials, refreshing an expiring platform token first.
37
+ *
38
+ * Returns a reason rather than exiting, so hosts that are not a terminal (the
39
+ * desktop app) can surface it in their own UI. A library that calls
40
+ * `process.exit` kills an Electron app with a message nobody ever sees.
41
+ */
42
+ export declare function resolveAuth(): Promise<{
43
+ ok: true;
44
+ auth: Auth;
45
+ } | {
46
+ ok: false;
47
+ reason: AuthFailure;
48
+ }>;
49
+ /** CLI entry point: resolve credentials, or print and exit. */
50
+ export declare function requireAuth(): Promise<Auth>;
51
+ export declare function usageLine(u: UsageTotals): string;
52
+ /**
53
+ * What delegation kept out of the context. Empty when nothing was delegated,
54
+ * so callers can append it unconditionally without printing a zero every turn.
55
+ *
56
+ * Phrased as context rather than money on purpose: those tokens are not merely
57
+ * unspent once, they are tokens that never get resent on any later turn, which
58
+ * is the whole reason a session window lasts longer with delegation on.
59
+ */
60
+ export declare function delegationLine(d: DelegationTotals): string;