@rubytech/create-maxy 1.0.708 → 1.0.710

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 (59) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +53 -0
  3. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -0
  4. package/payload/platform/lib/mcp-spawn-tee/dist/index.js +132 -0
  5. package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -0
  6. package/payload/platform/lib/mcp-spawn-tee/src/index.ts +134 -0
  7. package/payload/platform/lib/mcp-spawn-tee/tsconfig.json +8 -0
  8. package/payload/platform/lib/oauth-llm/dist/index.d.ts +101 -0
  9. package/payload/platform/lib/oauth-llm/dist/index.d.ts.map +1 -0
  10. package/payload/platform/lib/oauth-llm/dist/index.js +353 -0
  11. package/payload/platform/lib/oauth-llm/dist/index.js.map +1 -0
  12. package/payload/platform/lib/oauth-llm/src/index.ts +526 -0
  13. package/payload/platform/lib/oauth-llm/tsconfig.json +8 -0
  14. package/payload/platform/neo4j/schema.cypher +37 -11
  15. package/payload/platform/package.json +2 -2
  16. package/payload/platform/plugins/docs/references/plugins-guide.md +12 -4
  17. package/payload/platform/plugins/email/mcp/dist/lib/screening.d.ts +3 -3
  18. package/payload/platform/plugins/email/mcp/dist/lib/screening.d.ts.map +1 -1
  19. package/payload/platform/plugins/email/mcp/dist/lib/screening.js +12 -12
  20. package/payload/platform/plugins/email/mcp/dist/lib/screening.js.map +1 -1
  21. package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js +14 -28
  22. package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js.map +1 -1
  23. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +9 -19
  24. package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/index.js +46 -18
  26. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +34 -1
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  29. package/payload/platform/plugins/memory/mcp/dist/lib/document-hierarchy.d.ts.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/lib/document-hierarchy.js +22 -18
  31. package/payload/platform/plugins/memory/mcp/dist/lib/document-hierarchy.js.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +98 -24
  33. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +176 -86
  35. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.d.ts.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js +12 -46
  38. package/payload/platform/plugins/memory/mcp/dist/lib/llm-ranker.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +10 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +22 -3
  42. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.d.ts.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.js +24 -12
  45. package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.js.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +27 -11
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +276 -238
  49. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/package.json +3 -1
  51. package/payload/platform/plugins/memory/mcp/scripts/boot-smoke.sh +69 -0
  52. package/payload/platform/plugins/memory/references/graph-primitives.md +22 -0
  53. package/payload/platform/plugins/memory/references/schema-base.md +66 -14
  54. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +53 -20
  55. package/payload/platform/templates/specialists/agents/database-operator.md +18 -0
  56. package/payload/server/chunk-A5K3CFMI.js +12297 -0
  57. package/payload/server/chunk-Y57ACANQ.js +12292 -0
  58. package/payload/server/maxy-edge.js +1 -1
  59. package/payload/server/server.js +25 -44
@@ -0,0 +1,526 @@
1
+ /**
2
+ * OAuth-bearer LLM calls for admin-side classifiers (Task 740).
3
+ *
4
+ * Every admin-side classifier (memory-classify, memory-rank, commitment,
5
+ * adherence, inbound-gateway, query classifier, summarisation, email
6
+ * screening) uses Claude Code OAuth credentials — never the Anthropic
7
+ * API key. The API-key path is reserved for the public agent.
8
+ *
9
+ * Mechanism: read the access token from ~/.claude/.credentials.json,
10
+ * refresh it via the standard refresh-token grant if expired, then call
11
+ * api.anthropic.com with `Authorization: Bearer <token>` and the
12
+ * `anthropic-beta: oauth-2025-04-20` header. Billed against the operator's
13
+ * Claude Code subscription, not the API-key key. Refresh uses a
14
+ * module-level mutex so concurrent classifier calls during expiry don't
15
+ * race and invalidate each other's tokens (Anthropic rotates both tokens
16
+ * on refresh).
17
+ *
18
+ * Implementation note: this lib uses raw fetch() rather than the
19
+ * @anthropic-ai/sdk package to keep the lib dependency-free — every
20
+ * consumer already has its own SDK copy for other purposes, but the
21
+ * shared lib would create a circular package dependency. The Messages
22
+ * API surface we use (model + system + one user message + max_tokens)
23
+ * is small and stable.
24
+ *
25
+ * Failure modes are surfaced as a structured `Result` so callers can
26
+ * pattern-match without parsing error strings. The skill / agent layer
27
+ * is responsible for translating `fallback` results into operator-visible
28
+ * blocker messages (loud failure doctrine — Task 540, 740).
29
+ */
30
+
31
+ import { readFileSync, writeFileSync } from "node:fs";
32
+ import { resolve } from "node:path";
33
+ import { homedir } from "node:os";
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Constants — token endpoint, client id, beta header, file path.
37
+ // All four are pinned values that match the Claude Code CLI's own usage.
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const CREDENTIALS_FILE = resolve(homedir(), ".claude", ".credentials.json");
41
+ const TOKEN_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
42
+ const ANTHROPIC_MESSAGES_ENDPOINT = "https://api.anthropic.com/v1/messages";
43
+ const ANTHROPIC_VERSION = "2023-06-01";
44
+ const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
45
+ const OAUTH_BETA_HEADER = "oauth-2025-04-20";
46
+
47
+ /** Refresh proactively when the token expires within this window. */
48
+ const EXPIRING_THRESHOLD_MS = 5 * 60 * 1000;
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Types
52
+ // ---------------------------------------------------------------------------
53
+
54
+ interface ClaudeCredentials {
55
+ accessToken: string;
56
+ refreshToken: string | null;
57
+ expiresAt: number;
58
+ }
59
+
60
+ /** Anthropic tool definition — structural-output enforcement via function calling. */
61
+ export interface OauthLlmTool {
62
+ name: string;
63
+ description: string;
64
+ input_schema: Record<string, unknown>;
65
+ }
66
+
67
+ export interface CallOauthLlmParams {
68
+ /** Anthropic model id (e.g. claude-haiku-4-5). */
69
+ model: string;
70
+ /** System prompt — the classifier instructions. */
71
+ system: string;
72
+ /** User message — the input to classify / rank / etc. */
73
+ userMessage: string;
74
+ /** Max output tokens. Default 4096. */
75
+ maxTokens?: number;
76
+ /** Hard timeout in ms for the model call. Default 60_000. */
77
+ timeoutMs?: number;
78
+ /**
79
+ * Optional tools for structured output. When provided with a forced
80
+ * `toolChoiceName`, the model returns a `tool_use` block whose `input` is
81
+ * a structured object matching the tool's `input_schema` — strictly typed
82
+ * JSON without the parsing brittleness of free-form text output.
83
+ */
84
+ tools?: OauthLlmTool[];
85
+ /** Force the model to call this tool. Required when `tools` is provided. */
86
+ toolChoiceName?: string;
87
+ }
88
+
89
+ /** Result when the call did not declare any tool — only text or fallback. */
90
+ export type CallOauthLlmTextResult =
91
+ | { kind: "ok"; text: string }
92
+ | CallOauthLlmFallback;
93
+
94
+ /** Result when the call forced a tool — only the tool input or fallback. */
95
+ export type CallOauthLlmToolResult =
96
+ | { kind: "ok-tool"; toolName: string; input: Record<string, unknown> }
97
+ | CallOauthLlmFallback;
98
+
99
+ /** Discriminated union of every possible result. Returned by the no-overload form. */
100
+ export type CallOauthLlmResult = CallOauthLlmTextResult | CallOauthLlmToolResult;
101
+
102
+ export interface CallOauthLlmFallback {
103
+ kind: "fallback";
104
+ /** Human-readable single-line reason (for operator-visible blocker). */
105
+ reason: string;
106
+ /** Stable classifier — callers can pattern-match for retry/abort policy. */
107
+ cause:
108
+ | "missing-creds"
109
+ | "dead-token"
110
+ | "refresh-failed"
111
+ | "auth-error"
112
+ | "rate-limit"
113
+ | "server-error"
114
+ | "network-error"
115
+ | "timeout"
116
+ | "empty-response"
117
+ | "malformed-response"
118
+ | "unexpected";
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Credential reading + refresh — minimal duplicate of platform/ui claude-auth.
123
+ // We duplicate (not import) so this lib stays self-contained and importable
124
+ // from MCP plugins via dist/. The duplicated logic is small and the auth
125
+ // mechanism is pinned by Anthropic — divergence risk is low.
126
+ // ---------------------------------------------------------------------------
127
+
128
+ function readCredentials(): ClaudeCredentials | null {
129
+ let raw: string;
130
+ try {
131
+ raw = readFileSync(CREDENTIALS_FILE, "utf-8");
132
+ } catch {
133
+ return null;
134
+ }
135
+
136
+ let data: Record<string, unknown>;
137
+ try {
138
+ data = JSON.parse(raw) as Record<string, unknown>;
139
+ } catch {
140
+ return null;
141
+ }
142
+
143
+ const oauth = data.claudeAiOauth as Record<string, unknown> | undefined;
144
+ if (!oauth || typeof oauth !== "object") return null;
145
+
146
+ const accessToken = oauth.accessToken;
147
+ const refreshToken = oauth.refreshToken;
148
+ const expiresAt = oauth.expiresAt;
149
+
150
+ if (typeof accessToken !== "string" || !accessToken) return null;
151
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt) || expiresAt <= 0) {
152
+ return null;
153
+ }
154
+
155
+ return {
156
+ accessToken,
157
+ refreshToken:
158
+ typeof refreshToken === "string" && refreshToken ? refreshToken : null,
159
+ expiresAt,
160
+ };
161
+ }
162
+
163
+ function writeCredentials(tokens: {
164
+ accessToken: string;
165
+ refreshToken: string | null;
166
+ expiresIn: number | undefined;
167
+ }): number {
168
+ const newExpiresAt =
169
+ typeof tokens.expiresIn === "number" && tokens.expiresIn > 0
170
+ ? Date.now() + tokens.expiresIn * 1000
171
+ : Date.now() + 3600 * 1000;
172
+
173
+ let fileData: Record<string, unknown> = {};
174
+ try {
175
+ fileData = JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8")) as Record<
176
+ string,
177
+ unknown
178
+ >;
179
+ } catch {
180
+ // start fresh
181
+ }
182
+
183
+ const existing = fileData.claudeAiOauth as Record<string, unknown> | undefined;
184
+ fileData.claudeAiOauth = {
185
+ ...existing,
186
+ accessToken: tokens.accessToken,
187
+ ...(tokens.refreshToken != null ? { refreshToken: tokens.refreshToken } : {}),
188
+ expiresAt: newExpiresAt,
189
+ };
190
+
191
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(fileData, null, 2), "utf-8");
192
+ return newExpiresAt;
193
+ }
194
+
195
+ // Module-level mutex serialises refresh calls. Anthropic rotates both
196
+ // tokens on refresh; concurrent unsynchronised refreshes invalidate
197
+ // each other.
198
+ let refreshLock: Promise<ClaudeCredentials | null> | null = null;
199
+
200
+ async function refreshAccessToken(
201
+ current: ClaudeCredentials,
202
+ ): Promise<ClaudeCredentials | null> {
203
+ if (refreshLock) return refreshLock;
204
+
205
+ refreshLock = (async () => {
206
+ // Re-read inside the lock — another caller may have refreshed already.
207
+ const fresh = readCredentials();
208
+ if (fresh && fresh.expiresAt - Date.now() > EXPIRING_THRESHOLD_MS) {
209
+ return fresh;
210
+ }
211
+ if (!current.refreshToken) {
212
+ return null;
213
+ }
214
+
215
+ let res: Response;
216
+ try {
217
+ res = await fetch(TOKEN_ENDPOINT, {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
220
+ body: new URLSearchParams({
221
+ grant_type: "refresh_token",
222
+ refresh_token: current.refreshToken,
223
+ client_id: CLIENT_ID,
224
+ }),
225
+ });
226
+ } catch (err) {
227
+ const msg = err instanceof Error ? err.message : String(err);
228
+ console.error(`[oauth-llm] refresh network error: ${msg}`);
229
+ return null;
230
+ }
231
+
232
+ if (!res.ok) {
233
+ const body = await res.text().catch(() => "");
234
+ console.error(
235
+ `[oauth-llm] refresh failed status=${res.status} body=${body.slice(0, 200)}`,
236
+ );
237
+ return null;
238
+ }
239
+
240
+ let tokenData: Record<string, unknown>;
241
+ try {
242
+ tokenData = (await res.json()) as Record<string, unknown>;
243
+ } catch {
244
+ console.error("[oauth-llm] refresh returned malformed JSON");
245
+ return null;
246
+ }
247
+
248
+ const newAccessToken = tokenData.access_token;
249
+ const newRefreshToken = tokenData.refresh_token;
250
+ const expiresIn = tokenData.expires_in;
251
+
252
+ if (typeof newAccessToken !== "string" || !newAccessToken) {
253
+ console.error("[oauth-llm] refresh response missing access_token");
254
+ return null;
255
+ }
256
+
257
+ const newExpiresAt = writeCredentials({
258
+ accessToken: newAccessToken,
259
+ refreshToken:
260
+ typeof newRefreshToken === "string" && newRefreshToken
261
+ ? newRefreshToken
262
+ : current.refreshToken,
263
+ expiresIn: typeof expiresIn === "number" && expiresIn > 0 ? expiresIn : undefined,
264
+ });
265
+
266
+ console.error(
267
+ `[oauth-llm] refresh ok expiresAt=${new Date(newExpiresAt).toISOString()}`,
268
+ );
269
+
270
+ return {
271
+ accessToken: newAccessToken,
272
+ refreshToken:
273
+ typeof newRefreshToken === "string" && newRefreshToken
274
+ ? newRefreshToken
275
+ : current.refreshToken,
276
+ expiresAt: newExpiresAt,
277
+ };
278
+ })();
279
+
280
+ try {
281
+ return await refreshLock;
282
+ } finally {
283
+ refreshLock = null;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Read credentials and proactively refresh if approaching expiry.
289
+ * Returns null when the credentials are missing or unrecoverable
290
+ * (operator must re-authenticate via Claude Code login).
291
+ */
292
+ async function ensureValidToken(): Promise<ClaudeCredentials | null> {
293
+ const creds = readCredentials();
294
+ if (!creds) return null;
295
+
296
+ const remaining = creds.expiresAt - Date.now();
297
+ if (remaining > EXPIRING_THRESHOLD_MS) return creds;
298
+ if (remaining > 0 && !creds.refreshToken) return creds; // valid but no way to renew
299
+ if (remaining <= 0 && !creds.refreshToken) return null; // dead
300
+
301
+ return refreshAccessToken(creds);
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // First-call beta-header diagnostic — emitted once per process so server.log
306
+ // makes the OAuth path visible without flooding on every classifier call.
307
+ // ---------------------------------------------------------------------------
308
+
309
+ let betaHeaderLogged = false;
310
+
311
+ function logBetaHeaderOnce(): void {
312
+ if (betaHeaderLogged) return;
313
+ betaHeaderLogged = true;
314
+ console.error(`[oauth-llm] beta-header=${OAUTH_BETA_HEADER}`);
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Anthropic Messages API response shape — narrow subset we depend on.
319
+ // ---------------------------------------------------------------------------
320
+
321
+ interface AnthropicMessagesResponse {
322
+ content?: Array<
323
+ | { type: "text"; text?: string }
324
+ | { type: "tool_use"; name?: string; input?: Record<string, unknown> }
325
+ | { type: string }
326
+ >;
327
+ error?: { type?: string; message?: string };
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Public entry point
332
+ // ---------------------------------------------------------------------------
333
+
334
+ /**
335
+ * Call an Anthropic model via OAuth bearer auth.
336
+ *
337
+ * Returns:
338
+ * { kind: "ok", text } — model's text response
339
+ * { kind: "ok-tool", toolName, input } — when `tools` + `toolChoiceName` provided
340
+ * { kind: "fallback", reason, cause } — caller decides whether to abort
341
+ *
342
+ * The caller is responsible for translating fallback results into
343
+ * operator-visible blocker messages — this wrapper never silently
344
+ * substitutes a degraded response.
345
+ *
346
+ * Overloads narrow the return type by call shape: text-only callers (no
347
+ * `tools`) statically rule out `ok-tool`; tool callers statically rule out
348
+ * `ok`. Mixed callers get the full union.
349
+ */
350
+ export function callOauthLlm(
351
+ params: Omit<CallOauthLlmParams, "tools" | "toolChoiceName">,
352
+ ): Promise<CallOauthLlmTextResult>;
353
+ export function callOauthLlm(
354
+ params: CallOauthLlmParams & { tools: OauthLlmTool[]; toolChoiceName: string },
355
+ ): Promise<CallOauthLlmToolResult>;
356
+ export function callOauthLlm(
357
+ params: CallOauthLlmParams,
358
+ ): Promise<CallOauthLlmResult>;
359
+ export async function callOauthLlm(
360
+ params: CallOauthLlmParams,
361
+ ): Promise<CallOauthLlmResult> {
362
+ const {
363
+ model,
364
+ system,
365
+ userMessage,
366
+ maxTokens = 4096,
367
+ timeoutMs = 60_000,
368
+ tools,
369
+ toolChoiceName,
370
+ } = params;
371
+
372
+ if (tools && tools.length > 0 && !toolChoiceName) {
373
+ return {
374
+ kind: "fallback",
375
+ cause: "unexpected",
376
+ reason: "callOauthLlm: `tools` provided without `toolChoiceName`. Forced tool selection is required.",
377
+ };
378
+ }
379
+
380
+ logBetaHeaderOnce();
381
+
382
+ const creds = await ensureValidToken();
383
+ if (!creds) {
384
+ return {
385
+ kind: "fallback",
386
+ cause: "missing-creds",
387
+ reason:
388
+ "Claude Code OAuth credentials missing or expired with no refresh token available — operator must re-authenticate.",
389
+ };
390
+ }
391
+
392
+ const controller = new AbortController();
393
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
394
+
395
+ let res: Response;
396
+ try {
397
+ res = await fetch(ANTHROPIC_MESSAGES_ENDPOINT, {
398
+ method: "POST",
399
+ headers: {
400
+ "Content-Type": "application/json",
401
+ "anthropic-version": ANTHROPIC_VERSION,
402
+ "anthropic-beta": OAUTH_BETA_HEADER,
403
+ Authorization: `Bearer ${creds.accessToken}`,
404
+ },
405
+ body: JSON.stringify({
406
+ model,
407
+ max_tokens: maxTokens,
408
+ system,
409
+ messages: [{ role: "user", content: userMessage }],
410
+ ...(tools && tools.length > 0 ? { tools } : {}),
411
+ ...(toolChoiceName
412
+ ? { tool_choice: { type: "tool", name: toolChoiceName } }
413
+ : {}),
414
+ }),
415
+ signal: controller.signal,
416
+ });
417
+ } catch (err) {
418
+ clearTimeout(timer);
419
+ const msg = err instanceof Error ? err.message : String(err);
420
+ if (controller.signal.aborted) {
421
+ return {
422
+ kind: "fallback",
423
+ cause: "timeout",
424
+ reason: `Model call timed out after ${timeoutMs}ms.`,
425
+ };
426
+ }
427
+ return {
428
+ kind: "fallback",
429
+ cause: "network-error",
430
+ reason: `Network error reaching Anthropic: ${msg}`,
431
+ };
432
+ }
433
+ clearTimeout(timer);
434
+
435
+ if (!res.ok) {
436
+ const body = await res.text().catch(() => "");
437
+ if (res.status === 401 || res.status === 403) {
438
+ return {
439
+ kind: "fallback",
440
+ cause: "auth-error",
441
+ reason: `OAuth bearer rejected (status=${res.status}) — token may be revoked or beta header rejected. body=${body.slice(0, 200)}`,
442
+ };
443
+ }
444
+ if (res.status === 429) {
445
+ return {
446
+ kind: "fallback",
447
+ cause: "rate-limit",
448
+ reason: `Anthropic rate limit hit (status=429): ${body.slice(0, 200)}`,
449
+ };
450
+ }
451
+ if (res.status >= 500) {
452
+ return {
453
+ kind: "fallback",
454
+ cause: "server-error",
455
+ reason: `Anthropic server error (status=${res.status}): ${body.slice(0, 200)}`,
456
+ };
457
+ }
458
+ return {
459
+ kind: "fallback",
460
+ cause: "unexpected",
461
+ reason: `Anthropic returned status=${res.status}: ${body.slice(0, 200)}`,
462
+ };
463
+ }
464
+
465
+ let payload: AnthropicMessagesResponse;
466
+ try {
467
+ payload = (await res.json()) as AnthropicMessagesResponse;
468
+ } catch {
469
+ return {
470
+ kind: "fallback",
471
+ cause: "malformed-response",
472
+ reason: "Anthropic returned malformed JSON.",
473
+ };
474
+ }
475
+
476
+ if (payload.error) {
477
+ return {
478
+ kind: "fallback",
479
+ cause: "unexpected",
480
+ reason: `Anthropic API error: ${payload.error.type ?? "unknown"} ${payload.error.message ?? ""}`,
481
+ };
482
+ }
483
+
484
+ // When the caller forced a tool, prefer the tool_use block — its input
485
+ // is the structured output the caller wants.
486
+ if (toolChoiceName) {
487
+ const toolBlock = payload.content?.find(
488
+ (block): block is { type: "tool_use"; name?: string; input?: Record<string, unknown> } =>
489
+ block.type === "tool_use",
490
+ );
491
+ if (!toolBlock || !toolBlock.input || typeof toolBlock.input !== "object") {
492
+ return {
493
+ kind: "fallback",
494
+ cause: "empty-response",
495
+ reason: "Model returned no tool_use block despite forced tool selection.",
496
+ };
497
+ }
498
+ return {
499
+ kind: "ok-tool",
500
+ toolName: toolBlock.name ?? toolChoiceName,
501
+ input: toolBlock.input,
502
+ };
503
+ }
504
+
505
+ const textBlock = payload.content?.find(
506
+ (block): block is { type: "text"; text: string } => {
507
+ if (block.type !== "text") return false;
508
+ const candidate = (block as { text?: unknown }).text;
509
+ return typeof candidate === "string" && candidate.trim().length > 0;
510
+ },
511
+ );
512
+ if (!textBlock) {
513
+ return {
514
+ kind: "fallback",
515
+ cause: "empty-response",
516
+ reason: "Model returned no text content.",
517
+ };
518
+ }
519
+ return { kind: "ok", text: textBlock.text };
520
+ }
521
+
522
+ // ---------------------------------------------------------------------------
523
+ // Re-exports for callers that want to compose their own auth flow.
524
+ // ---------------------------------------------------------------------------
525
+
526
+ export { OAUTH_BETA_HEADER };
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
@@ -201,15 +201,20 @@ OPTIONS {
201
201
  };
202
202
 
203
203
  // ----------------------------------------------------------
204
- // KnowledgeDocument / Section / Chunk hierarchical document ingestion
205
- // PRD §7: three-level hierarchy with LLM-generated summaries
204
+ // KnowledgeDocument / Section — single-Section document ingestion (Task 740,
205
+ // replacing the Task 737 three-level KnowledgeDocument/Section/Chunk model).
206
206
  //
207
- // KnowledgeDocument -[HAS_SECTION]-> Section -[HAS_CHUNK]-> Chunk
208
- // KnowledgeDocument -[REFERENCES]-> (any entity node)
207
+ // KnowledgeDocument -[HAS_SECTION]-> Section
208
+ // Section -[NEXT]-> Section (reading-order chain)
209
+ // KnowledgeDocument -[PARTY]-> (Person|Organization) (contracts only)
210
+ // KnowledgeDocument -[REFERENCES]-> (any entity) (legacy; no new writes)
209
211
  //
210
- // Summaries are embedded (not raw text). Raw chunk content is stored
211
- // on Chunk.content for retrieval. Full-text BM25 index spans all three
212
- // levels for hybrid search (0.7 vector + 0.3 BM25).
212
+ // Every classified section is one Section node. Recognised kinds carry a
213
+ // secondary label (Section:Position, Section:Education, Section:Chapter,
214
+ // Section:Parties, Section:Other, etc.) anchor edges from the document
215
+ // subject (UserProfile/LocalBusiness) point at the multi-labeled node
216
+ // directly. The :Chunk overflow level is gone; sections store their body
217
+ // inline. Identifying property remains attachmentId on KnowledgeDocument.
213
218
  // ----------------------------------------------------------
214
219
 
215
220
  CREATE CONSTRAINT knowledge_doc_id_unique IF NOT EXISTS
@@ -241,6 +246,9 @@ OPTIONS {
241
246
  }
242
247
  };
243
248
 
249
+ // Pre-Task 740 :Chunk vector index — kept for backwards compatibility with
250
+ // Sections written before the writer rewrite. New ingests do not produce
251
+ // :Chunk nodes; the index becomes inert once landfill is purged.
244
252
  CREATE VECTOR INDEX chunk_embedding IF NOT EXISTS
245
253
  FOR (c:Chunk) ON (c.embedding)
246
254
  OPTIONS {
@@ -250,12 +258,30 @@ OPTIONS {
250
258
  }
251
259
  };
252
260
 
253
- // Full-text BM25 index for hybrid keyword search across all document levels.
254
- // Indexes summaries (KnowledgeDocument.summary, Section.summary) and raw
255
- // chunk content (Chunk.content) for keyword matching alongside vector search.
261
+ // Full-text BM25 index for hybrid keyword search across document levels.
262
+ // Post-Task 740: sections carry their body inline so the index covers
263
+ // KnowledgeDocument.summary, Section.summary, Section.body, and the legacy
264
+ // Chunk.content for any Chunks still present from pre-740 ingests.
256
265
  CREATE FULLTEXT INDEX knowledge_fulltext IF NOT EXISTS
257
266
  FOR (k:KnowledgeDocument|Section|Chunk)
258
- ON EACH [k.summary, k.content];
267
+ ON EACH [k.summary, k.content, k.body];
268
+
269
+ // Project node (Task 740) — a standalone creative-output node distinct from
270
+ // :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
271
+ // (:Project)-[:UNDER]->(:Organization) when the project was done at an org.
272
+ // Written when a CV section classifies as `Project`.
273
+
274
+ CREATE INDEX project_account IF NOT EXISTS
275
+ FOR (p:Project) ON (p.accountId);
276
+
277
+ CREATE VECTOR INDEX project_embedding IF NOT EXISTS
278
+ FOR (p:Project) ON (p.embedding)
279
+ OPTIONS {
280
+ indexConfig: {
281
+ `vector.dimensions`: 768,
282
+ `vector.similarity_function`: 'cosine'
283
+ }
284
+ };
259
285
 
260
286
  // ----------------------------------------------------------
261
287
  // Position node — a role held by a UserProfile at an Organization.
@@ -6,8 +6,8 @@
6
6
  "plugins/*/mcp"
7
7
  ],
8
8
  "scripts": {
9
- "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
- "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
9
+ "build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
10
+ "build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
11
11
  "build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
12
12
  "build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
13
13
  "build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
@@ -115,10 +115,18 @@ After this, every `console.error("[your-tool] ...")` from any tool in the plugin
115
115
 
116
116
  **Main-subprocess stderr (Task 535).** The same teeing pattern applies to the main Claude Code subprocess's stderr — every line lands in the per-conversation stream log as `[subproc-stderr] …`, with lifecycle markers `[subproc-stderr-tee-attached] pid=…` and `[subproc-stderr-tee-detached] pid=… bytes=N lines=N`. A `bytes=0 lines=0` detach means the tee was attached but the subprocess emitted nothing on stderr — which is the normal state today, because the Claude Code CLI is a bundled Bun runtime binary that does not honour Node's `NODE_DEBUG` env var. The platform records this explicitly with one line per spawn: `[subproc-debug-unavailable] reason=bundled-bun-binary-ignores-node-debug pid=… cli=claude`. A reader who finds a `[spawn]` without these markers should treat that as a regression of the tee infrastructure, not as silence.
117
117
 
118
- ## Failure-path observability contract (Task 560)
118
+ ## Failure-path observability contract (Task 560 + Task 743)
119
119
 
120
- The `initStderrTee` wrapper writes to the per-conversation stream log and per-server raw file via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost: the event loop never drains the WriteStream before the process terminates. Plugins that call `process.exit()` during module load (rare `graph-mcp` is the only in-tree example today; it spawns a child at boot to proxy upstream stdio) MUST use `fs.appendFileSync` at every exit path to guarantee the cause lands in both log destinations before exit. Lines should follow the `[mcp:<name>] [<plugin-prefix>] <cause>` format so existing `grep '[mcp:<name>]'` investigator paths work. Each destination must be wrapped in its own try/catch an unwritable log must not mask the primary failure.
120
+ The `initStderrTee` wrapper writes to the per-conversation stream log and per-server raw file via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost: the event loop never drains the WriteStream before the process terminates. Same race for any synchronous module-load throw: Node's uncaught-exception handler writes the stack to raw fd 2 and exits before the patched async stream flushes. The platform's `[mcp-init-error] tail="(no stderr file)"` line operationally uselessis the public symptom of this race.
121
121
 
122
- A second observability layer closes the same gap from the platform side: when `claude-agent.ts` observes an `init` event with any MCP server reporting `status:"failed"`, it reads the last 512 bytes of `${LOG_DIR}/mcp-<name>-stderr-<date>.log` and emits `[mcp-init-error] server=<name> tail=<quoted>` into the stream log. Absent file → `tail="(no stderr file)"`; empty file → `tail="(empty)"`. This works for every plugin regardless of whether it adopted the sync-write discipline — the tail of whatever landed in the raw stderr file (from whichever destination made it out of the async buffer) is always captured.
122
+ **Two layers now close the gap, each load-bearing on its own:**
123
123
 
124
- Signal inventory after a failed session: `[init] FAILED MCP servers: <names>` (names), `[mcp-init-error] server=<name> tail=…` (cause for each, from platform), optionally `[mcp:<name>] [<plugin>] …` (cause for each, from plugin's own sync-writes when the plugin is disciplined). Their union gives the investigator two independent sources for the same failure.
124
+ 1. **Plugin-side sync-write discipline.** Plugins that call `process.exit()` during module load (rare `graph-mcp` is the in-tree example; it spawns a child at boot to proxy upstream stdio) use `fs.appendFileSync` at every named exit path to guarantee the cause lands in both log destinations before exit. Lines follow the `[mcp:<name>] [<plugin-prefix>] <cause>` format so existing `grep '[mcp:<name>]'` investigator paths work. Each destination is wrapped in its own try/catch an unwritable log must not mask the primary failure. This is the discipline propagated from Task 560 to any plugin author who knows their failure paths.
125
+
126
+ 2. **Parent-side `mcp-spawn-tee` wrapper (Task 743).** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` wrapper rather than `node <entry>` directly. The wrapper spawns the real entry with `stdio: ['inherit', 'inherit', 'pipe']` and writes child stderr chunks to `${LOG_DIR}/mcp-${name}-stderr-<date>.log` via `appendFileSync` while passing the same chunks through to its own stderr (Claude Code's consumer is unchanged). Synchronous `appendFileSync` survives `process.exit`, so the per-server file captures even (a) module-load throws before `initStderrTee` runs, (b) `MODULE_NOT_FOUND` on the entry script itself, and (c) anything else a plugin author missed. The wrapper writes `[mcp-spawn-tee-attached] server=<name> pid=<n>` on attach and forwards SIGTERM/SIGINT to the child. This is the layer that makes capture independent of plugin discipline. Playwright stays unwrapped because it spawns via `npx`, not `node`.
127
+
128
+ A third layer closes the same gap from the platform side: when `claude-agent.ts` observes an `init` event with any MCP server reporting `status:"failed"`, it reads the last 512 bytes of `${LOG_DIR}/mcp-<name>-stderr-<date>.log` and emits `[mcp-init-error] server=<name> tail=<quoted>` into the stream log. Absent file → `tail="(no stderr file)"`; empty file → `tail="(empty)"`. With the spawn-tee wrapper now interposing on every core MCP, `tail="(no stderr file)"` post-Task-743 means the wrapper itself is broken — file follow-up.
129
+
130
+ **Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-init-error] server=<name> tail=…` (cause for each, from the platform's tail probe), `[mcp-spawn-tee-attached] server=<name> pid=<n>` (proof the wrapper attached), `[mcp-spawn-tee-exit] server=<name> code=<n>|signal=<s>` (proof the wrapper saw the exit), and optionally `[mcp:<name>] [<plugin>] …` from plugin-side sync-writes. Their union gives the investigator three independent sources for the same failure.
131
+
132
+ **Boot-smoke as publish-time gate (Task 743).** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Email screening via Claude Haiku.
2
+ * Email screening via Claude Haiku (Task 740: OAuth, never API key — admin path).
3
3
  *
4
4
  * Classifies inbound emails before they enter Neo4j. Verdicts:
5
5
  * clean — legitimate correspondence, store normally
@@ -7,7 +7,7 @@
7
7
  * discard — obvious spam / bulk marketing / auto-generated noise, store without embedding
8
8
  *
9
9
  * On any failure the email defaults to "suspicious" (deny by default).
10
- * No file I/O the API key is passed in by the caller.
10
+ * Auth: runs on Claude Code OAuth via `callOauthLlm`.
11
11
  */
12
12
  export interface ScreeningInput {
13
13
  fromAddress: string;
@@ -25,5 +25,5 @@ export interface ScreeningResult {
25
25
  * On any failure (network, auth, malformed response), returns
26
26
  * { verdict: "suspicious", reason: "<error>", promptInjectionRisk: false }.
27
27
  */
28
- export declare function screenEmail(input: ScreeningInput, apiKey: string): Promise<ScreeningResult>;
28
+ export declare function screenEmail(input: ScreeningInput): Promise<ScreeningResult>;
29
29
  //# sourceMappingURL=screening.d.ts.map