wikimemory 0.2.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/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/npm-cli/scripts/cli-options.js +29 -0
- package/dist/npm-cli/scripts/cli.js +79 -0
- package/dist/npm-cli/scripts/client-tools.js +57 -0
- package/dist/npm-cli/scripts/deployment-record.js +72 -0
- package/dist/npm-cli/scripts/dev.js +49 -0
- package/dist/npm-cli/scripts/lifecycle-runtime.js +38 -0
- package/dist/npm-cli/scripts/package-root.js +15 -0
- package/dist/npm-cli/scripts/passkeys.js +197 -0
- package/dist/npm-cli/scripts/setup.js +523 -0
- package/dist/npm-cli/scripts/status.js +49 -0
- package/dist/npm-cli/scripts/uninstall.js +220 -0
- package/dist/npm-cli/scripts/upgrade.js +301 -0
- package/dist/npm-cli/src/version.js +2 -0
- package/dist/web/assets/index-CjnFBnXp.css +1 -0
- package/dist/web/assets/index-buhGyrxi.js +72 -0
- package/dist/web/index.html +14 -0
- package/migrations/0001_initial.sql +297 -0
- package/migrations/0002_passkeys.sql +30 -0
- package/migrations/0003_passkey_management.sql +38 -0
- package/migrations/0004_credential_bound_registration_tokens.sql +18 -0
- package/package.json +89 -0
- package/release-manifest.json +22 -0
- package/skills/wikimemory-ingest/SKILL.md +24 -0
- package/skills/wikimemory-ingest/agents/openai.yaml +4 -0
- package/skills/wikimemory-install/SKILL.md +81 -0
- package/skills/wikimemory-install/agents/openai.yaml +4 -0
- package/skills/wikimemory-lint/SKILL.md +18 -0
- package/skills/wikimemory-lint/agents/openai.yaml +4 -0
- package/skills/wikimemory-recall/SKILL.md +20 -0
- package/skills/wikimemory-recall/agents/openai.yaml +4 -0
- package/src/auth/local.ts +131 -0
- package/src/auth/passkey-api.ts +100 -0
- package/src/auth/passkey-management.ts +150 -0
- package/src/auth/passkey.ts +908 -0
- package/src/auth/props.ts +96 -0
- package/src/auth/resource.ts +36 -0
- package/src/domain/crypto.ts +27 -0
- package/src/domain/errors.ts +32 -0
- package/src/domain/export-service.ts +363 -0
- package/src/domain/guards.ts +9 -0
- package/src/domain/memory-service.ts +1092 -0
- package/src/domain/secret-scanner.ts +45 -0
- package/src/domain/text-chunk.ts +24 -0
- package/src/domain/types.ts +169 -0
- package/src/env.ts +20 -0
- package/src/index.ts +138 -0
- package/src/mcp/schemas.ts +117 -0
- package/src/mcp/server.ts +506 -0
- package/src/version.ts +2 -0
- package/src/web/app.ts +299 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface SecretFinding {
|
|
2
|
+
field: string;
|
|
3
|
+
category: "private_key" | "aws_access_key" | "provider_token" | "credential_assignment";
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const PATTERNS: Array<{ category: SecretFinding["category"]; regex: RegExp }> = [
|
|
8
|
+
{ category: "private_key", regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
9
|
+
{ category: "aws_access_key", regex: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
10
|
+
{
|
|
11
|
+
category: "provider_token",
|
|
12
|
+
regex: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
category: "credential_assignment",
|
|
16
|
+
regex: /\b(?:api[_-]?key|password|secret|token)\s*[:=]\s*["']?[^\s"']{12,}/gi
|
|
17
|
+
}
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
async function fingerprint(value: string): Promise<string> {
|
|
21
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
22
|
+
return Array.from(new Uint8Array(digest).slice(0, 8), (byte) =>
|
|
23
|
+
byte.toString(16).padStart(2, "0")
|
|
24
|
+
).join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function scanSecrets(
|
|
28
|
+
fields: Readonly<Record<string, string>>
|
|
29
|
+
): Promise<SecretFinding[]> {
|
|
30
|
+
const findings: SecretFinding[] = [];
|
|
31
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
for (const { category, regex } of PATTERNS) {
|
|
34
|
+
regex.lastIndex = 0;
|
|
35
|
+
for (const match of value.matchAll(regex)) {
|
|
36
|
+
const candidate = match[0];
|
|
37
|
+
const key = `${category}:${candidate}`;
|
|
38
|
+
if (seen.has(key)) continue;
|
|
39
|
+
seen.add(key);
|
|
40
|
+
findings.push({ field, category, fingerprint: await fingerprint(candidate) });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return findings;
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TextChunk {
|
|
2
|
+
body: string;
|
|
3
|
+
nextOffset: number | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function chunkText(value: string, offset: number, maximumCharacters: number): TextChunk {
|
|
7
|
+
const characters = Array.from(value);
|
|
8
|
+
const hardEnd = Math.min(characters.length, offset + maximumCharacters);
|
|
9
|
+
let end = hardEnd;
|
|
10
|
+
if (hardEnd < characters.length && maximumCharacters >= 8) {
|
|
11
|
+
const earliestBreak = offset + Math.floor(maximumCharacters * 0.6);
|
|
12
|
+
for (let index = hardEnd - 1; index >= earliestBreak; index -= 1) {
|
|
13
|
+
const character = characters[index];
|
|
14
|
+
if (character !== undefined && /\s/u.test(character)) {
|
|
15
|
+
end = index + 1;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
body: characters.slice(offset, end).join(""),
|
|
22
|
+
nextOffset: end < characters.length ? end : null
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export const DOCUMENT_TYPES: readonly ["system", "project", "topic", "source", "note"] = [
|
|
2
|
+
"system",
|
|
3
|
+
"project",
|
|
4
|
+
"topic",
|
|
5
|
+
"source",
|
|
6
|
+
"note"
|
|
7
|
+
];
|
|
8
|
+
export type DocumentType = (typeof DOCUMENT_TYPES)[number];
|
|
9
|
+
|
|
10
|
+
export const LINK_KINDS: readonly ["related", "part_of", "supersedes", "cites", "contradicts"] = [
|
|
11
|
+
"related",
|
|
12
|
+
"part_of",
|
|
13
|
+
"supersedes",
|
|
14
|
+
"cites",
|
|
15
|
+
"contradicts"
|
|
16
|
+
];
|
|
17
|
+
export type LinkKind = (typeof LINK_KINDS)[number];
|
|
18
|
+
|
|
19
|
+
export type MemoryScope = "memory:read" | "memory:write" | "memory:admin";
|
|
20
|
+
|
|
21
|
+
export interface ActorContext {
|
|
22
|
+
workspaceId: string;
|
|
23
|
+
principalId: string;
|
|
24
|
+
clientId: string;
|
|
25
|
+
agentLabel?: string;
|
|
26
|
+
scopes: ReadonlySet<MemoryScope>;
|
|
27
|
+
requestId: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MetadataPatch {
|
|
31
|
+
set?: Record<string, string | null>;
|
|
32
|
+
multi?: Record<string, { replace?: string[]; add?: string[]; remove?: string[] }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LinkValue {
|
|
36
|
+
kind: LinkKind;
|
|
37
|
+
targetSlug: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LinkPatch {
|
|
41
|
+
add?: LinkValue[];
|
|
42
|
+
remove?: LinkValue[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface IngestRequest {
|
|
46
|
+
operationId: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
slug: string;
|
|
49
|
+
expectedRevisionId?: string;
|
|
50
|
+
type?: DocumentType;
|
|
51
|
+
title?: string;
|
|
52
|
+
body?: string;
|
|
53
|
+
summary?: string | null;
|
|
54
|
+
metadata?: MetadataPatch;
|
|
55
|
+
links?: LinkPatch;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MetadataValue {
|
|
59
|
+
key: string;
|
|
60
|
+
value: string;
|
|
61
|
+
cardinality: "singleton" | "multi";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface StoredLink extends LinkValue {
|
|
65
|
+
origin: "explicit" | "body";
|
|
66
|
+
targetDocumentId: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DocumentSnapshot {
|
|
70
|
+
documentId: string;
|
|
71
|
+
workspaceId: string;
|
|
72
|
+
slug: string;
|
|
73
|
+
type: DocumentType;
|
|
74
|
+
revisionId: string;
|
|
75
|
+
revisionNumber: number;
|
|
76
|
+
parentRevisionId: string | null;
|
|
77
|
+
title: string;
|
|
78
|
+
body: string;
|
|
79
|
+
summary: string | null;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
principalId: string;
|
|
82
|
+
clientId: string;
|
|
83
|
+
agentLabel: string | null;
|
|
84
|
+
reason: string;
|
|
85
|
+
restoredFromRevisionId: string | null;
|
|
86
|
+
metadata: MetadataValue[];
|
|
87
|
+
links: StoredLink[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface IngestResult {
|
|
91
|
+
documentId: string;
|
|
92
|
+
revisionId: string;
|
|
93
|
+
revisionNumber: number;
|
|
94
|
+
slug: string;
|
|
95
|
+
idempotentReplay: boolean;
|
|
96
|
+
unresolvedReferences: string[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RestoreRequest {
|
|
100
|
+
operationId: string;
|
|
101
|
+
reason: string;
|
|
102
|
+
slug: string;
|
|
103
|
+
targetRevisionId: string;
|
|
104
|
+
expectedRevisionId: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface LinkRequest {
|
|
108
|
+
operationId: string;
|
|
109
|
+
reason: string;
|
|
110
|
+
sourceSlug: string;
|
|
111
|
+
expectedRevisionId: string;
|
|
112
|
+
action: "add" | "remove";
|
|
113
|
+
link: LinkValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface OwnerContext extends ActorContext {
|
|
117
|
+
role: "owner";
|
|
118
|
+
reauthenticatedAt: string;
|
|
119
|
+
credentialId?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface PurgeAuthorization {
|
|
123
|
+
id: string;
|
|
124
|
+
documentId: string;
|
|
125
|
+
slug: string;
|
|
126
|
+
expiresAt: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface RecallHit {
|
|
130
|
+
documentId: string;
|
|
131
|
+
revisionId: string;
|
|
132
|
+
slug: string;
|
|
133
|
+
type: DocumentType;
|
|
134
|
+
title: string;
|
|
135
|
+
summary: string | null;
|
|
136
|
+
snippet: string;
|
|
137
|
+
score: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface DocumentIndexEntry {
|
|
141
|
+
documentId: string;
|
|
142
|
+
revisionId: string;
|
|
143
|
+
revisionNumber: number;
|
|
144
|
+
slug: string;
|
|
145
|
+
type: DocumentType;
|
|
146
|
+
title: string;
|
|
147
|
+
summary: string | null;
|
|
148
|
+
updatedAt: string;
|
|
149
|
+
status: string | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface RevisionHeader {
|
|
153
|
+
revisionId: string;
|
|
154
|
+
revisionNumber: number;
|
|
155
|
+
parentRevisionId: string | null;
|
|
156
|
+
createdAt: string;
|
|
157
|
+
principalId: string;
|
|
158
|
+
clientId: string;
|
|
159
|
+
agentLabel: string | null;
|
|
160
|
+
reason: string;
|
|
161
|
+
restoredFromRevisionId: string | null;
|
|
162
|
+
requestHash: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface LintFinding {
|
|
166
|
+
kind: "unresolved_reference" | "orphan" | "missing_summary" | "stale_active_project";
|
|
167
|
+
slug: string;
|
|
168
|
+
detail: string;
|
|
169
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OAuthHelpers } from "@cloudflare/workers-oauth-provider";
|
|
2
|
+
|
|
3
|
+
export interface Env {
|
|
4
|
+
DB: D1Database;
|
|
5
|
+
OAUTH_KV: KVNamespace;
|
|
6
|
+
ASSETS: Fetcher;
|
|
7
|
+
OAUTH_PROVIDER: OAuthHelpers;
|
|
8
|
+
APP_ENV: "local" | "production";
|
|
9
|
+
APP_BASE_URL?: string;
|
|
10
|
+
SETUP_TOKEN_HASH?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateEnvironment(env: Env): void {
|
|
14
|
+
if (env.APP_ENV === "production") {
|
|
15
|
+
if (!env.APP_BASE_URL?.startsWith("https://"))
|
|
16
|
+
throw new Error("production APP_BASE_URL must use HTTPS");
|
|
17
|
+
if (!/^[a-f0-9]{64}$/u.test(env.SETUP_TOKEN_HASH ?? ""))
|
|
18
|
+
throw new Error("production SETUP_TOKEN_HASH must be a SHA-256 hex digest");
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
approveLocalAuthorization,
|
|
5
|
+
handleLocalAuthorization,
|
|
6
|
+
localAuthorizationOptions
|
|
7
|
+
} from "./auth/local";
|
|
8
|
+
import {
|
|
9
|
+
beginPasskeyAuthorization,
|
|
10
|
+
passkeyAuthorizationOptions,
|
|
11
|
+
productionWebOwner,
|
|
12
|
+
registrationOptions,
|
|
13
|
+
registrationVerify,
|
|
14
|
+
setupOptions,
|
|
15
|
+
setupVerify,
|
|
16
|
+
verifyPasskeyAuthorization
|
|
17
|
+
} from "./auth/passkey";
|
|
18
|
+
import { handlePasskeyApi } from "./auth/passkey-api";
|
|
19
|
+
import { downscopeAccessToken } from "./auth/props";
|
|
20
|
+
import { DomainError } from "./domain/errors";
|
|
21
|
+
import type { Env } from "./env";
|
|
22
|
+
import { validateEnvironment } from "./env";
|
|
23
|
+
import { mcpHandler } from "./mcp/server";
|
|
24
|
+
import { LATEST_SCHEMA_VERSION, WIKIMEMORY_VERSION } from "./version";
|
|
25
|
+
import { handleWebApi } from "./web/app";
|
|
26
|
+
|
|
27
|
+
const webHandler = {
|
|
28
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
29
|
+
validateEnvironment(env);
|
|
30
|
+
const url = new URL(request.url);
|
|
31
|
+
if (url.pathname === "/authorize")
|
|
32
|
+
return env.APP_ENV === "local"
|
|
33
|
+
? handleLocalAuthorization(request, env)
|
|
34
|
+
: beginPasskeyAuthorization(request, env, "mcp");
|
|
35
|
+
if (url.pathname === "/api/local-authorize/options" && request.method === "GET")
|
|
36
|
+
return safeJson(() => localAuthorizationOptions(request, env));
|
|
37
|
+
if (url.pathname === "/api/local-authorize/approve" && request.method === "POST")
|
|
38
|
+
return safeJson(() => approveLocalAuthorization(request, env));
|
|
39
|
+
if (url.pathname === "/api/auth/options" && request.method === "GET")
|
|
40
|
+
return safeJson(() => passkeyAuthorizationOptions(request, env));
|
|
41
|
+
if (url.pathname === "/auth/passkey/verify" && request.method === "POST")
|
|
42
|
+
return safeJson(() => verifyPasskeyAuthorization(request, env));
|
|
43
|
+
if (url.pathname === "/setup/options" && request.method === "POST")
|
|
44
|
+
return safeJson(() => setupOptions(request, env));
|
|
45
|
+
if (url.pathname === "/setup/verify" && request.method === "POST")
|
|
46
|
+
return safeJson(() => setupVerify(request, env));
|
|
47
|
+
if (url.pathname === "/passkeys/add/options" && request.method === "POST")
|
|
48
|
+
return safeJson(() => registrationOptions(request, env));
|
|
49
|
+
if (url.pathname === "/passkeys/add/verify" && request.method === "POST")
|
|
50
|
+
return safeJson(() => registrationVerify(request, env));
|
|
51
|
+
if (url.pathname === "/api/passkeys") return safeJson(() => handlePasskeyApi(request, env));
|
|
52
|
+
if (env.APP_ENV === "production" && url.pathname === "/app/login")
|
|
53
|
+
return beginPasskeyAuthorization(request, env, "web");
|
|
54
|
+
if (env.APP_ENV === "local" && url.pathname === "/test/passkey/login")
|
|
55
|
+
return beginPasskeyAuthorization(request, env, "web");
|
|
56
|
+
if (env.APP_ENV === "local" && url.pathname === "/app/test-passkey-whoami") {
|
|
57
|
+
return Response.json({ authenticated: (await productionWebOwner(request, env)) !== null });
|
|
58
|
+
}
|
|
59
|
+
if (url.pathname === "/api/app" || url.pathname.startsWith("/api/app/"))
|
|
60
|
+
return handleWebApi(request, env);
|
|
61
|
+
if (url.pathname === "/health") {
|
|
62
|
+
return Response.json({ status: "ok", service: "wikimemory", version: WIKIMEMORY_VERSION });
|
|
63
|
+
}
|
|
64
|
+
if (url.pathname === "/ready") {
|
|
65
|
+
await env.DB.batch([
|
|
66
|
+
env.DB.prepare("SELECT 1 FROM passkey_credentials LIMIT 1"),
|
|
67
|
+
env.DB.prepare("SELECT 1 FROM passkey_bootstrap LIMIT 1"),
|
|
68
|
+
env.DB.prepare("SELECT 1 FROM passkey_challenges LIMIT 1"),
|
|
69
|
+
env.DB.prepare("SELECT 1 FROM passkey_registration_tokens LIMIT 1")
|
|
70
|
+
]);
|
|
71
|
+
const schema = await env.DB.prepare(
|
|
72
|
+
"SELECT name FROM d1_migrations ORDER BY id DESC LIMIT 1"
|
|
73
|
+
).first<{ name: string }>();
|
|
74
|
+
if (schema?.name !== LATEST_SCHEMA_VERSION)
|
|
75
|
+
throw new Error("Wikimemory database schema does not match this Worker version");
|
|
76
|
+
return Response.json({
|
|
77
|
+
status: "ready",
|
|
78
|
+
service: "wikimemory",
|
|
79
|
+
version: WIKIMEMORY_VERSION,
|
|
80
|
+
schemaVersion: schema.name
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const asset = await env.ASSETS.fetch(request);
|
|
84
|
+
const headers = new Headers(asset.headers);
|
|
85
|
+
if (headers.get("content-type")?.startsWith("text/html")) {
|
|
86
|
+
headers.set(
|
|
87
|
+
"content-security-policy",
|
|
88
|
+
"default-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
|
|
89
|
+
);
|
|
90
|
+
headers.set("referrer-policy", "no-referrer");
|
|
91
|
+
headers.set("x-frame-options", "DENY");
|
|
92
|
+
headers.set("x-content-type-options", "nosniff");
|
|
93
|
+
headers.set("cache-control", "no-store");
|
|
94
|
+
}
|
|
95
|
+
return new Response(asset.body, {
|
|
96
|
+
status: asset.status,
|
|
97
|
+
statusText: asset.statusText,
|
|
98
|
+
headers
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
} satisfies ExportedHandler<Env>;
|
|
102
|
+
|
|
103
|
+
async function safeJson(operation: () => Promise<Response>): Promise<Response> {
|
|
104
|
+
try {
|
|
105
|
+
return await operation();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (
|
|
108
|
+
error instanceof z.ZodError ||
|
|
109
|
+
error instanceof SyntaxError ||
|
|
110
|
+
error instanceof DomainError
|
|
111
|
+
) {
|
|
112
|
+
return Response.json({ error: "Invalid request." }, { status: 400 });
|
|
113
|
+
}
|
|
114
|
+
return Response.json({ error: "The authentication operation failed." }, { status: 500 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default new OAuthProvider<Env>({
|
|
119
|
+
apiRoute: "/mcp",
|
|
120
|
+
apiHandler: mcpHandler,
|
|
121
|
+
defaultHandler: webHandler,
|
|
122
|
+
authorizeEndpoint: "/authorize",
|
|
123
|
+
tokenEndpoint: "/oauth/token",
|
|
124
|
+
clientRegistrationEndpoint: "/oauth/register",
|
|
125
|
+
scopesSupported: ["memory:read", "memory:write", "memory:admin"],
|
|
126
|
+
allowPlainPKCE: false,
|
|
127
|
+
allowImplicitFlow: false,
|
|
128
|
+
disallowPublicClientRegistration: false,
|
|
129
|
+
accessTokenTTL: 3600,
|
|
130
|
+
refreshTokenTTL: 2_592_000,
|
|
131
|
+
clientRegistrationTTL: 7_776_000,
|
|
132
|
+
tokenExchangeCallback: downscopeAccessToken,
|
|
133
|
+
resourceMetadata: {
|
|
134
|
+
scopes_supported: ["memory:read", "memory:write", "memory:admin"],
|
|
135
|
+
bearer_methods_supported: ["header"],
|
|
136
|
+
resource_name: "Wikimemory"
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const documentType = z.enum(["system", "project", "topic", "source", "note"]);
|
|
4
|
+
const metadata = z.object({
|
|
5
|
+
key: z.string(),
|
|
6
|
+
value: z.string(),
|
|
7
|
+
cardinality: z.enum(["singleton", "multi"])
|
|
8
|
+
});
|
|
9
|
+
const link = z.object({
|
|
10
|
+
kind: z.enum(["related", "part_of", "supersedes", "cites", "contradicts"]),
|
|
11
|
+
targetSlug: z.string(),
|
|
12
|
+
origin: z.enum(["explicit", "body"]),
|
|
13
|
+
targetDocumentId: z.string().nullable()
|
|
14
|
+
});
|
|
15
|
+
const document = z.object({
|
|
16
|
+
documentId: z.string(),
|
|
17
|
+
workspaceId: z.string(),
|
|
18
|
+
slug: z.string(),
|
|
19
|
+
type: documentType,
|
|
20
|
+
revisionId: z.string(),
|
|
21
|
+
revisionNumber: z.number().int(),
|
|
22
|
+
parentRevisionId: z.string().nullable(),
|
|
23
|
+
title: z.string(),
|
|
24
|
+
body: z.string(),
|
|
25
|
+
summary: z.string().nullable(),
|
|
26
|
+
createdAt: z.string(),
|
|
27
|
+
principalId: z.string(),
|
|
28
|
+
clientId: z.string(),
|
|
29
|
+
agentLabel: z.string().nullable(),
|
|
30
|
+
reason: z.string(),
|
|
31
|
+
restoredFromRevisionId: z.string().nullable(),
|
|
32
|
+
metadata: z.array(metadata),
|
|
33
|
+
links: z.array(link)
|
|
34
|
+
});
|
|
35
|
+
const indexEntry = z.object({
|
|
36
|
+
documentId: z.string(),
|
|
37
|
+
revisionId: z.string(),
|
|
38
|
+
revisionNumber: z.number().int(),
|
|
39
|
+
slug: z.string(),
|
|
40
|
+
type: documentType,
|
|
41
|
+
title: z.string(),
|
|
42
|
+
summary: z.string().nullable(),
|
|
43
|
+
updatedAt: z.string(),
|
|
44
|
+
status: z.string().nullable()
|
|
45
|
+
});
|
|
46
|
+
const revision = z.object({
|
|
47
|
+
revisionId: z.string(),
|
|
48
|
+
revisionNumber: z.number().int(),
|
|
49
|
+
parentRevisionId: z.string().nullable(),
|
|
50
|
+
createdAt: z.string(),
|
|
51
|
+
principalId: z.string(),
|
|
52
|
+
clientId: z.string(),
|
|
53
|
+
agentLabel: z.string().nullable(),
|
|
54
|
+
reason: z.string(),
|
|
55
|
+
restoredFromRevisionId: z.string().nullable(),
|
|
56
|
+
requestHash: z.string()
|
|
57
|
+
});
|
|
58
|
+
const finding = z.object({
|
|
59
|
+
kind: z.enum(["unresolved_reference", "orphan", "missing_summary", "stale_active_project"]),
|
|
60
|
+
slug: z.string(),
|
|
61
|
+
detail: z.string()
|
|
62
|
+
});
|
|
63
|
+
const ingestResult = {
|
|
64
|
+
documentId: z.string(),
|
|
65
|
+
revisionId: z.string(),
|
|
66
|
+
revisionNumber: z.number().int(),
|
|
67
|
+
slug: z.string(),
|
|
68
|
+
idempotentReplay: z.boolean(),
|
|
69
|
+
unresolvedReferences: z.array(z.string())
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const MCP_OUTPUT_SCHEMAS = {
|
|
73
|
+
orient: {
|
|
74
|
+
now: document,
|
|
75
|
+
activeProjects: z.array(indexEntry),
|
|
76
|
+
recentRevisions: z.array(
|
|
77
|
+
z.object({
|
|
78
|
+
slug: z.string(),
|
|
79
|
+
revision_number: z.number().int(),
|
|
80
|
+
created_at: z.string(),
|
|
81
|
+
reason: z.string()
|
|
82
|
+
})
|
|
83
|
+
),
|
|
84
|
+
lintCounts: z.record(z.string(), z.number().int().nonnegative())
|
|
85
|
+
},
|
|
86
|
+
recall: {
|
|
87
|
+
hits: z.array(
|
|
88
|
+
z.object({
|
|
89
|
+
documentId: z.string(),
|
|
90
|
+
revisionId: z.string(),
|
|
91
|
+
slug: z.string(),
|
|
92
|
+
type: documentType,
|
|
93
|
+
title: z.string(),
|
|
94
|
+
summary: z.string().nullable(),
|
|
95
|
+
snippet: z.string(),
|
|
96
|
+
score: z.number().min(0).max(1)
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
get: { ...document.shape, nextCursor: z.string().nullable() },
|
|
101
|
+
index: { items: z.array(indexEntry), nextCursor: z.string().nullable() },
|
|
102
|
+
history: { revisions: z.array(revision) },
|
|
103
|
+
lint: { findings: z.array(finding) },
|
|
104
|
+
ingest: ingestResult,
|
|
105
|
+
link: ingestResult,
|
|
106
|
+
restore_preview: {
|
|
107
|
+
slug: z.string(),
|
|
108
|
+
targetRevisionId: z.string(),
|
|
109
|
+
expectedCurrentRevisionId: z.string(),
|
|
110
|
+
titleChanged: z.boolean(),
|
|
111
|
+
bodyChanged: z.boolean(),
|
|
112
|
+
summaryChanged: z.boolean(),
|
|
113
|
+
metadataChanged: z.boolean(),
|
|
114
|
+
linksChanged: z.boolean()
|
|
115
|
+
},
|
|
116
|
+
restore_apply: ingestResult
|
|
117
|
+
} satisfies Record<string, z.ZodRawShape>;
|