@rubytech/create-realagent 1.0.712 → 1.0.714

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 (28) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/dist/index.d.ts +22 -1
  3. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-search/dist/index.js +69 -39
  5. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-search/src/__tests__/bm25-label-gate.test.ts +88 -0
  7. package/payload/platform/lib/graph-search/src/__tests__/expand-batch.test.ts +206 -0
  8. package/payload/platform/lib/graph-search/src/index.ts +100 -43
  9. package/payload/platform/plugins/docs/references/graph.md +25 -0
  10. package/payload/platform/plugins/docs/references/platform.md +3 -1
  11. package/payload/platform/scripts/check-sdk-oauth.mjs +178 -0
  12. package/payload/server/public/assets/{Checkbox-CjbS9JcG.js → Checkbox-DD2mv2dU.js} +1 -1
  13. package/payload/server/public/assets/{admin-Ce9DbUuu.js → admin-BmuLrDs2.js} +1 -1
  14. package/payload/server/public/assets/{data-C-SxjLC9.js → data-BiexCwhp.js} +1 -1
  15. package/payload/server/public/assets/{file-D4cbAAuo.js → file-CXzgVus7.js} +1 -1
  16. package/payload/server/public/assets/{graph-DJ7IfYHV.js → graph-DOWTWpqy.js} +12 -12
  17. package/payload/server/public/assets/{house-CYsVygEQ.js → house-DuPFyfb8.js} +1 -1
  18. package/payload/server/public/assets/{jsx-runtime-DPXE45W9.css → jsx-runtime-UA_LoTFj.css} +1 -1
  19. package/payload/server/public/assets/{public-BTOF98iO.js → public-Tjx3543Z.js} +1 -1
  20. package/payload/server/public/assets/{share-2-B-sbkB36.js → share-2-C_nKw-UZ.js} +1 -1
  21. package/payload/server/public/assets/{useVoiceRecorder-DLVFx3ms.js → useVoiceRecorder-aDBUrrIb.js} +1 -1
  22. package/payload/server/public/assets/{x-BNidzSAn.js → x-Do7BO9Ow.js} +1 -1
  23. package/payload/server/public/data.html +6 -6
  24. package/payload/server/public/graph.html +7 -7
  25. package/payload/server/public/index.html +8 -8
  26. package/payload/server/public/public.html +5 -5
  27. package/payload/server/server.js +69 -47
  28. /package/payload/server/public/assets/{jsx-runtime-BUs3sHtV.js → jsx-runtime-DV3X_CC7.js} +0 -0
@@ -46,6 +46,12 @@ export interface SearchHit {
46
46
 
47
47
  export interface SearchResult extends SearchHit {
48
48
  related: Array<{
49
+ /**
50
+ * Task 747 — neighbour `elementId`. Required by the /graph canvas to
51
+ * render edges in pipeline-collapse mode (search response IS the canvas
52
+ * data). The MCP memory-search tool ignores it; the field is additive.
53
+ */
54
+ nodeId: string;
49
55
  relationship: string;
50
56
  direction: string;
51
57
  labels: string[];
@@ -71,10 +77,17 @@ export interface Bm25OnlyParams {
71
77
  agentSlug?: string;
72
78
  keywords?: string[];
73
79
  keywordMatch?: "any" | "all";
80
+ /**
81
+ * Task 747 — gate BM25 hits to nodes carrying at least one of these labels.
82
+ * Mirrors hybrid()'s vector-half label filter so the Ollama-down fallback
83
+ * honours the same gate the operator applied via /graph chips. Empty array
84
+ * is treated as "no gate" (matches hybrid's `labels && labels.length > 0`
85
+ * guard); admin route enforces non-empty `labels` as a precondition.
86
+ */
87
+ labels?: string[];
74
88
  }
75
89
 
76
90
  export interface HybridParams extends Bm25OnlyParams {
77
- labels?: string[];
78
91
  expandHops?: number;
79
92
  keywordSubscriptions?: string[];
80
93
  /**
@@ -91,6 +104,14 @@ export interface HybridResponse {
91
104
  results: SearchResult[];
92
105
  /** Populated when degradeOnEmbedFailure fired. Caller logs it. */
93
106
  embedError?: string;
107
+ /**
108
+ * Task 747 — milliseconds spent on the batched expand round-trip alone
109
+ * (separate from embed + vector + BM25). The /graph admin route emits
110
+ * this as `expand-ms=N` so a regression on the post-Task-747 batching
111
+ * surfaces in server.log without needing a profiler. Zero when
112
+ * `expandHops === 0` or no merged results.
113
+ */
114
+ expandMs: number;
94
115
  }
95
116
 
96
117
  export type EmbedFn = (text: string) => Promise<number[]>;
@@ -170,13 +191,16 @@ export async function bm25Only(
170
191
  session: Session,
171
192
  params: Bm25OnlyParams,
172
193
  ): Promise<SearchHit[]> {
173
- const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch } = params;
194
+ const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch, labels } = params;
174
195
  const scopeClause = allowedScopes
175
196
  ? "AND (node.scope IS NULL OR node.scope IN $allowedScopes)"
176
197
  : "";
177
198
  const agentClause = agentSlug
178
199
  ? "AND node.agents IS NOT NULL AND $agentSlug IN node.agents"
179
200
  : "";
201
+ const labelClause = labels && labels.length > 0
202
+ ? "AND any(l IN labels(node) WHERE l IN $labels)"
203
+ : "";
180
204
  const keywordFilter = buildKeywordFilter(keywords, keywordMatch);
181
205
  const kwClause = keywordFilter?.clause ?? "";
182
206
  const escaped = escapeLucene(query);
@@ -188,6 +212,7 @@ export async function bm25Only(
188
212
  WHERE node.accountId = $accountId
189
213
  ${scopeClause}
190
214
  ${agentClause}
215
+ ${labelClause}
191
216
  AND ${notTrashed("node")}
192
217
  ${kwClause}
193
218
  RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
@@ -200,6 +225,7 @@ export async function bm25Only(
200
225
  limit: int(limit),
201
226
  ...(allowedScopes ? { allowedScopes } : {}),
202
227
  ...(agentSlug ? { agentSlug } : {}),
228
+ ...(labels && labels.length > 0 ? { labels } : {}),
203
229
  ...(keywordFilter?.params ?? {}),
204
230
  },
205
231
  );
@@ -270,7 +296,7 @@ export async function hybrid(
270
296
  const msg = err instanceof Error ? err.message : String(err);
271
297
  const bm25Hits = await bm25Only(session, params);
272
298
  const results: SearchResult[] = bm25Hits.map((h) => ({ ...h, related: [] }));
273
- return { mode: "bm25", results, embedError: msg };
299
+ return { mode: "bm25", results, embedError: msg, expandMs: 0 };
274
300
  }
275
301
 
276
302
  const labelToIndex = await discoverIndexes(session);
@@ -288,7 +314,7 @@ export async function hybrid(
288
314
  .map((l) => labelToIndex.get(l))
289
315
  .filter((idx): idx is string => idx !== undefined);
290
316
  if (indexesToQuery.length === 0) {
291
- return { mode: "hybrid", results: [] };
317
+ return { mode: "hybrid", results: [], expandMs: 0 };
292
318
  }
293
319
  } else {
294
320
  indexesToQuery = [...new Set(labelToIndex.values())];
@@ -418,50 +444,81 @@ export async function hybrid(
418
444
  .sort((a, b) => b.combinedScore - a.combinedScore)
419
445
  .slice(0, limit);
420
446
 
421
- // --- Graph expand ---
422
- const results: SearchResult[] = [];
423
- for (const node of merged) {
424
- const result: SearchResult = {
425
- nodeId: node.nodeId,
426
- labels: node.labels,
427
- properties: node.properties,
428
- score: node.combinedScore,
429
- related: [],
430
- };
431
- if (expandHops > 0) {
432
- const expandScopeClause = allowedScopes
433
- ? "AND (related.scope IS NULL OR related.scope IN $allowedScopes)"
434
- : "";
435
- const expandAgentClause = agentSlug
436
- ? "AND (related.agents IS NULL OR $agentSlug IN related.agents)"
437
- : "";
438
- const expandResult = await session.run(
439
- `MATCH (n)-[r]-(related)
440
- WHERE elementId(n) = $nodeId
441
- AND ${notTrashed("related")}
442
- ${expandScopeClause}
443
- ${expandAgentClause}
444
- RETURN type(r) AS relType,
445
- CASE WHEN startNode(r) = n THEN 'outgoing' ELSE 'incoming' END AS direction,
446
- labels(related) AS relatedLabels,
447
- related
448
- LIMIT 20`,
449
- { nodeId: node.nodeId, ...scopeParams, ...agentParams },
450
- );
451
- for (const rec of expandResult.records) {
452
- const related = rec.get("related") as { properties: Record<string, unknown> };
453
- result.related.push({
454
- relationship: rec.get("relType") as string,
455
- direction: rec.get("direction") as string,
456
- labels: rec.get("relatedLabels") as string[],
457
- properties: plainProperties(related.properties),
447
+ // --- Graph expand (Task 747 — single batched round-trip) ---
448
+ //
449
+ // Pre-Task-747: one Cypher per merged node, in a JS for-loop. At the admin
450
+ // route's slider=2000 this produced 2000 driver round-trips per search.
451
+ //
452
+ // Post-Task-747: one UNWIND-driven query for all merged nodeIds. The
453
+ // `WITH nid, collect({...})[0..20]` clause preserves the per-result cap of
454
+ // 20 neighbours that the per-node query enforced via `LIMIT 20`. Slice
455
+ // notation is order-preserving over the rows it consumes (no upstream
456
+ // ORDER BY changes that), so canvas-edge density per hit is unchanged.
457
+ //
458
+ // Each `related` entry now carries `relatedNodeId` (the neighbour's
459
+ // elementId) so the /graph canvas can render edges in pipeline-collapse
460
+ // mode (search response IS the canvas data when `q` is set).
461
+ const results: SearchResult[] = merged.map((node) => ({
462
+ nodeId: node.nodeId,
463
+ labels: node.labels,
464
+ properties: node.properties,
465
+ score: node.combinedScore,
466
+ related: [],
467
+ }));
468
+
469
+ let expandMs = 0;
470
+ if (expandHops > 0 && results.length > 0) {
471
+ const expandScopeClause = allowedScopes
472
+ ? "AND (related.scope IS NULL OR related.scope IN $allowedScopes)"
473
+ : "";
474
+ const expandAgentClause = agentSlug
475
+ ? "AND (related.agents IS NULL OR $agentSlug IN related.agents)"
476
+ : "";
477
+ const expandStart = Date.now();
478
+ const expandResult = await session.run(
479
+ `UNWIND $nodeIds AS nid
480
+ MATCH (n)-[r]-(related)
481
+ WHERE elementId(n) = nid
482
+ AND ${notTrashed("related")}
483
+ ${expandScopeClause}
484
+ ${expandAgentClause}
485
+ WITH nid, n, r, related
486
+ WITH nid, collect({
487
+ relType: type(r),
488
+ direction: CASE WHEN startNode(r) = n THEN 'outgoing' ELSE 'incoming' END,
489
+ relatedNodeId: elementId(related),
490
+ relatedLabels: labels(related),
491
+ related: related
492
+ })[0..20] AS items
493
+ RETURN nid, items`,
494
+ { nodeIds: results.map((r) => r.nodeId), ...scopeParams, ...agentParams },
495
+ );
496
+ expandMs = Date.now() - expandStart;
497
+ const byNodeId = new Map<string, SearchResult>(results.map((r) => [r.nodeId, r]));
498
+ for (const rec of expandResult.records) {
499
+ const nid = rec.get("nid") as string;
500
+ const target = byNodeId.get(nid);
501
+ if (!target) continue;
502
+ const items = rec.get("items") as Array<{
503
+ relType: string;
504
+ direction: string;
505
+ relatedNodeId: string;
506
+ relatedLabels: string[];
507
+ related: { properties: Record<string, unknown> };
508
+ }>;
509
+ for (const item of items) {
510
+ target.related.push({
511
+ nodeId: item.relatedNodeId,
512
+ relationship: item.relType,
513
+ direction: item.direction,
514
+ labels: item.relatedLabels,
515
+ properties: plainProperties(item.related.properties),
458
516
  });
459
517
  }
460
518
  }
461
- results.push(result);
462
519
  }
463
520
 
464
- return { mode: "hybrid", results };
521
+ return { mode: "hybrid", results, expandMs };
465
522
  }
466
523
 
467
524
  function mergeBm25Hit(
@@ -29,6 +29,31 @@ Hovering a node still shows the full 5-line tooltip (display name, labels,
29
29
  id, created at, updated at). Clicking a Conversation opens the side panel
30
30
  with the full property table — zoom-tier changes never alter these paths.
31
31
 
32
+ The side panel carries a **Trash** button for live nodes and a **Restore**
33
+ button for trashed nodes. Soft-delete is reversible: trashed nodes
34
+ remain in the graph and reappear when **Show trashed** is on.
35
+
36
+ ## Deleting a node
37
+
38
+ Two surfaces, same outcome:
39
+
40
+ - **Mouse (desktop):** drag a node to the dashed Trash zone in the upper-
41
+ right corner of the canvas.
42
+ - **Touch (mobile/tablet):** the dashed Trash zone is hidden because
43
+ vis-network's drag hit-test never fires on touch. Tap the node to open
44
+ the side panel, then tap **Trash**.
45
+
46
+ Both paths POST to the same soft-delete endpoint; the operator-side
47
+ behaviour is identical.
48
+
49
+ ## Mobile layout
50
+
51
+ Below 640px viewport width the toolbar wraps: the search input claims
52
+ its own row, the search-result slider claims its own row (full-width with
53
+ an enlarged thumb for touch), and the Filter button + node count share
54
+ the bottom row. The "← Back" control collapses to a left-arrow icon to
55
+ preserve toolbar space at depth.
56
+
32
57
  ## Trashed conversations
33
58
 
34
59
  Trashed Conversation nodes are hidden by default. Toggle **Show trashed** in
@@ -17,7 +17,7 @@ The Pi runs the web interface, the AI agent, and all the plugin servers. When yo
17
17
 
18
18
  Maxy runs two agents simultaneously:
19
19
 
20
- **Admin agent (you)** — full access to all tools and plugins. This is the agent you interact with at your local or remote URL. It can read and write contacts, send Telegram messages, manage your account, and perform any task you have plugins for. Protected by your PIN.
20
+ **Admin agent (you)** — full access to all tools and plugins. This is the agent you interact with at your local or remote URL. It can read and write contacts, send Telegram messages, manage your account, and perform any task you have plugins for. Protected by your PIN. Your admin agent runs through your own Claude Code OAuth session — it never bills the Anthropic API. Authentication and SDK details are documented in the developer doc `.docs/platform.md` admin-agent section.
21
21
 
22
22
  **Public agent (visitors)** — read-only access. Handles enquiries from people who reach your public URL. It can answer questions about your business and collect waitlist signups, but it cannot access your private data or take actions.
23
23
 
@@ -51,6 +51,8 @@ Maxy maintains a graph database (Neo4j) of everything you've told it. People, co
51
51
 
52
52
  The memory graph is stored on your Pi. It never leaves your network.
53
53
 
54
+ The graph view (at `/graph`) lets you explore the memory directly. Pick a category from the filter, then type to search inside it — typing makes the canvas narrower, not wider. Drag the slider to control how many matches you see (1 to 2000). If the search shows a yellow banner saying "Vector ranking unavailable," it means the local AI ranking model is offline; results are still returned using keyword match, but ordering is less semantic until the ranker recovers.
55
+
54
56
  ## The Web Interface
55
57
 
56
58
  The web app runs on your Pi on port 19200. A small always-on front door (`maxy-edge`) owns that port and the remote terminal transport — so when the Software Update command restarts the app server, the browser-side terminal keeps streaming bytes exactly like an SSH session would. The edge also hosts the update flow's own routes (the sudo prompt, the action launcher, the SSE progress stream, the installed-version poll), so the Software Update modal's log panel does not go blank during the app-server restart window — it keeps receiving lines, heartbeats, and the final exit event unbroken. Login cookies are HMAC-signed with a shared key on disk, so both processes recognise the same session without any coordination and you do not have to log in again after an update. Every request is also classified as LAN or external based on the network shape it arrived on — LAN browsers reach admin directly; the remote password screen only appears on the tunnel-exposed admin domain. It provides:
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ // Task 746 — OAuth + wire-identity check for @anthropic-ai/claude-agent-sdk.
3
+ //
4
+ // One-shot spike. Pi-runnable. Intentionally orphan/standalone — not imported
5
+ // by any production module, not wired into any cron, hook, or route. Task 606
6
+ // owns SDK adoption in production; this script gates that adoption.
7
+ //
8
+ // Precondition (exact):
9
+ // ssh <pi> # see memory/reference_device_ssh.md for hosts
10
+ // cd /tmp && mkdir -p sdk-spike && cd sdk-spike
11
+ // npm install @anthropic-ai/claude-agent-sdk@0.2.119 # pin matches static evidence
12
+ // cp <repo>/platform/scripts/check-sdk-oauth.mjs .
13
+ // unset ANTHROPIC_API_KEY
14
+ // node check-sdk-oauth.mjs 2>&1 | tee spike.log
15
+ //
16
+ // PASS condition: apiKeySource ∈ {'oauth', 'none'}. Both indicate OAuth-only mode:
17
+ // 'oauth' = SDK supplied an OAuth-issued API key; 'none' = SDK supplied no key
18
+ // at all and the claude binary's own ~/.claude/.credentials.json OAuth state is
19
+ // in use. Cross-check with `claude --print --output-format json "Reply…"` to
20
+ // confirm the same value: parity = SDK is faithfully proxying claude's auth.
21
+ //
22
+ // Verdict reasons (exit 1 unless PASS):
23
+ // env-set ANTHROPIC_API_KEY present (operator must `unset`)
24
+ // no-oauth-credentials ~/.claude/.credentials.json unreadable, empty,
25
+ // or contains no OAuth-shaped fields (run `claude login`)
26
+ // exception:<msg> SDK import or runtime error
27
+ // no-system-init SDK never emitted system.init message
28
+ // wrong-api-key-source:<value> apiKeySource ∉ {oauth, none} — value verbatim;
29
+ // MISSING-FIELD if SDK message shape drifted
30
+ // (raw msg dumped on next line)
31
+ // assistant-error:<value> SDK reported assistant error: authentication_failed |
32
+ // billing_error | rate_limit | invalid_request |
33
+ // server_error | unknown | max_output_tokens
34
+ // no-response no assistant message arrived
35
+ // empty-response assistant arrived, no text content
36
+ // timeout 60s elapsed waiting for response
37
+ //
38
+ // Wire-identity (mitmproxy header diff vs `claude -p`) and subscription-billing
39
+ // (VNC dashboard) are operator-manual per Task 746 brief — not script-side.
40
+ // .docs/platform.md captures the consolidated verdict (script + mitm + VNC).
41
+
42
+ import { accessSync, readFileSync, constants as FS } from 'node:fs'
43
+ import { homedir } from 'node:os'
44
+ import { join } from 'node:path'
45
+
46
+ const TIMEOUT_MS = 60_000
47
+ const PROMPT = 'Reply with the literal string OK and nothing else.'
48
+ // Claude Code emits apiKeySource='none' when the SDK supplies no API key and
49
+ // the claude binary uses its own OAuth credentials at ~/.claude/.credentials.json.
50
+ // Brief assumed 'oauth' based on stale type-defs; runtime-correct OAuth-only
51
+ // indicator on Claude Code 2.1.x is 'none'. Both accepted; cross-check with
52
+ // `claude --print --output-format json` to confirm parity.
53
+ const OAUTH_API_KEY_SOURCES = new Set(['oauth', 'none'])
54
+
55
+ const log = (line) => process.stdout.write(line + '\n')
56
+ const safeStringify = (v) => { try { return JSON.stringify(v) } catch { return '<unstringifiable>' } }
57
+
58
+ let verdictEmitted = false
59
+ const fail = (reason) => {
60
+ if (verdictEmitted) return
61
+ verdictEmitted = true
62
+ log(`[sdk-spike] verdict: FAIL reason=${reason}`)
63
+ process.exit(1)
64
+ }
65
+ const pass = (note) => {
66
+ if (verdictEmitted) return
67
+ verdictEmitted = true
68
+ log(`[sdk-spike] verdict: PASS${note ? ` ${note}` : ''}`)
69
+ process.exit(0)
70
+ }
71
+
72
+ async function main() {
73
+ const envState = process.env.ANTHROPIC_API_KEY ? 'set' : 'unset'
74
+ log(`[sdk-spike] env: ANTHROPIC_API_KEY=${envState}`)
75
+ if (envState === 'set') fail('env-set')
76
+
77
+ const credsPath = join(homedir(), '.claude', '.credentials.json')
78
+ try { accessSync(credsPath, FS.R_OK) }
79
+ catch { fail('no-oauth-credentials') }
80
+
81
+ let credsSize = 0
82
+ let credsHasOauthFields = false
83
+ let credsTopKeys = []
84
+ try {
85
+ const credsRaw = readFileSync(credsPath, 'utf8')
86
+ credsSize = credsRaw.length
87
+ const credsObj = JSON.parse(credsRaw)
88
+ credsTopKeys = Object.keys(credsObj).sort()
89
+ credsHasOauthFields = credsTopKeys.some((k) => /oauth|access|refresh|bearer/i.test(k)) ||
90
+ Object.values(credsObj).some((v) => v && typeof v === 'object' &&
91
+ Object.keys(v).some((k) => /oauth|access|refresh|bearer/i.test(k)))
92
+ } catch { /* malformed creds file — surface in log, fail with no-oauth-credentials below */ }
93
+ log(`[sdk-spike] creds: size=${credsSize} topKeys=${credsTopKeys.join(',')} hasOauthFields=${credsHasOauthFields}`)
94
+ if (credsSize === 0 || !credsHasOauthFields) fail('no-oauth-credentials')
95
+
96
+ let query
97
+ try {
98
+ ({ query } = await import('@anthropic-ai/claude-agent-sdk'))
99
+ } catch (err) {
100
+ fail(`exception:${String(err?.message ?? err).slice(0, 200)}`)
101
+ }
102
+
103
+ let sdkVersion = '<unknown>'
104
+ try {
105
+ const pkgPath = join(process.cwd(), 'node_modules', '@anthropic-ai', 'claude-agent-sdk', 'package.json')
106
+ sdkVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version
107
+ } catch { /* leave as <unknown> — diagnostic, not load-bearing */ }
108
+ log(`[sdk-spike] sdk-version: ${sdkVersion}`)
109
+
110
+ let gotInit = false
111
+ let apiKeySource = ''
112
+ let apiProvider = ''
113
+ let model = ''
114
+ let sessionId = ''
115
+ let claudeCodeVersion = ''
116
+ let gotAssistant = false
117
+ let assistantError = ''
118
+ let responseText = ''
119
+
120
+ const result = query({ prompt: PROMPT })
121
+
122
+ let timeoutId
123
+ const timer = new Promise((_, reject) => {
124
+ timeoutId = setTimeout(() => reject(new Error(`timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
125
+ })
126
+
127
+ const consume = (async () => {
128
+ for await (const msg of result) {
129
+ if (msg?.type === 'system' && msg?.subtype === 'init') {
130
+ gotInit = true
131
+ // apiKeySource is required (sdk.d.ts:3286 — non-optional). MISSING-FIELD here = SDK shape drift, dump raw.
132
+ apiKeySource = msg.apiKeySource ?? msg.api_key_source ?? 'MISSING-FIELD'
133
+ // apiProvider is optional (sdk.d.ts:32 declares `?:`); absent on Claude Code 2.1.119. <absent> ≠ defect.
134
+ apiProvider = msg.apiProvider ?? msg.api_provider ?? '<absent>'
135
+ model = msg.model ?? '<unknown>'
136
+ sessionId = msg.session_id ?? msg.sessionId ?? '<unknown>'
137
+ claudeCodeVersion = msg.claude_code_version ?? '<unknown>'
138
+ log(`[sdk-spike] system-init: apiKeySource=${apiKeySource} apiProvider=${apiProvider} model=${model} sessionId=${sessionId} claudeCodeVersion=${claudeCodeVersion}`)
139
+ if (apiKeySource === 'MISSING-FIELD') {
140
+ log(`[sdk-spike] system-init-raw: ${safeStringify(msg)}`)
141
+ }
142
+ } else if (msg?.type === 'assistant') {
143
+ gotAssistant = true
144
+ if (msg.error && !assistantError) assistantError = msg.error
145
+ const blocks = msg.message?.content ?? []
146
+ for (const block of blocks) {
147
+ if (block?.type === 'text' && typeof block.text === 'string') {
148
+ responseText += block.text
149
+ }
150
+ }
151
+ }
152
+ }
153
+ })()
154
+
155
+ try {
156
+ await Promise.race([consume, timer])
157
+ } catch (err) {
158
+ try { Promise.resolve(result.return?.()).catch(() => {}) } catch { /* best-effort cleanup */ }
159
+ if (String(err?.message ?? '').startsWith('timeout after')) fail('timeout')
160
+ fail(`exception:${String(err?.message ?? err).slice(0, 200)}`)
161
+ } finally {
162
+ clearTimeout(timeoutId)
163
+ try { Promise.resolve(result.return?.()).catch(() => {}) } catch { /* best-effort cleanup */ }
164
+ }
165
+
166
+ log(`[sdk-spike] response: ${responseText}`)
167
+
168
+ if (!gotInit) fail('no-system-init')
169
+ if (!OAUTH_API_KEY_SOURCES.has(apiKeySource)) fail(`wrong-api-key-source:${apiKeySource}`)
170
+ if (assistantError) fail(`assistant-error:${assistantError}`)
171
+ if (!gotAssistant) fail('no-response')
172
+ if (responseText.trim() === '') fail('empty-response')
173
+ pass(`apiKeySource=${apiKeySource} apiProvider=${apiProvider} sdk=${sdkVersion} cli=${claudeCodeVersion}`)
174
+ }
175
+
176
+ main().catch((err) => {
177
+ fail(`exception:${String(err?.message ?? err).slice(0, 200)}`)
178
+ })
@@ -1 +1 @@
1
- import{t as e}from"./jsx-runtime-BUs3sHtV.js";var t=e();function n({checked:e,onChange:n,label:r,disabled:i}){return(0,t.jsxs)(`label`,{className:`maxy-checkbox${i?` maxy-checkbox--disabled`:``}`,children:[(0,t.jsx)(`input`,{type:`checkbox`,checked:e,onChange:e=>n(e.target.checked),disabled:i}),(0,t.jsx)(`span`,{className:`maxy-checkbox__box`,children:`✱`}),r&&(0,t.jsx)(`span`,{className:`maxy-checkbox__label`,children:r})]})}export{n as t};
1
+ import{t as e}from"./jsx-runtime-DV3X_CC7.js";var t=e();function n({checked:e,onChange:n,label:r,disabled:i}){return(0,t.jsxs)(`label`,{className:`maxy-checkbox${i?` maxy-checkbox--disabled`:``}`,children:[(0,t.jsx)(`input`,{type:`checkbox`,checked:e,onChange:e=>n(e.target.checked),disabled:i}),(0,t.jsx)(`span`,{className:`maxy-checkbox__box`,children:`✱`}),r&&(0,t.jsx)(`span`,{className:`maxy-checkbox__label`,children:r})]})}export{n as t};