@connextar/house 0.9.0 → 0.11.0
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.
- package/README.md +461 -1
- package/dist/ai/config-index.d.ts +14 -0
- package/dist/ai/config-index.d.ts.map +1 -0
- package/dist/ai/config-index.js +14 -0
- package/dist/ai/config-index.js.map +1 -0
- package/dist/ai/images.d.ts +14 -0
- package/dist/ai/images.d.ts.map +1 -0
- package/dist/ai/images.js +10 -0
- package/dist/ai/images.js.map +1 -0
- package/dist/ai/index.d.ts +7 -3
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +7 -3
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/provider.d.ts +1 -9
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +0 -5
- package/dist/ai/provider.js.map +1 -1
- package/dist/jobs/counts.d.ts +82 -0
- package/dist/jobs/counts.d.ts.map +1 -0
- package/dist/jobs/counts.js +112 -0
- package/dist/jobs/counts.js.map +1 -0
- package/dist/jobs/cron-auth.d.ts +44 -0
- package/dist/jobs/cron-auth.d.ts.map +1 -0
- package/dist/jobs/cron-auth.js +20 -0
- package/dist/jobs/cron-auth.js.map +1 -0
- package/dist/jobs/health.d.ts +157 -0
- package/dist/jobs/health.d.ts.map +1 -0
- package/dist/jobs/health.js +146 -0
- package/dist/jobs/health.js.map +1 -0
- package/dist/jobs/index.d.ts +26 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +26 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/job-health-list.d.ts +66 -0
- package/dist/jobs/job-health-list.d.ts.map +1 -0
- package/dist/jobs/job-health-list.js +115 -0
- package/dist/jobs/job-health-list.js.map +1 -0
- package/dist/jobs/react-index.d.ts +19 -0
- package/dist/jobs/react-index.d.ts.map +1 -0
- package/dist/jobs/react-index.js +18 -0
- package/dist/jobs/react-index.js.map +1 -0
- package/dist/jobs/registry.d.ts +104 -0
- package/dist/jobs/registry.d.ts.map +1 -0
- package/dist/jobs/registry.js +72 -0
- package/dist/jobs/registry.js.map +1 -0
- package/dist/jobs/scheduler.d.ts +89 -0
- package/dist/jobs/scheduler.d.ts.map +1 -0
- package/dist/jobs/scheduler.js +282 -0
- package/dist/jobs/scheduler.js.map +1 -0
- package/dist/jobs/store.d.ts +94 -0
- package/dist/jobs/store.d.ts.map +1 -0
- package/dist/jobs/store.js +56 -0
- package/dist/jobs/store.js.map +1 -0
- package/dist/jobs/windows.d.ts +66 -0
- package/dist/jobs/windows.d.ts.map +1 -0
- package/dist/jobs/windows.js +130 -0
- package/dist/jobs/windows.js.map +1 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -60,6 +60,15 @@ is one right answer to that — which is exactly the shape of thing that belongs
|
|
|
60
60
|
here and nowhere else. The module is the union of the five, not a copy of the
|
|
61
61
|
best one.
|
|
62
62
|
|
|
63
|
+
`jobs`, in 0.11.0, is the same case with two copies. AltEd and Quickie grew
|
|
64
|
+
their scheduled jobs from one seed and each proved something the other lacked —
|
|
65
|
+
AltEd a switch with a name and a date against it, Quickie a Run now fenced to the
|
|
66
|
+
caller's own organisation — and both printed a job's result as its variable
|
|
67
|
+
names, so production read "0 chargesRaised · 0 totalOutstanding", with an amount
|
|
68
|
+
of money shown as a number of things. The module is their union, with that
|
|
69
|
+
made impossible rather than fixed: a count reaches the screen only through a
|
|
70
|
+
declaration that says its words and its kind.
|
|
71
|
+
|
|
63
72
|
## Modules
|
|
64
73
|
|
|
65
74
|
### `@connextar/house/errors`
|
|
@@ -712,11 +721,462 @@ rate was checked. **Where rates are kept is the app's decision**: AltEd and
|
|
|
712
721
|
PharmaLine keep a dated table an operator can correct; a smaller app can price
|
|
713
722
|
straight from the defaults. An unknown model is unpriced (`null`), never free.
|
|
714
723
|
|
|
724
|
+
**In the browser, import `@connextar/house/ai/config`.** `/ai` carries both
|
|
725
|
+
SDKs, which no client bundle should. `/ai/config` is everything that never calls
|
|
726
|
+
a provider — `PROVIDER_LABELS` and the other provider names, the job-table and
|
|
727
|
+
status types, `AI_IMAGE_TYPES` / `isAiImageType`, and the pricing arithmetic
|
|
728
|
+
and defaults — and imports no package at all (a test follows the emitted
|
|
729
|
+
imports to hold it to that). `/ai` re-exports all of it, so server code needs
|
|
730
|
+
only the one import:
|
|
731
|
+
|
|
732
|
+
```tsx
|
|
733
|
+
"use client";
|
|
734
|
+
import { PROVIDER_LABELS, isAiImageType } from "@connextar/house/ai/config";
|
|
735
|
+
```
|
|
736
|
+
|
|
715
737
|
`@anthropic-ai/sdk`, `openai` and `zod` are optional peer dependencies — only an
|
|
716
|
-
app that imports `/ai` needs them. The SDKs refuse to run in a browser-like
|
|
738
|
+
app that imports `/ai` needs them; `/ai/config` needs none. The SDKs refuse to run in a browser-like
|
|
717
739
|
environment, so a Vitest suite that calls the harness needs
|
|
718
740
|
`// @vitest-environment node` on the file.
|
|
719
741
|
|
|
742
|
+
### `@connextar/house/jobs`, `/jobs/react`
|
|
743
|
+
|
|
744
|
+
Scheduled jobs: declared once, run once per window, switched off and on by
|
|
745
|
+
name, and honest about their health. `/jobs` is server code — the registry, the
|
|
746
|
+
dispatcher, Run now, the switch, health, the words for counts and the tick
|
|
747
|
+
route's secret check. `/jobs/react` is the health screen's list, and re-exports
|
|
748
|
+
the wording and types a client component needs, so nothing in the browser ever
|
|
749
|
+
imports `/jobs` (which reaches `node:crypto`; `src/jobs/entry-points.test.ts`
|
|
750
|
+
holds both sides of that line against the built output).
|
|
751
|
+
|
|
752
|
+
AltEd and Quickie each had this, from one seed. What each proved, and what each
|
|
753
|
+
got wrong:
|
|
754
|
+
|
|
755
|
+
| Concern | AltEd | Quickie | Here |
|
|
756
|
+
| ----------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
757
|
+
| Registry | `JobSpec`: name, label, `when` in words, scope, `maxAgeMs`, `attempt` + `windowKey`, `run` | The same, without `when` | `defineJob`: id, label, cadence, one `window`, threshold, timeout, switchable, Run now rule, counts |
|
|
758
|
+
| Window and idempotency | Unique `(job, windowKey)` claim; tracker keyed on the UK date after a twice-sent day | The same claim; hour, day, six-hour and month keys | The same claim, one function instead of two; calendar helpers take a time zone and keep both apps' keys |
|
|
759
|
+
| Missed window | Weekly digest only on Monday — a failed Monday loses the week | Monthly report only on the 1st — a failed 1st loses the month | `weekly` and `monthly` stay open (after `from` each day) until the period ends |
|
|
760
|
+
| One job failing | Isolated — but a failed claim or ledger write threw out of the loop | The same | Isolated, store failures included; each job's result says what happened |
|
|
761
|
+
| Ledger | `CronRun`: job, window, start, finish, ok, counts, error | Identical table | The same eight columns plus `tenant_id` and `triggered_by`; the app owns it behind `JobStore` |
|
|
762
|
+
| Health states | Paused · Never run · Errored · Stale · Healthy | Never run · Errored · Stale · Healthy | Paused · Never run · **Running** · Failed · Stale · Healthy — derived, never stored |
|
|
763
|
+
| A run in progress | Shown as **Errored** (`ok` is false until it finishes); one cut off stays so, with no reason | The same | Running inside its timeout; past it, Failed — "never finished, probably cut off" |
|
|
764
|
+
| Staleness | Last OK older than `maxAgeMs` (strictly) | The same — and a tenant's Run now reset it for everyone | Last successful _whole_ run older than the threshold; healthy exactly at it |
|
|
765
|
+
| Pause / resume | Per job, who and when, audited; harvest's switch shared with its own screen | None | Per job, who and when; platform authority; audit in the adapter's transaction; tick says it skipped |
|
|
766
|
+
| Run now | `runJobNow` exists but has no caller, and every job is `global`, so it would refuse them all | Owner-only, scoped to the caller's organisation; no cool-down, and `force` skips reminder throttles | `tenant` or `platform` scope, refused outside it, while paused, while running, and within a cool-down |
|
|
767
|
+
| Jobs on their own clock | Harvest (10 min) and imports (1 min): switchable, but record no runs, so no health | — | `schedule: "own"` + `runOwn`: the switch and the ledger, with or without a window |
|
|
768
|
+
| Errors | Message stored; shown to administrators | Message stored; shown to **every** organisation's owner, whoever's data it came from | Message stored; a tenant viewer sees whether a sweep failed, not its words or its numbers |
|
|
769
|
+
| `CRON_SECRET` | Bearer or `?secret=`; `timingSafeEqual` after a length check | Bearer (prefix optional) or `?secret=`; SHA-256 digests; dev secret refused in production | Bearer required; digests; dev secret refused; `?secret=` only when a route asks |
|
|
770
|
+
| Counts | `Record<string, number>` printed as keys; `totalOutstanding` is pounds shown as a count | The same, in the card and the Run now toast ("3 remindersSent") | Declared: words (singular and plural) and a kind — count, money (currency and unit) or duration |
|
|
771
|
+
| Legacy routes | — | Three per-job routes still live, bypassing the ledger; `?force=1` sweeps every tenant | One tick route; nothing else runs a tick job |
|
|
772
|
+
|
|
773
|
+
**Declaring the jobs** — once, beside the store adapter, server-only:
|
|
774
|
+
|
|
775
|
+
```ts
|
|
776
|
+
// lib/jobs/index.ts
|
|
777
|
+
import "server-only";
|
|
778
|
+
import { createScheduler, daily, defineJob, defineJobs, hourly } from "@connextar/house/jobs";
|
|
779
|
+
|
|
780
|
+
import { jobStore } from "./store";
|
|
781
|
+
|
|
782
|
+
const HOUR = 60 * 60 * 1000;
|
|
783
|
+
|
|
784
|
+
export const jobs = defineJobs([
|
|
785
|
+
defineJob({
|
|
786
|
+
id: "billing-housekeeping", // stored: never rename one
|
|
787
|
+
label: "Billing & payment reminders",
|
|
788
|
+
cadence: "Daily",
|
|
789
|
+
window: daily(),
|
|
790
|
+
staleAfterMs: 26 * HOUR,
|
|
791
|
+
counts: {
|
|
792
|
+
chargesRaised: { label: { one: "charge raised", other: "charges raised" } },
|
|
793
|
+
sent: { label: { one: "family reminded", other: "families reminded" } },
|
|
794
|
+
skipped: { label: "already reminded this week" },
|
|
795
|
+
totalOutstanding: { label: "outstanding", kind: "money", currency: "GBP", unit: "major" },
|
|
796
|
+
},
|
|
797
|
+
run: async ({ now }) => {
|
|
798
|
+
const { charged } = await rollUpCharges(now);
|
|
799
|
+
return { chargesRaised: charged, ...(await sendPaymentReminders(now)) };
|
|
800
|
+
},
|
|
801
|
+
}),
|
|
802
|
+
defineJob({
|
|
803
|
+
id: "payment-reminders",
|
|
804
|
+
label: "Payment reminders",
|
|
805
|
+
cadence: "Daily, from 09:00",
|
|
806
|
+
window: daily({ from: 9 }),
|
|
807
|
+
staleAfterMs: 26 * HOUR,
|
|
808
|
+
runNow: { scope: "tenant" }, // an owner sweeps their own organisation, and nobody else's
|
|
809
|
+
counts: { remindersSent: { label: { one: "reminder sent", other: "reminders sent" } } },
|
|
810
|
+
// `tenantId` is null for the schedule's sweep and always set for an owner's Run now.
|
|
811
|
+
run: async ({ now, tenantId }) => {
|
|
812
|
+
const { remindersSent } = await sendPaymentReminders(now, { organizationId: tenantId ?? undefined });
|
|
813
|
+
return { remindersSent };
|
|
814
|
+
},
|
|
815
|
+
}),
|
|
816
|
+
defineJob({
|
|
817
|
+
id: "harvest",
|
|
818
|
+
label: "Harvest",
|
|
819
|
+
cadence: "Every 10 minutes",
|
|
820
|
+
schedule: "own", // its own route and clock; the tick never runs it
|
|
821
|
+
staleAfterMs: HOUR,
|
|
822
|
+
counts: { sites: { label: { one: "site read", other: "sites read" } } },
|
|
823
|
+
run: async () => ({ sites: (await runHarvest({ budgetMs: 150_000, maxSites: 4 })).sites.length }),
|
|
824
|
+
}),
|
|
825
|
+
]);
|
|
826
|
+
|
|
827
|
+
export const scheduler = createScheduler({ jobs, store: jobStore });
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
`defineJobs` checks the list when the module loads and throws every problem at
|
|
831
|
+
once (`jobDefinitionProblems` returns them as sentences, for a test). `defineJob`
|
|
832
|
+
holds `run` to its counts at compile time: every declared count must be
|
|
833
|
+
reported, and nothing undeclared may be. At run time an undeclared key is dropped
|
|
834
|
+
before it is stored, and one in an old row is not shown — there is no fallback
|
|
835
|
+
that could print a variable name.
|
|
836
|
+
|
|
837
|
+
**The routes** — each app answers in its own envelope; the module returns
|
|
838
|
+
decisions, not `Response`s:
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
// app/api/cron/tick/route.ts — the one entry in vercel.json's crons
|
|
842
|
+
export async function GET(request: Request) {
|
|
843
|
+
const auth = checkCronRequest(request, { publicSecrets: ["dev-cron-secret"] });
|
|
844
|
+
if (!auth.ok) return apiError(auth.message, auth.status);
|
|
845
|
+
const results = await scheduler.tick();
|
|
846
|
+
return apiSuccess({ results }, summariseTick(results)); // "2 ran, 1 failed, 1 paused, 5 not due"
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// app/api/cron/harvest/route.ts — a job on its own clock
|
|
850
|
+
const result = await scheduler.runOwn("harvest");
|
|
851
|
+
|
|
852
|
+
// app/api/cron/run/route.ts — Run now, behind the session
|
|
853
|
+
const outcome = await scheduler.runNow(body.job, {
|
|
854
|
+
actor: { id: user.id, name: user.name },
|
|
855
|
+
platform: user.platformAdmin,
|
|
856
|
+
tenantId: user.organizationId,
|
|
857
|
+
});
|
|
858
|
+
if (outcome.status === "refused") return apiError(outcome.message, refusalStatus(outcome.reason));
|
|
859
|
+
if (outcome.status === "failed") return apiError(outcome.message, 500);
|
|
860
|
+
return apiSuccess(outcome, outcome.message); // "Ran Payment reminders — 3 reminders sent."
|
|
861
|
+
|
|
862
|
+
// app/api/admin/cron/route.ts — the switch
|
|
863
|
+
const outcome = await scheduler.setPaused(body.job, body.action === "pause", {
|
|
864
|
+
actor: { id: admin.id, name: admin.name },
|
|
865
|
+
platform: true,
|
|
866
|
+
});
|
|
867
|
+
if (outcome.status === "refused") return apiError(outcome.message, refusalStatus(outcome.reason));
|
|
868
|
+
return apiSuccess(outcome, outcome.message);
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**The screen** — the server works out the view for whoever is looking; the list
|
|
872
|
+
draws it inside the app's own card:
|
|
873
|
+
|
|
874
|
+
```tsx
|
|
875
|
+
// app/dashboard/settings/page.tsx (server)
|
|
876
|
+
const jobs = await scheduler.views({ platform: isAdmin, tenantId: user.organizationId });
|
|
877
|
+
|
|
878
|
+
// components/settings/automation-card.tsx
|
|
879
|
+
("use client");
|
|
880
|
+
import { JobHealthList } from "@connextar/house/jobs/react";
|
|
881
|
+
|
|
882
|
+
<JobHealthList
|
|
883
|
+
jobs={jobs}
|
|
884
|
+
onSwitch={async (job, on) => {
|
|
885
|
+
await commitApi("/api/admin/cron", { job, action: on ? "resume" : "pause" });
|
|
886
|
+
}}
|
|
887
|
+
onRunNow={(job) => commitApi("/api/cron/run", { job })}
|
|
888
|
+
onSettled={router.refresh}
|
|
889
|
+
extra={(job) => (job.id === "harvest" ? <Link href={routes.harvest}>What it searches next</Link> : null)}
|
|
890
|
+
/>;
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
`onSwitch` and `onRunNow` throw to refuse — the ux layer's one failure
|
|
894
|
+
contract. The switch is `useOptimisticValue`: it moves on the click, the badge
|
|
895
|
+
moves with it (to the job's health as if on, which the view carries), and a
|
|
896
|
+
refusal puts both back and reports the reason through the
|
|
897
|
+
`ActionFeedbackProvider`. No other row is disabled while one saves. Run now is
|
|
898
|
+
`useKeyedAction`: that row's button is busy (`data-busy`), the route's message is
|
|
899
|
+
said, and the screen refreshes whether the run worked or not, since a failure is
|
|
900
|
+
on the ledger too. Dates are drawn in `timeZone` (default `Europe/London`),
|
|
901
|
+
never the runtime's own — the server draws the list in UTC and the browser
|
|
902
|
+
again in BST, and two times in one element is a hydration error. `classNames`
|
|
903
|
+
replaces any part's look.
|
|
904
|
+
|
|
905
|
+
The rules it keeps:
|
|
906
|
+
|
|
907
|
+
- **One run per job per window.** The window is `(now) => key | null`; the
|
|
908
|
+
store's unique index on `(job, windowKey)` is the idempotency. A second tick
|
|
909
|
+
in the window finds the key taken and says `already-ran`. `hourly`,
|
|
910
|
+
`everyHours`, `daily`, `weekly` and `monthly` produce the keys both apps
|
|
911
|
+
already store, so moving onto them runs nothing twice.
|
|
912
|
+
- **One failure stops nothing else.** A job that throws, a store that cannot
|
|
913
|
+
claim, a window function with a bug — each is that job's `failed` result and
|
|
914
|
+
its error on the ledger, and the next job runs. A run whose result could not
|
|
915
|
+
be written says so rather than calling the work failed.
|
|
916
|
+
- **A paused job's tick skips it and says so** ("switched off by AltEd Admin on
|
|
917
|
+
29 Sept 2026; not started"). Its window is not claimed, so switched back on
|
|
918
|
+
inside the same window it still runs. If the switches cannot be read at all, no
|
|
919
|
+
switchable job starts: a switch nobody can read must not send a job out
|
|
920
|
+
spending. `pausedFromStored` reads a stored value AltEd's way — a row that
|
|
921
|
+
exists but does not parse is _paused_.
|
|
922
|
+
- **Switching takes platform authority** — it stops a job for every tenant — and
|
|
923
|
+
records who and when. `JobStore.writeSwitch` is where the audit entry goes,
|
|
924
|
+
in the same transaction as the switch. A switch to where it already is writes
|
|
925
|
+
nothing.
|
|
926
|
+
- **Health is derived, in this order:** paused · never run · running · failed ·
|
|
927
|
+
stale · healthy. A run past its timeout (default 15 minutes) and still
|
|
928
|
+
unfinished is failed, and says it never finished. Stale means no successful
|
|
929
|
+
run over every tenant for longer than the threshold — exactly at the threshold
|
|
930
|
+
is still healthy — and a job with `staleAfterMs: null` (one that runs only
|
|
931
|
+
when there is work) is never stale.
|
|
932
|
+
- **Run now is scoped and fenced.** `tenant` sweeps the caller's own tenant and
|
|
933
|
+
needs one; `platform` sweeps everything and needs platform authority. It is
|
|
934
|
+
refused while the job is off, while a run it would overlap is in flight (the
|
|
935
|
+
schedule's sweep covers the caller's tenant too), and within its cool-down
|
|
936
|
+
(default ten minutes, per job and scope) — a manual run sets `force`, so a
|
|
937
|
+
second press would otherwise send the whole batch again. `force` is for a
|
|
938
|
+
job's hour gates; a per-recipient throttle should stay on.
|
|
939
|
+
- **A tenant sees their own.** To a tenant viewer, a sweep over every tenant
|
|
940
|
+
shows whether it ran and when, but not its numbers (which are everyone's) or
|
|
941
|
+
its error (which may name another organisation's data). Their own Run now
|
|
942
|
+
shows in full. Other tenants' runs never appear, and none of them resets the
|
|
943
|
+
job's staleness.
|
|
944
|
+
- **Every count is declared.** A label (the words after the number; singular and
|
|
945
|
+
plural where they differ) and a kind: `count`, `money` (a currency _and_ a
|
|
946
|
+
unit — `major` for AltEd's pounds, `minor` for pence — because a default would
|
|
947
|
+
be out by a hundred for one of them), or `duration`. Money is formatted by
|
|
948
|
+
`Intl` in its currency, which also rounds away a summed decimal's float noise.
|
|
949
|
+
- **The tick's door** (`checkCronRequest`): an unset `CRON_SECRET` refuses with
|
|
950
|
+
503; the secret is compared as SHA-256 digests with `timingSafeEqual`; the
|
|
951
|
+
Bearer scheme is required; `publicSecrets` (the committed development value)
|
|
952
|
+
are refused in production; `?secret=` is accepted only with
|
|
953
|
+
`allowQuery: true`, because an address lands in request logs.
|
|
954
|
+
|
|
955
|
+
**Storage.** The package owns no schema. The app implements `JobStore` — claim,
|
|
956
|
+
finish, latest run, read switches, write a switch — over its own tables. This is
|
|
957
|
+
the reference model; both apps' existing `cron_run` tables are it minus two
|
|
958
|
+
nullable columns, so every row they hold maps across as it is:
|
|
959
|
+
|
|
960
|
+
```prisma
|
|
961
|
+
/// One row per job per window: the claim that makes a second tick skip, and the result.
|
|
962
|
+
model CronRun {
|
|
963
|
+
id String @id @default(cuid())
|
|
964
|
+
job String @db.VarChar(60)
|
|
965
|
+
/// The window, e.g. "2026-09-30", or "manual:tenant:<id>:<time>" for a Run now.
|
|
966
|
+
windowKey String @map("window_key") @db.VarChar(120)
|
|
967
|
+
/// Null: a run over every tenant. Otherwise the one tenant a Run now swept.
|
|
968
|
+
tenantId String? @map("tenant_id") @db.VarChar(64)
|
|
969
|
+
/// Who pressed Run now, by name. Null for the schedule.
|
|
970
|
+
triggeredBy String? @map("triggered_by") @db.VarChar(120)
|
|
971
|
+
startedAt DateTime @default(now()) @map("started_at")
|
|
972
|
+
finishedAt DateTime? @map("finished_at")
|
|
973
|
+
ok Boolean @default(false)
|
|
974
|
+
/// The declared counts, e.g. { "remindersSent": 3 }.
|
|
975
|
+
counts Json?
|
|
976
|
+
error String? @db.Text
|
|
977
|
+
|
|
978
|
+
@@unique([job, windowKey])
|
|
979
|
+
@@index([job, startedAt])
|
|
980
|
+
@@index([job, tenantId, startedAt])
|
|
981
|
+
@@map("cron_run")
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/// A job switched off or on — the latest change. No row means on.
|
|
985
|
+
model CronSwitch {
|
|
986
|
+
job String @id @db.VarChar(60)
|
|
987
|
+
paused Boolean
|
|
988
|
+
switchedAt DateTime @map("switched_at")
|
|
989
|
+
switchedById String? @map("switched_by_id")
|
|
990
|
+
switchedByName String @map("switched_by_name") @db.VarChar(120)
|
|
991
|
+
|
|
992
|
+
@@map("cron_switch")
|
|
993
|
+
}
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
For an app that already has `cron_run`:
|
|
997
|
+
|
|
998
|
+
```sql
|
|
999
|
+
ALTER TABLE "cron_run" ADD COLUMN "tenant_id" VARCHAR(64), ADD COLUMN "triggered_by" VARCHAR(120);
|
|
1000
|
+
CREATE INDEX "cron_run_job_tenant_id_started_at_idx" ON "cron_run"("job", "tenant_id", "started_at");
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
```ts
|
|
1004
|
+
// lib/jobs/store.ts
|
|
1005
|
+
import "server-only";
|
|
1006
|
+
import type { JobStore } from "@connextar/house/jobs";
|
|
1007
|
+
|
|
1008
|
+
import { Prisma } from "@/lib/db/generated/client";
|
|
1009
|
+
import { prisma } from "@lib/db/prisma";
|
|
1010
|
+
|
|
1011
|
+
export const jobStore: JobStore = {
|
|
1012
|
+
async claimRun({ job, windowKey, tenantId, by, startedAt }) {
|
|
1013
|
+
try {
|
|
1014
|
+
const row = await prisma.cronRun.create({ data: { job, windowKey, tenantId, triggeredBy: by, startedAt } });
|
|
1015
|
+
return row.id;
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
// The unique index refusing a second claim is the answer "already ran", not a failure.
|
|
1018
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") return null;
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
async finishRun(id, { finishedAt, ok, counts, error }) {
|
|
1023
|
+
await prisma.cronRun.update({ where: { id }, data: { finishedAt, ok, error, ...(counts ? { counts } : {}) } });
|
|
1024
|
+
},
|
|
1025
|
+
async latestRun({ job, tenants, ok, manualOnly }) {
|
|
1026
|
+
const row = await prisma.cronRun.findFirst({
|
|
1027
|
+
where: {
|
|
1028
|
+
job,
|
|
1029
|
+
OR: tenants.map((tenantId) => ({ tenantId })), // `{ tenantId: null }` matches the whole-tenant runs
|
|
1030
|
+
...(ok ? { ok: true } : {}),
|
|
1031
|
+
...(manualOnly ? { windowKey: { startsWith: "manual:" } } : {}),
|
|
1032
|
+
},
|
|
1033
|
+
orderBy: { startedAt: "desc" },
|
|
1034
|
+
});
|
|
1035
|
+
return row && { ...row, by: row.triggeredBy };
|
|
1036
|
+
},
|
|
1037
|
+
async readSwitches() {
|
|
1038
|
+
const rows = await prisma.cronSwitch.findMany();
|
|
1039
|
+
return new Map(rows.map((r) => [r.job, { paused: r.paused, at: r.switchedAt, by: r.switchedByName }]));
|
|
1040
|
+
},
|
|
1041
|
+
async writeSwitch({ job, label, paused, at, by }) {
|
|
1042
|
+
await prisma.$transaction([
|
|
1043
|
+
prisma.cronSwitch.upsert({
|
|
1044
|
+
where: { job },
|
|
1045
|
+
create: { job, paused, switchedAt: at, switchedById: by.id, switchedByName: by.name },
|
|
1046
|
+
update: { paused, switchedAt: at, switchedById: by.id, switchedByName: by.name },
|
|
1047
|
+
}),
|
|
1048
|
+
prisma.auditLog.create({
|
|
1049
|
+
data: {
|
|
1050
|
+
actorId: by.id,
|
|
1051
|
+
actorName: by.name,
|
|
1052
|
+
action: paused ? "cron.paused" : "cron.resumed",
|
|
1053
|
+
entityType: "cron_job",
|
|
1054
|
+
entityId: job,
|
|
1055
|
+
summary: `${paused ? "Paused" : "Resumed"} ${label}`,
|
|
1056
|
+
},
|
|
1057
|
+
}),
|
|
1058
|
+
]);
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
`createMemoryJobStore()` is the same contract in memory, unique index included,
|
|
1064
|
+
for the app's own tests of its binding. Prune the ledger in the app's
|
|
1065
|
+
housekeeping job like any other growing table, keeping more than the longest
|
|
1066
|
+
threshold — health reads the latest run and the latest success per job, so an
|
|
1067
|
+
older row is history, not state.
|
|
1068
|
+
|
|
1069
|
+
**What should not be a scheduled job.**
|
|
1070
|
+
|
|
1071
|
+
- **Anything a person is waiting for.** A report they asked for, a model call,
|
|
1072
|
+
an import step with its page open — do it in the request or straight after it.
|
|
1073
|
+
A tick is up to an hour away, and a spinner cannot wait that long.
|
|
1074
|
+
- **A reaction to an event.** A payment landing, a sign-up, a status change:
|
|
1075
|
+
handle it where it happens. A sweep that looks for "anything that changed" is
|
|
1076
|
+
a webhook written late and paid for on every tick.
|
|
1077
|
+
- **A job per record, or per tenant.** A reminder due seven days after an
|
|
1078
|
+
invoice is one daily sweep over "due today", not a job per invoice. The
|
|
1079
|
+
registry is a short list a person reads on one screen.
|
|
1080
|
+
- **Anything that must happen at a moment.** Windows are as coarse as the tick —
|
|
1081
|
+
AltEd's runs hourly, Quickie's every fifteen minutes. "At 09:00 sharp" is a
|
|
1082
|
+
queue with a due time, not a window.
|
|
1083
|
+
- **Work longer than one request.** A job that can outlive the platform's time
|
|
1084
|
+
limit is cut off and reads as never finished. Give it a budget and let the
|
|
1085
|
+
next window continue — AltEd's harvest stops starting new sites after two and
|
|
1086
|
+
a half minutes — or give it its own runner with `schedule: "own"`.
|
|
1087
|
+
- **Housekeeping a query already does.** An expired row the reads already ignore
|
|
1088
|
+
does not need deleting on a schedule unless something promises it will be
|
|
1089
|
+
gone — a retention period in a privacy policy does, and is then a job.
|
|
1090
|
+
|
|
1091
|
+
**Adopting it — AltEd.** Nothing changes in what runs or when:
|
|
1092
|
+
|
|
1093
|
+
1. **Jobs.** Each `JobSpec` becomes a `defineJob`: `name` → `id` (unchanged, so
|
|
1094
|
+
the ledger and the `cron:<name>` switches still match), `when` → `cadence`,
|
|
1095
|
+
`maxAgeMs` → `staleAfterMs`, `attempt` + `windowKey` → one `window` with the
|
|
1096
|
+
same key: `daily()` for billing and housekeeping (the UTC date),
|
|
1097
|
+
`daily({ timeZone: "Europe/London" })` for the tracker and class sessions
|
|
1098
|
+
(`todayKey`), `daily({ from: 7, timeZone: "Europe/London" })` for the admin
|
|
1099
|
+
summary (its London date equals the UTC date from 07:00). The hourly
|
|
1100
|
+
moderation sweep's key is `2026-09-30-14` where `hourly()` gives
|
|
1101
|
+
`2026-09-30T14` — keep a one-line custom window, or accept one extra sweep in
|
|
1102
|
+
the cutover hour (it re-reads flagged messages, which is harmless). The weekly
|
|
1103
|
+
digest keeps ``(now) => (isMondayMorning(now) ? `digest-${todayKey(now)}` : null)``,
|
|
1104
|
+
or moves to `weekly({ from: 7, timeZone: "Europe/London" })` — which also
|
|
1105
|
+
catches up a missed Monday — cut over on any day but a Monday.
|
|
1106
|
+
2. **Counts.** Declare every key each job already returns, under the same name,
|
|
1107
|
+
so every stored row reads: `totalOutstanding` as
|
|
1108
|
+
`{ kind: "money", currency: "GBP", unit: "major" }` (it is pounds, summed from
|
|
1109
|
+
the decimal column), the rest as counts with their words.
|
|
1110
|
+
3. **Run now.** `scope: "global"` becomes `runNow: false` — what it effectively
|
|
1111
|
+
was, since `runJobNow` never had a caller. Where an administrator should be
|
|
1112
|
+
able to run one (the admin summary, class sessions, the tracker), give it
|
|
1113
|
+
`runNow: { scope: "platform" }` and add the run route; leave billing
|
|
1114
|
+
scheduled-only.
|
|
1115
|
+
4. **Own schedules.** The harvest and the import runner become
|
|
1116
|
+
`schedule: "own"` and their routes call `scheduler.runOwn(...)` after the
|
|
1117
|
+
secret check — so both write runs and get health for the first time. The
|
|
1118
|
+
harvest takes `staleAfterMs: HOUR`; imports take `null` (idle is not stale),
|
|
1119
|
+
keep their cached nothing-to-do answer ahead of `runOwn`, and drop their own
|
|
1120
|
+
`isJobPaused("imports")`.
|
|
1121
|
+
5. **Switches.** Keep `PlatformSetting`: `readSwitches` merges
|
|
1122
|
+
`readJobSettings()` with `readHarvestSetting()` under `harvest`, exactly as
|
|
1123
|
+
`jobSwitches()` does now, and `writeSwitch` calls
|
|
1124
|
+
`writeHarvestSetting`/`writeJobSetting` and `recordJobSwitched`. The
|
|
1125
|
+
round-23 rule is `pausedFromStored`.
|
|
1126
|
+
6. **Ledger.** Run the two-column SQL above. No backfill: there are no Run now
|
|
1127
|
+
rows, because nothing ever called it
|
|
1128
|
+
(`SELECT count(*) FROM cron_run WHERE window_key LIKE 'manual:%'` should say
|
|
1129
|
+
0 before and after).
|
|
1130
|
+
7. **Routes and screen.** The tick, harvest and imports routes use
|
|
1131
|
+
`checkCronRequest` — `?secret=` now needs `allowQuery: true`, and Vercel's
|
|
1132
|
+
cron never sends it. `/api/admin/cron` calls `setPaused`. `AutomationCard`
|
|
1133
|
+
keeps its card and description and renders `JobHealthList` with
|
|
1134
|
+
`scheduler.views({ platform: true })`, the harvest link as `extra`.
|
|
1135
|
+
`lib/repos/cron.repo.ts`, `lib/server/cron-auth.ts` and the dispatcher half of
|
|
1136
|
+
`lib/services/cron.service.ts` go; `cron-switch.db.test.ts` stays as the test
|
|
1137
|
+
of the binding.
|
|
1138
|
+
|
|
1139
|
+
**Adopting it — Quickie.**
|
|
1140
|
+
|
|
1141
|
+
1. **Jobs.** Every key it stores is one a helper makes, so the cutover runs
|
|
1142
|
+
nothing twice: `hourly()` for the follow-up digest and review requests;
|
|
1143
|
+
`daily({ from: 9 })`, `daily({ from: 17 })` and `daily({ from: 18 })` for
|
|
1144
|
+
payment reminders, booking reminders and budget alerts; `daily()` for bank
|
|
1145
|
+
consents and housekeeping; `everyHours(6)` for the bank feed;
|
|
1146
|
+
`(now) => (now.getUTCDay() === 1 ? hourly()(now) : null)` for the finance
|
|
1147
|
+
digest; `monthly()` for the monthly report, which now catches up a failed 1st
|
|
1148
|
+
on the 2nd. `scope: "tenant"` → `runNow: { scope: "tenant" }`; housekeeping
|
|
1149
|
+
→ `runNow: false`.
|
|
1150
|
+
2. **Counts.** Declare each key (`orgsEmailed`, `emailsSent`,
|
|
1151
|
+
`invoicesConsidered`, `remindersSent`, …) with its words; the Run now toast
|
|
1152
|
+
and the guide stop saying "3 remindersSent".
|
|
1153
|
+
3. **Ledger.** Run the two-column SQL, then give the old Run now rows a tenant:
|
|
1154
|
+
`UPDATE "cron_run" SET "tenant_id" = 'unrecorded' WHERE "window_key" LIKE 'manual:%';`
|
|
1155
|
+
Every one was a single organisation's run, but which was never recorded.
|
|
1156
|
+
Marked so, none of them counts as a whole run for staleness and none appears
|
|
1157
|
+
on any owner's screen; nothing is deleted.
|
|
1158
|
+
4. **Switches.** Add `CronSwitch`, and write the audit entry in `writeSwitch`
|
|
1159
|
+
with `organizationId: null` — a switch is a platform-level action, which
|
|
1160
|
+
`AuditLog` already allows. The switch belongs to platform operators
|
|
1161
|
+
(`User.platformAdmin`): an Automation page in the admin panel renders
|
|
1162
|
+
`scheduler.views({ platform: true })` with `onSwitch`.
|
|
1163
|
+
5. **Owners.** Settings › Automation health renders
|
|
1164
|
+
`scheduler.views({ platform: false, tenantId: user.organizationId })` with
|
|
1165
|
+
`onRunNow` and no switch. `/api/cron/run` keeps its owner check and the bank
|
|
1166
|
+
feed's rate limit, then calls `runNow` with `platform: false` and the
|
|
1167
|
+
organisation. Owners now see their own runs in full and the platform's sweeps
|
|
1168
|
+
without other organisations' numbers or errors.
|
|
1169
|
+
6. **Remove the three per-job routes** (`payment-reminders`, `finance-digest`,
|
|
1170
|
+
`follow-up-digest`). Nothing schedules them, and anyone holding the secret
|
|
1171
|
+
could run a sweep outside the ledger and its window — `?force=1` over every
|
|
1172
|
+
organisation, ignoring the hour and the throttle.
|
|
1173
|
+
7. **The tick** calls `checkCronRequest(request, { publicSecrets: ["dev-cron-secret"] })`
|
|
1174
|
+
and `scheduler.tick()`. Payment reminders' Run now should stop passing
|
|
1175
|
+
`ignoreThrottle` — the cool-down stops a double press, but a throttle per
|
|
1176
|
+
invoice is what stops a customer being chased twice in a day.
|
|
1177
|
+
8. **The guide** (`docs/guides/automation.md`): Errored is now Failed, a run in
|
|
1178
|
+
progress says Running, and the counts read as words.
|
|
1179
|
+
|
|
720
1180
|
### Styles — `@connextar/house/house.css`
|
|
721
1181
|
|
|
722
1182
|
What the components need that utility classes cannot say: rich text, the editor's
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ai/config` — everything in `ai` that never calls a provider.
|
|
3
|
+
*
|
|
4
|
+
* Provider names, the job table, status shapes, image types and prices: what a
|
|
5
|
+
* settings screen, an upload form or a price table in the browser needs. Its own
|
|
6
|
+
* entry point because `@connextar/house/ai` carries both provider SDKs, which no
|
|
7
|
+
* client bundle should — so AltEd and PharmaLine each kept a copy of the labels
|
|
8
|
+
* and image types for their client components instead. `package-exports.test.ts`
|
|
9
|
+
* fails if anything reachable from here imports an SDK or Zod.
|
|
10
|
+
*/
|
|
11
|
+
export { AI_PROVIDERS, PROVIDER_KEY_VARS, PROVIDER_LABELS, aiStatus, chooseProvider, configuredProviders, modelFor, publicAiStatus, resolveAi, type AiEnv, type AiModelSpec, type AiProviderName, type AiStatus, type AiTaskSpec, type AiTaskTable, type ProviderChoice, type PublicAiStatus, type ResolvedAi, } from "./config.js";
|
|
12
|
+
export { AI_IMAGE_TYPES, isAiImageType, type AiImage, type AiImageType } from "./images.js";
|
|
13
|
+
export { DEFAULT_PRICES, costUsd, findPrice, modelFamily, type ModelPrice, type PriceRates, type TokenCounts, } from "./pricing.js";
|
|
14
|
+
//# sourceMappingURL=config-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-index.d.ts","sourceRoot":"","sources":["../../src/ai/config-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,cAAc,EACd,SAAS,EACT,KAAK,KAAK,EACV,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EACL,cAAc,EACd,OAAO,EACP,SAAS,EACT,WAAW,EACX,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ai/config` — everything in `ai` that never calls a provider.
|
|
3
|
+
*
|
|
4
|
+
* Provider names, the job table, status shapes, image types and prices: what a
|
|
5
|
+
* settings screen, an upload form or a price table in the browser needs. Its own
|
|
6
|
+
* entry point because `@connextar/house/ai` carries both provider SDKs, which no
|
|
7
|
+
* client bundle should — so AltEd and PharmaLine each kept a copy of the labels
|
|
8
|
+
* and image types for their client components instead. `package-exports.test.ts`
|
|
9
|
+
* fails if anything reachable from here imports an SDK or Zod.
|
|
10
|
+
*/
|
|
11
|
+
export { AI_PROVIDERS, PROVIDER_KEY_VARS, PROVIDER_LABELS, aiStatus, chooseProvider, configuredProviders, modelFor, publicAiStatus, resolveAi, } from "./config.js";
|
|
12
|
+
export { AI_IMAGE_TYPES, isAiImageType } from "./images.js";
|
|
13
|
+
export { DEFAULT_PRICES, costUsd, findPrice, modelFamily, } from "./pricing.js";
|
|
14
|
+
//# sourceMappingURL=config-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-index.js","sourceRoot":"","sources":["../../src/ai/config-index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,QAAQ,EACR,cAAc,EACd,SAAS,GAUV,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAkC,MAAM,aAAa,CAAC;AAC5F,OAAO,EACL,cAAc,EACd,OAAO,EACP,SAAS,EACT,WAAW,GAIZ,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pictures a structured call can carry. Their own module, free of either SDK,
|
|
3
|
+
* so an upload form can check a file's type from `@connextar/house/ai/config`.
|
|
4
|
+
*/
|
|
5
|
+
/** The image types both providers accept inline. */
|
|
6
|
+
export declare const AI_IMAGE_TYPES: readonly ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
7
|
+
export type AiImageType = (typeof AI_IMAGE_TYPES)[number];
|
|
8
|
+
export declare function isAiImageType(value: string): value is AiImageType;
|
|
9
|
+
export interface AiImage {
|
|
10
|
+
mediaType: AiImageType;
|
|
11
|
+
/** Base64, no data-URL prefix. */
|
|
12
|
+
base64: string;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=images.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/ai/images.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,oDAAoD;AACpD,eAAO,MAAM,cAAc,iEAAkE,CAAC;AAC9F,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,WAAW,CAEjE;AAED,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,WAAW,CAAC;IACvB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;CAChB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pictures a structured call can carry. Their own module, free of either SDK,
|
|
3
|
+
* so an upload form can check a file's type from `@connextar/house/ai/config`.
|
|
4
|
+
*/
|
|
5
|
+
/** The image types both providers accept inline. */
|
|
6
|
+
export const AI_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
7
|
+
export function isAiImageType(value) {
|
|
8
|
+
return AI_IMAGE_TYPES.includes(value);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=images.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"images.js","sourceRoot":"","sources":["../../src/ai/images.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,oDAAoD;AACpD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,CAAU,CAAC;AAG9F,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAQ,cAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/D,CAAC"}
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ai` — the harness, and everything it is configured with.
|
|
3
|
+
* Server-only in practice: it carries both provider SDKs. A client component
|
|
4
|
+
* imports `@connextar/house/ai/config` instead.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./config-index.js";
|
|
7
|
+
export { AiServiceError, AiUnavailableError, AiUnusableAnswerError, OPENAI_MIN_OUTPUT_TOKENS, createAiHarness, type AiCallResult, type AiHarness, type AiHarnessOptions, type AiStructuredCall, type AiTextCall, } from "./provider.js";
|
|
4
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/ai/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,mBAAmB,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,wBAAwB,EACxB,eAAe,EACf,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,eAAe,CAAC"}
|
package/dist/ai/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `@connextar/house/ai` — the harness, and everything it is configured with.
|
|
3
|
+
* Server-only in practice: it carries both provider SDKs. A client component
|
|
4
|
+
* imports `@connextar/house/ai/config` instead.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./config-index.js";
|
|
7
|
+
export { AiServiceError, AiUnavailableError, AiUnusableAnswerError, OPENAI_MIN_OUTPUT_TOKENS, createAiHarness, } from "./provider.js";
|
|
4
8
|
//# sourceMappingURL=index.js.map
|
package/dist/ai/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ai/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,mBAAmB,CAAC;AAClC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,wBAAwB,EACxB,eAAe,GAMhB,MAAM,eAAe,CAAC"}
|
package/dist/ai/provider.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
import { type AiEnv, type AiProviderName, type AiStatus, type AiTaskTable, type PublicAiStatus, type ResolvedAi } from "./config.js";
|
|
3
|
+
import type { AiImage } from "./images.js";
|
|
3
4
|
/**
|
|
4
5
|
* The one place either provider's SDK is called.
|
|
5
6
|
*
|
|
@@ -66,15 +67,6 @@ export interface AiTextCall<Task extends string> {
|
|
|
66
67
|
*/
|
|
67
68
|
timeoutMs?: number;
|
|
68
69
|
}
|
|
69
|
-
/** The image types both providers accept inline. */
|
|
70
|
-
export declare const AI_IMAGE_TYPES: readonly ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
71
|
-
export type AiImageType = (typeof AI_IMAGE_TYPES)[number];
|
|
72
|
-
export declare function isAiImageType(value: string): value is AiImageType;
|
|
73
|
-
export interface AiImage {
|
|
74
|
-
mediaType: AiImageType;
|
|
75
|
-
/** Base64, no data-URL prefix. */
|
|
76
|
-
base64: string;
|
|
77
|
-
}
|
|
78
70
|
export interface AiStructuredCall<Task extends string, T> extends AiTextCall<Task> {
|
|
79
71
|
schema: z.ZodType<T>;
|
|
80
72
|
/** A name for the schema. OpenAI requires one; Anthropic ignores it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,EAKL,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/ai/provider.ts"],"names":[],"mappings":"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"}
|