@astrale-os/adapter-cloudflare 0.1.7 → 0.1.9
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/dist/assets-pack.d.ts +1 -1
- package/dist/assets-pack.js +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +15 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -1
- package/dist/client.js.map +1 -1
- package/dist/cloudflare.d.ts +15 -3
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +73 -21
- package/dist/cloudflare.js.map +1 -1
- package/dist/codegen/worker.d.ts +26 -6
- package/dist/codegen/worker.d.ts.map +1 -1
- package/dist/codegen/worker.js +70 -54
- package/dist/codegen/worker.js.map +1 -1
- package/dist/codegen/wrangler.d.ts +11 -2
- package/dist/codegen/wrangler.d.ts.map +1 -1
- package/dist/codegen/wrangler.js +11 -5
- package/dist/codegen/wrangler.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/params.d.ts +30 -30
- package/dist/params.d.ts.map +1 -1
- package/dist/parse-output.d.ts +1 -1
- package/dist/parse-output.js +1 -1
- package/package.json +6 -2
- package/src/assets-pack.ts +1 -1
- package/src/build.ts +15 -0
- package/src/client.ts +11 -1
- package/src/cloudflare.ts +77 -23
- package/src/codegen/worker.ts +79 -59
- package/src/codegen/wrangler.ts +15 -5
- package/src/index.ts +6 -3
- package/src/params.ts +32 -31
- package/src/parse-output.ts +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +25 -11
- package/template/.agents/skills/astrale-domain/SKILL.md +60 -32
- package/template/.env.example +8 -0
- package/template/README.md +26 -10
- package/template/astrale.config.ts +22 -29
- package/template/client/README.md +2 -2
- package/template/client/src/styles.css +4 -1
- package/template/client/tsconfig.json +1 -1
- package/template/client/vite.config.ts +3 -3
- package/template/client/vitest.config.ts +1 -1
- package/template/core/keys.ts +28 -0
- package/template/{methods → core}/note.ts +42 -25
- package/template/deps.ts +25 -0
- package/template/domain.ts +33 -0
- package/template/env.ts +11 -0
- package/template/integrations/summary/heuristic.ts +25 -0
- package/template/integrations/summary/http.ts +69 -0
- package/template/integrations/summary/port.ts +21 -0
- package/template/integrations/summary/registry.ts +52 -0
- package/template/package.json +2 -3
- package/template/runtime/index.ts +62 -0
- package/template/schema/index.ts +2 -0
- package/template/schema/note.ts +5 -2
- package/template/tsconfig.json +13 -2
- package/template/views/note.ts +1 -1
- package/dist/astrale.d.ts +0 -27
- package/dist/astrale.d.ts.map +0 -1
- package/dist/astrale.js +0 -212
- package/dist/astrale.js.map +0 -1
- package/src/astrale.ts +0 -244
- package/template/methods/index.ts +0 -66
- package/template/schema/compiled.ts +0 -14
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiled-schema accessors — the ONE place class paths, method paths, and
|
|
3
|
+
* qualified prop keys come from. Pure (schema-derived); never hand-write key
|
|
4
|
+
* strings. `D` (the compiled domain) is the public `schema/compiled` entry,
|
|
5
|
+
* re-exported here so `core/` and `runtime/` read it — plus the named keys —
|
|
6
|
+
* from one place.
|
|
7
|
+
*/
|
|
8
|
+
import { K } from '@astrale-os/kernel-core'
|
|
9
|
+
|
|
10
|
+
import { D } from '../schema/compiled'
|
|
11
|
+
|
|
12
|
+
export { D, K }
|
|
13
|
+
|
|
14
|
+
/** Kernel ops/classes the logic addresses. */
|
|
15
|
+
export const NODE_CREATE = K.Node.createNode.path.method.raw
|
|
16
|
+
export const FOLDER_CLASS = K.Folder.path.class.raw
|
|
17
|
+
export const NAME_KEY = K.Named.name.key
|
|
18
|
+
|
|
19
|
+
/** Domain class paths. */
|
|
20
|
+
export const NOTE_CLASS = D.Note.path.class.raw
|
|
21
|
+
export const REFERENCES_EDGE = D.references.path.class.raw
|
|
22
|
+
|
|
23
|
+
/** Qualified storage keys for Note node props. */
|
|
24
|
+
export const NOTE_KEYS = {
|
|
25
|
+
title: D.Note.title.key,
|
|
26
|
+
body: D.Note.body.key,
|
|
27
|
+
summary: D.Note.summary.key,
|
|
28
|
+
} as const
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Note context — method LOGIC, written once, transport-agnostic.
|
|
2
|
+
* Note context — method LOGIC, written once, transport-agnostic and testable.
|
|
3
3
|
*
|
|
4
4
|
* Each handler touches the kernel ONLY through `kernel.call(...)` (the universal
|
|
5
|
-
* syscalls) plus its `params
|
|
6
|
-
* `path.raw`.
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
5
|
+
* syscalls) plus its `params`, the resolved `Summarizer` PORT, and — for
|
|
6
|
+
* instance methods — the source node's `path.raw`. It never names `fetch`, the
|
|
7
|
+
* worker `env`, or a concrete provider: the port arrives ready-to-use from
|
|
8
|
+
* `runtime/` (which built it from `deps`). Address callables/edges with
|
|
9
|
+
* layout-independent forms — a `ClassPath` for the edge class, the `<id>::link`
|
|
10
|
+
* instance form, and an `@<id>` id-form target.
|
|
9
11
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
import type { Summarizer } from '../integrations/summary/port'
|
|
13
|
+
import {
|
|
14
|
+
FOLDER_CLASS,
|
|
15
|
+
NAME_KEY,
|
|
16
|
+
NODE_CREATE,
|
|
17
|
+
NOTE_CLASS,
|
|
18
|
+
NOTE_KEYS,
|
|
19
|
+
REFERENCES_EDGE,
|
|
20
|
+
} from './keys'
|
|
13
21
|
|
|
14
22
|
/** The minimal kernel surface the worker runtime exposes to a handler. */
|
|
15
23
|
export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
|
|
16
24
|
|
|
17
|
-
const NODE_CREATE = K.Node.createNode.path.method.raw
|
|
18
|
-
const NAME_KEY = K.Named.name.key
|
|
19
|
-
const NOTE_CLASS = D.Note.path.class.raw
|
|
20
|
-
const REFERENCES_EDGE = D.references.path.class.raw
|
|
21
|
-
const TITLE_KEY = D.Note.title.key
|
|
22
|
-
const BODY_KEY = D.Note.body.key
|
|
23
|
-
|
|
24
25
|
/** Notes live under a `/notes` folder at the graph root (created by `seed`). */
|
|
25
26
|
const NOTES_PARENT = '/notes'
|
|
26
27
|
|
|
@@ -37,16 +38,26 @@ function slugify(text: string): string {
|
|
|
37
38
|
return `${stem}-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* `NoteOps.createNote` (static) — create a Note under `/notes`, stamping a
|
|
43
|
+
* one-line `summary` from the resolved summarizer port (the external-API seam).
|
|
44
|
+
*/
|
|
41
45
|
export async function createNote(
|
|
42
46
|
kernel: CallableKernel,
|
|
47
|
+
summarizer: Summarizer,
|
|
43
48
|
params: { title: string; body: string },
|
|
44
49
|
): Promise<{ id: string; path: string }> {
|
|
50
|
+
const summary = await summarizer.summarize(params.body)
|
|
45
51
|
const path = `${NOTES_PARENT}/${slugify(params.title)}`
|
|
46
52
|
const created = (await kernel.call(NODE_CREATE, {
|
|
47
53
|
class: NOTE_CLASS,
|
|
48
54
|
path,
|
|
49
|
-
props: {
|
|
55
|
+
props: {
|
|
56
|
+
[NAME_KEY]: params.title,
|
|
57
|
+
[NOTE_KEYS.title]: params.title,
|
|
58
|
+
[NOTE_KEYS.body]: params.body,
|
|
59
|
+
[NOTE_KEYS.summary]: summary,
|
|
60
|
+
},
|
|
50
61
|
})) as { id: string }
|
|
51
62
|
return { id: created.id, path }
|
|
52
63
|
}
|
|
@@ -64,8 +75,6 @@ export async function reference(
|
|
|
64
75
|
return { linked: params.target }
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
const FOLDER_CLASS = K.Folder.path.class.raw
|
|
68
|
-
|
|
69
78
|
const STARTERS: ReadonlyArray<{ slug: string; title: string; body: string }> = [
|
|
70
79
|
{
|
|
71
80
|
slug: 'welcome',
|
|
@@ -81,12 +90,15 @@ function isPathConflict(e: unknown): boolean {
|
|
|
81
90
|
|
|
82
91
|
/**
|
|
83
92
|
* `Note.seed` (static) — the domain's post-install bootstrap. The kernel calls
|
|
84
|
-
* it ONCE after install, as __SYSTEM__ (see `postInstall` in
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
93
|
+
* it ONCE after install, as __SYSTEM__ (see `postInstall` in `domain.ts`), so
|
|
94
|
+
* the domain can lay down its initial state: the `/notes` folder, a couple of
|
|
95
|
+
* starter Notes (each summarized through the port), and a `references` edge
|
|
96
|
+
* between them. Idempotent: a re-run swallows `PATH_CONFLICT` per node.
|
|
88
97
|
*/
|
|
89
|
-
export async function seed(
|
|
98
|
+
export async function seed(
|
|
99
|
+
kernel: CallableKernel,
|
|
100
|
+
summarizer: Summarizer,
|
|
101
|
+
): Promise<{ seeded: number }> {
|
|
90
102
|
// 1. The `/notes` folder at the graph root.
|
|
91
103
|
try {
|
|
92
104
|
await kernel.call(NODE_CREATE, {
|
|
@@ -106,7 +118,12 @@ export async function seed(kernel: CallableKernel): Promise<{ seeded: number }>
|
|
|
106
118
|
const created = (await kernel.call(NODE_CREATE, {
|
|
107
119
|
class: NOTE_CLASS,
|
|
108
120
|
path: `${NOTES_PARENT}/${s.slug}`,
|
|
109
|
-
props: {
|
|
121
|
+
props: {
|
|
122
|
+
[NAME_KEY]: s.title,
|
|
123
|
+
[NOTE_KEYS.title]: s.title,
|
|
124
|
+
[NOTE_KEYS.body]: s.body,
|
|
125
|
+
[NOTE_KEYS.summary]: await summarizer.summarize(s.body),
|
|
126
|
+
},
|
|
110
127
|
})) as { id: string }
|
|
111
128
|
ids[s.slug] = created.id
|
|
112
129
|
seeded++
|
package/template/deps.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env → handler dependency container — the ONE place the worker env becomes the
|
|
3
|
+
* typed `ctx.deps` every method reads. `defineDomain({ deps })` mounts it; the
|
|
4
|
+
* generated worker imports it. Runs ONCE per cold isolate (NOT per request), so
|
|
5
|
+
* it's where ports/clients are wired once instead of being re-derived in every
|
|
6
|
+
* handler.
|
|
7
|
+
*
|
|
8
|
+
* The summarizer port is resolved ON-REQUEST (lazy `summarizer()` via the
|
|
9
|
+
* registry), not here — so this stays a cheap synchronous wiring step and a
|
|
10
|
+
* worker never validates a provider's env until a handler actually uses it.
|
|
11
|
+
*
|
|
12
|
+
* Omit the `deps` field in `domain.ts` for the trivial case (handlers then get
|
|
13
|
+
* raw `Env`). Transform here the moment a handler should depend on a PORT
|
|
14
|
+
* instead of raw config — that's the whole point of this seam.
|
|
15
|
+
*/
|
|
16
|
+
import { buildSummarizerRegistry, type SummarizerRegistry } from './integrations/summary/registry'
|
|
17
|
+
import type { Env } from './env'
|
|
18
|
+
|
|
19
|
+
/** Typed dependency container handed to every method as `ctx.deps`. */
|
|
20
|
+
export interface Deps extends SummarizerRegistry {}
|
|
21
|
+
|
|
22
|
+
/** Map the worker env to the handler deps (the seam `defineDomain({ deps })` mounts). */
|
|
23
|
+
export function deps(env: Env): Deps {
|
|
24
|
+
return buildSummarizerRegistry(env)
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* domain.ts — the WORKER-SAFE domain definition: what the domain IS (its
|
|
3
|
+
* `schema`, `methods`, `deps`, `views`, `functions`, `client`) plus its identity
|
|
4
|
+
* (`origin` defaults to `schema.domain`; `postInstall`). Everything is wired
|
|
5
|
+
* EXPLICITLY here — a renamed or mistyped module is a compile error at this
|
|
6
|
+
* call, never a silently-missing route. The handlers live in `runtime/` (the
|
|
7
|
+
* composition root), the pure logic in `core/`, the external-API ports in
|
|
8
|
+
* `integrations/` — but the worker only ever sees this one wired definition.
|
|
9
|
+
*
|
|
10
|
+
* The generated worker (`.astrale/worker.gen.ts`) imports THIS and spreads it,
|
|
11
|
+
* so your folder layout is invisible to it — reorganize freely; only this wiring
|
|
12
|
+
* has to stay put. The deploy adapter is attached separately in
|
|
13
|
+
* `astrale.config.ts` via `deploy(domain, …)`, keeping its node-only code
|
|
14
|
+
* (wrangler, filesystem) out of the worker bundle.
|
|
15
|
+
*/
|
|
16
|
+
import { defineDomain } from '@astrale-os/sdk'
|
|
17
|
+
|
|
18
|
+
import { deps } from './deps'
|
|
19
|
+
import { functions } from './functions'
|
|
20
|
+
import { methods } from './runtime'
|
|
21
|
+
import { schema } from './schema'
|
|
22
|
+
import { views } from './views'
|
|
23
|
+
|
|
24
|
+
export const domain = defineDomain({
|
|
25
|
+
schema,
|
|
26
|
+
methods,
|
|
27
|
+
deps,
|
|
28
|
+
views,
|
|
29
|
+
functions,
|
|
30
|
+
client: { dir: 'client' },
|
|
31
|
+
postInstall: `/:${schema.domain}:class.Note:seed`,
|
|
32
|
+
requires: [],
|
|
33
|
+
})
|
package/template/env.ts
CHANGED
|
@@ -18,5 +18,16 @@ export interface Env {
|
|
|
18
18
|
SELF?: { fetch(request: Request): Promise<Response> }
|
|
19
19
|
/** Dev-only: forward /ui/* to a running Vite dev server. */
|
|
20
20
|
VIEW_DEV_URL?: string
|
|
21
|
+
|
|
22
|
+
// ── Summarizer (the example external-API integration) ─────────────
|
|
23
|
+
/** Which summarizer adapter to bind: `heuristic` (default, no secret) | `http`. */
|
|
24
|
+
NOTE_SUMMARIZER?: string
|
|
25
|
+
/** `http` only — bearer token for the OpenAI-compatible upstream (a secret). */
|
|
26
|
+
SUMMARIZER_API_KEY?: string
|
|
27
|
+
/** `http` only — OpenAI-compatible base URL (e.g. https://api.openai.com/v1). */
|
|
28
|
+
SUMMARIZER_BASE_URL?: string
|
|
29
|
+
/** `http` only — model id (default gpt-4o-mini). */
|
|
30
|
+
SUMMARIZER_MODEL?: string
|
|
31
|
+
|
|
21
32
|
[key: string]: unknown
|
|
22
33
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic summarizer — the ZERO-CONFIG default adapter. No network, no
|
|
3
|
+
* secrets: a fresh scaffold summarizes notes out of the box on `pnpm dev`.
|
|
4
|
+
* It takes the first sentence (clamped), which is good enough to demonstrate
|
|
5
|
+
* the seam; flip `NOTE_SUMMARIZER=http` (see `registry.ts`) for a real model.
|
|
6
|
+
*/
|
|
7
|
+
import type { Summarizer } from './port'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_LENGTH = 140
|
|
10
|
+
|
|
11
|
+
/** Build the local, dependency-free summarizer. */
|
|
12
|
+
export function createHeuristicSummarizer(opts: { maxLength?: number } = {}): Summarizer {
|
|
13
|
+
const maxLength = opts.maxLength ?? DEFAULT_MAX_LENGTH
|
|
14
|
+
return {
|
|
15
|
+
summarize(body) {
|
|
16
|
+
const trimmed = body.trim()
|
|
17
|
+
// First sentence, else the whole (clamped) body.
|
|
18
|
+
const firstSentence = trimmed.split(/(?<=[.!?])\s/)[0] ?? trimmed
|
|
19
|
+
const text = firstSentence || trimmed
|
|
20
|
+
const summary =
|
|
21
|
+
text.length <= maxLength ? text : `${text.slice(0, maxLength - 1).trimEnd()}…`
|
|
22
|
+
return Promise.resolve(summary)
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP summarizer — the REAL external-API adapter: a single OpenAI-compatible
|
|
3
|
+
* `POST /chat/completions` call. This is the shape your domain's external calls
|
|
4
|
+
* take — config + secret from `env`, a timeout via `AbortSignal`, upstream
|
|
5
|
+
* detail preserved in `cause`. Selected by `NOTE_SUMMARIZER=http` (registry.ts);
|
|
6
|
+
* the default stays the no-network `heuristic` adapter so a fresh scaffold needs
|
|
7
|
+
* no secret.
|
|
8
|
+
*
|
|
9
|
+
* `baseUrl` is anything OpenAI-compatible — `https://api.openai.com/v1`, a
|
|
10
|
+
* Workers-AI gateway, or your OWN `ai-gateway` domain's `/v1` surface. On a soft
|
|
11
|
+
* upstream failure it falls back to the heuristic rather than throwing: a flaky
|
|
12
|
+
* summarizer must never block creating a note (see the port contract).
|
|
13
|
+
*/
|
|
14
|
+
import { createHeuristicSummarizer } from './heuristic'
|
|
15
|
+
import type { Summarizer } from './port'
|
|
16
|
+
|
|
17
|
+
export interface HttpSummarizerConfig {
|
|
18
|
+
/** Bearer token for the upstream (a secret — ships via `.env.<env>`). */
|
|
19
|
+
apiKey: string
|
|
20
|
+
/** OpenAI-compatible base URL, e.g. `https://api.openai.com/v1`. */
|
|
21
|
+
baseUrl: string
|
|
22
|
+
/** Model id, e.g. `gpt-4o-mini`. */
|
|
23
|
+
model: string
|
|
24
|
+
/** Upstream request timeout in ms (default 15000). */
|
|
25
|
+
timeoutMs?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 15_000
|
|
29
|
+
const SYSTEM_PROMPT = 'Summarize the user note in one short sentence. Reply with the summary only.'
|
|
30
|
+
|
|
31
|
+
/** Build the OpenAI-compatible HTTP summarizer (with a heuristic fallback). */
|
|
32
|
+
export function createHttpSummarizer(config: HttpSummarizerConfig): Summarizer {
|
|
33
|
+
const fallback = createHeuristicSummarizer()
|
|
34
|
+
const endpoint = `${config.baseUrl.replace(/\/+$/, '')}/chat/completions`
|
|
35
|
+
return {
|
|
36
|
+
async summarize(body) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(endpoint, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'content-type': 'application/json',
|
|
42
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model: config.model,
|
|
46
|
+
messages: [
|
|
47
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
48
|
+
{ role: 'user', content: body },
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
signal: AbortSignal.timeout(config.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
52
|
+
})
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new Error(`summarizer upstream returned ${res.status}`, {
|
|
55
|
+
cause: await res.text().catch(() => undefined),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
const data = (await res.json()) as {
|
|
59
|
+
choices?: Array<{ message?: { content?: string } }>
|
|
60
|
+
}
|
|
61
|
+
const text = data.choices?.[0]?.message?.content?.trim()
|
|
62
|
+
return text || (await fallback.summarize(body))
|
|
63
|
+
} catch {
|
|
64
|
+
// Soft-fail: never block a note create on the summarizer.
|
|
65
|
+
return fallback.summarize(body)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarizer — the PORT between the domain's logic and whatever produces a
|
|
3
|
+
* note's one-line summary. This is the external-API seam every real domain has
|
|
4
|
+
* (here: an LLM/summarization API), reduced to the narrowest interface the
|
|
5
|
+
* logic needs.
|
|
6
|
+
*
|
|
7
|
+
* `core/` logic depends on THIS interface only — never on `fetch`, an SDK, or
|
|
8
|
+
* `env`. Adapters in this folder implement it: `heuristic.ts` (the zero-config
|
|
9
|
+
* default, no network) and `http.ts` (a real OpenAI-compatible call). The
|
|
10
|
+
* registry picks one from `env`; the handler logic only ever sees the resolved
|
|
11
|
+
* port. Swap or add a provider = one adapter + one arm in `registry.ts`.
|
|
12
|
+
*/
|
|
13
|
+
export interface Summarizer {
|
|
14
|
+
/**
|
|
15
|
+
* Produce a short, single-line summary of `body`. Called once per note
|
|
16
|
+
* create/seed, so keep it fast and resilient. Adapters MUST resolve to a
|
|
17
|
+
* usable string (fall back to a heuristic rather than throwing on a soft
|
|
18
|
+
* upstream failure) — a flaky summarizer should never block creating a note.
|
|
19
|
+
*/
|
|
20
|
+
summarize(body: string): Promise<string>
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarizer registry — the ONE place the worker env becomes the live
|
|
3
|
+
* `Summarizer` port. Adding a provider = one more arm in `selectSummarizer`;
|
|
4
|
+
* handler logic only ever sees the resolved port.
|
|
5
|
+
*
|
|
6
|
+
* Selection is ON-REQUEST: the provider is chosen + built lazily on the first
|
|
7
|
+
* handler that calls `summarizer()`, then cached for the isolate — NOT at
|
|
8
|
+
* `deps()` construction. A worker left on the default never validates the HTTP
|
|
9
|
+
* provider's env, and a per-request override could slot in here later.
|
|
10
|
+
*/
|
|
11
|
+
import type { Env } from '../../env'
|
|
12
|
+
import { createHeuristicSummarizer } from './heuristic'
|
|
13
|
+
import { createHttpSummarizer } from './http'
|
|
14
|
+
import type { Summarizer } from './port'
|
|
15
|
+
|
|
16
|
+
export interface SummarizerRegistry {
|
|
17
|
+
/** Resolve the configured summarizer port (lazy + cached per isolate). */
|
|
18
|
+
summarizer(): Summarizer
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Build the registry from the worker env (the port is built lazily + cached). */
|
|
22
|
+
export function buildSummarizerRegistry(env: Env): SummarizerRegistry {
|
|
23
|
+
let cached: Summarizer | undefined
|
|
24
|
+
return {
|
|
25
|
+
summarizer() {
|
|
26
|
+
return (cached ??= selectSummarizer(env))
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_HTTP_MODEL = 'gpt-4o-mini'
|
|
32
|
+
|
|
33
|
+
/** Bind the abstract summarizer port to the configured concrete adapter. */
|
|
34
|
+
function selectSummarizer(env: Env): Summarizer {
|
|
35
|
+
const provider = (env.NOTE_SUMMARIZER || 'heuristic').toLowerCase()
|
|
36
|
+
|
|
37
|
+
if (provider === 'http') {
|
|
38
|
+
if (!env.SUMMARIZER_API_KEY || !env.SUMMARIZER_BASE_URL) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'NOTE_SUMMARIZER=http requires SUMMARIZER_API_KEY and SUMMARIZER_BASE_URL',
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return createHttpSummarizer({
|
|
44
|
+
apiKey: env.SUMMARIZER_API_KEY,
|
|
45
|
+
baseUrl: env.SUMMARIZER_BASE_URL,
|
|
46
|
+
model: env.SUMMARIZER_MODEL || DEFAULT_HTTP_MODEL,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Default: the zero-config local heuristic (no network, no secret).
|
|
51
|
+
return createHeuristicSummarizer()
|
|
52
|
+
}
|
package/template/package.json
CHANGED
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
"typecheck": "tsgo --noEmit"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@astrale-os/adapter-cloudflare": ">=0.1.
|
|
18
|
-
"@astrale-os/devkit": ">=0.1.5 <1.0.0",
|
|
17
|
+
"@astrale-os/adapter-cloudflare": ">=0.1.9 <1.0.0",
|
|
19
18
|
"@astrale-os/kernel-core": ">=0.4.3 <1.0.0",
|
|
20
19
|
"@astrale-os/kernel-dsl": ">=0.1.2 <1.0.0",
|
|
21
|
-
"@astrale-os/sdk": ">=0.1.
|
|
20
|
+
"@astrale-os/sdk": ">=0.1.7 <1.0.0",
|
|
22
21
|
"@hono/node-server": "^1.19.0",
|
|
23
22
|
"zod": "^4.3.6"
|
|
24
23
|
},
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime composition root — the ONLY place request context (kernel/self/params/
|
|
3
|
+
* auth) and `deps` meet the per-feature logic. Each `execute` resolves the port
|
|
4
|
+
* it needs (`deps.summarizer()`, on-request) and delegates to the transport-
|
|
5
|
+
* agnostic functions in `core/`. No business logic lives here.
|
|
6
|
+
*
|
|
7
|
+
* - `createNote` is interface-hosted (static) → `remoteInterfaceMethods`.
|
|
8
|
+
* - `reference` is class-hosted (instance) → `remoteMethod` + `remoteClassMethods`.
|
|
9
|
+
* - `seed` is class-hosted (static) — the `postInstall` bootstrap.
|
|
10
|
+
*
|
|
11
|
+
* SDK-level `authorize` is an additive throw-to-deny check (returns void). For
|
|
12
|
+
* finer worker checks see `assertPerm` / `requireOwnership` from `@astrale-os/sdk`.
|
|
13
|
+
*
|
|
14
|
+
* Exports the `methods` map the domain definition (`domain.ts`) wires in.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
remoteClassMethods,
|
|
18
|
+
remoteInterfaceMethods,
|
|
19
|
+
remoteMethod,
|
|
20
|
+
type SchemaMethodsImpl,
|
|
21
|
+
} from '@astrale-os/sdk'
|
|
22
|
+
|
|
23
|
+
import { createNote, reference, seed } from '../core/note'
|
|
24
|
+
import type { Deps } from '../deps'
|
|
25
|
+
import { schema } from '../schema'
|
|
26
|
+
|
|
27
|
+
const method = remoteMethod<Deps>()
|
|
28
|
+
const interfaceMethods = remoteInterfaceMethods<Deps>()
|
|
29
|
+
const classMethods = remoteClassMethods<Deps>()
|
|
30
|
+
|
|
31
|
+
const NoteOpsMethods = interfaceMethods(schema, 'NoteOps', {
|
|
32
|
+
createNote: {
|
|
33
|
+
authorize: async () => undefined,
|
|
34
|
+
execute: ({ kernel, params, deps }) => {
|
|
35
|
+
if (!kernel) throw new Error('createNote requires a kernel credential')
|
|
36
|
+
return createNote(kernel, deps.summarizer(), params)
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const referenceMethod = method(schema, 'Note', 'reference', {
|
|
42
|
+
authorize: async () => undefined,
|
|
43
|
+
execute: ({ kernel, self, params }) => {
|
|
44
|
+
if (!kernel) throw new Error('reference requires a kernel credential')
|
|
45
|
+
return reference(kernel, self.path.raw, params)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const seedMethod = method(schema, 'Note', 'seed', {
|
|
50
|
+
authorize: async () => undefined,
|
|
51
|
+
execute: ({ kernel, deps }) => {
|
|
52
|
+
if (!kernel) throw new Error('seed requires a kernel credential')
|
|
53
|
+
return seed(kernel, deps.summarizer())
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const NoteMethods = classMethods(schema, 'Note', { reference: referenceMethod, seed: seedMethod })
|
|
58
|
+
|
|
59
|
+
export const methods: SchemaMethodsImpl<typeof schema, Deps> = {
|
|
60
|
+
interface: { NoteOps: NoteOpsMethods },
|
|
61
|
+
class: { Note: NoteMethods },
|
|
62
|
+
}
|
package/template/schema/index.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* them in the `interfaces` / `classes` maps below.
|
|
10
10
|
*/
|
|
11
11
|
import { defineSchema, KernelSchema } from '@astrale-os/kernel-core'
|
|
12
|
+
import { compileDomain, type Domain } from '@astrale-os/kernel-core/domain'
|
|
12
13
|
|
|
13
14
|
import { Note, NoteOps, references } from './note'
|
|
14
15
|
|
|
@@ -17,5 +18,6 @@ export const schema = defineSchema('astrale-domain.example.dev', {
|
|
|
17
18
|
classes: { Note, references },
|
|
18
19
|
imports: [KernelSchema],
|
|
19
20
|
})
|
|
21
|
+
export const D: Domain<typeof schema> = compileDomain(schema)
|
|
20
22
|
|
|
21
23
|
export * from './note'
|
package/template/schema/note.ts
CHANGED
|
@@ -41,6 +41,9 @@ export const Note = nodeClass({
|
|
|
41
41
|
props: {
|
|
42
42
|
title: z.string(),
|
|
43
43
|
body: z.string(),
|
|
44
|
+
// One-line summary, stamped at create/seed time from the `Summarizer` port
|
|
45
|
+
// (see integrations/summary/) — the external-API seam wired through `deps`.
|
|
46
|
+
summary: z.string().optional(),
|
|
44
47
|
},
|
|
45
48
|
methods: {
|
|
46
49
|
reference: fn({
|
|
@@ -58,7 +61,7 @@ export const Note = nodeClass({
|
|
|
58
61
|
})
|
|
59
62
|
|
|
60
63
|
export const references = edgeClass(
|
|
61
|
-
{ as: 'from_note', types: [Note] },
|
|
62
|
-
{ as: 'to_note', types: [Note] },
|
|
64
|
+
{ as: 'from_note', types: [Note], cardinality: '0..*' },
|
|
65
|
+
{ as: 'to_note', types: [Note], cardinality: '0..*' },
|
|
63
66
|
{ props: { reason: z.string().optional() } },
|
|
64
67
|
)
|
package/template/tsconfig.json
CHANGED
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
"esModuleInterop": true,
|
|
13
13
|
"resolveJsonModule": true
|
|
14
14
|
},
|
|
15
|
-
"include": [
|
|
16
|
-
|
|
15
|
+
"include": [
|
|
16
|
+
"schema",
|
|
17
|
+
"core",
|
|
18
|
+
"integrations",
|
|
19
|
+
"runtime",
|
|
20
|
+
"views",
|
|
21
|
+
"functions",
|
|
22
|
+
"deps.ts",
|
|
23
|
+
"env.ts",
|
|
24
|
+
"domain.ts",
|
|
25
|
+
"astrale.config.ts"
|
|
26
|
+
],
|
|
27
|
+
"exclude": ["node_modules", ".astrale", ".dist"]
|
|
17
28
|
}
|
package/template/views/note.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* an inline-HTML `render` like `welcome`). Because it declares `mount` rather
|
|
4
4
|
* than `render`, the View node's iframe binding points at `<serving url>/ui/note`
|
|
5
5
|
* — the SDK stamps it from the worker's live URL when it builds the install
|
|
6
|
-
* bundle. The Cloudflare adapter serves `/ui/*` from
|
|
6
|
+
* bundle. The Cloudflare adapter serves `/ui/*` from `../.dist` (built by
|
|
7
7
|
* `client/` with base `/ui/`) via the Worker's `ASSETS` binding.
|
|
8
8
|
*
|
|
9
9
|
* `viewFor: selfOf(Note)` attaches a `view_for` edge to the `Note` class
|
package/dist/astrale.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `astrale(envs)` — the Astrale-managed deployment adapter.
|
|
3
|
-
*
|
|
4
|
-
* Ships the domain THROUGH the platform instead of to the author's own cloud
|
|
5
|
-
* account: `deploy` builds the same single-module workerd bundle the Cloudflare
|
|
6
|
-
* adapter ships (shared codegen + `wrangler deploy --dry-run`), then publishes
|
|
7
|
-
* it via the admin domain's catalog — `DomainPublished.register` →
|
|
8
|
-
* `::release { bundleBase64 }` (the worker stages the bytes in the platform
|
|
9
|
-
* registry) → `::install { instanceId, source: { kind: 'package' } }`, which
|
|
10
|
-
* deploys it as a host-local Service next to the target instance and installs
|
|
11
|
-
* the domain on it. No Cloudflare account, no wrangler auth — the only
|
|
12
|
-
* credential is the author's Astrale identity, supplied by the `astrale` CLI
|
|
13
|
-
* session this adapter shells out to (the same trust source as
|
|
14
|
-
* `astrale instance install`).
|
|
15
|
-
*
|
|
16
|
-
* `watch` is identical to the Cloudflare adapter's local dev (wrangler dev on
|
|
17
|
-
* localhost) — managed deploys change WHERE the worker runs, not how you
|
|
18
|
-
* iterate locally.
|
|
19
|
-
*
|
|
20
|
-
* Managed deploys ship the client SPA (served under `/ui` by the box) and
|
|
21
|
-
* runtime secrets (dotenv map on the install call, encrypted at rest
|
|
22
|
-
* platform-side; platform-managed env keys always win precedence).
|
|
23
|
-
*/
|
|
24
|
-
import type { DomainAdapter } from '@astrale-os/devkit';
|
|
25
|
-
import type { AstraleParams } from './params';
|
|
26
|
-
export declare function astrale(envs: Record<string, AstraleParams>): DomainAdapter<AstraleParams>;
|
|
27
|
-
//# sourceMappingURL=astrale.d.ts.map
|
package/dist/astrale.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"astrale.d.ts","sourceRoot":"","sources":["../src/astrale.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAQvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAY7C,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,aAAa,CAAC,aAAa,CAAC,CAoIzF"}
|