memorix 1.1.7 → 1.1.8
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/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Image Loader — Vision LLM Integration
|
|
3
|
-
*
|
|
4
|
-
* Analyzes images via OpenAI Vision API (or compatible),
|
|
5
|
-
* extracting descriptions, tags, and entities.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { getLLMApiKey, getLLMBaseUrl, getLLMModel } from '../config.js';
|
|
9
|
-
import { isLLMEnabled, getLLMConfig } from '../llm/provider.js';
|
|
10
|
-
|
|
11
|
-
// Providers that use the OpenAI-compatible /chat/completions Vision endpoint
|
|
12
|
-
const OPENAI_COMPATIBLE_PROVIDERS = new Set(['openai', 'openrouter', 'custom']);
|
|
13
|
-
|
|
14
|
-
// ── Types ────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export interface ImageInput {
|
|
17
|
-
/** Base64-encoded image data */
|
|
18
|
-
base64: string;
|
|
19
|
-
/** Image MIME type (default: image/png) */
|
|
20
|
-
mimeType?: string;
|
|
21
|
-
/** Original filename */
|
|
22
|
-
filename?: string;
|
|
23
|
-
/** Custom analysis prompt */
|
|
24
|
-
prompt?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ImageAnalysisResult {
|
|
28
|
-
/** Natural language description of the image */
|
|
29
|
-
description: string;
|
|
30
|
-
/** Relevant tags/categories */
|
|
31
|
-
tags: string[];
|
|
32
|
-
/** Key entities/concepts depicted */
|
|
33
|
-
entities: string[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Internal Vision LLM Call ─────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
async function callVisionLLM(
|
|
39
|
-
systemPrompt: string,
|
|
40
|
-
imageBase64: string,
|
|
41
|
-
mimeType: string,
|
|
42
|
-
): Promise<string> {
|
|
43
|
-
const apiKey = getLLMApiKey();
|
|
44
|
-
if (!apiKey) {
|
|
45
|
-
throw new Error('No LLM API key configured for image analysis.');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let baseUrl = getLLMBaseUrl('https://api.openai.com/v1').replace(/\/+$/, '');
|
|
49
|
-
if (!baseUrl.endsWith('/v1')) baseUrl += '/v1';
|
|
50
|
-
const model = getLLMModel('gpt-4o');
|
|
51
|
-
|
|
52
|
-
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
headers: {
|
|
55
|
-
'Content-Type': 'application/json',
|
|
56
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify({
|
|
59
|
-
model,
|
|
60
|
-
messages: [{
|
|
61
|
-
role: 'user',
|
|
62
|
-
content: [
|
|
63
|
-
{ type: 'text', text: systemPrompt },
|
|
64
|
-
{
|
|
65
|
-
type: 'image_url',
|
|
66
|
-
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
}],
|
|
70
|
-
temperature: 0.1,
|
|
71
|
-
max_tokens: 1024,
|
|
72
|
-
}),
|
|
73
|
-
signal: AbortSignal.timeout(30_000),
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
const errorText = await response.text().catch(() => 'unknown error');
|
|
78
|
-
throw new Error(`Vision LLM error (${response.status}): ${errorText}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const data = await response.json() as {
|
|
82
|
-
choices: Array<{ message: { content: string } }>;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return data.choices[0]?.message?.content ?? '';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ── Public API ───────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
const DEFAULT_PROMPT =
|
|
91
|
-
'Analyze this image. Return ONLY a JSON object with this exact format: ' +
|
|
92
|
-
'{"description": "detailed description", "tags": ["tag1", "tag2"], "entities": ["entity1", "entity2"]}';
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Analyze an image using Vision LLM.
|
|
96
|
-
*
|
|
97
|
-
* @throws Error if LLM not configured.
|
|
98
|
-
*/
|
|
99
|
-
export async function analyzeImage(input: ImageInput): Promise<ImageAnalysisResult> {
|
|
100
|
-
if (!isLLMEnabled()) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
'LLM not configured for image analysis. ' +
|
|
103
|
-
'Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.',
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const config = getLLMConfig()!;
|
|
108
|
-
if (!OPENAI_COMPATIBLE_PROVIDERS.has(config.provider)) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Image analysis requires an OpenAI-compatible provider (openai, openrouter, or custom). ` +
|
|
111
|
-
`Current provider "${config.provider}" uses a different API shape. ` +
|
|
112
|
-
`Set MEMORIX_LLM_PROVIDER=openai or configure an OpenAI-compatible base URL.`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const mimeType = input.mimeType ?? 'image/png';
|
|
117
|
-
const prompt = input.prompt ?? DEFAULT_PROMPT;
|
|
118
|
-
|
|
119
|
-
const response = await callVisionLLM(prompt, input.base64, mimeType);
|
|
120
|
-
|
|
121
|
-
// Try to parse structured JSON response
|
|
122
|
-
try {
|
|
123
|
-
// Extract JSON from response (may be wrapped in markdown code block)
|
|
124
|
-
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
125
|
-
if (jsonMatch) {
|
|
126
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
127
|
-
return {
|
|
128
|
-
description: parsed.description ?? response,
|
|
129
|
-
tags: Array.isArray(parsed.tags) ? parsed.tags : [],
|
|
130
|
-
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
} catch {
|
|
134
|
-
// JSON parse failed — fall through to text extraction
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Fallback: treat entire response as description
|
|
138
|
-
return {
|
|
139
|
-
description: response,
|
|
140
|
-
tags: [],
|
|
141
|
-
entities: [],
|
|
142
|
-
};
|
|
143
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Image Loader — Vision LLM Integration
|
|
3
|
+
*
|
|
4
|
+
* Analyzes images via OpenAI Vision API (or compatible),
|
|
5
|
+
* extracting descriptions, tags, and entities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLLMApiKey, getLLMBaseUrl, getLLMModel } from '../config.js';
|
|
9
|
+
import { isLLMEnabled, getLLMConfig } from '../llm/provider.js';
|
|
10
|
+
|
|
11
|
+
// Providers that use the OpenAI-compatible /chat/completions Vision endpoint
|
|
12
|
+
const OPENAI_COMPATIBLE_PROVIDERS = new Set(['openai', 'openrouter', 'custom']);
|
|
13
|
+
|
|
14
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface ImageInput {
|
|
17
|
+
/** Base64-encoded image data */
|
|
18
|
+
base64: string;
|
|
19
|
+
/** Image MIME type (default: image/png) */
|
|
20
|
+
mimeType?: string;
|
|
21
|
+
/** Original filename */
|
|
22
|
+
filename?: string;
|
|
23
|
+
/** Custom analysis prompt */
|
|
24
|
+
prompt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ImageAnalysisResult {
|
|
28
|
+
/** Natural language description of the image */
|
|
29
|
+
description: string;
|
|
30
|
+
/** Relevant tags/categories */
|
|
31
|
+
tags: string[];
|
|
32
|
+
/** Key entities/concepts depicted */
|
|
33
|
+
entities: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Internal Vision LLM Call ─────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
async function callVisionLLM(
|
|
39
|
+
systemPrompt: string,
|
|
40
|
+
imageBase64: string,
|
|
41
|
+
mimeType: string,
|
|
42
|
+
): Promise<string> {
|
|
43
|
+
const apiKey = getLLMApiKey();
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
throw new Error('No LLM API key configured for image analysis.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let baseUrl = getLLMBaseUrl('https://api.openai.com/v1').replace(/\/+$/, '');
|
|
49
|
+
if (!baseUrl.endsWith('/v1')) baseUrl += '/v1';
|
|
50
|
+
const model = getLLMModel('gpt-4o');
|
|
51
|
+
|
|
52
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
model,
|
|
60
|
+
messages: [{
|
|
61
|
+
role: 'user',
|
|
62
|
+
content: [
|
|
63
|
+
{ type: 'text', text: systemPrompt },
|
|
64
|
+
{
|
|
65
|
+
type: 'image_url',
|
|
66
|
+
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
}],
|
|
70
|
+
temperature: 0.1,
|
|
71
|
+
max_tokens: 1024,
|
|
72
|
+
}),
|
|
73
|
+
signal: AbortSignal.timeout(30_000),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text().catch(() => 'unknown error');
|
|
78
|
+
throw new Error(`Vision LLM error (${response.status}): ${errorText}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const data = await response.json() as {
|
|
82
|
+
choices: Array<{ message: { content: string } }>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return data.choices[0]?.message?.content ?? '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Public API ───────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const DEFAULT_PROMPT =
|
|
91
|
+
'Analyze this image. Return ONLY a JSON object with this exact format: ' +
|
|
92
|
+
'{"description": "detailed description", "tags": ["tag1", "tag2"], "entities": ["entity1", "entity2"]}';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Analyze an image using Vision LLM.
|
|
96
|
+
*
|
|
97
|
+
* @throws Error if LLM not configured.
|
|
98
|
+
*/
|
|
99
|
+
export async function analyzeImage(input: ImageInput): Promise<ImageAnalysisResult> {
|
|
100
|
+
if (!isLLMEnabled()) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'LLM not configured for image analysis. ' +
|
|
103
|
+
'Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const config = getLLMConfig()!;
|
|
108
|
+
if (!OPENAI_COMPATIBLE_PROVIDERS.has(config.provider)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Image analysis requires an OpenAI-compatible provider (openai, openrouter, or custom). ` +
|
|
111
|
+
`Current provider "${config.provider}" uses a different API shape. ` +
|
|
112
|
+
`Set MEMORIX_LLM_PROVIDER=openai or configure an OpenAI-compatible base URL.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mimeType = input.mimeType ?? 'image/png';
|
|
117
|
+
const prompt = input.prompt ?? DEFAULT_PROMPT;
|
|
118
|
+
|
|
119
|
+
const response = await callVisionLLM(prompt, input.base64, mimeType);
|
|
120
|
+
|
|
121
|
+
// Try to parse structured JSON response
|
|
122
|
+
try {
|
|
123
|
+
// Extract JSON from response (may be wrapped in markdown code block)
|
|
124
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
125
|
+
if (jsonMatch) {
|
|
126
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
127
|
+
return {
|
|
128
|
+
description: parsed.description ?? response,
|
|
129
|
+
tags: Array.isArray(parsed.tags) ? parsed.tags : [],
|
|
130
|
+
entities: Array.isArray(parsed.entities) ? parsed.entities : [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// JSON parse failed — fall through to text extraction
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback: treat entire response as description
|
|
138
|
+
return {
|
|
139
|
+
description: response,
|
|
140
|
+
tags: [],
|
|
141
|
+
entities: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code stream-json parser.
|
|
3
|
-
*
|
|
4
|
-
* Parses NDJSON from `claude -p --output-format stream-json --verbose`
|
|
5
|
-
* into AgentMessage events. Based on observed format from Multica reference
|
|
6
|
-
* and Claude Code documentation.
|
|
7
|
-
*
|
|
8
|
-
* Message types:
|
|
9
|
-
* - "system" → session_id, status
|
|
10
|
-
* - "assistant" → content blocks (text, thinking, tool_use) + usage
|
|
11
|
-
* - "user" → tool_result content blocks
|
|
12
|
-
* - "result" → final result text, is_error, session_id
|
|
13
|
-
* - "log" → debug/info log entries
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { AgentMessage, TokenUsage } from './types.js';
|
|
17
|
-
|
|
18
|
-
// ── Claude SDK JSON shapes ──────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
interface ClaudeStreamLine {
|
|
21
|
-
type: string;
|
|
22
|
-
message?: unknown;
|
|
23
|
-
subtype?: string;
|
|
24
|
-
session_id?: string;
|
|
25
|
-
result?: string;
|
|
26
|
-
is_error?: boolean;
|
|
27
|
-
duration_ms?: number;
|
|
28
|
-
num_turns?: number;
|
|
29
|
-
log?: { level: string; message: string };
|
|
30
|
-
/** Per-model token usage — only present on 'result' messages */
|
|
31
|
-
modelUsage?: Record<string, {
|
|
32
|
-
inputTokens?: number;
|
|
33
|
-
outputTokens?: number;
|
|
34
|
-
cacheReadInputTokens?: number;
|
|
35
|
-
cacheCreationInputTokens?: number;
|
|
36
|
-
costUSD?: number;
|
|
37
|
-
}>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface ClaudeMessageContent {
|
|
41
|
-
role: string;
|
|
42
|
-
model?: string;
|
|
43
|
-
content?: ClaudeContentBlock[];
|
|
44
|
-
usage?: {
|
|
45
|
-
input_tokens?: number;
|
|
46
|
-
output_tokens?: number;
|
|
47
|
-
cache_read_input_tokens?: number;
|
|
48
|
-
cache_creation_input_tokens?: number;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface ClaudeContentBlock {
|
|
53
|
-
type: string;
|
|
54
|
-
text?: string;
|
|
55
|
-
id?: string;
|
|
56
|
-
name?: string;
|
|
57
|
-
input?: unknown;
|
|
58
|
-
tool_use_id?: string;
|
|
59
|
-
content?: unknown;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ── State ───────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
export interface ClaudeStreamState {
|
|
65
|
-
sessionId?: string;
|
|
66
|
-
usage: Record<string, TokenUsage>;
|
|
67
|
-
resultText?: string;
|
|
68
|
-
isError: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ── Parser ──────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Parse a single NDJSON line from Claude stream-json output.
|
|
75
|
-
* Returns zero or more AgentMessage events.
|
|
76
|
-
*/
|
|
77
|
-
export function parseClaudeStreamLine(
|
|
78
|
-
raw: string,
|
|
79
|
-
state: ClaudeStreamState,
|
|
80
|
-
): AgentMessage[] {
|
|
81
|
-
const line = raw.trim();
|
|
82
|
-
if (!line) return [];
|
|
83
|
-
|
|
84
|
-
let parsed: ClaudeStreamLine;
|
|
85
|
-
try {
|
|
86
|
-
parsed = JSON.parse(line);
|
|
87
|
-
} catch {
|
|
88
|
-
return [];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const messages: AgentMessage[] = [];
|
|
92
|
-
|
|
93
|
-
switch (parsed.type) {
|
|
94
|
-
case 'system':
|
|
95
|
-
if (parsed.session_id) {
|
|
96
|
-
state.sessionId = parsed.session_id;
|
|
97
|
-
}
|
|
98
|
-
break;
|
|
99
|
-
|
|
100
|
-
case 'assistant':
|
|
101
|
-
if (parsed.message) {
|
|
102
|
-
const content = parsed.message as ClaudeMessageContent;
|
|
103
|
-
// Accumulate usage
|
|
104
|
-
if (content.usage && content.model) {
|
|
105
|
-
const model = content.model;
|
|
106
|
-
const prev = state.usage[model] ?? { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, model };
|
|
107
|
-
prev.inputTokens += content.usage.input_tokens ?? 0;
|
|
108
|
-
prev.outputTokens += content.usage.output_tokens ?? 0;
|
|
109
|
-
prev.cacheReadTokens += content.usage.cache_read_input_tokens ?? 0;
|
|
110
|
-
prev.cacheWriteTokens += content.usage.cache_creation_input_tokens ?? 0;
|
|
111
|
-
state.usage[model] = prev;
|
|
112
|
-
}
|
|
113
|
-
// Parse content blocks
|
|
114
|
-
for (const block of content.content ?? []) {
|
|
115
|
-
switch (block.type) {
|
|
116
|
-
case 'text':
|
|
117
|
-
if (block.text) {
|
|
118
|
-
messages.push({ type: 'text', content: block.text });
|
|
119
|
-
}
|
|
120
|
-
break;
|
|
121
|
-
case 'thinking':
|
|
122
|
-
if (block.text) {
|
|
123
|
-
messages.push({ type: 'thinking', content: block.text });
|
|
124
|
-
}
|
|
125
|
-
break;
|
|
126
|
-
case 'tool_use':
|
|
127
|
-
messages.push({
|
|
128
|
-
type: 'tool_use',
|
|
129
|
-
tool: block.name ?? 'unknown',
|
|
130
|
-
callId: block.id,
|
|
131
|
-
input: typeof block.input === 'object' && block.input !== null
|
|
132
|
-
? block.input as Record<string, unknown>
|
|
133
|
-
: undefined,
|
|
134
|
-
});
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
break;
|
|
140
|
-
|
|
141
|
-
case 'user':
|
|
142
|
-
if (parsed.message) {
|
|
143
|
-
const content = parsed.message as ClaudeMessageContent;
|
|
144
|
-
for (const block of content.content ?? []) {
|
|
145
|
-
if (block.type === 'tool_result') {
|
|
146
|
-
const output = block.content != null ? String(block.content) : '';
|
|
147
|
-
messages.push({
|
|
148
|
-
type: 'tool_result',
|
|
149
|
-
callId: block.tool_use_id,
|
|
150
|
-
output: output.length > 4096 ? output.slice(0, 4096) : output,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
break;
|
|
156
|
-
|
|
157
|
-
case 'result':
|
|
158
|
-
if (parsed.session_id) state.sessionId = parsed.session_id;
|
|
159
|
-
if (parsed.result) state.resultText = parsed.result;
|
|
160
|
-
if (parsed.is_error) {
|
|
161
|
-
state.isError = true;
|
|
162
|
-
messages.push({ type: 'error', content: parsed.result ?? 'Unknown error' });
|
|
163
|
-
}
|
|
164
|
-
// Extract real token usage from result.modelUsage (per-model, camelCase)
|
|
165
|
-
// Note: assistant message usage fields are always 0 during streaming
|
|
166
|
-
if (parsed.modelUsage) {
|
|
167
|
-
for (const [model, mu] of Object.entries(parsed.modelUsage)) {
|
|
168
|
-
state.usage[model] = {
|
|
169
|
-
inputTokens: mu.inputTokens ?? 0,
|
|
170
|
-
outputTokens: mu.outputTokens ?? 0,
|
|
171
|
-
cacheReadTokens: mu.cacheReadInputTokens ?? 0,
|
|
172
|
-
cacheWriteTokens: mu.cacheCreationInputTokens ?? 0,
|
|
173
|
-
model,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
|
|
179
|
-
case 'log':
|
|
180
|
-
// Skip debug noise — only surface warnings/errors
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return messages;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Create a fresh parser state.
|
|
189
|
-
*/
|
|
190
|
-
export function createStreamState(): ClaudeStreamState {
|
|
191
|
-
return { usage: {}, isError: false };
|
|
192
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code stream-json parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses NDJSON from `claude -p --output-format stream-json --verbose`
|
|
5
|
+
* into AgentMessage events. Based on observed format from Multica reference
|
|
6
|
+
* and Claude Code documentation.
|
|
7
|
+
*
|
|
8
|
+
* Message types:
|
|
9
|
+
* - "system" → session_id, status
|
|
10
|
+
* - "assistant" → content blocks (text, thinking, tool_use) + usage
|
|
11
|
+
* - "user" → tool_result content blocks
|
|
12
|
+
* - "result" → final result text, is_error, session_id
|
|
13
|
+
* - "log" → debug/info log entries
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { AgentMessage, TokenUsage } from './types.js';
|
|
17
|
+
|
|
18
|
+
// ── Claude SDK JSON shapes ──────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
interface ClaudeStreamLine {
|
|
21
|
+
type: string;
|
|
22
|
+
message?: unknown;
|
|
23
|
+
subtype?: string;
|
|
24
|
+
session_id?: string;
|
|
25
|
+
result?: string;
|
|
26
|
+
is_error?: boolean;
|
|
27
|
+
duration_ms?: number;
|
|
28
|
+
num_turns?: number;
|
|
29
|
+
log?: { level: string; message: string };
|
|
30
|
+
/** Per-model token usage — only present on 'result' messages */
|
|
31
|
+
modelUsage?: Record<string, {
|
|
32
|
+
inputTokens?: number;
|
|
33
|
+
outputTokens?: number;
|
|
34
|
+
cacheReadInputTokens?: number;
|
|
35
|
+
cacheCreationInputTokens?: number;
|
|
36
|
+
costUSD?: number;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface ClaudeMessageContent {
|
|
41
|
+
role: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
content?: ClaudeContentBlock[];
|
|
44
|
+
usage?: {
|
|
45
|
+
input_tokens?: number;
|
|
46
|
+
output_tokens?: number;
|
|
47
|
+
cache_read_input_tokens?: number;
|
|
48
|
+
cache_creation_input_tokens?: number;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ClaudeContentBlock {
|
|
53
|
+
type: string;
|
|
54
|
+
text?: string;
|
|
55
|
+
id?: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
input?: unknown;
|
|
58
|
+
tool_use_id?: string;
|
|
59
|
+
content?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── State ───────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export interface ClaudeStreamState {
|
|
65
|
+
sessionId?: string;
|
|
66
|
+
usage: Record<string, TokenUsage>;
|
|
67
|
+
resultText?: string;
|
|
68
|
+
isError: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Parser ──────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse a single NDJSON line from Claude stream-json output.
|
|
75
|
+
* Returns zero or more AgentMessage events.
|
|
76
|
+
*/
|
|
77
|
+
export function parseClaudeStreamLine(
|
|
78
|
+
raw: string,
|
|
79
|
+
state: ClaudeStreamState,
|
|
80
|
+
): AgentMessage[] {
|
|
81
|
+
const line = raw.trim();
|
|
82
|
+
if (!line) return [];
|
|
83
|
+
|
|
84
|
+
let parsed: ClaudeStreamLine;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(line);
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const messages: AgentMessage[] = [];
|
|
92
|
+
|
|
93
|
+
switch (parsed.type) {
|
|
94
|
+
case 'system':
|
|
95
|
+
if (parsed.session_id) {
|
|
96
|
+
state.sessionId = parsed.session_id;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
case 'assistant':
|
|
101
|
+
if (parsed.message) {
|
|
102
|
+
const content = parsed.message as ClaudeMessageContent;
|
|
103
|
+
// Accumulate usage
|
|
104
|
+
if (content.usage && content.model) {
|
|
105
|
+
const model = content.model;
|
|
106
|
+
const prev = state.usage[model] ?? { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, model };
|
|
107
|
+
prev.inputTokens += content.usage.input_tokens ?? 0;
|
|
108
|
+
prev.outputTokens += content.usage.output_tokens ?? 0;
|
|
109
|
+
prev.cacheReadTokens += content.usage.cache_read_input_tokens ?? 0;
|
|
110
|
+
prev.cacheWriteTokens += content.usage.cache_creation_input_tokens ?? 0;
|
|
111
|
+
state.usage[model] = prev;
|
|
112
|
+
}
|
|
113
|
+
// Parse content blocks
|
|
114
|
+
for (const block of content.content ?? []) {
|
|
115
|
+
switch (block.type) {
|
|
116
|
+
case 'text':
|
|
117
|
+
if (block.text) {
|
|
118
|
+
messages.push({ type: 'text', content: block.text });
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case 'thinking':
|
|
122
|
+
if (block.text) {
|
|
123
|
+
messages.push({ type: 'thinking', content: block.text });
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case 'tool_use':
|
|
127
|
+
messages.push({
|
|
128
|
+
type: 'tool_use',
|
|
129
|
+
tool: block.name ?? 'unknown',
|
|
130
|
+
callId: block.id,
|
|
131
|
+
input: typeof block.input === 'object' && block.input !== null
|
|
132
|
+
? block.input as Record<string, unknown>
|
|
133
|
+
: undefined,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'user':
|
|
142
|
+
if (parsed.message) {
|
|
143
|
+
const content = parsed.message as ClaudeMessageContent;
|
|
144
|
+
for (const block of content.content ?? []) {
|
|
145
|
+
if (block.type === 'tool_result') {
|
|
146
|
+
const output = block.content != null ? String(block.content) : '';
|
|
147
|
+
messages.push({
|
|
148
|
+
type: 'tool_result',
|
|
149
|
+
callId: block.tool_use_id,
|
|
150
|
+
output: output.length > 4096 ? output.slice(0, 4096) : output,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'result':
|
|
158
|
+
if (parsed.session_id) state.sessionId = parsed.session_id;
|
|
159
|
+
if (parsed.result) state.resultText = parsed.result;
|
|
160
|
+
if (parsed.is_error) {
|
|
161
|
+
state.isError = true;
|
|
162
|
+
messages.push({ type: 'error', content: parsed.result ?? 'Unknown error' });
|
|
163
|
+
}
|
|
164
|
+
// Extract real token usage from result.modelUsage (per-model, camelCase)
|
|
165
|
+
// Note: assistant message usage fields are always 0 during streaming
|
|
166
|
+
if (parsed.modelUsage) {
|
|
167
|
+
for (const [model, mu] of Object.entries(parsed.modelUsage)) {
|
|
168
|
+
state.usage[model] = {
|
|
169
|
+
inputTokens: mu.inputTokens ?? 0,
|
|
170
|
+
outputTokens: mu.outputTokens ?? 0,
|
|
171
|
+
cacheReadTokens: mu.cacheReadInputTokens ?? 0,
|
|
172
|
+
cacheWriteTokens: mu.cacheCreationInputTokens ?? 0,
|
|
173
|
+
model,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'log':
|
|
180
|
+
// Skip debug noise — only surface warnings/errors
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return messages;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a fresh parser state.
|
|
189
|
+
*/
|
|
190
|
+
export function createStreamState(): ClaudeStreamState {
|
|
191
|
+
return { usage: {}, isError: false };
|
|
192
|
+
}
|