mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 07:41:23 +00:00
Merge pull request #32 from Dvorinka/feat/agent-setup-prompt
feat: agent setup prompt for new projects
This commit is contained in:
10 files changed
+381
-2
No files matched your search
@@ -6,6 +6,7 @@ All notable changes to Primora. Format follows [Keep a Changelog](https://keepac
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Agent setup prompt** — a "Set up with an AI agent" dialog that mints a fresh `write`-scoped API key (ingest + reads + non-admin mutations, never key-minting) and renders a copyable prompt handing a coding agent everything it needs: the deployment endpoint, project id, the key itself, plus tasks to instrument error/metric/log/heartbeat reporting per component (`POST /api/v1/ingest` — works from any language), register the app's databases via `db-connections` (postgres → cassandra), wire the MCP server / CLI for future agents, and verify with a test event + deploy marker. Opens at the end of the new-project onboarding and is re-reachable from the project dashboard and Projects page; keys are shown only inside the prompt (revoke under Settings → API keys).
|
||||||
- **Local secrets vault in the CLI** (`@primora/cli` 0.6.0) — `primora vault init/status` and `primora secrets set|get|list|rm|import` manage an encrypted `~/.config/primora/vault.bin` (Argon2id 64 MiB/3/4 → XChaCha20-Poly1305, header bound as AEAD AAD, atomic writes). Entries carry value + optional `url`/`notes` metadata. Password via `PRIMORA_VAULT_PASSWORD`, `--password-file`, or hidden prompt; file path overridable with `PRIMORA_VAULT`. Clean-room format — no external vault compatibility.
|
- **Local secrets vault in the CLI** (`@primora/cli` 0.6.0) — `primora vault init/status` and `primora secrets set|get|list|rm|import` manage an encrypted `~/.config/primora/vault.bin` (Argon2id 64 MiB/3/4 → XChaCha20-Poly1305, header bound as AEAD AAD, atomic writes). Entries carry value + optional `url`/`notes` metadata. Password via `PRIMORA_VAULT_PASSWORD`, `--password-file`, or hidden prompt; file path overridable with `PRIMORA_VAULT`. Clean-room format — no external vault compatibility.
|
||||||
- **Vault sessions** — `primora vault unlock --ttl 900` proves the password once and writes a TTL'd `vault.session` (0600); agents and scripts use the vault without the password. `primora vault lock` revokes immediately.
|
- **Vault sessions** — `primora vault unlock --ttl 900` proves the password once and writes a TTL'd `vault.session` (0600); agents and scripts use the vault without the password. `primora vault lock` revokes immediately.
|
||||||
- **`primora agent` credential broker** — wraps a child process with dummy tokens and a loopback base URL; the proxy attaches the real secret on the wire per grant (TTL, cap 3600 s). Presets: `anthropic`, `openai`, `openrouter`, `groq`, `deepseek`, `xai`, `github`, `npm`; `--upstream name=https://…` covers arbitrary Bearer APIs. Base-URL broker, not TLS MITM — tools must honor a base-URL override.
|
- **`primora agent` credential broker** — wraps a child process with dummy tokens and a loopback base URL; the proxy attaches the real secret on the wire per grant (TTL, cap 3600 s). Presets: `anthropic`, `openai`, `openrouter`, `groq`, `deepseek`, `xai`, `github`, `npm`; `--upstream name=https://…` covers arbitrary Bearer APIs. Base-URL broker, not TLS MITM — tools must honor a base-URL override.
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Supabase, with no hosted tier - your data stays on your hardware.
|
|||||||
- **Automation** - scheduled jobs, inbound webhooks, telemetry alert rules, and `secret://` refs resolved at delivery.
|
- **Automation** - scheduled jobs, inbound webhooks, telemetry alert rules, and `secret://` refs resolved at delivery.
|
||||||
- **Realtime** - SSE event stream with a typed client SDK (`@primora/client`) — channel subscriptions, presence counts, and client-published `custom.*` events that fan out to webhooks and functions.
|
- **Realtime** - SSE event stream with a typed client SDK (`@primora/client`) — channel subscriptions, presence counts, and client-published `custom.*` events that fan out to webhooks and functions.
|
||||||
- **API keys** - `prm_<prefix>_<secret>` credentials, shown once.
|
- **API keys** - `prm_<prefix>_<secret>` credentials, shown once.
|
||||||
|
- **Agent setup** - every project can mint a `write`-scoped key and generate a copyable AI-agent prompt that wires telemetry, database tracking, and MCP/CLI access into any repository.
|
||||||
- **Audit log** - every mutating request recorded, with CSV/JSON export.
|
- **Audit log** - every mutating request recorded, with CSV/JSON export.
|
||||||
- **Demo mode** - fully client-side seeded workspace via `?demo=true`.
|
- **Demo mode** - fully client-side seeded workspace via `?demo=true`.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { authClient, fetchApiToken } from "./lib/auth-client";
|
|||||||
import { configureApiToken, errorMessage } from "./lib/api";
|
import { configureApiToken, errorMessage } from "./lib/api";
|
||||||
import { isDemoMode, disableDemoMode, enableDemoMode, demoSession, demoService } from "./lib/demo-mode";
|
import { isDemoMode, disableDemoMode, enableDemoMode, demoSession, demoService } from "./lib/demo-mode";
|
||||||
import {
|
import {
|
||||||
|
AgentSetupModal,
|
||||||
AppShell,
|
AppShell,
|
||||||
CommandPaletteEnhanced,
|
CommandPaletteEnhanced,
|
||||||
NetworkError,
|
NetworkError,
|
||||||
@@ -127,6 +128,7 @@ export default function App() {
|
|||||||
const [name, setName] = createSignal("");
|
const [name, setName] = createSignal("");
|
||||||
const [activeView, setActiveView] = createSignal<ViewType>("dashboard");
|
const [activeView, setActiveView] = createSignal<ViewType>("dashboard");
|
||||||
const [showOnboarding, setShowOnboarding] = createSignal(false);
|
const [showOnboarding, setShowOnboarding] = createSignal(false);
|
||||||
|
const [agentSetupOpen, setAgentSetupOpen] = createSignal(false);
|
||||||
const [paletteOpen, setPaletteOpen] = createSignal(false);
|
const [paletteOpen, setPaletteOpen] = createSignal(false);
|
||||||
const [theme, setTheme] = createSignal<"dark" | "light">(
|
const [theme, setTheme] = createSignal<"dark" | "light">(
|
||||||
(localStorage.getItem("primora_theme") as "dark" | "light") ?? "dark",
|
(localStorage.getItem("primora_theme") as "dark" | "light") ?? "dark",
|
||||||
@@ -1696,6 +1698,7 @@ export default function App() {
|
|||||||
overview={projectOverview()}
|
overview={projectOverview()}
|
||||||
recentAudit={auditLogs()}
|
recentAudit={auditLogs()}
|
||||||
onNavigate={(view) => setActiveView(view as ViewType)}
|
onNavigate={(view) => setActiveView(view as ViewType)}
|
||||||
|
onAgentSetup={() => setAgentSetupOpen(true)}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -1716,6 +1719,7 @@ export default function App() {
|
|||||||
onDeleteProject={deleteActiveProject}
|
onDeleteProject={deleteActiveProject}
|
||||||
onSelectProject={setSelectedProjectID}
|
onSelectProject={setSelectedProjectID}
|
||||||
onNavigateToDashboard={() => setActiveView("dashboard")}
|
onNavigateToDashboard={() => setActiveView("dashboard")}
|
||||||
|
onAgentSetup={() => setAgentSetupOpen(true)}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
@@ -1966,6 +1970,23 @@ export default function App() {
|
|||||||
isOpen={showOnboarding()}
|
isOpen={showOnboarding()}
|
||||||
projectName={activeProject()?.name ?? "Your project"}
|
projectName={activeProject()?.name ?? "Your project"}
|
||||||
onClose={() => setShowOnboarding(false)}
|
onClose={() => setShowOnboarding(false)}
|
||||||
|
onDone={() => {
|
||||||
|
setShowOnboarding(false);
|
||||||
|
setAgentSetupOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AgentSetupModal
|
||||||
|
open={agentSetupOpen()}
|
||||||
|
onClose={() => setAgentSetupOpen(false)}
|
||||||
|
project={activeProject()}
|
||||||
|
canManage={canUpdateProject() || isDemo}
|
||||||
|
demoMode={isDemo}
|
||||||
|
onKeyCreated={() => {
|
||||||
|
void refetchAPIKeys();
|
||||||
|
void refetchAuditLogs();
|
||||||
|
void refreshProjectOverviewSnapshot();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Show, createEffect, createSignal } from "solid-js";
|
||||||
|
import { ProjectsService } from "@primora/api-client";
|
||||||
|
import { demoService } from "../lib/demo-mode";
|
||||||
|
import { buildAgentPrompt } from "../lib/agent-prompt";
|
||||||
|
import { errorMessage } from "../lib/api";
|
||||||
|
import { Modal, ModalFooter } from "./Modal";
|
||||||
|
import { IconCheck, IconCopy, IconKey, IconRefresh } from "./Icons";
|
||||||
|
|
||||||
|
export interface AgentSetupProject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentSetupModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
project?: AgentSetupProject;
|
||||||
|
canManage: boolean;
|
||||||
|
demoMode: boolean;
|
||||||
|
/** called after a key is minted so the app can refetch keys/audit/overview */
|
||||||
|
onKeyCreated?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentSetupModal(props: AgentSetupModalProps) {
|
||||||
|
const [prompt, setPrompt] = createSignal("");
|
||||||
|
const [keyPrefix, setKeyPrefix] = createSignal("");
|
||||||
|
const [busy, setBusy] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal("");
|
||||||
|
const [copied, setCopied] = createSignal(false);
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
const project = props.project;
|
||||||
|
if (!project || busy()) return;
|
||||||
|
setBusy(true);
|
||||||
|
setError("");
|
||||||
|
setCopied(false);
|
||||||
|
try {
|
||||||
|
const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, "");
|
||||||
|
const result = props.demoMode
|
||||||
|
? await demoService.createApiKey({ requestBody: { name: `agent-setup-${stamp}` } })
|
||||||
|
: await ProjectsService.createApiKey({
|
||||||
|
projectId: project.id,
|
||||||
|
requestBody: { name: `agent-setup-${stamp}`, scopes: ["write"] },
|
||||||
|
});
|
||||||
|
setKeyPrefix(result.prefix);
|
||||||
|
setPrompt(
|
||||||
|
buildAgentPrompt({
|
||||||
|
baseUrl: window.location.origin,
|
||||||
|
projectId: project.id,
|
||||||
|
projectName: project.name,
|
||||||
|
projectSlug: project.slug,
|
||||||
|
apiKey: result.secret,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
props.onKeyCreated?.();
|
||||||
|
} catch (e) {
|
||||||
|
setError(errorMessage(e, "Could not create an API key"));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mint a fresh write-scoped key each time the modal opens — secrets are
|
||||||
|
// shown once, so a cached prompt would carry a dead credential. The error
|
||||||
|
// guard prevents a retry loop when minting fails; the user retries manually.
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.open && props.project && props.canManage && !prompt() && !busy() && !error()) {
|
||||||
|
void generate();
|
||||||
|
}
|
||||||
|
if (!props.open) {
|
||||||
|
setPrompt("");
|
||||||
|
setKeyPrefix("");
|
||||||
|
setError("");
|
||||||
|
setCopied(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyPrompt = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(prompt());
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
} catch {
|
||||||
|
/* clipboard unavailable — the text is selectable instead */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={props.open}
|
||||||
|
onClose={props.onClose}
|
||||||
|
title="Set up with an AI agent"
|
||||||
|
description="Paste this prompt into a coding agent (Claude Code, Cursor, Devin…) working in your repository. It instruments monitoring, registers your databases, and connects agent tooling — end to end."
|
||||||
|
size="xl"
|
||||||
|
error={error()}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.canManage}
|
||||||
|
fallback={
|
||||||
|
<p class="text-sm text-text-2">
|
||||||
|
Generating credentials requires the project <strong>admin</strong> role. Ask a project
|
||||||
|
admin to open this dialog and copy the prompt for you.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={prompt()}
|
||||||
|
fallback={
|
||||||
|
<div class="flex items-center gap-3 py-8 justify-center text-sm text-text-2">
|
||||||
|
<Show when={busy()}>
|
||||||
|
<span class="spinner" /> Creating a write-scoped API key…
|
||||||
|
</Show>
|
||||||
|
<Show when={!busy() && error()}>
|
||||||
|
<button class="btn btn-secondary btn-sm" onClick={() => void generate()}>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="message message-warning mb-3">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<strong>Fresh key {keyPrefix()}… minted — shown only inside this prompt.</strong>{" "}
|
||||||
|
It grants write access to this project; revoke it under Settings → API keys if the
|
||||||
|
prompt leaks or the agent finishes.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
class="text-xs rounded-lg border border-border bg-surface-2 p-4 overflow-y-auto"
|
||||||
|
style="max-height: 22rem; white-space: pre-wrap; word-break: break-word"
|
||||||
|
>
|
||||||
|
{prompt()}
|
||||||
|
</pre>
|
||||||
|
<ModalFooter class="px-0 pb-0 pt-4" align="between">
|
||||||
|
<button class="btn btn-ghost btn-sm" onClick={() => void generate()} disabled={busy()}>
|
||||||
|
<IconRefresh class="w-4 h-4" />
|
||||||
|
New key & prompt
|
||||||
|
</button>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button class="btn btn-secondary" onClick={props.onClose}>
|
||||||
|
<IconKey class="w-4 h-4" />
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onClick={() => void copyPrompt()}>
|
||||||
|
<Show when={!copied()} fallback={<><IconCheck class="w-4 h-4" /> Copied</>}>
|
||||||
|
<IconCopy class="w-4 h-4" /> Copy prompt
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ interface OnboardingModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
/** fired on the final "Done" step instead of onClose — used to chain into agent setup */
|
||||||
|
onDone?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -88,7 +90,16 @@ export function OnboardingModal(props: OnboardingModalProps) {
|
|||||||
</Show>
|
</Show>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
onClick={() => (step() < steps.length - 1 ? setStep(step() + 1) : handleClose())}
|
onClick={() => {
|
||||||
|
if (step() < steps.length - 1) {
|
||||||
|
setStep(step() + 1);
|
||||||
|
} else if (props.onDone) {
|
||||||
|
setStep(0);
|
||||||
|
props.onDone();
|
||||||
|
} else {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{step() === steps.length - 1 ? "Done" : "Next"}
|
{step() === steps.length - 1 ? "Done" : "Next"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
IconCollections,
|
IconCollections,
|
||||||
IconIntegrations,
|
IconIntegrations,
|
||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
|
IconTerminal,
|
||||||
} from "./Icons";
|
} from "./Icons";
|
||||||
|
|
||||||
interface ProjectDashboardProps {
|
interface ProjectDashboardProps {
|
||||||
@@ -15,6 +16,7 @@ interface ProjectDashboardProps {
|
|||||||
overview?: ProjectOverview;
|
overview?: ProjectOverview;
|
||||||
recentAudit?: AuditLog[];
|
recentAudit?: AuditLog[];
|
||||||
onNavigate: (view: string) => void;
|
onNavigate: (view: string) => void;
|
||||||
|
onAgentSetup?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytes(bytes?: number): string {
|
function formatBytes(bytes?: number): string {
|
||||||
@@ -135,6 +137,12 @@ export function ProjectDashboard(props: ProjectDashboardProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="page-actions">
|
<div class="page-actions">
|
||||||
|
<Show when={props.onAgentSetup}>
|
||||||
|
<button class="btn btn-secondary btn-sm" onClick={() => props.onAgentSetup!()}>
|
||||||
|
<IconTerminal class="w-3.5 h-3.5" />
|
||||||
|
Agent setup
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
<button class="btn btn-secondary btn-sm" onClick={() => props.onNavigate("settings")}>
|
<button class="btn btn-secondary btn-sm" onClick={() => props.onNavigate("settings")}>
|
||||||
<IconKey class="w-3.5 h-3.5" />
|
<IconKey class="w-3.5 h-3.5" />
|
||||||
Keys
|
Keys
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// Components barrel export
|
// Components barrel export
|
||||||
|
export { AgentSetupModal, type AgentSetupProject } from "./AgentSetupModal";
|
||||||
export { AppShell, type ViewType } from "./AppShell";
|
export { AppShell, type ViewType } from "./AppShell";
|
||||||
export { Badge, StatusBadge } from "./Badge";
|
export { Badge, StatusBadge } from "./Badge";
|
||||||
export { Button } from "./Button";
|
export { Button } from "./Button";
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { buildAgentPrompt } from "../agent-prompt";
|
||||||
|
|
||||||
|
const base = {
|
||||||
|
baseUrl: "https://primora.example.com",
|
||||||
|
projectId: "proj-123",
|
||||||
|
projectName: "Production API",
|
||||||
|
projectSlug: "production-api",
|
||||||
|
apiKey: "prm_abcd_secretkey",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("buildAgentPrompt", () => {
|
||||||
|
const prompt = buildAgentPrompt(base);
|
||||||
|
|
||||||
|
it("embeds credentials verbatim", () => {
|
||||||
|
expect(prompt).toContain(`PRIMORA_BASE_URL=${base.baseUrl}`);
|
||||||
|
expect(prompt).toContain(`PRIMORA_API_KEY=${base.apiKey}`);
|
||||||
|
expect(prompt).toContain(`PRIMORA_PROJECT=${base.projectId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("points ingest at the deployment's API base", () => {
|
||||||
|
expect(prompt).toContain(`POST ${base.baseUrl}/api/v1/ingest`);
|
||||||
|
expect(prompt).not.toContain(`${base.baseUrl}/api/v1/api/v1`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("covers monitoring, databases and agent access", () => {
|
||||||
|
expect(prompt).toContain("/db-connections");
|
||||||
|
expect(prompt).toContain("deploy-markers");
|
||||||
|
expect(prompt).toContain("PRIMORA_PROJECT");
|
||||||
|
for (const t of ["postgres", "mysql", "mongodb", "redis"]) {
|
||||||
|
expect(prompt).toContain(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never fabricates database credentials", () => {
|
||||||
|
expect(prompt).toMatch(/never fabricate credentials/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims a trailing slash on the base URL", () => {
|
||||||
|
const p = buildAgentPrompt({ ...base, baseUrl: "http://localhost/" });
|
||||||
|
expect(p).toContain("PRIMORA_BASE_URL=http://localhost\n");
|
||||||
|
expect(p).toContain("http://localhost/api/v1/ingest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes a concrete MCP stdio block when a source path is given", () => {
|
||||||
|
const p = buildAgentPrompt({ ...base, primoraSourcePath: "/opt/Primora" });
|
||||||
|
expect(p).toContain("/opt/Primora/apps/mcp/dist/server.js");
|
||||||
|
expect(p).toContain('"mcpServers"');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Builds the copy-paste prompt that hands an AI coding agent everything it
|
||||||
|
* needs to wire a repository into a Primora project: telemetry ingest,
|
||||||
|
* database connections, and agent-facing access (MCP/CLI). Pure function so
|
||||||
|
* it can be unit-tested and reused by any surface.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AgentPromptInput {
|
||||||
|
/** Deployment origin, e.g. http://localhost — /api/v1 is appended here. */
|
||||||
|
baseUrl: string;
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
projectSlug: string;
|
||||||
|
/** Full prm_… key shown once at mint time. */
|
||||||
|
apiKey: string;
|
||||||
|
/** Optional absolute path to a local Primora source checkout (enables the MCP build step). */
|
||||||
|
primoraSourcePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAgentPrompt(input: AgentPromptInput): string {
|
||||||
|
const base = input.baseUrl.replace(/\/+$/, "");
|
||||||
|
const api = `${base}/api/v1`;
|
||||||
|
const p = input.projectId;
|
||||||
|
|
||||||
|
const mcpSection = input.primoraSourcePath
|
||||||
|
? `A Primora source checkout exists at \`${input.primoraSourcePath}\`. Build the MCP server once:
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
cd ${input.primoraSourcePath} && npm run build --workspace @primora/mcp # → apps/mcp/dist/server.js
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Then register a stdio MCP server named \`primora\` in your client's MCP config:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"primora": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["${input.primoraSourcePath}/apps/mcp/dist/server.js"],
|
||||||
|
"env": {
|
||||||
|
"PRIMORA_BASE_URL": "${base}",
|
||||||
|
"PRIMORA_API_KEY": "$PRIMORA_API_KEY",
|
||||||
|
"PRIMORA_PROJECT": "${p}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
This exposes 30+ \`primora_*\` tools (telemetry query, collections, storage, jobs, audit) to every agent that loads the config.`
|
||||||
|
: `If a Primora source checkout is available on this machine, build \`@primora/mcp\` and register it as a stdio MCP server with env \`PRIMORA_BASE_URL=${base}\`, \`PRIMORA_API_KEY\`, \`PRIMORA_PROJECT=${p}\`. Otherwise skip — the REST API above covers the same surface.`;
|
||||||
|
|
||||||
|
return `You are an AI agent. Wire this repository into the Primora project "${input.projectName}" so it reports telemetry, tracks its databases, and is reachable by future agents. Work through every section; do not stop after the first.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
PRIMORA_BASE_URL=${base}
|
||||||
|
PRIMORA_API_KEY=${input.apiKey}
|
||||||
|
PRIMORA_PROJECT=${p}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- API base: \`${api}\`
|
||||||
|
- Send the key as header \`X-Primora-Key\` (alias \`X-API-Key\` also accepted).
|
||||||
|
- Key scope is \`write\`: it covers telemetry ingest, reads, and non-admin mutations (connections, buckets, collections, webhooks). It cannot mint keys or manage members — that is intentional.
|
||||||
|
- Put these in the project's \`.env\` (add to \`.env.example\` with placeholders) and reference them via env vars in code. Never hardcode or commit the literal key.
|
||||||
|
|
||||||
|
## 1. Monitoring — instrument every component
|
||||||
|
|
||||||
|
All telemetry goes to one endpoint; components self-register on first event:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
POST ${api}/ingest
|
||||||
|
X-Primora-Key: $PRIMORA_API_KEY
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"events":[{"type":"error","component":"${input.projectSlug}-api","kind":"backend","severity":"error","message":"…","payload":{"stack":"…"},"ts":"<RFC3339>"}]}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- \`type\`: \`error\` | \`metric\` | \`log\` | \`heartbeat\` | \`event\`. Batches up to 500 events.
|
||||||
|
- \`kind\`: \`frontend\` | \`backend\` | \`database\` | \`web\` | \`android\` | \`desktop\` | \`other\`.
|
||||||
|
- Errors with the same \`message\` (or explicit \`fingerprint\`) group into Issues.
|
||||||
|
|
||||||
|
Detect this repo's stack and instrument each deployable unit — frontend, backend, workers, landing page, all of them:
|
||||||
|
|
||||||
|
- **Browser / React / any web frontend**: add a small telemetry module that batches to /ingest (fetch + \`keepalive: true\`), hooks \`window.onerror\` + \`unhandledrejection\`, and flushes on \`visibilitychange\`. Component kind \`frontend\` or \`web\`.
|
||||||
|
- **Node / TypeScript backends**: hook the framework's error middleware and \`process.on("uncaughtException")\`; send \`heartbeat "up"\` every 30s. Kind \`backend\`.
|
||||||
|
- **Go / Python / Rust / other**: same HTTP contract — wrap panic/recover or global exception handlers and a periodic heartbeat. Any language works; there is no required SDK.
|
||||||
|
- Name components \`${input.projectSlug}-frontend\`, \`${input.projectSlug}-api\`, \`${input.projectSlug}-worker\`, etc.
|
||||||
|
|
||||||
|
Verify now: send one test event and confirm it lands (see §4).
|
||||||
|
|
||||||
|
## 2. Databases — register what this app uses
|
||||||
|
|
||||||
|
Ask the user for the connection details of each database this project uses — never fabricate credentials, never commit them. Register each one so it appears under Primora → Databases (schema browser, query console):
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
POST ${api}/projects/${p}/db-connections
|
||||||
|
X-Primora-Key: $PRIMORA_API_KEY
|
||||||
|
|
||||||
|
{"name":"primary","db_type":"postgres","host":"…","port":5432,"database":"…","username":"…","password":"…","ssl":true}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Supported \`db_type\`: postgres, mysql, sqlite, redis, mongodb, sqlserver, duckdb, clickhouse, cassandra. After creating each connection verify it:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
POST ${api}/projects/${p}/db-connections/{connectionID}/test → {"ok":true|false,"error":"…"}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Report per-database status to the user. If a database is unreachable, say so — do not silently skip it.
|
||||||
|
|
||||||
|
## 3. Agent access — let future agents manage this project
|
||||||
|
|
||||||
|
${mcpSection}
|
||||||
|
|
||||||
|
Optional CLI path: \`primora login --api-key $PRIMORA_API_KEY\`, then \`primora use\`. For wrapping third-party agents with brokered credentials (the agent never holds real keys), use \`primora agent --<provider> <vault-secret> -- <cmd>\`.
|
||||||
|
|
||||||
|
## 4. Prove it works
|
||||||
|
|
||||||
|
1. \`curl -sS -X POST ${api}/ingest -H "X-Primora-Key: $PRIMORA_API_KEY" -H "Content-Type: application/json" -d '{"events":[{"type":"event","component":"setup","message":"primora.setup.done"}]}'\` — expect \`{"accepted":1}\`.
|
||||||
|
2. Record a deploy marker: \`curl -sS -X POST ${api}/projects/${p}/deploy-markers -H "X-Primora-Key: $PRIMORA_API_KEY" -H "Content-Type: application/json" -d '{"ref":"<git sha>","environment":"dev","note":"agent setup"}'\`.
|
||||||
|
3. Finish with a short summary: components instrumented, database connections tested, and where the data lands (${base} → Telemetry, Databases, Integrations).
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import type { ProjectSummary } from "@primora/api-client";
|
|||||||
import { Badge } from "../components/Badge";
|
import { Badge } from "../components/Badge";
|
||||||
import { Modal, ModalFooter } from "../components/Modal";
|
import { Modal, ModalFooter } from "../components/Modal";
|
||||||
import { Input, Textarea } from "../components/Input";
|
import { Input, Textarea } from "../components/Input";
|
||||||
import { IconPlus, IconProjects, IconArrowRight, IconEdit, IconTrash } from "../components/Icons";
|
import { IconPlus, IconProjects, IconArrowRight, IconEdit, IconTrash, IconTerminal } from "../components/Icons";
|
||||||
|
|
||||||
interface ProjectInput {
|
interface ProjectInput {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -32,6 +32,7 @@ interface ProjectsPageProps {
|
|||||||
onDeleteProject: () => void;
|
onDeleteProject: () => void;
|
||||||
onSelectProject: (id: string) => void;
|
onSelectProject: (id: string) => void;
|
||||||
onNavigateToDashboard: () => void;
|
onNavigateToDashboard: () => void;
|
||||||
|
onAgentSetup?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectsPage(props: ProjectsPageProps) {
|
export function ProjectsPage(props: ProjectsPageProps) {
|
||||||
@@ -169,6 +170,12 @@ export function ProjectsPage(props: ProjectsPageProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<Show when={props.onAgentSetup}>
|
||||||
|
<button class="btn btn-secondary" onClick={() => props.onAgentSetup!()}>
|
||||||
|
<IconTerminal class="w-4 h-4" />
|
||||||
|
Agent setup
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
<button
|
<button
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
onClick={() => setSettingsOpen(true)}
|
onClick={() => setSettingsOpen(true)}
|
||||||
|
|||||||
Reference in new issue
Block a user