@rubytech/create-realagent 1.0.712 → 1.0.713
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/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +22 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-search/dist/index.js +69 -39
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/bm25-label-gate.test.ts +88 -0
- package/payload/platform/lib/graph-search/src/__tests__/expand-batch.test.ts +206 -0
- package/payload/platform/lib/graph-search/src/index.ts +100 -43
- package/payload/platform/plugins/docs/references/platform.md +3 -1
- package/payload/platform/scripts/check-sdk-oauth.mjs +178 -0
- package/payload/server/public/assets/{Checkbox-CjbS9JcG.js → Checkbox-Dr9MqNdk.js} +1 -1
- package/payload/server/public/assets/{admin-Ce9DbUuu.js → admin-CZ1QdDIj.js} +1 -1
- package/payload/server/public/assets/{data-C-SxjLC9.js → data-KcxxS-x3.js} +1 -1
- package/payload/server/public/assets/{file-D4cbAAuo.js → file-KlvYstdJ.js} +1 -1
- package/payload/server/public/assets/{graph-DJ7IfYHV.js → graph-BjGlgDDX.js} +12 -12
- package/payload/server/public/assets/{house-CYsVygEQ.js → house-CyE0Xd3r.js} +1 -1
- package/payload/server/public/assets/{jsx-runtime-DPXE45W9.css → jsx-runtime-CPtXdEwZ.css} +1 -1
- package/payload/server/public/assets/{public-BTOF98iO.js → public-C1gnzTxk.js} +1 -1
- package/payload/server/public/assets/{share-2-B-sbkB36.js → share-2-Q9lo8ZrW.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-DLVFx3ms.js → useVoiceRecorder-BH8HP7l_.js} +1 -1
- package/payload/server/public/assets/{x-BNidzSAn.js → x-BwY4lg-U.js} +1 -1
- package/payload/server/public/data.html +6 -6
- package/payload/server/public/graph.html +7 -7
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +69 -47
- /package/payload/server/public/assets/{jsx-runtime-BUs3sHtV.js → jsx-runtime-BKpb2FvO.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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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(
|
|
@@ -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-
|
|
1
|
+
import{t as e}from"./jsx-runtime-BKpb2FvO.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};
|