@tokenfactory/acc-runner 0.41.7 → 0.42.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.
Files changed (50) hide show
  1. package/README.md +87 -4
  2. package/dist/acc.d.ts +3 -0
  3. package/dist/acc.d.ts.map +1 -0
  4. package/dist/acc.js +21 -0
  5. package/dist/acc.js.map +1 -0
  6. package/dist/cli-name.d.ts +29 -0
  7. package/dist/cli-name.d.ts.map +1 -0
  8. package/dist/cli-name.js +17 -0
  9. package/dist/cli-name.js.map +1 -0
  10. package/dist/cli.d.ts +20 -2
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +45 -289
  13. package/dist/cli.js.map +1 -1
  14. package/dist/keychain.d.ts +17 -0
  15. package/dist/keychain.d.ts.map +1 -1
  16. package/dist/keychain.js +36 -0
  17. package/dist/keychain.js.map +1 -1
  18. package/dist/login.d.ts +45 -1
  19. package/dist/login.d.ts.map +1 -1
  20. package/dist/login.js +91 -4
  21. package/dist/login.js.map +1 -1
  22. package/dist/program.d.ts +16 -0
  23. package/dist/program.d.ts.map +1 -0
  24. package/dist/program.js +342 -0
  25. package/dist/program.js.map +1 -0
  26. package/dist/user-auth/device-login.d.ts +47 -0
  27. package/dist/user-auth/device-login.d.ts.map +1 -0
  28. package/dist/user-auth/device-login.js +189 -0
  29. package/dist/user-auth/device-login.js.map +1 -0
  30. package/dist/user-auth/liveness.d.ts +27 -0
  31. package/dist/user-auth/liveness.d.ts.map +1 -0
  32. package/dist/user-auth/liveness.js +128 -0
  33. package/dist/user-auth/liveness.js.map +1 -0
  34. package/dist/user-auth/session.d.ts +82 -0
  35. package/dist/user-auth/session.d.ts.map +1 -0
  36. package/dist/user-auth/session.js +172 -0
  37. package/dist/user-auth/session.js.map +1 -0
  38. package/dist/user-cli/chat.d.ts +44 -0
  39. package/dist/user-cli/chat.d.ts.map +1 -0
  40. package/dist/user-cli/chat.js +199 -0
  41. package/dist/user-cli/chat.js.map +1 -0
  42. package/dist/user-cli/client.d.ts +20 -0
  43. package/dist/user-cli/client.d.ts.map +1 -0
  44. package/dist/user-cli/client.js +60 -0
  45. package/dist/user-cli/client.js.map +1 -0
  46. package/dist/user-cli/tasks.d.ts +37 -0
  47. package/dist/user-cli/tasks.d.ts.map +1 -0
  48. package/dist/user-cli/tasks.js +131 -0
  49. package/dist/user-cli/tasks.js.map +1 -0
  50. package/package.json +3 -2
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The PERSON's CLI session (AX-G1 DEVICE-FLOW-GENERALIZE, gap G-16).
3
+ *
4
+ * `acc-runner login` enrols a MACHINE: it ends holding an ACC-signed JWT that
5
+ * impersonates its owner, plus a runner row in the fleet. This module is the
6
+ * other thing a terminal might want to be — a human, signed in as themselves,
7
+ * with no runner attached. It is the credential AX-G2 acts under.
8
+ *
9
+ * WHAT THE CREDENTIAL IS
10
+ * ----------------------------------------------------------------------------
11
+ * An ordinary Supabase session: the same access/refresh pair the user's browser
12
+ * holds, obtained by spending the one-time grant /api/auth/device-code hands
13
+ * back (see that file for why the CLI, not the server, spends it). Everything
14
+ * follows from that choice:
15
+ *
16
+ * * REFRESH is GoTrue's, not ACC's. `/auth/v1/token?grant_type=refresh_token`,
17
+ * with rotation — so the successor MUST be written back to the keychain on
18
+ * every refresh, or the next start replays a spent token.
19
+ * * REVOCATION is real and it is not ours. The session is a row in
20
+ * `auth.sessions`; Settings → Sessions deletes it and the delete cascades
21
+ * the refresh-token family. The next refresh here fails, and `signedOut`
22
+ * below is how that becomes "you were signed out" rather than a stack trace.
23
+ * * STORAGE is the OS keychain and nothing else. Its own account slot, so it
24
+ * cannot disturb the runner session's frozen entry.
25
+ *
26
+ * The USER AGENT is stamped deliberately. GoTrue records it on the session row,
27
+ * and api/auth/sessions-core.ts turns it into the device label on the sessions
28
+ * surface — so this string is the difference between "Unknown device" and
29
+ * "acc CLI · my-laptop" when the user decides what to revoke.
30
+ */
31
+ import os from "node:os";
32
+ import { clearSlot, loadSlot, saveSlot } from "../keychain.js";
33
+ import { signInCommand } from "../cli-name.js";
34
+ import { PACKAGE_VERSION } from "../pkg-version.js";
35
+ /** Keychain account for the person session. The runner's is "default". */
36
+ export const USER_SESSION_SLOT = "user";
37
+ /** Refresh this far ahead of expiry, so a long RPC never straddles it. */
38
+ export const REFRESH_SKEW_SECONDS = 120;
39
+ /**
40
+ * `acc-cli/1.2.3 (darwin; my-laptop)`. The hostname is filtered to header-safe
41
+ * characters — a hostname with a stray byte must not make an auth call throw
42
+ * where it would otherwise have succeeded.
43
+ */
44
+ export function cliUserAgent(hostname = os.hostname()) {
45
+ const host = hostname.replace(/[^\w.-]+/g, "-").slice(0, 63) || "unknown-host";
46
+ return `acc-cli/${PACKAGE_VERSION} (${process.platform}; ${host})`;
47
+ }
48
+ /** Raised when the session is gone for good — revoked, expired, or never there. */
49
+ export class SignedOutError extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = "SignedOutError";
53
+ }
54
+ }
55
+ // ── Keychain ───────────────────────────────────────────────────────────────
56
+ export function saveUserSession(session) {
57
+ return saveSlot(USER_SESSION_SLOT, session);
58
+ }
59
+ export function loadUserSession() {
60
+ return loadSlot(USER_SESSION_SLOT);
61
+ }
62
+ export function clearUserSession() {
63
+ return clearSlot(USER_SESSION_SLOT);
64
+ }
65
+ async function postGoTrue(target, path, body, deps) {
66
+ const doFetch = deps.fetch ?? fetch;
67
+ const res = await doFetch(`${target.supabaseUrl.replace(/\/+$/, "")}${path}`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ apikey: target.anonKey,
72
+ // Recorded on auth.sessions.user_agent — this is the device label.
73
+ "User-Agent": deps.userAgent ?? cliUserAgent(),
74
+ },
75
+ body: JSON.stringify(body),
76
+ });
77
+ let json = {};
78
+ try {
79
+ json = (await res.json());
80
+ }
81
+ catch {
82
+ /* a non-JSON body is handled by the shape check below */
83
+ }
84
+ return { status: res.status, json };
85
+ }
86
+ /**
87
+ * Fold a token response into a session, or fail loudly. A response missing
88
+ * either token, or the user id, is not a session however cheerful its status
89
+ * code was — better to refuse than to persist a credential we cannot refresh.
90
+ */
91
+ export function toStoredSession(json, target, now) {
92
+ const access = json.access_token;
93
+ const refresh = json.refresh_token;
94
+ const userId = json.user?.id;
95
+ if (!access || !refresh || !userId) {
96
+ throw new Error("auth response did not contain a usable session");
97
+ }
98
+ const expiresAt = typeof json.expires_at === "number"
99
+ ? json.expires_at * 1000
100
+ : now + (typeof json.expires_in === "number" ? json.expires_in : 3600) * 1000;
101
+ return {
102
+ access_token: access,
103
+ refresh_token: refresh,
104
+ access_expires_at: new Date(expiresAt).toISOString(),
105
+ user_id: userId,
106
+ email: json.user?.email ?? null,
107
+ supabase_url: target.supabaseUrl,
108
+ anon_key: target.anonKey,
109
+ };
110
+ }
111
+ /** Spend the one-time grant from /api/auth/device-code for a real session. */
112
+ export async function verifyEmailOtp(target, params, deps = {}) {
113
+ const { status, json } = await postGoTrue(target, "/auth/v1/verify", { type: "email", email: params.email, token: params.token }, deps);
114
+ if (status !== 200) {
115
+ throw new Error(`sign-in grant was rejected (HTTP ${status})`);
116
+ }
117
+ return toStoredSession(json, target, deps.now?.() ?? Date.now());
118
+ }
119
+ /**
120
+ * Trade the refresh token for a fresh pair. A 4xx here is the revocation path,
121
+ * not an outage: GoTrue answers 400/401 for a token that was revoked (the
122
+ * sessions surface deleted the row) or already rotated, and both mean the same
123
+ * thing to the operator — sign in again.
124
+ */
125
+ export async function refreshUserSession(session, deps = {}) {
126
+ const target = { supabaseUrl: session.supabase_url, anonKey: session.anon_key };
127
+ const { status, json } = await postGoTrue(target, "/auth/v1/token?grant_type=refresh_token", { refresh_token: session.refresh_token }, deps);
128
+ if (status >= 400 && status < 500) {
129
+ throw new SignedOutError(`This CLI session was revoked or expired. Run \`${signInCommand()}\`.`);
130
+ }
131
+ if (status !== 200) {
132
+ throw new Error(`token refresh failed (HTTP ${status})`);
133
+ }
134
+ return toStoredSession(json, target, deps.now?.() ?? Date.now());
135
+ }
136
+ export function accessTokenIsFresh(session, now = Date.now()) {
137
+ const expiry = new Date(session.access_expires_at).getTime();
138
+ if (!Number.isFinite(expiry))
139
+ return false;
140
+ return expiry - now > REFRESH_SKEW_SECONDS * 1000;
141
+ }
142
+ /**
143
+ * The accessor everything downstream should use: a live access token for the
144
+ * signed-in human, refreshed and re-persisted when it is close to expiry.
145
+ *
146
+ * The rotated successor is written back BEFORE the token is returned, because a
147
+ * caller that used a token whose refresh partner never landed would be one crash
148
+ * away from replaying a spent token — which GoTrue treats as reuse.
149
+ *
150
+ * A revoked session clears the keychain slot on the way out. Leaving a dead
151
+ * credential in the keychain would make every later command fail the same way
152
+ * with nothing to show for it; removing it makes `auth status` honest.
153
+ */
154
+ export async function getUserAccessToken(deps = {}) {
155
+ const session = await loadUserSession();
156
+ if (!session) {
157
+ throw new SignedOutError(`Not signed in. Run \`${signInCommand()}\`.`);
158
+ }
159
+ if (accessTokenIsFresh(session, deps.now?.() ?? Date.now()))
160
+ return session.access_token;
161
+ try {
162
+ const rotated = await refreshUserSession(session, deps);
163
+ await saveUserSession(rotated);
164
+ return rotated.access_token;
165
+ }
166
+ catch (err) {
167
+ if (err instanceof SignedOutError)
168
+ await clearUserSession();
169
+ throw err;
170
+ }
171
+ }
172
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/user-auth/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import { type UserApiDeps } from "./client.js";
2
+ export interface ChatOptions {
3
+ conversation?: string;
4
+ json?: boolean;
5
+ }
6
+ export interface SseEvent {
7
+ event: string;
8
+ data: unknown;
9
+ }
10
+ /**
11
+ * Split a raw SSE buffer into whole events, returning the unconsumed tail.
12
+ *
13
+ * The tail matters: a chunk boundary lands mid-frame constantly on a token
14
+ * stream, and an implementation that parsed each chunk independently would drop
15
+ * every event unlucky enough to straddle one.
16
+ */
17
+ export declare function drainSseBuffer(buffer: string): {
18
+ events: SseEvent[];
19
+ rest: string;
20
+ };
21
+ /** What one completed turn tells the caller, so a REPL can continue the thread. */
22
+ export interface TurnResult {
23
+ conversationId: string | null;
24
+ text: string;
25
+ queued: boolean;
26
+ }
27
+ /**
28
+ * Send one message and render the reply. Exported separately from the command so
29
+ * the REPL below can loop over it while holding the conversation id, and so the
30
+ * tests can drive a whole turn through a fetch double.
31
+ */
32
+ export declare function streamTurn(message: string, opts: ChatOptions, deps?: UserApiDeps & {
33
+ write?: (chunk: string) => void;
34
+ }): Promise<TurnResult>;
35
+ /**
36
+ * `acc chat "question"` for one shot, `echo q | acc chat` for a pipe, and a bare
37
+ * `acc chat` on a terminal for a REPL that holds the conversation open. The REPL
38
+ * is not a nicety: without it every follow-up question would need the operator
39
+ * to copy a conversation id back out of the previous line.
40
+ */
41
+ export declare function chatCommand(words: string[], opts?: ChatOptions, deps?: UserApiDeps & {
42
+ write?: (chunk: string) => void;
43
+ }): Promise<void>;
44
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/user-cli/chat.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1D,MAAM,WAAW,WAAW;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwBnF;AAED,mFAAmF;AACnF,MAAM,WAAW,UAAU;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,EACjB,IAAI,GAAE,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC3D,OAAO,CAAC,UAAU,CAAC,CAkFrB;AASD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,GAAE,WAAgB,EACtB,IAAI,GAAE,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC3D,OAAO,CAAC,IAAI,CAAC,CAoCf"}
@@ -0,0 +1,199 @@
1
+ /**
2
+ * `acc chat` — Ask-ACC from the terminal.
3
+ *
4
+ * Posts to the same `/api/chat/stream` endpoint the browser uses, authenticated
5
+ * with the operator's own session, and renders the SSE stream as it arrives.
6
+ * Using the shared endpoint rather than calling a model directly is the whole
7
+ * point: the tool catalog, the delegation ceiling, the capability manifest, the
8
+ * per-turn tool cap and the chat audit trail are all server-side, so a terminal
9
+ * turn is governed exactly like a browser turn and cannot become a side door.
10
+ *
11
+ * TWO SHAPES OF ANSWER. With a BYOK key the endpoint streams `token` events and
12
+ * the reply appears here word by word. Without one, an eligible turn is ENQUEUED
13
+ * for a companion runner to drain and the stream ends after `queued` — there is
14
+ * no inline answer to print, so this says so and hands back the conversation id
15
+ * rather than pretending the turn failed.
16
+ */
17
+ import chalk from "chalk";
18
+ import { createInterface } from "node:readline/promises";
19
+ import { userFetch } from "./client.js";
20
+ import { cliName } from "../cli-name.js";
21
+ /**
22
+ * Split a raw SSE buffer into whole events, returning the unconsumed tail.
23
+ *
24
+ * The tail matters: a chunk boundary lands mid-frame constantly on a token
25
+ * stream, and an implementation that parsed each chunk independently would drop
26
+ * every event unlucky enough to straddle one.
27
+ */
28
+ export function drainSseBuffer(buffer) {
29
+ const events = [];
30
+ let rest = buffer;
31
+ for (;;) {
32
+ const boundary = rest.indexOf("\n\n");
33
+ if (boundary === -1)
34
+ break;
35
+ const frame = rest.slice(0, boundary);
36
+ rest = rest.slice(boundary + 2);
37
+ let event = "message";
38
+ const dataLines = [];
39
+ for (const line of frame.split("\n")) {
40
+ if (line.startsWith("event:"))
41
+ event = line.slice(6).trim();
42
+ else if (line.startsWith("data:"))
43
+ dataLines.push(line.slice(5).trim());
44
+ }
45
+ if (dataLines.length === 0)
46
+ continue;
47
+ let data = dataLines.join("\n");
48
+ try {
49
+ data = JSON.parse(dataLines.join("\n"));
50
+ }
51
+ catch {
52
+ /* a non-JSON payload is passed through as the raw string */
53
+ }
54
+ events.push({ event, data });
55
+ }
56
+ return { events, rest };
57
+ }
58
+ function asRecord(data) {
59
+ return data && typeof data === "object" ? data : {};
60
+ }
61
+ /**
62
+ * Send one message and render the reply. Exported separately from the command so
63
+ * the REPL below can loop over it while holding the conversation id, and so the
64
+ * tests can drive a whole turn through a fetch double.
65
+ */
66
+ export async function streamTurn(message, opts, deps = {}) {
67
+ const say = deps.log ?? ((line) => console.log(line));
68
+ const write = deps.write ?? ((chunk) => process.stdout.write(chunk));
69
+ const res = await userFetch("/api/chat/stream", { body: { message, conversation_id: opts.conversation } }, deps);
70
+ if (!res.ok) {
71
+ // The endpoint's own body carries the actionable reason (no provider, no
72
+ // membership, rate limit); a bare status code would send the operator to
73
+ // the logs for something they can read here.
74
+ let detail = `HTTP ${res.status}`;
75
+ try {
76
+ const body = asRecord(await res.json());
77
+ if (typeof body.error === "string")
78
+ detail = `${body.error} (HTTP ${res.status})`;
79
+ }
80
+ catch {
81
+ /* keep the status-only detail */
82
+ }
83
+ throw new Error(`Chat request failed: ${detail}`);
84
+ }
85
+ if (!res.body)
86
+ throw new Error("Chat response carried no stream.");
87
+ let conversationId = opts.conversation ?? null;
88
+ let text = "";
89
+ let queued = false;
90
+ let buffer = "";
91
+ let wroteAnything = false;
92
+ const decoder = new TextDecoder();
93
+ const reader = res.body.getReader();
94
+ for (;;) {
95
+ const { done, value } = await reader.read();
96
+ if (done)
97
+ break;
98
+ buffer += decoder.decode(value, { stream: true });
99
+ const drained = drainSseBuffer(buffer);
100
+ buffer = drained.rest;
101
+ for (const { event, data } of drained.events) {
102
+ const payload = asRecord(data);
103
+ switch (event) {
104
+ case "conversation":
105
+ if (typeof payload.id === "string")
106
+ conversationId = payload.id;
107
+ break;
108
+ case "token": {
109
+ const chunk = typeof payload.text === "string" ? payload.text : "";
110
+ text += chunk;
111
+ if (!opts.json) {
112
+ write(chunk);
113
+ wroteAnything = wroteAnything || chunk.length > 0;
114
+ }
115
+ break;
116
+ }
117
+ case "tool_call":
118
+ if (!opts.json)
119
+ say(chalk.gray(`\n · ${String(payload.name ?? "tool")}…`));
120
+ break;
121
+ case "queued":
122
+ case "turn":
123
+ queued = true;
124
+ break;
125
+ case "error":
126
+ throw new Error(String(payload.message ?? "chat stream failed"));
127
+ case "done":
128
+ if (typeof payload.conversation_id === "string")
129
+ conversationId = payload.conversation_id;
130
+ if (payload.queued === true)
131
+ queued = true;
132
+ break;
133
+ default:
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ if (wroteAnything)
139
+ write("\n");
140
+ if (opts.json) {
141
+ say(JSON.stringify({ conversation_id: conversationId, text, queued }, null, 2));
142
+ }
143
+ else if (queued && !text) {
144
+ say(chalk.yellow("· Queued to a companion runner — no inline answer. Open the conversation in ACC to read the reply."));
145
+ }
146
+ return { conversationId, text, queued };
147
+ }
148
+ /** Read a piped message when one was not passed as an argument. */
149
+ async function readStdin() {
150
+ const chunks = [];
151
+ for await (const chunk of process.stdin)
152
+ chunks.push(Buffer.from(chunk));
153
+ return Buffer.concat(chunks).toString("utf8").trim();
154
+ }
155
+ /**
156
+ * `acc chat "question"` for one shot, `echo q | acc chat` for a pipe, and a bare
157
+ * `acc chat` on a terminal for a REPL that holds the conversation open. The REPL
158
+ * is not a nicety: without it every follow-up question would need the operator
159
+ * to copy a conversation id back out of the previous line.
160
+ */
161
+ export async function chatCommand(words, opts = {}, deps = {}) {
162
+ const say = deps.log ?? ((line) => console.log(line));
163
+ const inline = words.join(" ").trim();
164
+ if (inline) {
165
+ const result = await streamTurn(inline, opts, deps);
166
+ if (!opts.json && result.conversationId) {
167
+ say(chalk.gray(`\nContinue: ${cliName()} chat --conversation ${result.conversationId}`));
168
+ }
169
+ return;
170
+ }
171
+ if (!process.stdin.isTTY) {
172
+ const piped = await readStdin();
173
+ if (!piped)
174
+ throw new Error("No message given (pass one as an argument or pipe it in).");
175
+ await streamTurn(piped, opts, deps);
176
+ return;
177
+ }
178
+ say(chalk.bold("Ask ACC. Blank line or Ctrl-D to exit."));
179
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
180
+ // Ctrl-D closes the input without settling the pending question(), which would
181
+ // hang the process on an invisible prompt. Racing the close event turns EOF
182
+ // into the same clean exit a blank line gives.
183
+ const closed = new Promise((resolve) => rl.once("close", () => resolve(null)));
184
+ let conversation = opts.conversation;
185
+ try {
186
+ for (;;) {
187
+ const answer = await Promise.race([rl.question(chalk.cyan("\n› ")), closed]);
188
+ const line = (answer ?? "").trim();
189
+ if (!line)
190
+ break;
191
+ const result = await streamTurn(line, { ...opts, conversation }, deps);
192
+ conversation = result.conversationId ?? conversation;
193
+ }
194
+ }
195
+ finally {
196
+ rl.close();
197
+ }
198
+ }
199
+ //# sourceMappingURL=chat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.js","sourceRoot":"","sources":["../../src/user-cli/chat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /** Injectable so the command tests never touch a network or a keychain. */
2
+ export interface UserApiDeps {
3
+ fetch?: typeof fetch;
4
+ now?: () => number;
5
+ log?: (line: string) => void;
6
+ }
7
+ /**
8
+ * A PostgREST client on the `acc` schema, authenticated as the signed-in human.
9
+ * Realtime is not configured: these commands are request/response, and pulling
10
+ * in the ws transport would make every `acc task list` pay for a socket it never
11
+ * opens.
12
+ */
13
+ export declare function userClient(deps?: UserApiDeps): Promise<import("@supabase/supabase-js").SupabaseClient<any, "public", "acc", any, any>>;
14
+ /** POST to an ACC API route as the signed-in human. Relative path, e.g. `/api/chat/stream`. */
15
+ export declare function userFetch(path: string, init?: {
16
+ method?: string;
17
+ body?: unknown;
18
+ headers?: Record<string, string>;
19
+ }, deps?: UserApiDeps): Promise<Response>;
20
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/user-cli/client.ts"],"names":[],"mappings":"AA8BA,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,GAAE,WAAgB,2FAWtD;AAED,+FAA+F;AAC/F,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,EAChF,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,QAAQ,CAAC,CAenB"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The two seams every person-lane command talks to the control plane through.
3
+ *
4
+ * Both are built from the SAME credential the generalized device flow stored —
5
+ * the operator's own Supabase session — so a command run here sees exactly what
6
+ * that human sees in the browser, no more. There is no service-role path and no
7
+ * runner impersonation JWT anywhere in this lane:
8
+ *
9
+ * * `userClient()` is a PostgREST client scoped to the `acc` schema and
10
+ * carrying the user's access token, so RLS (acc.is_member, the repo-scoping
11
+ * predicates) does the authorization. A command cannot read a task the
12
+ * operator could not open in the UI.
13
+ * * `userFetch()` is for the Vercel endpoints, which authenticate the same
14
+ * bearer through requireUser.
15
+ *
16
+ * Both route through getUserAccessToken(), which refreshes-and-repersists when
17
+ * the access token is close to expiry and raises SignedOutError when the session
18
+ * was revoked — so a long-running `chat` session cannot half-expire mid-turn,
19
+ * and a revoked one fails with an instruction rather than a 401 body.
20
+ */
21
+ import { createClient } from "@supabase/supabase-js";
22
+ import { loadConfig } from "../config.js";
23
+ import { signInCommand } from "../cli-name.js";
24
+ import { cliUserAgent, getUserAccessToken, loadUserSession, SignedOutError, } from "../user-auth/session.js";
25
+ /**
26
+ * A PostgREST client on the `acc` schema, authenticated as the signed-in human.
27
+ * Realtime is not configured: these commands are request/response, and pulling
28
+ * in the ws transport would make every `acc task list` pay for a socket it never
29
+ * opens.
30
+ */
31
+ export async function userClient(deps = {}) {
32
+ const session = await loadUserSession();
33
+ if (!session) {
34
+ throw new SignedOutError(`Not signed in. Run \`${signInCommand()}\`.`);
35
+ }
36
+ const token = await getUserAccessToken(deps);
37
+ return createClient(session.supabase_url, session.anon_key, {
38
+ db: { schema: "acc" },
39
+ auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },
40
+ global: { headers: { Authorization: `Bearer ${token}` } },
41
+ });
42
+ }
43
+ /** POST to an ACC API route as the signed-in human. Relative path, e.g. `/api/chat/stream`. */
44
+ export async function userFetch(path, init = {}, deps = {}) {
45
+ const token = await getUserAccessToken(deps);
46
+ const doFetch = deps.fetch ?? fetch;
47
+ const base = loadConfig().publicUrl.replace(/\/+$/, "");
48
+ return doFetch(`${base}${path}`, {
49
+ method: init.method ?? "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ Accept: "text/event-stream",
53
+ Authorization: `Bearer ${token}`,
54
+ "User-Agent": cliUserAgent(),
55
+ ...(init.headers ?? {}),
56
+ },
57
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
58
+ });
59
+ }
60
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/user-cli/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,cAAc,GACf,MAAM,yBAAyB,CAAC;AASjC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAoB,EAAE;IACrD,MAAM,OAAO,GAAG,MAAM,eAAe,EAAE,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,cAAc,CAAC,wBAAwB,aAAa,EAAE,KAAK,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC7C,OAAO,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,EAAE;QAC1D,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE;QACrB,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE;QACnF,MAAM,EAAE,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,EAAE;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,+FAA+F;AAC/F,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAY,EACZ,OAA8E,EAAE,EAChF,OAAoB,EAAE;IAEtB,MAAM,KAAK,GAAG,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC;IACpC,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACxD,OAAO,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE;QAC/B,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM;QAC7B,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,MAAM,EAAE,mBAAmB;YAC3B,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,YAAY,EAAE,YAAY,EAAE;YAC5B,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;SACxB;QACD,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;KACtE,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { type UserApiDeps } from "./client.js";
2
+ export interface TaskListOptions {
3
+ status?: string;
4
+ repo?: string;
5
+ limit?: string | number;
6
+ json?: boolean;
7
+ }
8
+ interface TaskRow {
9
+ id: string;
10
+ title: string | null;
11
+ status: string | null;
12
+ priority: string | null;
13
+ risk: string | null;
14
+ repo: string | null;
15
+ branch: string | null;
16
+ pr_number: number | null;
17
+ agent_id: string | null;
18
+ runner_id: string | null;
19
+ updated_at: string | null;
20
+ [key: string]: unknown;
21
+ }
22
+ /** 1..100, defaulting to 25 — the same clamp the MCP read tools use. */
23
+ export declare function clampLimit(raw: string | number | undefined, fallback?: number): number;
24
+ /** Colour a status the way the board does: terminal states cool, active states warm. */
25
+ export declare function statusColour(status: string | null): (s: string) => string;
26
+ /**
27
+ * One task per line, padded to the widest id/status in THIS result set rather
28
+ * than to a fixed width — a board of short ids should not be indented for the
29
+ * sake of a long one that is not on screen.
30
+ */
31
+ export declare function formatTaskRows(rows: TaskRow[]): string[];
32
+ export declare function taskListCommand(opts?: TaskListOptions, deps?: UserApiDeps): Promise<void>;
33
+ export declare function taskShowCommand(taskId: string, opts?: {
34
+ json?: boolean;
35
+ }, deps?: UserApiDeps): Promise<void>;
36
+ export {};
37
+ //# sourceMappingURL=tasks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/user-cli/tasks.ts"],"names":[],"mappings":"AAWA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAM3D,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,QAAQ,SAAK,GAAG,MAAM,CAIlF;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAczE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CASxD;AAED,wBAAsB,eAAe,CACnC,IAAI,GAAE,eAAoB,EAC1B,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAoBD,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,EAC7B,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CA8Bf"}