pierre-review 0.1.60 → 0.1.62

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.
@@ -32,6 +32,7 @@ export function registerAccountContext(app) {
32
32
  isLocal: true,
33
33
  plan: 'free',
34
34
  stripeCustomerId: null,
35
+ aiCreditAllowance: null,
35
36
  };
36
37
  }
37
38
  });
@@ -11,17 +11,17 @@ function parseIntList(raw) {
11
11
  }
12
12
  export async function activityRoutes(app) {
13
13
  // The Activity aggregate: per repo, current-state stats + thread totals +
14
- // attention/unread flags + open PRs. Scoped to the account; `repoIds` + `userIds`
15
- // narrow to the active FilterBar repo + member selection (across ALL the account's
16
- // repos — watched-only was dropped). Pure DB read — no GitHub sync, no AI.
14
+ // attention/unread flags + open PRs. Scoped to the account's WATCHED repos (inboxWatch);
15
+ // `repoIds` + `userIds` narrow to the active FilterBar repo + member selection WITHIN
16
+ // watched. Pure DB read — no GitHub sync, no AI.
17
17
  app.get('/api/activity', async (req) => {
18
18
  const q = req.query;
19
19
  return getActivity(accountIdOf(req), parseIntList(q.repoIds), parseIntList(q.userIds));
20
20
  });
21
21
  // The consolidated Feed (the Activity "Feed" entry): one flat, chronological stream
22
- // merging My Turn actionables + the activity feed, deduped. Scoped by the FilterBar
23
- // repo + member selection across ALL the account's repos (watched-only dropped).
24
- // Pure DB read — no GitHub sync, no AI.
22
+ // merging My Turn actionables + the activity feed, deduped. Scoped to the account's
23
+ // WATCHED repos (inboxWatch); the FilterBar repo + member selection narrow WITHIN
24
+ // watched. Pure DB read — no GitHub sync, no AI.
25
25
  app.get('/api/activity/feed', async (req) => {
26
26
  const q = req.query;
27
27
  const limit = q.limit != null ? Number(q.limit) : null;
@@ -27,9 +27,20 @@ export async function meRoutes(app) {
27
27
  // baseline exists (feedLastSeenAt set by the first feed view) so a fresh account
28
28
  // never sees a scary first-load number. FYI is a Pro capability — the count comes
29
29
  // from the plugin's enricher (0 on the free tier, where there's no provider).
30
+ // Per-account entitlement: local = full capabilities (unchanged); cloud = full only when
31
+ // the account's plan isn't 'free' (Stripe billing seam). Computed once and reused for both
32
+ // the FYI gate and the `pro` passthrough below.
33
+ const entitled = req.account
34
+ ? entitledProCapabilities(req.account)
35
+ : EMPTY_CAPABILITIES;
30
36
  const feedLastSeen = await getFeedLastSeenAt(accountId);
31
37
  const fyi = getFyiProvider();
32
- const newFeedItems = feedLastSeen && fyi ? await fyi.countNewSince(accountId, feedLastSeen) : 0;
38
+ // FYI is the Pro `feedMyTurn` capability. Gate the count on ENTITLEMENT, not just provider
39
+ // presence: once Pro loads in cloud the provider is registered process-globally for every
40
+ // account, so a free cloud account would otherwise leak an FYI count it hasn't paid for.
41
+ const newFeedItems = feedLastSeen && fyi && entitled.feedMyTurn
42
+ ? await fyi.countNewSince(accountId, feedLastSeen)
43
+ : 0;
33
44
  return {
34
45
  user,
35
46
  counts: {
@@ -44,9 +55,7 @@ export async function meRoutes(app) {
44
55
  newFeedItems,
45
56
  // Claude Review is now the Pro `claudeReview` capability (in `pro` below).
46
57
  deploymentMode: config.deploymentMode,
47
- // Per-account entitlement: local = full capabilities (unchanged); cloud =
48
- // full only when the account's plan isn't 'free' (Stripe billing seam).
49
- pro: req.account ? entitledProCapabilities(req.account) : EMPTY_CAPABILITIES,
58
+ pro: entitled,
50
59
  };
51
60
  });
52
61
  app.get('/api/my-turn', async (req) => getMyTurn(accountIdOf(req)));
@@ -81,6 +81,7 @@ export async function timelineRoutes(app) {
81
81
  from: parseDate(q.from, new Date(now.getTime() - 14 * DAY_MS)),
82
82
  to: parseDate(q.to, now),
83
83
  repoIds: parseIntList(q.repoIds),
84
+ prIds: parseIntList(q.prIds),
84
85
  userIds: parseIntList(q.userIds),
85
86
  types: parseTypes(q.types),
86
87
  statuses: parseStatuses(q.statuses),
@@ -34,6 +34,7 @@ function rowToAccount(row) {
34
34
  isLocal: row.isLocal,
35
35
  plan: row.plan === 'pro' ? 'pro' : 'free',
36
36
  stripeCustomerId: row.stripeCustomerId,
37
+ aiCreditAllowance: row.aiCreditAllowance ?? null,
37
38
  };
38
39
  }
39
40
  // Module cache of the local account, set by ensureLocalAccount() at startup so
package/dist/config.js CHANGED
@@ -135,10 +135,15 @@ export const config = {
135
135
  // Webhook signing secret (whsec_…) for POST /api/billing/webhook. Empty = the
136
136
  // webhook replies 501 and no billing state ever changes.
137
137
  stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET ?? '',
138
- // Pro plugin master gate. Pro is local-only for now; bind.ts skips the dynamic
139
- // import entirely when false. PRO_DISABLED=true forces pure-OSS mode even when
140
- // the submodule is checked out (used to exercise/capture the free tier).
141
- proEnabled: !isCloud && process.env.PRO_DISABLED !== 'true',
138
+ // Pro plugin master gate. bind.ts skips the dynamic import entirely when false.
139
+ // PRO_DISABLED=true forces pure-OSS mode even when the submodule is checked out (used to
140
+ // exercise/capture the free tier). Local defaults ON. In CLOUD it stays OFF unless
141
+ // PRO_CLOUD_ENABLED=true (the paid summary-AI tier — set on the Railway image), so the
142
+ // public Dockerfile / OSS npm path is byte-identical to before. Even when true in cloud,
143
+ // per-account entitlement (plan !== 'free') + the /api/pro/* 402 gate decide who actually
144
+ // gets Pro; agentic AI stays independently gated by PRO_ADVANCED_AI_ENABLED (unset → off).
145
+ proEnabled: process.env.PRO_DISABLED !== 'true' &&
146
+ (!isCloud || process.env.PRO_CLOUD_ENABLED === 'true'),
142
147
  // Per-repo digest (Pro, Workstream 2) config — consumed by @pierre/pro, kept in
143
148
  // core so the model id / budgets live in one place and aren't hardcoded at call
144
149
  // sites. Inert until the plugin is present AND digestEnabled.
@@ -235,5 +240,13 @@ export function assertCloudConfig() {
235
240
  if (Buffer.from(config.encryptionKey, 'hex').length !== 32) {
236
241
  throw new Error('ENCRYPTION_KEY must be 32 bytes as 64 hex chars (generate with `openssl rand -hex 32`).');
237
242
  }
243
+ // When Pro is cloud-enabled the SUMMARY seam needs its OWN metered credential: there is no
244
+ // ambient logged-in `claude` session on Railway, so without SUMMARY_ANTHROPIC_API_KEY every
245
+ // summary completion has no credential and silently fails. Fail loud instead.
246
+ if (config.proEnabled && !process.env.SUMMARY_ANTHROPIC_API_KEY) {
247
+ throw new Error('Cloud Pro (PRO_CLOUD_ENABLED=true) requires SUMMARY_ANTHROPIC_API_KEY — the metered ' +
248
+ 'Anthropic key the summary seam spends against (no ambient Claude session exists in cloud). ' +
249
+ 'Set it or unset PRO_CLOUD_ENABLED. See docs/DEPLOY-RAILWAY.md.');
250
+ }
238
251
  }
239
252
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,46 @@
1
+ import { getAiUsageSummary } from './usage.js';
2
+ // USD → credits conversion, MIRRORED from @pierre-review/shared AI_CREDITS_PER_USD. Inlined
3
+ // here (not imported) because the release guard forbids a real runtime import of the
4
+ // types-only shared package into release/dist — the same reason the pro plugin inlines its
5
+ // own copy. Keep all three in lockstep (shared + this + packages/pro/src/insights/routes.ts).
6
+ // $1 of model cost = 1250 credits, so the paid plan's 2,500-credit allowance ≈ $2.00 of spend.
7
+ export const AI_CREDITS_PER_USD = 1250;
8
+ // The default monthly SUMMARY-AI credit allowance for a paid cloud account. Overridable per
9
+ // account via accounts.aiCreditAllowance (a forward hook for top-ups / alternate plans).
10
+ export const DEFAULT_AI_CREDIT_ALLOWANCE = 2500;
11
+ const toCredits = (usd) => Math.round(usd * AI_CREDITS_PER_USD);
12
+ // UTC first-of-month epoch ms — the month-to-date window start (matches the /api/pro/ai-usage
13
+ // route). Monthly reset is automatic: the window rolls at the UTC month boundary.
14
+ export function monthStartMs(nowMs) {
15
+ const d = new Date(nowMs);
16
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
17
+ }
18
+ // The account's allowance in credits, or null when unmetered.
19
+ // - local (isLocal): unmetered — null (never blocked; today's behavior).
20
+ // - paid cloud (plan !== 'free'): the per-account override, else the plan default (2,500).
21
+ // - free cloud: a zero allowance (belt-and-braces — the /api/pro/* 402 gate already blocks
22
+ // these accounts before any Pro route runs).
23
+ function allowanceFor(account) {
24
+ if (account.isLocal)
25
+ return null;
26
+ if (account.plan !== 'free')
27
+ return account.aiCreditAllowance ?? DEFAULT_AI_CREDIT_ALLOWANCE;
28
+ return 0;
29
+ }
30
+ // The PURE credit-ledger computation (no I/O) — month-to-date spend vs. the allowance. Split
31
+ // out from aiCreditStatus so the budget math is unit-testable without a DB. usedCredits sums
32
+ // each seam rounded independently, matching the /api/pro/ai-usage meter exactly.
33
+ export function computeAiCreditStatus(account, usage) {
34
+ const allowanceCredits = allowanceFor(account);
35
+ const usedCredits = toCredits(usage.summaryUsd) + toCredits(usage.agentUsd);
36
+ const remainingCredits = allowanceCredits == null ? null : Math.max(0, allowanceCredits - usedCredits);
37
+ const blocked = remainingCredits != null && remainingCredits <= 0;
38
+ return { allowanceCredits, usedCredits, remainingCredits, blocked };
39
+ }
40
+ // The per-account credit-ledger status: reads the month-to-date ledger and applies the pure
41
+ // computation above. This is what ctx.aiCredits.check calls per generation.
42
+ export async function aiCreditStatus(account, nowMs) {
43
+ const usage = await getAiUsageSummary(account.id, monthStartMs(nowMs));
44
+ return computeAiCreditStatus(account, usage);
45
+ }
46
+ //# sourceMappingURL=credits.js.map
@@ -0,0 +1,5 @@
1
+ -- Per-account monthly SUMMARY-AI credit allowance override (metered cloud plan). Nullable,
2
+ -- no default: null = use the plan default (2,500 for a paid cloud account); local/unlimited
3
+ -- accounts ignore it. A forward hook for top-ups / alternate plans without another migration.
4
+ -- The Postgres baseline is regenerated separately via `pnpm db:generate:pg`.
5
+ ALTER TABLE `accounts` ADD `ai_credit_allowance` integer;
@@ -183,6 +183,13 @@
183
183
  "when": 1783296000000,
184
184
  "tag": "0025_account_billing",
185
185
  "breakpoints": true
186
+ },
187
+ {
188
+ "idx": 26,
189
+ "version": "6",
190
+ "when": 1783382400000,
191
+ "tag": "0026_ai_credit_allowance",
192
+ "breakpoints": true
186
193
  }
187
194
  ]
188
195
  }
@@ -0,0 +1 @@
1
+ ALTER TABLE "accounts" ADD COLUMN "ai_credit_allowance" integer;