sf snapshot: uncommitted changes after 268m inactivity

This commit is contained in:
Mikael Hugo 2026-05-15 02:08:06 +02:00
parent 7e1631618a
commit def1edefa9
21 changed files with 306 additions and 23 deletions

View file

@ -1,2 +1,2 @@
[tools]
node = "26.1.0"
node = "26"

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -12,7 +12,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"types": ["node"],

View file

@ -0,0 +1,66 @@
import { type ContentGenerator } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js";
export interface GeminiCliContentGeneratorOptions {
modelId: string;
cwd?: string;
targetDir?: string;
}
/**
* Create a Gemini CLI Core content generator for a model.
*
* Purpose: centralize the Code Assist setup and OAuth bootstrap logic in a
* reusable package so SF's Gemini provider can stay focused on stream shaping.
*
* Consumer: the Google Gemini provider in pi-ai.
*/
export declare function createGeminiCliContentGenerator(options: GeminiCliContentGeneratorOptions): Promise<ContentGenerator>;
/**
* Per-model quota bucket from CodeAssistServer.retrieveUserQuota.
*/
export interface GeminiQuotaBucket {
modelId: string;
usedFraction: number;
remainingFraction: number;
resetTime?: string;
}
/**
* Snapshot of the active gemini-cli account: tier identity, project, and the
* full per-model quota table.
*
* Why a single struct: every consumer (model picker, usage UI, capacity
* diagnostics, catalog cache) needs the same three pieces of data. Returning
* them together avoids three separate OAuth round trips.
*/
export interface GeminiAccountSnapshot {
projectId: string;
/** Active tier id from setupUser.userTier (e.g. "free-tier", "standard-tier"). */
userTierId?: string;
/** Active tier human label from setupUser.userTierName. */
userTierName?: string;
/**
* Paid tier descriptor when the account has one (e.g. AI Ultra). Carries
* id like "g1-ultra-tier" and the marketing name. Distinct from the
* effective userTier a free-tier session can still have a paidTier
* marker if the underlying account is subscribed.
*/
paidTier?: {
id?: string;
name?: string;
};
models: GeminiQuotaBucket[];
}
/**
* Discover the active gemini-cli account: tier, project, and every model the
* account has access to (with per-model usage fraction and reset time).
*
* Best-effort: any failure (OAuth expired, no project, network) returns null
* silently so callers can downgrade gracefully.
*
* Consumer: SF-side background catalog cache, usage UI, capacity diagnostics.
*/
export declare function snapshotGeminiCliAccount(cwd?: string): Promise<GeminiAccountSnapshot | null>;
/**
* Convenience wrapper: just the model IDs the active gemini-cli account has
* access to. Returns null on failure (same contract as snapshotGeminiCliAccount).
*/
export declare function discoverGeminiCliModels(cwd?: string): Promise<string[] | null>;
//# sourceMappingURL=index.d.ts.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,99 @@
/**
* Google Gemini CLI transport helper.
*
* Purpose: keep the Gemini CLI Core auth and content-generator wiring in a
* dedicated workspace package so provider code can depend on one small helper
* instead of embedding the upstream integration inline.
*
* Consumer: `@singularity-forge/ai` Google Gemini provider, plus SF-side
* background catalog discovery.
*/
import { AuthType, CodeAssistServer, getOauthClient, makeFakeConfig, setupUser, } from "@google/gemini-cli-core";
import { createContentGenerator, createContentGeneratorConfig, } from "@google/gemini-cli-core/dist/src/core/contentGenerator.js";
/**
* Create a Gemini CLI Core content generator for a model.
*
* Purpose: centralize the Code Assist setup and OAuth bootstrap logic in a
* reusable package so SF's Gemini provider can stay focused on stream shaping.
*
* Consumer: the Google Gemini provider in pi-ai.
*/
export async function createGeminiCliContentGenerator(options) {
const cwd = options.cwd ?? process.cwd();
const config = makeFakeConfig({
model: options.modelId,
cwd,
targetDir: options.targetDir ?? cwd,
});
const generatorConfig = await createContentGeneratorConfig(config, AuthType.LOGIN_WITH_GOOGLE);
return createContentGenerator(generatorConfig, config);
}
/**
* Discover the active gemini-cli account: tier, project, and every model the
* account has access to (with per-model usage fraction and reset time).
*
* Best-effort: any failure (OAuth expired, no project, network) returns null
* silently so callers can downgrade gracefully.
*
* Consumer: SF-side background catalog cache, usage UI, capacity diagnostics.
*/
export async function snapshotGeminiCliAccount(cwd) {
try {
const config = makeFakeConfig({ cwd: cwd ?? process.cwd() });
const authClient = await getOauthClient(AuthType.LOGIN_WITH_GOOGLE, config);
const userData = await setupUser(authClient, config);
const projectId = userData?.projectId;
if (!projectId || typeof projectId !== "string")
return null;
const server = new CodeAssistServer(authClient, projectId, { headers: {} });
const data = await server.retrieveUserQuota({ project: projectId });
// Dedup buckets per modelId, keeping the WORST quota (lowest
// remainingFraction). Code Assist sometimes returns multiple buckets
// for the same model when more than one quota window applies; the
// pessimistic choice is what every consumer (UI, capacity diagnostics,
// model picker) actually wants to surface.
const byModel = new Map();
for (const b of data?.buckets ?? []) {
const modelId = typeof b.modelId === "string" ? b.modelId : "";
if (!modelId)
continue;
const remainingFraction = typeof b.remainingFraction === "number" ? b.remainingFraction : 1;
const bucket = {
modelId,
usedFraction: 1 - remainingFraction,
remainingFraction,
resetTime: typeof b.resetTime === "string" ? b.resetTime : undefined,
};
const existing = byModel.get(modelId);
if (!existing || bucket.remainingFraction < existing.remainingFraction) {
byModel.set(modelId, bucket);
}
}
const models = Array.from(byModel.values()).sort((a, b) => a.modelId.localeCompare(b.modelId));
if (models.length === 0)
return null;
return {
projectId,
userTierId: typeof userData?.userTier === "string" ? userData.userTier : undefined,
userTierName: userData?.userTierName,
paidTier: userData?.paidTier
? { id: userData.paidTier.id, name: userData.paidTier.name }
: undefined,
models,
};
}
catch {
return null;
}
}
/**
* Convenience wrapper: just the model IDs the active gemini-cli account has
* access to. Returns null on failure (same contract as snapshotGeminiCliAccount).
*/
export async function discoverGeminiCliModels(cwd) {
const snap = await snapshotGeminiCliAccount(cwd);
if (!snap)
return null;
return snap.models.map((m) => m.modelId);
}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=index.test.d.ts.map

View file

@ -0,0 +1 @@
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["index.test.ts"],"names":[],"mappings":"","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, test, vi } from \"vitest\";\n\nconst helperState = vi.hoisted(() => ({\n\tauthType: undefined as unknown,\n\tconfigParams: undefined as Record<string, unknown> | undefined,\n}));\n\nvi.mock(\"@google/gemini-cli-core\", () => ({\n\tAuthType: { LOGIN_WITH_GOOGLE: \"LOGIN_WITH_GOOGLE\" },\n\tmakeFakeConfig: vi.fn((params: Record<string, unknown>) => {\n\t\thelperState.configParams = params;\n\t\treturn { params };\n\t}),\n}));\n\nvi.mock(\"@google/gemini-cli-core/dist/src/core/contentGenerator.js\", () => ({\n\tcreateContentGeneratorConfig: vi.fn(async (_config, authType) => {\n\t\thelperState.authType = authType;\n\t\treturn { authType };\n\t}),\n\tcreateContentGenerator: vi.fn(async () => ({\n\t\tasync generateContentStream(): Promise<AsyncGenerator<unknown>> {\n\t\t\treturn (async function* emptyStream() {})();\n\t\t},\n\t})),\n}));\n\nimport { createGeminiCliContentGenerator } from \"./index.js\";\n\ndescribe(\"google-gemini-cli-provider\", () => {\n\ttest(\"createGeminiCliContentGenerator_uses_google_login_auth\", async () => {\n\t\tawait createGeminiCliContentGenerator({ modelId: \"gemini-3-pro\" });\n\n\t\tassert.equal(helperState.authType, \"LOGIN_WITH_GOOGLE\");\n\t\tassert.equal(helperState.configParams?.model, \"gemini-3-pro\");\n\t\tassert.equal(helperState.configParams?.cwd, process.cwd());\n\t\tassert.equal(helperState.configParams?.targetDir, process.cwd());\n\t});\n});\n"]}

View file

@ -0,0 +1,35 @@
import assert from "node:assert/strict";
import { describe, test, vi } from "vitest";
const helperState = vi.hoisted(() => ({
authType: undefined,
configParams: undefined,
}));
vi.mock("@google/gemini-cli-core", () => ({
AuthType: { LOGIN_WITH_GOOGLE: "LOGIN_WITH_GOOGLE" },
makeFakeConfig: vi.fn((params) => {
helperState.configParams = params;
return { params };
}),
}));
vi.mock("@google/gemini-cli-core/dist/src/core/contentGenerator.js", () => ({
createContentGeneratorConfig: vi.fn(async (_config, authType) => {
helperState.authType = authType;
return { authType };
}),
createContentGenerator: vi.fn(async () => ({
async generateContentStream() {
return (async function* emptyStream() { })();
},
})),
}));
import { createGeminiCliContentGenerator } from "./index.js";
describe("google-gemini-cli-provider", () => {
test("createGeminiCliContentGenerator_uses_google_login_auth", async () => {
await createGeminiCliContentGenerator({ modelId: "gemini-3-pro" });
assert.equal(helperState.authType, "LOGIN_WITH_GOOGLE");
assert.equal(helperState.configParams?.model, "gemini-3-pro");
assert.equal(helperState.configParams?.cwd, process.cwd());
assert.equal(helperState.configParams?.targetDir, process.cwd());
});
});
//# sourceMappingURL=index.test.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["index.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE5C,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACrC,QAAQ,EAAE,SAAoB;IAC9B,YAAY,EAAE,SAAgD;CAC9D,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE,CAAC,CAAC;IACzC,QAAQ,EAAE,EAAE,iBAAiB,EAAE,mBAAmB,EAAE;IACpD,cAAc,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,MAA+B,EAAE,EAAE;QACzD,WAAW,CAAC,YAAY,GAAG,MAAM,CAAC;QAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IACnB,CAAC,CAAC;CACF,CAAC,CAAC,CAAC;AAEJ,EAAE,CAAC,IAAI,CAAC,2DAA2D,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3E,4BAA4B,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;QAC/D,WAAW,CAAC,QAAQ,GAAG,QAAQ,CAAC;QAChC,OAAO,EAAE,QAAQ,EAAE,CAAC;IACrB,CAAC,CAAC;IACF,sBAAsB,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;QAC1C,KAAK,CAAC,qBAAqB;YAC1B,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,WAAW,KAAI,CAAC,CAAC,EAAE,CAAC;QAC7C,CAAC;KACD,CAAC,CAAC;CACH,CAAC,CAAC,CAAC;AAEJ,OAAO,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAC;AAE7D,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC3C,IAAI,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,+BAA+B,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;QAEnE,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC;QACxD,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC;QAC9D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3D,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import assert from \"node:assert/strict\";\nimport { describe, test, vi } from \"vitest\";\n\nconst helperState = vi.hoisted(() => ({\n\tauthType: undefined as unknown,\n\tconfigParams: undefined as Record<string, unknown> | undefined,\n}));\n\nvi.mock(\"@google/gemini-cli-core\", () => ({\n\tAuthType: { LOGIN_WITH_GOOGLE: \"LOGIN_WITH_GOOGLE\" },\n\tmakeFakeConfig: vi.fn((params: Record<string, unknown>) => {\n\t\thelperState.configParams = params;\n\t\treturn { params };\n\t}),\n}));\n\nvi.mock(\"@google/gemini-cli-core/dist/src/core/contentGenerator.js\", () => ({\n\tcreateContentGeneratorConfig: vi.fn(async (_config, authType) => {\n\t\thelperState.authType = authType;\n\t\treturn { authType };\n\t}),\n\tcreateContentGenerator: vi.fn(async () => ({\n\t\tasync generateContentStream(): Promise<AsyncGenerator<unknown>> {\n\t\t\treturn (async function* emptyStream() {})();\n\t\t},\n\t})),\n}));\n\nimport { createGeminiCliContentGenerator } from \"./index.js\";\n\ndescribe(\"google-gemini-cli-provider\", () => {\n\ttest(\"createGeminiCliContentGenerator_uses_google_login_auth\", async () => {\n\t\tawait createGeminiCliContentGenerator({ modelId: \"gemini-3-pro\" });\n\n\t\tassert.equal(helperState.authType, \"LOGIN_WITH_GOOGLE\");\n\t\tassert.equal(helperState.configParams?.model, \"gemini-3-pro\");\n\t\tassert.equal(helperState.configParams?.cwd, process.cwd());\n\t\tassert.equal(helperState.configParams?.targetDir, process.cwd());\n\t});\n});\n"]}

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext"}}]}<()>;<|assistant to=multi_tool_use.parallel __(/*!json*/)## Step: Rebuild and rerun autonomous SF after tsconfig updates. Also, verify build and run success before marking complete. {
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1,12 +1,12 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"noEmit": true,
"types": ["node"],
"paths": {

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"types": ["node"],

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2024",
"module": "Node16",
"module": "NodeNext",
"lib": ["ES2024"],
"strict": true,
"esModuleInterop": true,
@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "Node16",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",

View file

@ -1,6 +1,6 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
import { homedir, platform } from "node:os";
import { dirname, join, resolve } from "node:path";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";

77
web/middleware.ts Normal file
View file

@ -0,0 +1,77 @@
import { type NextRequest, NextResponse } from "next/server";
/**
* Next.js middleware validates bearer token and origin on all API routes.
*
* The SF_WEB_AUTH_TOKEN env var is set at server launch. Every /api/* request
* must carry a matching `Authorization: Bearer <token>` header. EventSource
* (SSE) connections may use the `_token` query parameter instead since the
* EventSource API cannot set custom headers.
*
* Additionally, if an `Origin` header is present, it must match the expected
* localhost origin to prevent cross-site request forgery.
*/
export function middleware(request: NextRequest): NextResponse {
const { pathname } = request.nextUrl;
// Only gate API routes
if (!pathname.startsWith("/api/")) return NextResponse.next();
const expectedToken = process.env.SF_WEB_AUTH_TOKEN;
if (!expectedToken) {
// If no token was configured (e.g. dev mode without launch harness),
// allow everything — the server didn't opt into auth.
return NextResponse.next();
}
// ── Origin / CORS check ────────────────────────────────────────────
const origin = request.headers.get("origin");
if (origin) {
const host = process.env.SF_WEB_HOST || "127.0.0.1";
const port = process.env.SF_WEB_PORT || "3000";
// Default: localhost origin for the launched host:port
const allowed = new Set([`http://${host}:${port}`]);
// SF_WEB_ALLOWED_ORIGINS lets users whitelist additional origins for
// secure tunnel setups (Tailscale Serve, Cloudflare Tunnel, ngrok, etc.)
const extra = process.env.SF_WEB_ALLOWED_ORIGINS;
if (extra) {
for (const entry of extra.split(",")) {
const trimmed = entry.trim();
if (trimmed) allowed.add(trimmed);
}
}
if (!allowed.has(origin)) {
return NextResponse.json(
{ error: "Forbidden: origin mismatch" },
{ status: 403 },
);
}
}
// ── Bearer token check ─────────────────────────────────────────────
let token: string | null = null;
// 1. Authorization header (preferred)
const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.slice(7);
}
// 2. Query parameter fallback for EventSource / SSE
if (!token) {
token = request.nextUrl.searchParams.get("_token");
}
if (!token || token !== expectedToken) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.next();
}
export const config = {
matcher: "/api/:path*",
};