@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.
- package/README.md +87 -4
- package/dist/acc.d.ts +3 -0
- package/dist/acc.d.ts.map +1 -0
- package/dist/acc.js +21 -0
- package/dist/acc.js.map +1 -0
- package/dist/cli-name.d.ts +29 -0
- package/dist/cli-name.d.ts.map +1 -0
- package/dist/cli-name.js +17 -0
- package/dist/cli-name.js.map +1 -0
- package/dist/cli.d.ts +20 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -289
- package/dist/cli.js.map +1 -1
- package/dist/keychain.d.ts +17 -0
- package/dist/keychain.d.ts.map +1 -1
- package/dist/keychain.js +36 -0
- package/dist/keychain.js.map +1 -1
- package/dist/login.d.ts +45 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +91 -4
- package/dist/login.js.map +1 -1
- package/dist/program.d.ts +16 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +342 -0
- package/dist/program.js.map +1 -0
- package/dist/user-auth/device-login.d.ts +47 -0
- package/dist/user-auth/device-login.d.ts.map +1 -0
- package/dist/user-auth/device-login.js +189 -0
- package/dist/user-auth/device-login.js.map +1 -0
- package/dist/user-auth/liveness.d.ts +27 -0
- package/dist/user-auth/liveness.d.ts.map +1 -0
- package/dist/user-auth/liveness.js +128 -0
- package/dist/user-auth/liveness.js.map +1 -0
- package/dist/user-auth/session.d.ts +82 -0
- package/dist/user-auth/session.d.ts.map +1 -0
- package/dist/user-auth/session.js +172 -0
- package/dist/user-auth/session.js.map +1 -0
- package/dist/user-cli/chat.d.ts +44 -0
- package/dist/user-cli/chat.d.ts.map +1 -0
- package/dist/user-cli/chat.js +199 -0
- package/dist/user-cli/chat.js.map +1 -0
- package/dist/user-cli/client.d.ts +20 -0
- package/dist/user-cli/client.d.ts.map +1 -0
- package/dist/user-cli/client.js +60 -0
- package/dist/user-cli/client.js.map +1 -0
- package/dist/user-cli/tasks.d.ts +37 -0
- package/dist/user-cli/tasks.d.ts.map +1 -0
- package/dist/user-cli/tasks.js +131 -0
- package/dist/user-cli/tasks.js.map +1 -0
- 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":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAoB,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAYzC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,IAAI,IAAI,GAAG,MAAM,CAAC;IAClB,SAAS,CAAC;QACR,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,QAAQ,KAAK,CAAC,CAAC;YAAE,MAAM;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACtC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;QAChC,IAAI,KAAK,GAAG,SAAS,CAAC;QACtB,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;gBAAE,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACvD,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACrC,IAAI,IAAI,GAAY,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1C,CAAC;QAAC,MAAM,CAAC;YACP,4DAA4D;QAC9D,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC;AASD,SAAS,QAAQ,CAAC,IAAa;IAC7B,OAAO,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAgC,CAAC,CAAC,CAAC,EAAE,CAAC;AACnF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAe,EACf,IAAiB,EACjB,OAA0D,EAAE;IAE5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,KAAa,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,MAAM,SAAS,CACzB,kBAAkB,EAClB,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,YAAY,EAAE,EAAE,EACzD,IAAI,CACL,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,yEAAyE;QACzE,yEAAyE;QACzE,6CAA6C;QAC7C,IAAI,MAAM,GAAG,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YACxC,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;gBAAE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,UAAU,GAAG,CAAC,MAAM,GAAG,CAAC;QACpF,CAAC;QAAC,MAAM,CAAC;YACP,iCAAiC;QACnC,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAEnE,IAAI,cAAc,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC;IAC/C,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,aAAa,GAAG,KAAK,CAAC;IAE1B,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,MAAM,GAAI,GAAG,CAAC,IAAmC,CAAC,SAAS,EAAE,CAAC;IACpE,SAAS,CAAC;QACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,IAAI;YAAE,MAAM;QAChB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;QACtB,KAAK,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAC7C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC/B,QAAQ,KAAK,EAAE,CAAC;gBACd,KAAK,cAAc;oBACjB,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ;wBAAE,cAAc,GAAG,OAAO,CAAC,EAAE,CAAC;oBAChE,MAAM;gBACR,KAAK,OAAO,CAAC,CAAC,CAAC;oBACb,MAAM,KAAK,GAAG,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;oBACnE,IAAI,IAAI,KAAK,CAAC;oBACd,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;wBACf,KAAK,CAAC,KAAK,CAAC,CAAC;wBACb,aAAa,GAAG,aAAa,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;oBACpD,CAAC;oBACD,MAAM;gBACR,CAAC;gBACD,KAAK,WAAW;oBACd,IAAI,CAAC,IAAI,CAAC,IAAI;wBAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC5E,MAAM;gBACR,KAAK,QAAQ,CAAC;gBACd,KAAK,MAAM;oBACT,MAAM,GAAG,IAAI,CAAC;oBACd,MAAM;gBACR,KAAK,OAAO;oBACV,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAAC,CAAC,CAAC;gBACnE,KAAK,MAAM;oBACT,IAAI,OAAO,OAAO,CAAC,eAAe,KAAK,QAAQ;wBAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;oBAC1F,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI;wBAAE,MAAM,GAAG,IAAI,CAAC;oBAC3C,MAAM;gBACR;oBACE,MAAM;YACV,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,aAAa;QAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAE/B,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAClF,CAAC;SAAM,IAAI,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAC3B,GAAG,CACD,KAAK,CAAC,MAAM,CACV,oGAAoG,CACrG,CACF,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1C,CAAC;AAED,mEAAmE;AACnE,KAAK,UAAU,SAAS;IACtB,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK;QAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACzE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAe,EACf,OAAoB,EAAE,EACtB,OAA0D,EAAE;IAE5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAEtC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;YACxC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,OAAO,EAAE,wBAAwB,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;QAC3F,CAAC;QACD,OAAO;IACT,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;QACzF,MAAM,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACpC,OAAO;IACT,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC,CAAC;IAC1D,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7E,+EAA+E;IAC/E,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrF,IAAI,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;IACrC,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;YAC7E,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,CAAC,IAAI;gBAAE,MAAM;YACjB,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,EAAE,IAAI,CAAC,CAAC;YACvE,YAAY,GAAG,MAAM,CAAC,cAAc,IAAI,YAAY,CAAC;QACvD,CAAC;IACH,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
|
|
@@ -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"}
|