Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions firebase-debug.log

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions server/firebase-debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[debug] [2026-03-24T04:54:34.355Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.356Z] > authorizing via signed-in user (paulojack2011@gmail.com)
[debug] [2026-03-24T04:54:34.356Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.356Z] > authorizing via signed-in user (paulojack2011@gmail.com)
[debug] [2026-03-24T04:54:34.367Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.367Z] > authorizing via signed-in user (paulojack2011@gmail.com)
[debug] [2026-03-24T04:54:34.542Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.542Z] > authorizing via signed-in user (paulojack2011@gmail.com)
[debug] [2026-03-24T04:54:34.544Z] >>> [apiv2][query] POST https://developerknowledge.googleapis.com/mcp [none]
[debug] [2026-03-24T04:54:34.544Z] >>> [apiv2][body] POST https://developerknowledge.googleapis.com/mcp {"method":"tools/list","jsonrpc":"2.0","id":1}
[debug] [2026-03-24T04:54:34.546Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.546Z] > authorizing via signed-in user (paulojack2011@gmail.com)
[debug] [2026-03-24T04:54:34.546Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.547Z] > authorizing via signed-in user (paulojack2011@gmail.com)
[debug] [2026-03-24T04:54:34.940Z] <<< [apiv2][status] POST https://developerknowledge.googleapis.com/mcp 200
[debug] [2026-03-24T04:54:34.941Z] <<< [apiv2][body] POST https://developerknowledge.googleapis.com/mcp {"id":1,"jsonrpc":"2.0","result":{"tools":[{"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":true},"description":"Use this tool to find documentation about Google developer products. The documents contain official APIs, code snippets, release notes, best practices, guides, debugging info, and more. It covers the following products and domains:\n\n* Android: developer.android.com\n* Apigee: docs.apigee.com\n* Chrome: developer.chrome.com\n* Firebase: firebase.google.com\n* Fuchsia: fuchsia.dev\n* Google AI: ai.google.dev\n* Google Cloud: docs.cloud.google.com\n* Google Developers, Ads, Search, Google Maps, Youtube: developers.google.com\n* Google Home: developers.home.google.com\n* TensorFlow: www.tensorflow.org\n* Web: web.dev\n\nThis tool returns chunks of text, names, and URLs for matching documents. If the returned chunks are not detailed enough to answer the user's question, use `get_documents` with the `parent` from this tool's output to retrieve the full document content.","inputSchema":{"description":"Request schema for search_documents. Use the query field to search for related Google developer documentation.","properties":{"query":{"description":"Required. The raw query string provided by the user, such as \"How to create a Cloud Storage bucket?\".","type":"string"}},"required":["query"],"type":"object"},"name":"search_documents","outputSchema":{"$defs":{"DocumentChunk":{"description":"A DocumentChunk represents a piece of content from a Document in the DeveloperKnowledge corpus. To fetch the entire document content, pass the `parent` to get_document or batch_get_documents.","properties":{"content":{"description":"Output only. The content of the document chunk.","readOnly":true,"type":"string"},"id":{"description":"Output only. The ID of this chunk within the document. The chunk ID is unique within a document, but not globally unique across documents. The chunk ID is not stable and may change over time.","readOnly":true,"type":"string"},"parent":{"description":"Output only. The resource name of the document this chunk is from. Format: `documents/{uri_without_scheme}` Example: `documents/docs.cloud.google.com/storage/docs/creating-buckets`","readOnly":true,"type":"string"}},"type":"object"}},"description":"Response schema for search_documents.","properties":{"results":{"description":"The search results for the given query. Each Document in this list contains a snippet of content relevant to the search query. Use the DocumentChunk.name field of each result with get_documents to retrieve the full document content.","items":{"$ref":"#/$defs/DocumentChunk"},"type":"array"}},"type":"object"}},{"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":true},"description":"Use this tool to retrieve the full content of a single document or up to 20 documents in a single call. The document names should be obtained from the `parent` field of results from a call to the `search_documents` tool. Set the `names` parameter to a list of document names.","inputSchema":{"description":"Request schema for get_documents.","properties":{"names":{"description":"Required. The names of the documents to retrieve, as returned by search_documents. A maximum of 20 documents can be retrieved in one call. The documents are returned in the same order as the `names` in the request. Format: `documents/{uri_without_scheme}` Example: `documents/docs.cloud.google.com/storage/docs/creating-buckets`","items":{"type":"string"},"type":"array"}},"required":["names"],"type":"object"},"name":"get_documents","outputSchema":{"$defs":{"Document":{"description":"A Document represents a piece of content from the Developer Knowledge corpus.","properties":{"content":{"description":"Output only. The content of the document in Markdown format.","readOnly":true,"type":"string"},"description":{"description":"Output only. A description of the document.","readOnly":true,"type":"string"},"name":{"description":"Identifier. The resource name of the document. Format: `documents/{uri_without_scheme}` Example: `documents/docs.cloud.google.com/storage/docs/creating-buckets`","type":"string","x-google-identifier":true},"title":{"description":"Output only. The title of the document.","readOnly":true,"type":"string"},"uri":{"description":"Output only. The URI of the content, such as `https://cloud.google.com/storage/docs/creating-buckets`.","readOnly":true,"type":"string"}},"type":"object"}},"description":"Response schema for get_documents.","properties":{"documents":{"description":"Documents requested.","items":{"$ref":"#/$defs/Document"},"type":"array"}},"type":"object"}}]}}
[debug] [2026-03-24T04:54:34.945Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[debug] [2026-03-24T04:54:34.945Z] > authorizing via signed-in user (paulojack2011@gmail.com)
53 changes: 36 additions & 17 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,26 @@ import { NotFoundPage } from "./pages/NotFound";
import { queryKeys } from "./lib/queryKeys";
import { useCompany } from "./context/CompanyContext";
import { useDialog } from "./context/DialogContext";
import { useLanguage } from "./context/LanguageContext";
import { loadLastInboxTab } from "./lib/inbox";
import { shouldRedirectCompanylessRouteToOnboarding } from "./lib/onboarding-route";

function BootstrapPendingPage({ hasActiveInvite = false }: { hasActiveInvite?: boolean }) {
const { language } = useLanguage();
const isPt = language === "pt-BR";

return (
<div className="mx-auto max-w-xl py-10">
<div className="rounded-lg border border-border bg-card p-6">
<h1 className="text-xl font-semibold">Instance setup required</h1>
<h1 className="text-xl font-semibold">{isPt ? "Configuracao da instancia necessaria" : "Instance setup required"}</h1>
<p className="mt-2 text-sm text-muted-foreground">
{hasActiveInvite
? "No instance admin exists yet. A bootstrap invite is already active. Check your Paperclip startup logs for the first admin invite URL, or run this command to rotate it:"
: "No instance admin exists yet. Run this command in your Paperclip environment to generate the first admin invite URL:"}
? isPt
? "Ainda nao existe um administrador da instancia. Ja ha um convite bootstrap ativo. Verifique os logs de inicializacao do Paperclip para encontrar a URL do primeiro convite de admin, ou rode este comando para gerar uma nova:"
: "No instance admin exists yet. A bootstrap invite is already active. Check your Paperclip startup logs for the first admin invite URL, or run this command to rotate it:"
: isPt
? "Ainda nao existe um administrador da instancia. Rode este comando no ambiente do Paperclip para gerar a URL do primeiro convite de admin:"
: "No instance admin exists yet. Run this command in your Paperclip environment to generate the first admin invite URL:"}
</p>
<pre className="mt-4 overflow-x-auto rounded-md border border-border bg-muted/30 p-3 text-xs">
{`pnpm paperclipai auth bootstrap-ceo`}
Expand All @@ -67,6 +75,8 @@ function BootstrapPendingPage({ hasActiveInvite = false }: { hasActiveInvite?: b
}

function CloudAccessGate() {
const { language } = useLanguage();
const isPt = language === "pt-BR";
const location = useLocation();
const healthQuery = useQuery({
queryKey: queryKeys.health,
Expand All @@ -92,13 +102,13 @@ function CloudAccessGate() {
});

if (healthQuery.isLoading || (isAuthenticatedMode && sessionQuery.isLoading)) {
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">Loading...</div>;
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">{isPt ? "Carregando..." : "Loading..."}</div>;
}

if (healthQuery.error) {
return (
<div className="mx-auto max-w-xl py-10 text-sm text-destructive">
{healthQuery.error instanceof Error ? healthQuery.error.message : "Failed to load app state"}
{healthQuery.error instanceof Error ? healthQuery.error.message : isPt ? "Falha ao carregar o estado do aplicativo" : "Failed to load app state"}
</div>
);
}
Expand Down Expand Up @@ -188,6 +198,8 @@ function LegacySettingsRedirect() {
}

function OnboardingRoutePage() {
const { language } = useLanguage();
const isPt = language === "pt-BR";
const { companies } = useCompany();
const { openOnboarding } = useDialog();
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
Expand All @@ -196,15 +208,15 @@ function OnboardingRoutePage() {
: null;

const title = matchedCompany
? `Add another agent to ${matchedCompany.name}`
? isPt ? `Adicionar outro agente em ${matchedCompany.name}` : `Add another agent to ${matchedCompany.name}`
: companies.length > 0
? "Create another company"
: "Create your first company";
? isPt ? "Criar outra empresa" : "Create another company"
: isPt ? "Criar sua primeira empresa" : "Create your first company";
const description = matchedCompany
? "Run onboarding again to add an agent and a starter task for this company."
? isPt ? "Execute o onboarding novamente para adicionar um agente e uma tarefa inicial para esta empresa." : "Run onboarding again to add an agent and a starter task for this company."
: companies.length > 0
? "Run onboarding again to create another company and seed its first agent."
: "Get started by creating a company and your first agent.";
? isPt ? "Execute o onboarding novamente para criar outra empresa e configurar seu primeiro agente." : "Run onboarding again to create another company and seed its first agent."
: isPt ? "Comece criando uma empresa e seu primeiro agente." : "Get started by creating a company and your first agent.";

return (
<div className="mx-auto max-w-xl py-10">
Expand All @@ -219,7 +231,7 @@ function OnboardingRoutePage() {
: openOnboarding()
}
>
{matchedCompany ? "Add Agent" : "Start Onboarding"}
{matchedCompany ? (isPt ? "Adicionar agente" : "Add Agent") : (isPt ? "Iniciar onboarding" : "Start Onboarding")}
</Button>
</div>
</div>
Expand All @@ -231,8 +243,11 @@ function CompanyRootRedirect() {
const { companies, selectedCompany, loading } = useCompany();
const location = useLocation();

const { language } = useLanguage();
const isPt = language === "pt-BR";

if (loading) {
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">Loading...</div>;
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">{isPt ? "Carregando..." : "Loading..."}</div>;
}

const targetCompany = selectedCompany ?? companies[0] ?? null;
Expand All @@ -252,11 +267,13 @@ function CompanyRootRedirect() {
}

function UnprefixedBoardRedirect() {
const { language } = useLanguage();
const isPt = language === "pt-BR";
const location = useLocation();
const { companies, selectedCompany, loading } = useCompany();

if (loading) {
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">Loading...</div>;
return <div className="mx-auto max-w-xl py-10 text-sm text-muted-foreground">{isPt ? "Carregando..." : "Loading..."}</div>;
}

const targetCompany = selectedCompany ?? companies[0] ?? null;
Expand All @@ -281,17 +298,19 @@ function UnprefixedBoardRedirect() {
}

function NoCompaniesStartPage() {
const { language } = useLanguage();
const isPt = language === "pt-BR";
const { openOnboarding } = useDialog();

return (
<div className="mx-auto max-w-xl py-10">
<div className="rounded-lg border border-border bg-card p-6">
<h1 className="text-xl font-semibold">Create your first company</h1>
<h1 className="text-xl font-semibold">{isPt ? "Criar sua primeira empresa" : "Create your first company"}</h1>
<p className="mt-2 text-sm text-muted-foreground">
Get started by creating a company.
{isPt ? "Comece criando uma empresa." : "Get started by creating a company."}
</p>
<div className="mt-4">
<Button onClick={() => openOnboarding()}>New Company</Button>
<Button onClick={() => openOnboarding()}>{isPt ? "Nova empresa" : "New Company"}</Button>
</div>
</div>
</div>
Expand Down
26 changes: 13 additions & 13 deletions ui/src/components/AccountingModelCard.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import { Database, Gauge, ReceiptText } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { t } from "../lib/locale";

const SURFACES = [
{
title: "Inference ledger",
description: "Request-scoped usage and billed runs from cost_events.",
title: t("Inference ledger"),
description: t("Request-scoped usage and billed runs from cost_events."),
icon: Database,
points: ["tokens + billed dollars", "provider, biller, model", "subscription and overage aware"],
points: [t("tokens + billed dollars"), t("provider, biller, model"), t("subscription and overage aware")],
tone: "from-sky-500/12 via-sky-500/6 to-transparent",
},
{
title: "Finance ledger",
description: "Account-level charges that are not one prompt-response pair.",
title: t("Finance ledger"),
description: t("Account-level charges that are not one prompt-response pair."),
icon: ReceiptText,
points: ["top-ups, refunds, fees", "Bedrock provisioned or training charges", "credit expiries and adjustments"],
points: [t("top-ups, refunds, fees"), t("Bedrock provisioned or training charges"), t("credit expiries and adjustments")],
tone: "from-amber-500/14 via-amber-500/6 to-transparent",
},
{
title: "Live quotas",
description: "Provider or biller windows that can stop traffic in real time.",
title: t("Live quotas"),
description: t("Provider or biller windows that can stop traffic in real time."),
icon: Gauge,
points: ["provider quota windows", "biller credit systems", "errors surfaced directly"],
points: [t("provider quota windows"), t("biller credit systems"), t("errors surfaced directly")],
tone: "from-emerald-500/14 via-emerald-500/6 to-transparent",
},
] as const;
];

export function AccountingModelCard() {
return (
<Card className="relative overflow-hidden border-border/70">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(244,114,182,0.08),transparent_35%),radial-gradient(circle_at_bottom_right,rgba(56,189,248,0.1),transparent_32%)]" />
<CardHeader className="relative px-5 pt-5 pb-2">
<CardTitle className="text-sm font-semibold uppercase tracking-[0.22em] text-muted-foreground">
Accounting model
{t("Accounting model")}
</CardTitle>
<CardDescription className="max-w-2xl text-sm leading-6">
Paperclip now separates request-level inference usage from account-level finance events.
That keeps provider reporting honest when the biller is OpenRouter, Cloudflare, Bedrock, or another intermediary.
{t("Paperclip now separates request-level inference usage from account-level finance events. That keeps provider reporting honest when the biller is OpenRouter, Cloudflare, Bedrock, or another intermediary.")}
</CardDescription>
</CardHeader>
<CardContent className="relative grid gap-3 px-5 pb-5 md:grid-cols-3">
Expand Down
Loading
Loading