Add Prime Intellect client, API, and MCP server

packages/prime — a hand-written typed client, because the first-party SDK is
Python only. Deliberately narrow: PIG reads availability and nothing else, and
the key it holds should be scoped so it could not provision even if the code
tried. Rate limits are undocumented upstream, so it backs off empirically with
full jitter and honours Retry-After. Unknown fields survive in `raw` rather
than being dropped.

apps/api — Hono, with authentication and authorization kept firmly apart. A
verified JWT proves someone has an account in the identity project, which may
be shared with other applications; it does NOT prove they belong here. Access
requires a row in PIG's own users table, and a token without one gets 403
needs_profile rather than entry.

The capacity service is the business logic: availability counts sold and held
separately, so a live hold removes inventory from everyone else's availability
without inflating utilisation. Expired holds are ignored at read time, so the
numbers stay right even when the sweeper is behind. Matching treats
interconnect as a hard filter and excludes Unknown as well as Ethernet —
unverified is not the same as adequate.

apps/mcp — nine tools over stdio, so a team member drives PIG from Claude
Code, Codex, prime-agent, or a Buzz agent. It holds an API key and calls the
same HTTP API the browser does, with no database credentials, so an agent can
never reach further than the person it acts for. Results are formatted as
prose rather than raw JSON.

Theme preferences live in the database rather than localStorage, so a chosen
accent follows someone from laptop to phone. Status colours stay independent
of the accent: if "at risk" re-tinted to whatever a user picked, the signal
would be gone.

Note on the SDK import: its package exports use a `./*` wildcard whose types
entry resolves server/mcp.js to server/mcp.js.d.ts, which does not exist. The
runtime specifier must keep the .js suffix, so the types are mapped via
tsconfig paths rather than by writing an import that would fail at runtime.

Verified: all five packages typecheck; the MCP server constructs and registers
its tools.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:02:45 -07:00
parent d36762f264
commit 7aeec0c632
21 changed files with 3997 additions and 2 deletions
+130
View File
@@ -0,0 +1,130 @@
/**
* Inventory sync from the Prime Intellect availability API.
*
* Runs on an interval, upserting listings on their natural key so repeated
* syncs converge rather than accumulating near-duplicates.
*
* Two decisions worth explaining.
*
* **Stale listings are marked, not deleted.** A listing that disappears
* upstream has usually sold out rather than ceased to exist, and deleting it
* would destroy the price history that makes the inventory useful for
* negotiation. Anything not seen in this pass is marked `Unavailable`, keeping
* the record and its history while removing it from search.
*
* **A failed sync is not fatal.** The upstream publishes no rate limits, so
* some failures are expected. The job logs and waits for the next tick rather
* than crashing the server that people are using.
*/
import { and, eq, inArray, lt, sql } from 'drizzle-orm';
import type { Database } from '@pig/db';
import { inventoryListings } from '@pig/db';
import { PrimeClient, mapListing } from '@pig/prime';
import type { Config } from '../lib/config';
export function startPrimeSync(config: Config, db: Database): () => void {
if (!config.PRIME_SYNC_ENABLED || !config.PRIME_API_KEY) {
if (config.PRIME_SYNC_ENABLED) {
console.warn('[pig] Prime sync enabled but no API key — not starting.');
}
return () => {};
}
const client = new PrimeClient({
apiKey: config.PRIME_API_KEY,
baseUrl: config.PRIME_API_BASE,
onRetry: ({ attempt, delayMs, reason }) =>
console.warn(`[pig] prime retry ${attempt} in ${delayMs}ms (${reason})`),
});
let running = false;
async function runOnce() {
// Overlapping runs would fight over the same rows for no benefit.
if (running) return;
running = true;
const startedAt = new Date();
let upserted = 0;
try {
for await (const page of client.iterateGpuAvailability()) {
const rows = page
.map(mapListing)
.filter((row): row is NonNullable<typeof row> => row !== null);
if (rows.length === 0) continue;
for (const row of rows) {
await db
.insert(inventoryListings)
.values({
...row,
socket: row.socket as 'PCIe' | null,
source: 'prime_api',
observedAt: startedAt,
})
.onConflictDoUpdate({
target: [
inventoryListings.externalCloudId,
inventoryListings.gpuType,
inventoryListings.socket,
inventoryListings.gpuCount,
inventoryListings.securityTier,
],
set: {
stockStatus: row.stockStatus,
onDemandPriceCents: row.onDemandPriceCents,
communityPriceCents: row.communityPriceCents,
priceIsVariable: row.priceIsVariable,
interconnectGbps: row.interconnectGbps,
interconnectType: row.interconnectType,
provisioningMinutes: row.provisioningMinutes,
region: row.region,
country: row.country,
images: row.images,
raw: row.raw,
observedAt: startedAt,
updatedAt: new Date(),
},
});
upserted += 1;
}
}
// Anything the provider stopped listing is out of stock, not gone.
const stale = await db
.update(inventoryListings)
.set({ stockStatus: 'Unavailable', updatedAt: new Date() })
.where(
and(
eq(inventoryListings.source, 'prime_api'),
lt(inventoryListings.observedAt, startedAt),
sql`${inventoryListings.stockStatus} <> 'Unavailable'`,
),
)
.returning({ id: inventoryListings.id });
console.log(
`[pig] prime sync: ${upserted} listing(s) upserted, ${stale.length} marked unavailable`,
);
} catch (error) {
// Logged, not thrown. A provider outage must not take down the CRM.
console.error('[pig] prime sync failed:', error instanceof Error ? error.message : error);
} finally {
running = false;
}
}
// Delay the first run so startup is not competing with an outbound sync.
const initial = setTimeout(() => void runOnce(), 10_000);
const interval = setInterval(
() => void runOnce(),
config.PRIME_SYNC_INTERVAL_MINUTES * 60_000,
);
console.log(`[pig] prime sync every ${config.PRIME_SYNC_INTERVAL_MINUTES}m`);
return () => {
clearTimeout(initial);
clearInterval(interval);
};
}