create-oke 0.18.4 → 0.19.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 +1 -1
- package/package.json +3 -3
- package/src/agents-md.ts +6 -4
- package/src/ai-setup/apply.test.ts +278 -0
- package/src/ai-setup/apply.ts +430 -52
- package/src/ai-setup/catalog.ts +250 -1343
- package/src/ai-setup/from-pref.ts +3 -71
- package/src/ai-setup/prompts.ts +60 -443
- package/src/cli.test.ts +21 -15
- package/src/cli.ts +20 -11
- package/src/create-defaults.test.ts +4 -4
- package/src/create-defaults.ts +3 -3
- package/src/customize-flow.test.ts +9 -15
- package/src/customize-flow.ts +59 -66
- package/src/drivers-catalog.ts +18 -23
- package/src/transform.test.ts +69 -10
- package/src/transform.ts +10 -35
- package/templates/advanced/.env.example +28 -11
- package/templates/advanced/.github/workflows/ci.yml +1 -1
- package/templates/advanced/oke.config.ts +3 -4
- package/templates/advanced/package.json +11 -10
- package/templates/advanced/src/app.ts +36 -1
- package/templates/advanced/src/core.ts +12 -11
- package/templates/advanced/src/db/schema.decl.ts +6 -2
- package/templates/advanced/src/db/seed/index.ts +4 -4
- package/templates/advanced/src/flows/main/route.ts +3 -2
- package/templates/advanced/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/advanced/src/flows/notes/[id]/attach.ts +1 -4
- package/templates/advanced/src/flows/notes/[id]/get.ts +4 -7
- package/templates/advanced/src/flows/notes/[id]/summarize.ts +3 -4
- package/templates/advanced/src/flows/notes/create.ts +4 -6
- package/templates/advanced/src/flows/notes/digest.ts +6 -5
- package/templates/advanced/src/flows/notes/list.ts +4 -5
- package/templates/advanced/src/flows/notes/shapes.ts +13 -3
- package/templates/advanced/src/flows/notes/signals.ts +1 -2
- package/templates/advanced/src/vault.ts +138 -0
- package/templates/advanced/tests/advanced.test.ts +11 -8
- package/templates/advanced/web/src/App.css +7 -0
- package/templates/advanced/web/src/App.tsx +101 -1
- package/templates/advanced/web/src/client.ts +16 -3
- package/templates/advanced/web/vite.config.ts +1 -0
- package/templates/standard/.env.example +22 -11
- package/templates/standard/.github/workflows/ci.yml +1 -1
- package/templates/standard/oke.config.ts +3 -3
- package/templates/standard/package.json +11 -10
- package/templates/standard/src/app.ts +6 -1
- package/templates/standard/src/core.ts +10 -11
- package/templates/standard/src/db/schema.decl.ts +6 -2
- package/templates/standard/src/db/seed/index.ts +3 -3
- package/templates/standard/src/flows/main/route.ts +3 -2
- package/templates/standard/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/standard/src/flows/notes/[id]/get.ts +4 -7
- package/templates/standard/src/flows/notes/create.ts +4 -6
- package/templates/standard/src/flows/notes/list.ts +4 -5
- package/templates/standard/src/flows/notes/shapes.ts +12 -2
- package/templates/standard/src/flows/notes/signals.ts +1 -2
- package/templates/standard/src/vault.ts +123 -0
- package/templates/standard/tests/standard.test.ts +3 -1
- package/templates/standard/web/src/client.ts +2 -2
- package/templates/standard/web/vite.config.ts +1 -0
- package/src/ai-setup/detect-ollama.ts +0 -228
- package/src/ai-setup/recommend.ts +0 -220
package/src/transform.ts
CHANGED
|
@@ -15,14 +15,7 @@
|
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import type { CreateDefaults, CreateProxyId, EnvDriverPins } from "./create-defaults.ts";
|
|
18
|
-
import {
|
|
19
|
-
DEFAULT_IMAGES,
|
|
20
|
-
LLAMA_CPP_IMAGE,
|
|
21
|
-
OLLAMA_IMAGE,
|
|
22
|
-
PROXY_IMAGES,
|
|
23
|
-
SGLANG_IMAGE,
|
|
24
|
-
VLLM_IMAGE,
|
|
25
|
-
} from "./drivers-catalog.ts";
|
|
18
|
+
import { DEFAULT_IMAGES, PROXY_IMAGES } from "./drivers-catalog.ts";
|
|
26
19
|
import { materializeLocalOkengineDependency } from "./local-okengine.ts";
|
|
27
20
|
import { packageRoot } from "./templates.ts";
|
|
28
21
|
|
|
@@ -35,6 +28,12 @@ export type SqlDriverId = (typeof SQL_DRIVERS)[number];
|
|
|
35
28
|
/** Default when `--sql` / wizard choice is omitted (matches template sources). */
|
|
36
29
|
export const DEFAULT_SQL_DRIVER: SqlDriverId = "postgres";
|
|
37
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Manifest extract peer for `oke dev` / boot effects stamping.
|
|
33
|
+
* Kept in sync with root `okengine` `dependencies["oxc-parser"]`.
|
|
34
|
+
*/
|
|
35
|
+
export const OXC_PARSER_RANGE = "^0.142.0";
|
|
36
|
+
|
|
38
37
|
/**
|
|
39
38
|
* Whether `value` is a known {@link SqlDriverId}.
|
|
40
39
|
*
|
|
@@ -88,6 +87,9 @@ export function transformPackageJson(
|
|
|
88
87
|
if (dependencies["okengine"] !== undefined) {
|
|
89
88
|
dependencies["okengine"] = okengineDep;
|
|
90
89
|
}
|
|
90
|
+
// Always pin — published okengine ≤0.18.5 left oxc-parser optional; without it,
|
|
91
|
+
// Docker-first `oke dev` hard-fails OKE1020 (Manifest extract cannot run).
|
|
92
|
+
dependencies["oxc-parser"] ??= OXC_PARSER_RANGE;
|
|
91
93
|
|
|
92
94
|
return {
|
|
93
95
|
...source,
|
|
@@ -503,15 +505,6 @@ function syncImages(source: string, defaults: CreateDefaults): string {
|
|
|
503
505
|
if (d.store.index?.dev === "meilisearch") {
|
|
504
506
|
pin("store.index", DEFAULT_IMAGES["store.index"]!);
|
|
505
507
|
}
|
|
506
|
-
if (
|
|
507
|
-
d.ai &&
|
|
508
|
-
(d.ai.dev === "ollama" ||
|
|
509
|
-
d.ai.prod === "ollama" ||
|
|
510
|
-
d.ai.dev === "openai-compatible" ||
|
|
511
|
-
d.ai.prod === "openai-compatible")
|
|
512
|
-
) {
|
|
513
|
-
pin("ai", aiImageForDefaults(defaults));
|
|
514
|
-
}
|
|
515
508
|
if (defaults.proxy !== "none") {
|
|
516
509
|
pin("proxy", PROXY_IMAGES[defaults.proxy]);
|
|
517
510
|
}
|
|
@@ -519,24 +512,6 @@ function syncImages(source: string, defaults: CreateDefaults): string {
|
|
|
519
512
|
return replaceImagesBlock(source, images);
|
|
520
513
|
}
|
|
521
514
|
|
|
522
|
-
/**
|
|
523
|
-
* Resolve `images.ai` from create-defaults provider / driver pins.
|
|
524
|
-
*
|
|
525
|
-
* @param defaults - Create answers
|
|
526
|
-
*/
|
|
527
|
-
function aiImageForDefaults(defaults: CreateDefaults): string {
|
|
528
|
-
const provider = defaults.ai.provider;
|
|
529
|
-
if (provider === "ollama" || defaults.drivers.ai?.dev === "ollama") {
|
|
530
|
-
return OLLAMA_IMAGE;
|
|
531
|
-
}
|
|
532
|
-
if (provider === "vllm") return VLLM_IMAGE;
|
|
533
|
-
if (provider === "sglang") return SGLANG_IMAGE;
|
|
534
|
-
if (provider === "llama-cpp" || defaults.drivers.ai?.dev === "openai-compatible") {
|
|
535
|
-
return LLAMA_CPP_IMAGE;
|
|
536
|
-
}
|
|
537
|
-
return DEFAULT_IMAGES.ai!;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
515
|
/**
|
|
541
516
|
* Locate the `images: { … }` block by brace depth (not the first `}`) so
|
|
542
517
|
* nested `store: { … }` / `channel: { … }` sub-blocks don't close the match
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# Environment template — create-oke copies this to `.env.local` when you scaffold.
|
|
2
2
|
#
|
|
3
3
|
# Resolution order (first hit wins):
|
|
4
|
-
# vault driver → process.env → .env.local → dev fallback
|
|
4
|
+
# vault driver → process.env → .env.local → dev / fromDocker fallback
|
|
5
5
|
#
|
|
6
|
-
# --- Vault (
|
|
7
|
-
#
|
|
6
|
+
# --- Vault (contracts in src/vault.ts) ---
|
|
7
|
+
# `vault.secret` / `vault.config` names are declared in source so Console Vault
|
|
8
|
+
# lists them. Put values here, in process.env, via `oke vault set`, or Console —
|
|
9
|
+
# same contract either way. Uncomment a line to pin a dotenv value.
|
|
8
10
|
#
|
|
9
11
|
# ── vault — built-in encrypted-at-rest store ────────────────
|
|
10
|
-
# `drivers.vault` is "vault":
|
|
11
|
-
#
|
|
12
|
-
# over leftover process.env / dotenv pins.
|
|
12
|
+
# `drivers.vault` is "vault": encrypted values live in Postgres, no extra service.
|
|
13
|
+
# A value already in the vault backend wins over leftover process.env / dotenv.
|
|
13
14
|
#
|
|
14
15
|
# oke vault init # creates the store, prints the master key ONCE
|
|
15
16
|
# export OKE_VAULT_MASTER_KEY=… # every later command / boot unseals with this
|
|
@@ -36,10 +37,11 @@
|
|
|
36
37
|
# Uncomment to override compose credentials for a host-managed service.
|
|
37
38
|
# Host ports under docker mode are unique per project — do not assume defaults.
|
|
38
39
|
|
|
39
|
-
# ── App / Console
|
|
40
|
+
# ── App / Console (declared in src/vault.ts) ────────────────
|
|
40
41
|
# PORT=6530
|
|
41
42
|
# OKE_APP_URL=http://127.0.0.1:6530
|
|
42
|
-
#
|
|
43
|
+
# PUBLIC_API_URL=http://127.0.0.1:6530
|
|
44
|
+
# Vite web (`bun run web`): leave VITE_API_URL unset = same-origin proxy to oke dev
|
|
43
45
|
# VITE_API_URL=
|
|
44
46
|
# OKE_CONSOLE_SECRET=
|
|
45
47
|
# MAINTENANCE_MODE=0
|
|
@@ -113,10 +115,25 @@
|
|
|
113
115
|
# PGDATA=/var/lib/postgresql/data/pgdata
|
|
114
116
|
# POSTGRES_INITDB_ARGS=--data-checksums
|
|
115
117
|
|
|
118
|
+
# ── store.index — Meilisearch (when images.store.index is set) ─
|
|
119
|
+
# MEILI_URL=http://127.0.0.1:7700
|
|
120
|
+
# MEILI_MASTER_KEY=dev-notes-meili
|
|
121
|
+
# OKE_STORE_INDEX_URL=http://127.0.0.1:7700
|
|
122
|
+
# OKE_STORE_INDEX_KEY=dev-notes-meili
|
|
123
|
+
|
|
116
124
|
# ── AI ──────────────────────────────────────────────────────
|
|
117
|
-
#
|
|
125
|
+
# Prefer `oke ai setup` — writes ai.model() with registry providers.
|
|
126
|
+
# Zero Docker: OpenRouter (baseUrl auto-resolved)
|
|
127
|
+
# OPENROUTER_API_KEY=
|
|
128
|
+
# OKE_AI_CLOUD_MODEL=openrouter/free
|
|
129
|
+
# BYO OpenAI-compatible `/v1` — set OKE_AI_URL yourself (Compose does not manage inference)
|
|
118
130
|
# OKE_AI_DRIVER=openai-compatible
|
|
119
|
-
# OKE_AI_URL=http://127.0.0.1:
|
|
120
|
-
# OKE_AI_MODEL=
|
|
131
|
+
# OKE_AI_URL=http://127.0.0.1:1234/v1
|
|
132
|
+
# OKE_AI_MODEL=your-model-id
|
|
133
|
+
# Native Anthropic (production Claude)
|
|
121
134
|
# ANTHROPIC_API_KEY=
|
|
135
|
+
# Other registry keys (when selected in setup): GROQ_API_KEY, TOGETHER_API_KEY,
|
|
136
|
+
# DEEPSEEK_API_KEY, MISTRAL_API_KEY, XAI_API_KEY, GEMINI_API_KEY, …
|
|
122
137
|
# OPENAI_API_KEY=
|
|
138
|
+
# Optional proxy override for any registry provider:
|
|
139
|
+
# OPENAI_BASE_URL=
|
|
@@ -30,15 +30,14 @@ export default defineConfig({
|
|
|
30
30
|
store: {
|
|
31
31
|
sql: "postgres:18-alpine",
|
|
32
32
|
kv: "redis:8-alpine",
|
|
33
|
-
files: "rustfs/rustfs:1.0.0-rc.
|
|
33
|
+
files: "rustfs/rustfs:1.0.0-rc.5",
|
|
34
34
|
// index: "getmeili/meilisearch:v1.53",
|
|
35
35
|
},
|
|
36
36
|
channel: {
|
|
37
|
-
email: "axllent/mailpit:v1.
|
|
37
|
+
email: "axllent/mailpit:v1.31.1",
|
|
38
38
|
},
|
|
39
|
-
// pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
39
|
+
// pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57", // create-oke wizard / --pgdog
|
|
40
40
|
// proxy: "caddy:2-alpine", // or traefik:v3.7 / nginx:1.31-alpine
|
|
41
|
-
// ai: "ghcr.io/ggml-org/llama.cpp:server-b10450", // or ollama/ollama:0.32.13
|
|
42
41
|
},
|
|
43
42
|
i18n: { locales: ["en"], default: "en" },
|
|
44
43
|
});
|
|
@@ -5,22 +5,23 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"okengine": "file:../../../..",
|
|
8
|
-
"@duckdb/node-api": "^1.5.5-r.
|
|
8
|
+
"@duckdb/node-api": "^1.5.5-r.4",
|
|
9
9
|
"drizzle-orm": "1.0.0-rc.5-169397b",
|
|
10
|
-
"
|
|
11
|
-
"react
|
|
12
|
-
"
|
|
10
|
+
"oxc-parser": "^0.149.0",
|
|
11
|
+
"react": "^19.3.0",
|
|
12
|
+
"react-dom": "^19.3.0",
|
|
13
|
+
"zod": "^4.6.1"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
15
|
-
"@electric-sql/pglite": "^0.5.
|
|
16
|
-
"@electric-sql/pglite-pgvector": "^0.0.
|
|
16
|
+
"@electric-sql/pglite": "^0.5.8",
|
|
17
|
+
"@electric-sql/pglite-pgvector": "^0.0.9",
|
|
17
18
|
"@types/bun": "latest",
|
|
18
|
-
"@types/react": "^19.
|
|
19
|
-
"@types/react-dom": "^19.
|
|
20
|
-
"@vitejs/plugin-react": "^6.
|
|
19
|
+
"@types/react": "^19.3.0",
|
|
20
|
+
"@types/react-dom": "^19.3.0",
|
|
21
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
21
22
|
"drizzle-kit": "1.0.0-rc.5-ab785fc",
|
|
22
23
|
"typescript": "^7.0.2",
|
|
23
|
-
"vite": "^8.
|
|
24
|
+
"vite": "^8.3.0"
|
|
24
25
|
},
|
|
25
26
|
"trustedDependencies": ["@duckdb/node-api"],
|
|
26
27
|
"scripts": {
|
|
@@ -2,7 +2,42 @@ import "@/core";
|
|
|
2
2
|
import "@/flows/generated";
|
|
3
3
|
|
|
4
4
|
import { oke } from "okengine/http";
|
|
5
|
+
import { cors, csrf, passkey } from "okengine/plugins";
|
|
6
|
+
import { NOTES_VAULT } from "@/vault";
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
const viteOrigins = ["http://127.0.0.1:5173", "http://localhost:5173"] as const;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Notes app — cookie auth + CSRF + passkey demo (additive to Notes Flows).
|
|
12
|
+
* `vault.config` is not auto-registered (only `vault.secret` is), so pass
|
|
13
|
+
* {@link NOTES_VAULT} for configs + secrets to resolve together.
|
|
14
|
+
*/
|
|
15
|
+
export const app = oke({
|
|
16
|
+
name: "notes",
|
|
17
|
+
secrets: NOTES_VAULT,
|
|
18
|
+
gate: {
|
|
19
|
+
auth: {
|
|
20
|
+
cookies: { enabled: true },
|
|
21
|
+
emailAndPassword: { enabled: true },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
.plug(
|
|
26
|
+
csrf({
|
|
27
|
+
allowNoHeader: false,
|
|
28
|
+
allowOrigins: [...viteOrigins],
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
.plug(
|
|
32
|
+
cors({
|
|
33
|
+
origin: [...viteOrigins],
|
|
34
|
+
credentials: true,
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
.plug(
|
|
38
|
+
passkey({
|
|
39
|
+
origins: [...viteOrigins, "http://127.0.0.1:6530", "http://localhost:6530"],
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
7
42
|
|
|
8
43
|
export type App = typeof app;
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Order matches how you usually extend the starter:
|
|
5
5
|
* locales → store → gate → vault → channel → (AI via `oke ai setup`).
|
|
6
|
-
*
|
|
6
|
+
* Vault contracts live in `src/vault.ts` (re-exported below).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import "@/locales";
|
|
10
10
|
|
|
11
|
-
import { channel, gate, store
|
|
11
|
+
import { channel, gate, store } from "okengine";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import * as schema from "@/db/schema.decl";
|
|
14
14
|
|
|
15
|
+
export * from "@/vault";
|
|
16
|
+
|
|
15
17
|
// --- Store -------------------------------------------------------------------
|
|
16
18
|
|
|
17
19
|
/** SQL store for Notes (`schema.decl`). Drivers: pglite locally · postgres in docker. */
|
|
@@ -31,6 +33,8 @@ export const files = store.files("uploads");
|
|
|
31
33
|
* );
|
|
32
34
|
* export const notesMutate = gate.all(notesWrite, notesWriteRate);
|
|
33
35
|
* ```
|
|
36
|
+
*
|
|
37
|
+
* Cookie auth + passkey are wired in `app.ts`; client `Can` / `authorize` are UI-only.
|
|
34
38
|
*/
|
|
35
39
|
export const notesWrite = gate.policy("notes:write", () => true);
|
|
36
40
|
|
|
@@ -45,14 +49,6 @@ export const notesWriteRate = gate.rate({
|
|
|
45
49
|
/** Reuse on every notes mutate route. */
|
|
46
50
|
export const notesMutate = gate.all(notesWrite, notesWriteRate);
|
|
47
51
|
|
|
48
|
-
// --- Vault -------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
/** HMAC secret for outbound note webhooks (`fx.vault.get` on create). */
|
|
51
|
-
export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
|
|
52
|
-
description: "HMAC secret for outbound note webhooks",
|
|
53
|
-
dev: "dev-webhook-secret-change-me",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
52
|
// --- Channel -----------------------------------------------------------------
|
|
57
53
|
|
|
58
54
|
const mail = channel.email({ from: "Notes <notes@localhost>" });
|
|
@@ -67,4 +63,9 @@ export const noteCreatedMail = mail.template("note-created", {
|
|
|
67
63
|
});
|
|
68
64
|
|
|
69
65
|
// --- AI ----------------------------------------------------------------------
|
|
70
|
-
// `oke ai setup` / create-oke --ai
|
|
66
|
+
// Appended by `oke ai setup` / `create-oke --ai`.
|
|
67
|
+
// When an embed model is chosen, setup also wires Notes hybrid search:
|
|
68
|
+
// oke({ store: { search: { embed: { model: embedModel, dims: 768 } } } })
|
|
69
|
+
// body: field.text().searchable().embed()
|
|
70
|
+
// Registry cloud: provider "openrouter" + OPENROUTER_API_KEY (no baseUrl).
|
|
71
|
+
// Local self-host still needs an explicit baseUrl (or OKE_AI_URL on the binding).
|
|
@@ -3,11 +3,15 @@ import { store, field } from "okengine";
|
|
|
3
3
|
/**
|
|
4
4
|
* Notes domain — abstract declarations, emitted to
|
|
5
5
|
* `src/db/schema.drizzle.ts` for the active dialect by `oke db` / `oke dev`.
|
|
6
|
+
*
|
|
7
|
+
* `.searchable()` is free BM25. With `oke ai setup` (embed model chosen),
|
|
8
|
+
* body also gets bare `.embed()` and `oke({ store: { search: { embed } } })`
|
|
9
|
+
* stamps the project default (model + dims).
|
|
6
10
|
*/
|
|
7
11
|
export const notes = store.schema.table("notes", {
|
|
8
12
|
id: field.id().primaryKey(),
|
|
9
|
-
title: field.text().notNull(),
|
|
10
|
-
body: field.text().notNull(),
|
|
13
|
+
title: field.text().searchable({ weight: 2 }).notNull(),
|
|
14
|
+
body: field.text().searchable().notNull(),
|
|
11
15
|
archivedAt: field.timestamp(),
|
|
12
16
|
createdAt: field.timestamp().notNull().now(),
|
|
13
17
|
});
|
|
@@ -19,7 +19,7 @@ async function welcomeNote(fx: Fx) {
|
|
|
19
19
|
title: "Welcome",
|
|
20
20
|
body: "Advanced Notes starter — attach files, summarize with AI, daily digest clock.",
|
|
21
21
|
archivedAt: null,
|
|
22
|
-
createdAt:
|
|
22
|
+
createdAt: new Date("2026-01-15T10:00:00.000Z"),
|
|
23
23
|
},
|
|
24
24
|
);
|
|
25
25
|
}
|
|
@@ -33,7 +33,7 @@ async function sampleNotes(fx: Fx) {
|
|
|
33
33
|
title: "Shipping checklist",
|
|
34
34
|
body: "Confirm schema with oke db push (or migrate in docker), then oke db seed.",
|
|
35
35
|
archivedAt: null,
|
|
36
|
-
createdAt:
|
|
36
|
+
createdAt: new Date("2026-01-15T10:01:00.000Z"),
|
|
37
37
|
},
|
|
38
38
|
);
|
|
39
39
|
await fx.store(db).upsert(
|
|
@@ -44,7 +44,7 @@ async function sampleNotes(fx: Fx) {
|
|
|
44
44
|
title: "Try attach",
|
|
45
45
|
body: "POST /notes/:id/attach stores a text blob on files:uploads.",
|
|
46
46
|
archivedAt: null,
|
|
47
|
-
createdAt:
|
|
47
|
+
createdAt: new Date("2026-01-15T10:02:00.000Z"),
|
|
48
48
|
},
|
|
49
49
|
);
|
|
50
50
|
await fx.store(db).upsert(
|
|
@@ -55,7 +55,7 @@ async function sampleNotes(fx: Fx) {
|
|
|
55
55
|
title: "Try summarize",
|
|
56
56
|
body: "POST /notes/:id/summarize uses fx.ask with the prompt's via recovery chain.",
|
|
57
57
|
archivedAt: null,
|
|
58
|
-
createdAt:
|
|
58
|
+
createdAt: new Date("2026-01-15T10:03:00.000Z"),
|
|
59
59
|
},
|
|
60
60
|
);
|
|
61
61
|
}
|
|
@@ -3,14 +3,15 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
/** First-run welcome — visit :6530/ after `oke dev` (browser code block; curl stays JSON). */
|
|
5
5
|
export const root = on(
|
|
6
|
-
http.get(
|
|
7
|
-
flow({
|
|
6
|
+
http.get({
|
|
8
7
|
out: z.object({
|
|
9
8
|
ok: z.literal(true),
|
|
10
9
|
app: z.string(),
|
|
11
10
|
try: z.array(z.string()),
|
|
12
11
|
console: z.string(),
|
|
13
12
|
}),
|
|
13
|
+
}).public(),
|
|
14
|
+
flow({
|
|
14
15
|
do: () => ({
|
|
15
16
|
ok: true as const,
|
|
16
17
|
app: "notes",
|
|
@@ -3,26 +3,23 @@ import { eq } from "drizzle-orm";
|
|
|
3
3
|
|
|
4
4
|
import { db, notesMutate } from "@/core";
|
|
5
5
|
import { notes } from "@/db/schema.decl";
|
|
6
|
-
import { NoteIdIn, NoteOut, NotFound } from "../shapes";
|
|
6
|
+
import { NoteIdIn, NoteOut, NotFound, toIsoInstant } from "../shapes";
|
|
7
7
|
|
|
8
8
|
/** Soft-archive a note. */
|
|
9
9
|
export const archive = on(
|
|
10
|
-
http.post().gate(notesMutate),
|
|
10
|
+
http.post({ in: NoteIdIn, out: NoteOut, errors: { NotFound } }).gate(notesMutate),
|
|
11
11
|
flow({
|
|
12
|
-
in: NoteIdIn,
|
|
13
|
-
out: NoteOut,
|
|
14
|
-
errors: { NotFound },
|
|
15
12
|
do: async (input, fx) => {
|
|
16
13
|
const row = await fx.store(db).findById(notes, input.id);
|
|
17
14
|
if (!row) return fail("NotFound", { id: input.id });
|
|
18
|
-
const archivedAt = fx.clock.now();
|
|
15
|
+
const archivedAt = new Date(fx.clock.now());
|
|
19
16
|
await fx.store(db).update(notes).set({ archivedAt }).where(eq(notes.id, input.id));
|
|
20
17
|
return {
|
|
21
18
|
id: String(row.id),
|
|
22
19
|
title: String(row.title),
|
|
23
20
|
body: String(row.body),
|
|
24
|
-
archivedAt,
|
|
25
|
-
createdAt:
|
|
21
|
+
archivedAt: toIsoInstant(archivedAt),
|
|
22
|
+
createdAt: toIsoInstant(row.createdAt),
|
|
26
23
|
};
|
|
27
24
|
},
|
|
28
25
|
}),
|
|
@@ -6,11 +6,8 @@ import { NoteAttachIn, NoteAttachOut, NotFound } from "../shapes";
|
|
|
6
6
|
|
|
7
7
|
/** Store a text attachment next to a note (`files:uploads`). */
|
|
8
8
|
export const attach = on(
|
|
9
|
-
http.post().gate(notesMutate),
|
|
9
|
+
http.post({ in: NoteAttachIn, out: NoteAttachOut, errors: { NotFound } }).gate(notesMutate),
|
|
10
10
|
flow({
|
|
11
|
-
in: NoteAttachIn,
|
|
12
|
-
out: NoteAttachOut,
|
|
13
|
-
errors: { NotFound },
|
|
14
11
|
do: async (input, fx) => {
|
|
15
12
|
const row = await fx.store(db).findById(notes, input.id);
|
|
16
13
|
if (!row) return fail("NotFound", { id: input.id });
|
|
@@ -2,15 +2,12 @@ import { on, flow, http, fail } from "okengine/http";
|
|
|
2
2
|
|
|
3
3
|
import { db } from "@/core";
|
|
4
4
|
import { notes } from "@/db/schema.decl";
|
|
5
|
-
import { NoteIdIn, NoteOut, NotFound } from "../shapes";
|
|
5
|
+
import { NoteIdIn, NoteOut, NotFound, toIsoInstant } from "../shapes";
|
|
6
6
|
|
|
7
7
|
/** Fetch one note by id. */
|
|
8
8
|
export const get = on(
|
|
9
|
-
http.get().public(),
|
|
9
|
+
http.get({ in: NoteIdIn, out: NoteOut, errors: { NotFound } }).public(),
|
|
10
10
|
flow({
|
|
11
|
-
in: NoteIdIn,
|
|
12
|
-
out: NoteOut,
|
|
13
|
-
errors: { NotFound },
|
|
14
11
|
do: async (input, fx) => {
|
|
15
12
|
const row = await fx.store(db).findById(notes, input.id);
|
|
16
13
|
if (!row) return fail("NotFound", { id: input.id });
|
|
@@ -18,8 +15,8 @@ export const get = on(
|
|
|
18
15
|
id: String(row.id),
|
|
19
16
|
title: String(row.title),
|
|
20
17
|
body: String(row.body),
|
|
21
|
-
archivedAt: row.archivedAt == null ? null :
|
|
22
|
-
createdAt:
|
|
18
|
+
archivedAt: row.archivedAt == null ? null : toIsoInstant(row.archivedAt),
|
|
19
|
+
createdAt: toIsoInstant(row.createdAt),
|
|
23
20
|
};
|
|
24
21
|
},
|
|
25
22
|
}),
|
|
@@ -50,11 +50,10 @@ function unwrapSummaryText(text: string): string {
|
|
|
50
50
|
* Exhausted / failed asks surface as Unavailable — never a body excerpt.
|
|
51
51
|
*/
|
|
52
52
|
export const summarize = on(
|
|
53
|
-
http.post().gate(
|
|
53
|
+
http.post({ in: NoteSummarizeIn, out: NoteSummarizeOut, errors: { NotFound, Unavailable } }).gate(
|
|
54
|
+
notesMutate,
|
|
55
|
+
),
|
|
54
56
|
flow({
|
|
55
|
-
in: NoteSummarizeIn,
|
|
56
|
-
out: NoteSummarizeOut,
|
|
57
|
-
errors: { NotFound, Unavailable },
|
|
58
57
|
do: async (input, fx) => {
|
|
59
58
|
const row = await fx.store(db).findById(notes, input.id);
|
|
60
59
|
if (!row) return fail("NotFound", { id: input.id });
|
|
@@ -2,19 +2,17 @@ import { on, flow, http } from "okengine/http";
|
|
|
2
2
|
|
|
3
3
|
import { db, notesMutate, webhookSecret } from "@/core";
|
|
4
4
|
import { notes } from "@/db/schema.decl";
|
|
5
|
-
import { NoteCreateIn, NoteOut } from "./shapes";
|
|
5
|
+
import { NoteCreateIn, NoteOut, toIsoInstant } from "./shapes";
|
|
6
6
|
import { noteCreated } from "./signals";
|
|
7
7
|
|
|
8
8
|
/** Create a note, emit `note-created`, touch vault. */
|
|
9
9
|
export const create = on(
|
|
10
|
-
http.post().gate(notesMutate),
|
|
10
|
+
http.post({ in: NoteCreateIn, out: NoteOut }).gate(notesMutate),
|
|
11
11
|
flow({
|
|
12
|
-
in: NoteCreateIn,
|
|
13
|
-
out: NoteOut,
|
|
14
12
|
do: async (input, fx) => {
|
|
15
13
|
await fx.vault.get(webhookSecret);
|
|
16
14
|
const id = fx.id();
|
|
17
|
-
const createdAt = fx.clock.now();
|
|
15
|
+
const createdAt = new Date(fx.clock.now());
|
|
18
16
|
await fx.store(db).insert(notes).values({
|
|
19
17
|
id,
|
|
20
18
|
title: input.title,
|
|
@@ -28,7 +26,7 @@ export const create = on(
|
|
|
28
26
|
title: input.title,
|
|
29
27
|
body: input.body,
|
|
30
28
|
archivedAt: null,
|
|
31
|
-
createdAt,
|
|
29
|
+
createdAt: toIsoInstant(createdAt),
|
|
32
30
|
};
|
|
33
31
|
},
|
|
34
32
|
}),
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { on, flow,
|
|
1
|
+
import { on, flow, clock } from "okengine/http";
|
|
2
2
|
import { isNull } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
import { db } from "@/core";
|
|
5
5
|
import { notes } from "@/db/schema.decl";
|
|
6
|
-
import {
|
|
6
|
+
import { toIsoInstant } from "./shapes";
|
|
7
|
+
|
|
8
|
+
export const digestClock = clock("notes.digest", { every: "1d" });
|
|
7
9
|
|
|
8
10
|
/** Daily count of active notes (frozen under test drivers). */
|
|
9
11
|
export const digest = on(
|
|
10
|
-
|
|
12
|
+
digestClock,
|
|
11
13
|
flow({
|
|
12
|
-
out: NoteDigestOut,
|
|
13
14
|
do: async (_input, fx) => {
|
|
14
15
|
const rows = await fx.store(db).select().from(notes).where(isNull(notes.archivedAt));
|
|
15
|
-
return { active: rows.length, at: fx.clock.now() };
|
|
16
|
+
return { active: rows.length, at: toIsoInstant(new Date(fx.clock.now())) };
|
|
16
17
|
},
|
|
17
18
|
}),
|
|
18
19
|
);
|
|
@@ -3,13 +3,12 @@ import { isNull } from "drizzle-orm";
|
|
|
3
3
|
|
|
4
4
|
import { db } from "@/core";
|
|
5
5
|
import { notes } from "@/db/schema.decl";
|
|
6
|
-
import { NoteListOut } from "./shapes";
|
|
6
|
+
import { NoteListOut, toIsoInstant } from "./shapes";
|
|
7
7
|
|
|
8
8
|
/** List active (non-archived) notes, newest first. */
|
|
9
9
|
export const list = on(
|
|
10
|
-
http.get().public(),
|
|
10
|
+
http.get({ out: NoteListOut }).public(),
|
|
11
11
|
flow({
|
|
12
|
-
out: NoteListOut,
|
|
13
12
|
do: async (input, fx) => {
|
|
14
13
|
const rows = await fx.store(db).select().from(notes).where(isNull(notes.archivedAt));
|
|
15
14
|
const data = [...rows]
|
|
@@ -18,8 +17,8 @@ export const list = on(
|
|
|
18
17
|
id: String(r.id),
|
|
19
18
|
title: String(r.title),
|
|
20
19
|
body: String(r.body),
|
|
21
|
-
archivedAt: r.archivedAt == null ? null :
|
|
22
|
-
createdAt:
|
|
20
|
+
archivedAt: r.archivedAt == null ? null : toIsoInstant(r.archivedAt),
|
|
21
|
+
createdAt: toIsoInstant(r.createdAt),
|
|
23
22
|
}));
|
|
24
23
|
return fx.json.withQuery(data, input);
|
|
25
24
|
},
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
/** Instant on the HTTP wire — ISO-8601 UTC. */
|
|
4
|
+
export const IsoInstant = z.iso.datetime();
|
|
5
|
+
|
|
3
6
|
export const NoteCreateIn = z.object({
|
|
4
7
|
title: z.string().min(1).max(200),
|
|
5
8
|
body: z.string().min(1).max(10_000),
|
|
@@ -9,8 +12,8 @@ export const NoteOut = z.object({
|
|
|
9
12
|
id: z.string(),
|
|
10
13
|
title: z.string(),
|
|
11
14
|
body: z.string(),
|
|
12
|
-
archivedAt:
|
|
13
|
-
createdAt:
|
|
15
|
+
archivedAt: IsoInstant.nullable(),
|
|
16
|
+
createdAt: IsoInstant,
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
/** List `data` is the item array. Pagination lives in HTTP `meta`. */
|
|
@@ -37,7 +40,7 @@ export const NoteAttachOut = z.object({
|
|
|
37
40
|
|
|
38
41
|
export const NoteDigestOut = z.object({
|
|
39
42
|
active: z.number().int().nonnegative(),
|
|
40
|
-
at:
|
|
43
|
+
at: IsoInstant,
|
|
41
44
|
});
|
|
42
45
|
|
|
43
46
|
export const NoteSummarizeIn = z.object({
|
|
@@ -55,3 +58,10 @@ export const NoteSummarizeOut = z.object({
|
|
|
55
58
|
export const Unavailable = z.object({
|
|
56
59
|
message: z.string(),
|
|
57
60
|
});
|
|
61
|
+
|
|
62
|
+
/** Map a store temporal (`Date` / ISO / epoch-ms) to an ISO wire string. */
|
|
63
|
+
export function toIsoInstant(value: unknown): string {
|
|
64
|
+
if (value instanceof Date) return value.toISOString();
|
|
65
|
+
if (typeof value === "string") return new Date(value).toISOString();
|
|
66
|
+
return new Date(Number(value)).toISOString();
|
|
67
|
+
}
|
|
@@ -2,8 +2,7 @@ import { signal } from "okengine";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
/** Fired after a note is persisted — subscriber sends email. */
|
|
5
|
-
export const noteCreated = signal("note-created", {
|
|
6
|
-
delivery: "once",
|
|
5
|
+
export const noteCreated = signal.once("note-created", {
|
|
7
6
|
retries: 3,
|
|
8
7
|
deadLetter: true,
|
|
9
8
|
schema: z.object({
|