@recallkit/web 0.1.1

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.
Files changed (64) hide show
  1. package/next-env.d.ts +6 -0
  2. package/next.config.ts +13 -0
  3. package/package.json +40 -0
  4. package/public/logo.png +0 -0
  5. package/public/textures/bg-scene.png +0 -0
  6. package/src/app/api/_lib/guards.ts +35 -0
  7. package/src/app/api/_lib/limits.ts +6 -0
  8. package/src/app/api/_lib/responses.ts +9 -0
  9. package/src/app/api/commit/complete/route.ts +112 -0
  10. package/src/app/api/commit/preview/route.ts +71 -0
  11. package/src/app/api/commit/route.ts +16 -0
  12. package/src/app/api/memory-cache/route.ts +50 -0
  13. package/src/app/api/pending/[id]/delete/route.ts +21 -0
  14. package/src/app/api/pending/[id]/route.ts +47 -0
  15. package/src/app/api/pending/route.ts +41 -0
  16. package/src/app/api/security.ts +25 -0
  17. package/src/app/api/status/route.ts +35 -0
  18. package/src/app/dashboard/page.tsx +57 -0
  19. package/src/app/drafts/page.tsx +5 -0
  20. package/src/app/globals.css +10 -0
  21. package/src/app/icon.png +0 -0
  22. package/src/app/layout.tsx +43 -0
  23. package/src/app/page.tsx +132 -0
  24. package/src/app/settings/page.tsx +76 -0
  25. package/src/components/ArrowRightIcon.tsx +25 -0
  26. package/src/components/CommitPreview.tsx +156 -0
  27. package/src/components/CopyValue.tsx +49 -0
  28. package/src/components/MemoryInbox.tsx +74 -0
  29. package/src/components/RetrievedMemories.tsx +36 -0
  30. package/src/components/TopNav.tsx +39 -0
  31. package/src/components/WalletConnectButton.tsx +68 -0
  32. package/src/components/inbox/EmptyInbox.tsx +20 -0
  33. package/src/components/inbox/InboxStats.tsx +41 -0
  34. package/src/components/inbox/MemoryCandidateList.tsx +90 -0
  35. package/src/components/inbox/MemoryCandidateRow.tsx +195 -0
  36. package/src/components/memory-cache/CachedMemoryList.tsx +47 -0
  37. package/src/components/memory-cache/EmptyCache.tsx +13 -0
  38. package/src/hooks/useCommitFlow.ts +55 -0
  39. package/src/hooks/useMemoryCache.ts +44 -0
  40. package/src/hooks/usePendingMemories.ts +137 -0
  41. package/src/hooks/useWallet.ts +69 -0
  42. package/src/lib/api.ts +71 -0
  43. package/src/lib/wallet.ts +88 -0
  44. package/src/services/commitMemories.ts +153 -0
  45. package/src/services/signerApi.ts +67 -0
  46. package/src/services/types.ts +22 -0
  47. package/src/stores/appStore.ts +18 -0
  48. package/src/stores/createStore.ts +41 -0
  49. package/src/stores/slices/memoryCacheSlice.ts +29 -0
  50. package/src/stores/slices/pendingMemorySlice.ts +21 -0
  51. package/src/stores/slices/walletSlice.ts +24 -0
  52. package/src/styles/base.css +61 -0
  53. package/src/styles/buttons.css +53 -0
  54. package/src/styles/data-display.css +485 -0
  55. package/src/styles/forms.css +86 -0
  56. package/src/styles/landing.css +75 -0
  57. package/src/styles/layout.css +111 -0
  58. package/src/styles/navigation.css +121 -0
  59. package/src/styles/overlays.css +65 -0
  60. package/src/styles/tokens.css +26 -0
  61. package/src/styles/utilities.css +358 -0
  62. package/src/utils/errors.ts +5 -0
  63. package/src/utils/format.ts +37 -0
  64. package/tsconfig.json +44 -0
package/next-env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/next.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { NextConfig } from "next";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const monorepoRoot = path.join(process.cwd(), "../..");
6
+ const isMonorepo = fs.existsSync(path.join(monorepoRoot, "pnpm-workspace.yaml"));
7
+
8
+ const nextConfig: NextConfig = {
9
+ transpilePackages: ["@recallkit/core"],
10
+ ...(isMonorepo ? { outputFileTracingRoot: monorepoRoot } : {}),
11
+ };
12
+
13
+ export default nextConfig;
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@recallkit/web",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "dependencies": {
6
+ "@arkiv-network/sdk": "^0.6.8",
7
+ "lucide-react": "^1.16.0",
8
+ "next": "^16.2.6",
9
+ "react": "^19.2.6",
10
+ "react-dom": "^19.2.6",
11
+ "viem": "^2.50.4",
12
+ "@recallkit/core": "0.1.1"
13
+ },
14
+ "devDependencies": {
15
+ "@types/node": "^25.9.1",
16
+ "@types/react": "^19.2.15",
17
+ "@types/react-dom": "^19.2.3",
18
+ "typescript": "^5.9.3"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "files": [
24
+ "src",
25
+ "public",
26
+ "next.config.ts",
27
+ "tsconfig.json",
28
+ "next-env.d.ts"
29
+ ],
30
+ "engines": {
31
+ "node": ">=20.9.0"
32
+ },
33
+ "scripts": {
34
+ "dev": "next dev -p ${RECALLKIT_PORT:-4317}",
35
+ "build": "next build",
36
+ "start": "next start -p ${RECALLKIT_PORT:-4317}",
37
+ "typecheck": "next typegen && tsc -p tsconfig.json --noEmit",
38
+ "lint": "next typegen && tsc -p tsconfig.json --noEmit"
39
+ }
40
+ }
Binary file
Binary file
@@ -0,0 +1,35 @@
1
+ export const PENDING_ID_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
2
+
3
+ export function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+
7
+ export function isStringArray(value: unknown, maxItems: number, maxLength: number): value is string[] {
8
+ return (
9
+ Array.isArray(value) &&
10
+ value.length <= maxItems &&
11
+ value.every((item) => typeof item === "string" && item.length > 0 && item.length <= maxLength)
12
+ );
13
+ }
14
+
15
+ export function isStringRecord(
16
+ value: unknown,
17
+ maxKeys: number,
18
+ maxLength: number,
19
+ ): value is Record<string, string> {
20
+ if (!isRecord(value)) return false;
21
+ const entries = Object.entries(value);
22
+ return (
23
+ entries.length <= maxKeys &&
24
+ entries.every(
25
+ ([key, entryValue]) =>
26
+ key.length <= maxLength &&
27
+ typeof entryValue === "string" &&
28
+ entryValue.length <= maxLength,
29
+ )
30
+ );
31
+ }
32
+
33
+ export function isEvmAddress(value: string): boolean {
34
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
35
+ }
@@ -0,0 +1,6 @@
1
+ export const MAX_MEMORY_TEXT_LENGTH = 4_000;
2
+ export const MAX_REASON_LENGTH = 1_000;
3
+ export const MAX_ID_LENGTH = 128;
4
+ export const MAX_PENDING_IDS = 100;
5
+ export const MAX_PACKS = 20;
6
+ export const MAX_ENTITY_KEYS = 20;
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ export function badRequest(error: string) {
4
+ return NextResponse.json({ ok: false, error }, { status: 400 });
5
+ }
6
+
7
+ export function notFound(error: string) {
8
+ return NextResponse.json({ ok: false, error }, { status: 404 });
9
+ }
@@ -0,0 +1,112 @@
1
+ import {
2
+ clearCommittedPending,
3
+ isCachedMemoryPackPayload,
4
+ isMemoryIndexCheckpointPayload,
5
+ readCacheJson,
6
+ readConfig,
7
+ type MemoryIndexCheckpointPayload,
8
+ type CachedMemoryPackPayload,
9
+ writeCacheJson,
10
+ } from "@recallkit/core";
11
+ import { NextResponse } from "next/server";
12
+ import { rejectCrossOriginMutation } from "../../security";
13
+ import { isRecord, isStringArray, isStringRecord } from "../../_lib/guards";
14
+ import {
15
+ MAX_ENTITY_KEYS,
16
+ MAX_ID_LENGTH,
17
+ MAX_PACKS,
18
+ MAX_PENDING_IDS,
19
+ } from "../../_lib/limits";
20
+ import { badRequest } from "../../_lib/responses";
21
+
22
+ export async function POST(request: Request) {
23
+ const rejected = rejectCrossOriginMutation(request);
24
+ if (rejected) return rejected;
25
+
26
+ const body = await request.json();
27
+ const validation = await validateCommitCompleteBody(body);
28
+ if (!validation.ok) return badRequest(validation.error);
29
+
30
+ const { pendingIds, indexCheckpoint, packs, entityKeys } = validation.body;
31
+ if (indexCheckpoint) {
32
+ await writeCacheJson("latest-index.json", indexCheckpoint);
33
+ }
34
+ if (packs) {
35
+ const existingPacks = (await readCacheJson<CachedMemoryPackPayload[]>("latest-packs.json")) ?? [];
36
+ await writeCacheJson("latest-packs.json", mergeMemoryPacks(existingPacks, packs));
37
+ }
38
+ if (pendingIds.length) {
39
+ await clearCommittedPending(pendingIds);
40
+ }
41
+
42
+ return NextResponse.json({ ok: true, entityKeys });
43
+ }
44
+
45
+ type CommitCompleteBody = {
46
+ pendingIds: string[];
47
+ indexCheckpoint?: MemoryIndexCheckpointPayload;
48
+ packs?: CachedMemoryPackPayload[];
49
+ entityKeys: Record<string, string>;
50
+ };
51
+
52
+ type ValidationResult =
53
+ | { ok: true; body: CommitCompleteBody }
54
+ | { ok: false; error: string };
55
+
56
+ async function validateCommitCompleteBody(body: unknown): Promise<ValidationResult> {
57
+ if (!isRecord(body)) return { ok: false, error: "Request body must be an object." };
58
+
59
+ const pendingIds = body.pendingIds ?? [];
60
+ if (!isStringArray(pendingIds, MAX_PENDING_IDS, MAX_ID_LENGTH)) {
61
+ return { ok: false, error: "pendingIds must be a bounded array of strings." };
62
+ }
63
+
64
+ const entityKeys = body.entityKeys ?? {};
65
+ if (!isStringRecord(entityKeys, MAX_ENTITY_KEYS, MAX_ID_LENGTH)) {
66
+ return { ok: false, error: "entityKeys must be a bounded object of strings." };
67
+ }
68
+
69
+ const indexCheckpoint = body.indexCheckpoint;
70
+ if (indexCheckpoint !== undefined && !isMemoryIndexCheckpointPayload(indexCheckpoint)) {
71
+ return { ok: false, error: "indexCheckpoint is invalid." };
72
+ }
73
+
74
+ const packs = body.packs;
75
+ if (packs !== undefined && !isCachedMemoryPackArray(packs)) {
76
+ return { ok: false, error: "packs must be a bounded array of memory packs." };
77
+ }
78
+
79
+ const ownerWallet = indexCheckpoint?.ownerWallet ?? packs?.[0]?.ownerWallet;
80
+ if (ownerWallet && packs?.some((pack) => pack.ownerWallet.toLowerCase() !== ownerWallet.toLowerCase())) {
81
+ return { ok: false, error: "All committed packs must use the same owner wallet." };
82
+ }
83
+
84
+ const config = await readConfig();
85
+ if (config.ownerWallet && ownerWallet && config.ownerWallet.toLowerCase() !== ownerWallet.toLowerCase()) {
86
+ return { ok: false, error: "Commit owner wallet does not match the connected wallet." };
87
+ }
88
+
89
+ return {
90
+ ok: true,
91
+ body: {
92
+ pendingIds,
93
+ ...(indexCheckpoint ? { indexCheckpoint } : {}),
94
+ ...(packs ? { packs } : {}),
95
+ entityKeys,
96
+ },
97
+ };
98
+ }
99
+
100
+ function isCachedMemoryPackArray(value: unknown): value is CachedMemoryPackPayload[] {
101
+ return Array.isArray(value) && value.length <= MAX_PACKS && value.every(isCachedMemoryPackPayload);
102
+ }
103
+
104
+ function mergeMemoryPacks(
105
+ existingPacks: CachedMemoryPackPayload[],
106
+ newPacks: CachedMemoryPackPayload[],
107
+ ): CachedMemoryPackPayload[] {
108
+ const byEntityKey = new Map<string, CachedMemoryPackPayload>();
109
+ for (const pack of existingPacks) byEntityKey.set(pack.entityKey, pack);
110
+ for (const pack of newPacks) byEntityKey.set(pack.entityKey, pack);
111
+ return [...byEntityKey.values()];
112
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ createConsolidatedMemory,
3
+ detectConflicts,
4
+ markSuperseded,
5
+ normalizeMemoryScope,
6
+ pendingToMemoryRecord,
7
+ readCacheJson,
8
+ readConfig,
9
+ readPendingMemories,
10
+ type MemoryIndexCheckpointPayload,
11
+ type PendingMemoryCandidate,
12
+ } from "@recallkit/core";
13
+ import { NextResponse } from "next/server";
14
+ import { rejectCrossOriginMutation } from "../../security";
15
+ import { isRecord, isStringArray } from "../../_lib/guards";
16
+ import { MAX_ID_LENGTH, MAX_PENDING_IDS } from "../../_lib/limits";
17
+ import { badRequest } from "../../_lib/responses";
18
+
19
+ export async function POST(request: Request) {
20
+ const rejected = rejectCrossOriginMutation(request);
21
+ if (rejected) return rejected;
22
+
23
+ const body = await request.json();
24
+ if (!isRecord(body) || !isStringArray(body.ids, MAX_PENDING_IDS, MAX_ID_LENGTH)) {
25
+ return badRequest("ids must be a bounded array of strings.");
26
+ }
27
+
28
+ const selected = new Set(body.ids);
29
+ const [pending, config, index] = await Promise.all([
30
+ readPendingMemories(),
31
+ readConfig(),
32
+ readCacheJson<MemoryIndexCheckpointPayload>("latest-index.json"),
33
+ ]);
34
+
35
+ if (!config.ownerWallet) {
36
+ return badRequest("Connect a wallet before previewing commit.");
37
+ }
38
+
39
+ const selectedCandidates = pending.filter((candidate) => selected.has(candidate.id));
40
+ const candidates = selectedCandidates.filter(isCommittableCandidate);
41
+ const compiledMemories = candidates.map((candidate) =>
42
+ pendingToMemoryRecord(candidate, config.ownerWallet!),
43
+ );
44
+ const consolidated = createConsolidatedMemory(compiledMemories);
45
+ const memories = consolidated ? [...compiledMemories, consolidated] : compiledMemories;
46
+ const conflicts = detectConflicts(memories, index?.entries ?? []);
47
+ const baseIndexEntries = markSuperseded(index?.entries ?? [], conflicts);
48
+
49
+ return NextResponse.json({
50
+ ok: true,
51
+ signerWallet: config.ownerWallet,
52
+ network: "arkiv-braga",
53
+ selectedCount: selectedCandidates.length,
54
+ committablePendingIds: candidates.map((candidate) => candidate.id),
55
+ sessionDraftCount: selectedCandidates.filter(isSessionCandidate).length,
56
+ entities: ["memory_pack", "memory_index_checkpoint", "memory_commit", "memory_manifest"],
57
+ estimatedWrites: 4,
58
+ encryption: "enabled",
59
+ memories,
60
+ conflicts,
61
+ baseIndexEntries,
62
+ });
63
+ }
64
+
65
+ function isCommittableCandidate(candidate: PendingMemoryCandidate): boolean {
66
+ return candidate.status === "approved" && !isSessionCandidate(candidate);
67
+ }
68
+
69
+ function isSessionCandidate(candidate: PendingMemoryCandidate): boolean {
70
+ return normalizeMemoryScope(candidate.scope).level === "session";
71
+ }
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from "next/server";
2
+ import { rejectCrossOriginMutation } from "../security";
3
+
4
+ export async function POST(request: Request) {
5
+ const rejected = rejectCrossOriginMutation(request);
6
+ if (rejected) return rejected;
7
+
8
+ return NextResponse.json(
9
+ {
10
+ ok: false,
11
+ error:
12
+ "Commit must be executed from the approval app context so the connected wallet signs Arkiv writes.",
13
+ },
14
+ { status: 400 },
15
+ );
16
+ }
@@ -0,0 +1,50 @@
1
+ import { readCacheJson, type MemoryPackPayload } from "@recallkit/core";
2
+ import { NextResponse } from "next/server";
3
+
4
+ const MAX_PACKS = 20;
5
+ const MAX_MEMORIES = 8;
6
+
7
+ type CachedMemorySummary = {
8
+ id: string;
9
+ kind: string;
10
+ summary: string;
11
+ retrieval: { importance: number };
12
+ evidence: { confidence: number; source: string };
13
+ };
14
+
15
+ type CachePackSummary = {
16
+ memories: CachedMemorySummary[];
17
+ };
18
+
19
+ export async function GET() {
20
+ const packs = (await readCacheJson<MemoryPackPayload[]>("latest-packs.json")) ?? [];
21
+
22
+ return NextResponse.json({
23
+ packs: summarizePacks(packs),
24
+ });
25
+ }
26
+
27
+ function summarizePacks(packs: MemoryPackPayload[]): CachePackSummary[] {
28
+ let remaining = MAX_MEMORIES;
29
+ const summaries: CachePackSummary[] = [];
30
+
31
+ for (const pack of packs.slice(0, MAX_PACKS)) {
32
+ if (remaining <= 0) break;
33
+
34
+ const memories = pack.memories.slice(0, remaining).map((memory) => ({
35
+ id: memory.id,
36
+ kind: memory.kind,
37
+ summary: memory.summary,
38
+ retrieval: { importance: memory.retrieval.importance },
39
+ evidence: {
40
+ confidence: memory.evidence.confidence,
41
+ source: memory.evidence.source,
42
+ },
43
+ }));
44
+
45
+ remaining -= memories.length;
46
+ if (memories.length > 0) summaries.push({ memories });
47
+ }
48
+
49
+ return summaries;
50
+ }
@@ -0,0 +1,21 @@
1
+ import { deletePendingMemory } from "@recallkit/core";
2
+ import { NextResponse } from "next/server";
3
+ import { rejectCrossOriginMutation } from "../../../security";
4
+ import { PENDING_ID_PATTERN } from "../../../_lib/guards";
5
+ import { badRequest } from "../../../_lib/responses";
6
+
7
+ export async function POST(
8
+ request: Request,
9
+ context: { params: Promise<{ id: string }> },
10
+ ) {
11
+ const rejected = rejectCrossOriginMutation(request);
12
+ if (rejected) return rejected;
13
+
14
+ const params = await context.params;
15
+ if (!PENDING_ID_PATTERN.test(params.id)) {
16
+ return badRequest("Invalid pending memory id.");
17
+ }
18
+
19
+ const deleted = await deletePendingMemory(params.id);
20
+ return NextResponse.json({ ok: deleted });
21
+ }
@@ -0,0 +1,47 @@
1
+ import { isPendingMemoryStatus, normalizeMemoryScope, updatePendingMemory } from "@recallkit/core";
2
+ import type { MemoryScope } from "@recallkit/core/schemas";
3
+ import { NextResponse } from "next/server";
4
+ import { rejectCrossOriginMutation } from "../../security";
5
+ import { isRecord, PENDING_ID_PATTERN } from "../../_lib/guards";
6
+ import { MAX_MEMORY_TEXT_LENGTH, MAX_REASON_LENGTH } from "../../_lib/limits";
7
+ import { badRequest, notFound } from "../../_lib/responses";
8
+
9
+ export async function PATCH(
10
+ request: Request,
11
+ context: { params: Promise<{ id: string }> },
12
+ ) {
13
+ const rejected = rejectCrossOriginMutation(request);
14
+ if (rejected) return rejected;
15
+
16
+ const params = await context.params;
17
+ if (!PENDING_ID_PATTERN.test(params.id)) {
18
+ return badRequest("Invalid pending memory id.");
19
+ }
20
+
21
+ const body = await request.json();
22
+ if (!isRecord(body)) {
23
+ return badRequest("Request body must be an object.");
24
+ }
25
+ if (typeof body.text === "string" && body.text.length > MAX_MEMORY_TEXT_LENGTH) {
26
+ return badRequest("text is too long");
27
+ }
28
+ if (typeof body.reason === "string" && body.reason.length > MAX_REASON_LENGTH) {
29
+ return badRequest("reason is too long");
30
+ }
31
+ if (typeof body.scopeReason === "string" && body.scopeReason.length > MAX_REASON_LENGTH) {
32
+ return badRequest("scopeReason is too long");
33
+ }
34
+
35
+ const updated = await updatePendingMemory(params.id, {
36
+ ...(typeof body.text === "string" ? { text: body.text } : {}),
37
+ ...(typeof body.reason === "string" ? { reason: body.reason } : {}),
38
+ ...(typeof body.importance === "number" ? { importance: body.importance } : {}),
39
+ ...(isRecord(body.scope) ? { scope: normalizeMemoryScope(body.scope as Partial<MemoryScope>) } : {}),
40
+ ...(typeof body.scopeReason === "string" ? { scopeReason: body.scopeReason } : {}),
41
+ ...(isPendingMemoryStatus(body.status) ? { status: body.status } : {}),
42
+ });
43
+
44
+ if (!updated) return notFound("Memory draft not found.");
45
+
46
+ return NextResponse.json({ ok: true, candidate: updated });
47
+ }
@@ -0,0 +1,41 @@
1
+ import { addPendingMemory, compileMemoryCandidate, readPendingMemories } from "@recallkit/core";
2
+ import type { SuggestMemoryInput } from "@recallkit/core";
3
+ import { NextResponse } from "next/server";
4
+ import { rejectCrossOriginMutation } from "../security";
5
+ import { isRecord } from "../_lib/guards";
6
+ import { MAX_MEMORY_TEXT_LENGTH, MAX_REASON_LENGTH } from "../_lib/limits";
7
+ import { badRequest } from "../_lib/responses";
8
+
9
+ export async function GET() {
10
+ const pending = await readPendingMemories();
11
+ return NextResponse.json({ pending });
12
+ }
13
+
14
+ export async function POST(request: Request) {
15
+ const rejected = rejectCrossOriginMutation(request);
16
+ if (rejected) return rejected;
17
+
18
+ const body = await request.json();
19
+ if (!isRecord(body) || !isRecord(body.candidate)) {
20
+ return badRequest("candidate.text is required");
21
+ }
22
+ const candidateInput = body.candidate as SuggestMemoryInput;
23
+ if (!candidateInput.text) {
24
+ return badRequest("candidate.text is required");
25
+ }
26
+ if (candidateInput.text.length > MAX_MEMORY_TEXT_LENGTH) {
27
+ return badRequest("candidate.text is too long");
28
+ }
29
+ if (candidateInput.reason && candidateInput.reason.length > MAX_REASON_LENGTH) {
30
+ return badRequest("candidate.reason is too long");
31
+ }
32
+
33
+ const candidate = compileMemoryCandidate(candidateInput, "skill");
34
+ await addPendingMemory(candidate);
35
+
36
+ return NextResponse.json({
37
+ ok: true,
38
+ pendingId: candidate.id,
39
+ message: "Memory draft saved. It will only be committed to Arkiv after you approve and sign it.",
40
+ });
41
+ }
@@ -0,0 +1,25 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ export function rejectCrossOriginMutation(request: Request): NextResponse | undefined {
4
+ const origin = request.headers.get("origin");
5
+ if (!origin) return undefined;
6
+
7
+ const host = request.headers.get("host");
8
+ if (!host) {
9
+ return NextResponse.json({ ok: false, error: "Missing host header." }, { status: 403 });
10
+ }
11
+
12
+ let originHost: string;
13
+ try {
14
+ originHost = new URL(origin).host;
15
+ } catch (error) {
16
+ console.warn("Invalid origin header", error);
17
+ return NextResponse.json({ ok: false, error: "Invalid origin header." }, { status: 403 });
18
+ }
19
+
20
+ if (originHost !== host) {
21
+ return NextResponse.json({ ok: false, error: "Cross-origin request blocked." }, { status: 403 });
22
+ }
23
+
24
+ return undefined;
25
+ }
@@ -0,0 +1,35 @@
1
+ import { getWalletStatus, writeConfig } from "@recallkit/core";
2
+ import { NextResponse } from "next/server";
3
+ import { rejectCrossOriginMutation } from "../security";
4
+ import { isEvmAddress, isRecord } from "../_lib/guards";
5
+ import { badRequest } from "../_lib/responses";
6
+
7
+ export async function GET() {
8
+ return NextResponse.json(await getWalletStatus());
9
+ }
10
+
11
+ export async function POST(request: Request) {
12
+ const rejected = rejectCrossOriginMutation(request);
13
+ if (rejected) return rejected;
14
+
15
+ const body = await request.json();
16
+ if (!isRecord(body)) return badRequest("Request body must be an object.");
17
+ if (body.ownerWallet !== undefined && typeof body.ownerWallet !== "string") {
18
+ return badRequest("ownerWallet must be a string.");
19
+ }
20
+ if (body.ownerWallet && !isEvmAddress(body.ownerWallet)) {
21
+ return badRequest("ownerWallet must be an EVM address.");
22
+ }
23
+
24
+ const config = await writeConfig({
25
+ arkivChain: "braga",
26
+ ...(body.ownerWallet ? { ownerWallet: body.ownerWallet } : {}),
27
+ });
28
+
29
+ return NextResponse.json({
30
+ connected: Boolean(config.ownerWallet),
31
+ ownerWallet: config.ownerWallet,
32
+ network: config.ownerWallet ? config.arkivChain : undefined,
33
+ approvalAppUrl: config.approvalAppUrl,
34
+ });
35
+ }
@@ -0,0 +1,57 @@
1
+ import Link from "next/link";
2
+ import { getWalletStatus, readPendingMemories } from "@recallkit/core";
3
+ import { RetrievedMemories } from "@/components/RetrievedMemories";
4
+ import { ArrowRightIcon } from "@/components/ArrowRightIcon";
5
+
6
+ export const dynamic = "force-dynamic";
7
+ export const revalidate = 0;
8
+
9
+ export default async function DashboardPage() {
10
+ const [pending, status] = await Promise.all([
11
+ readPendingMemories(),
12
+ getWalletStatus(),
13
+ ]);
14
+
15
+ return (
16
+ <section className="page">
17
+ <div className="page-header">
18
+ <div className="page-header__title">
19
+ <div className="eyebrow">/ overview</div>
20
+ <h1 className="h1">Memory overview</h1>
21
+ <p className="lede">
22
+ Wallet-owned encrypted memory for AI agents. Review memory drafts, then sign approved commits.
23
+ </p>
24
+ </div>
25
+ <div className="page-header__actions">
26
+ <Link href="/drafts" className="btn btn--primary">
27
+ Review drafts
28
+ <ArrowRightIcon />
29
+ </Link>
30
+ </div>
31
+ </div>
32
+
33
+ <div className="stat-grid" style={{ gridTemplateColumns: "repeat(3, 1fr)" }}>
34
+ <div className="stat-grid__cell">
35
+ <div className="stat-grid__label">Arkiv Braga status</div>
36
+ <div
37
+ className={`stat-grid__value${status.connected ? " stat-grid__value--accent" : ""}`}
38
+ >
39
+ {status.connected ? "Ready" : "Wallet needed"}
40
+ </div>
41
+ </div>
42
+ <div className="stat-grid__cell">
43
+ <div className="stat-grid__label">Memory drafts</div>
44
+ <div className="stat-grid__value">{pending.length}</div>
45
+ </div>
46
+ <div className="stat-grid__cell">
47
+ <div className="stat-grid__label">Signing policy</div>
48
+ <div className="stat-grid__value" style={{ fontSize: 16, paddingTop: 6 }}>
49
+ Wallet only
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <RetrievedMemories />
55
+ </section>
56
+ );
57
+ }
@@ -0,0 +1,5 @@
1
+ import { MemoryInbox } from "@/components/MemoryInbox";
2
+
3
+ export default function InboxPage() {
4
+ return <MemoryInbox />;
5
+ }
@@ -0,0 +1,10 @@
1
+ @import "../styles/tokens.css";
2
+ @import "../styles/base.css";
3
+ @import "../styles/buttons.css";
4
+ @import "../styles/navigation.css";
5
+ @import "../styles/layout.css";
6
+ @import "../styles/landing.css";
7
+ @import "../styles/data-display.css";
8
+ @import "../styles/forms.css";
9
+ @import "../styles/overlays.css";
10
+ @import "../styles/utilities.css";
Binary file
@@ -0,0 +1,43 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter, Space_Grotesk, JetBrains_Mono } from "next/font/google";
3
+ import { TopNav } from "@/components/TopNav";
4
+ import "./globals.css";
5
+
6
+ const inter = Inter({
7
+ subsets: ["latin"],
8
+ weight: ["400", "500", "600", "700"],
9
+ display: "swap",
10
+ variable: "--font-sans",
11
+ });
12
+
13
+ const spaceGrotesk = Space_Grotesk({
14
+ subsets: ["latin"],
15
+ weight: ["400", "500", "600", "700"],
16
+ display: "swap",
17
+ variable: "--font-display",
18
+ });
19
+
20
+ const jetbrainsMono = JetBrains_Mono({
21
+ subsets: ["latin"],
22
+ weight: ["400", "500", "600"],
23
+ display: "swap",
24
+ variable: "--font-mono",
25
+ });
26
+
27
+ export const metadata: Metadata = {
28
+ title: "RecallKit",
29
+ description: "Wallet-owned encrypted memory for coding agents.",
30
+ };
31
+
32
+ export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
33
+ return (
34
+ <html lang="en" className={`${inter.variable} ${spaceGrotesk.variable} ${jetbrainsMono.variable}`}>
35
+ <body>
36
+ <main className="shell">
37
+ <TopNav />
38
+ {children}
39
+ </main>
40
+ </body>
41
+ </html>
42
+ );
43
+ }