@bububuger/spanory 0.1.14

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.
@@ -0,0 +1,570 @@
1
+ // @ts-nocheck
2
+ import { createHash } from 'node:crypto';
3
+ function toNumber(value) {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : undefined;
6
+ }
7
+ export function pickUsage(raw) {
8
+ if (!raw || typeof raw !== 'object')
9
+ return undefined;
10
+ const inputTokens = toNumber(raw.input_tokens ?? raw.prompt_tokens);
11
+ const outputTokens = toNumber(raw.output_tokens ?? raw.completion_tokens);
12
+ const totalTokens = toNumber(raw.total_tokens) ?? ((inputTokens ?? 0) + (outputTokens ?? 0) || undefined);
13
+ const cacheReadInputTokens = toNumber(raw.cache_read_input_tokens);
14
+ const cacheCreationInputTokens = toNumber(raw.cache_creation_input_tokens);
15
+ const usage = {};
16
+ if (inputTokens !== undefined)
17
+ usage.input_tokens = inputTokens;
18
+ if (outputTokens !== undefined)
19
+ usage.output_tokens = outputTokens;
20
+ if (totalTokens !== undefined)
21
+ usage.total_tokens = totalTokens;
22
+ if (cacheReadInputTokens !== undefined)
23
+ usage.cache_read_input_tokens = cacheReadInputTokens;
24
+ if (cacheCreationInputTokens !== undefined)
25
+ usage.cache_creation_input_tokens = cacheCreationInputTokens;
26
+ return Object.keys(usage).length ? usage : undefined;
27
+ }
28
+ function addUsage(total, usage) {
29
+ if (!usage)
30
+ return;
31
+ for (const [key, value] of Object.entries(usage)) {
32
+ total[key] = (total[key] ?? 0) + Number(value);
33
+ }
34
+ }
35
+ function usageAttributes(usage) {
36
+ if (!usage)
37
+ return {};
38
+ const attrs = {};
39
+ if (usage.input_tokens !== undefined) {
40
+ attrs['gen_ai.usage.input_tokens'] = usage.input_tokens;
41
+ attrs['gen_ai.usage.prompt_tokens'] = usage.input_tokens;
42
+ }
43
+ if (usage.output_tokens !== undefined) {
44
+ attrs['gen_ai.usage.output_tokens'] = usage.output_tokens;
45
+ attrs['gen_ai.usage.completion_tokens'] = usage.output_tokens;
46
+ }
47
+ if (usage.total_tokens !== undefined) {
48
+ attrs['gen_ai.usage.total_tokens'] = usage.total_tokens;
49
+ }
50
+ if (usage.cache_read_input_tokens !== undefined) {
51
+ attrs['gen_ai.usage.details.cache_read_input_tokens'] = usage.cache_read_input_tokens;
52
+ }
53
+ if (usage.cache_creation_input_tokens !== undefined) {
54
+ attrs['gen_ai.usage.details.cache_creation_input_tokens'] = usage.cache_creation_input_tokens;
55
+ }
56
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
57
+ const denominator = (usage.input_tokens ?? 0) + cacheRead;
58
+ const cacheHitRate = denominator > 0 ? cacheRead / denominator : 0;
59
+ attrs['gen_ai.usage.details.cache_hit_rate'] = Number(cacheHitRate.toFixed(6));
60
+ attrs['langfuse.observation.usage_details'] = JSON.stringify({
61
+ ...(usage.input_tokens !== undefined ? { input: usage.input_tokens } : {}),
62
+ ...(usage.output_tokens !== undefined ? { output: usage.output_tokens } : {}),
63
+ ...(usage.total_tokens !== undefined ? { total: usage.total_tokens } : {}),
64
+ ...(usage.cache_read_input_tokens !== undefined ? { input_cache_read: usage.cache_read_input_tokens } : {}),
65
+ ...(usage.cache_creation_input_tokens !== undefined
66
+ ? { input_cache_creation: usage.cache_creation_input_tokens }
67
+ : {}),
68
+ });
69
+ return attrs;
70
+ }
71
+ function modelAttributes(model) {
72
+ if (!model)
73
+ return {};
74
+ return {
75
+ 'langfuse.observation.model.name': model,
76
+ 'gen_ai.request.model': model,
77
+ };
78
+ }
79
+ function extractText(content) {
80
+ if (typeof content === 'string')
81
+ return content;
82
+ if (!Array.isArray(content))
83
+ return '';
84
+ return content
85
+ .map((block) => {
86
+ if (typeof block === 'string')
87
+ return block;
88
+ if (block && typeof block === 'object' && block.type === 'text')
89
+ return String(block.text ?? '');
90
+ return '';
91
+ })
92
+ .filter(Boolean)
93
+ .join('\n');
94
+ }
95
+ function extractToolUses(content) {
96
+ if (!Array.isArray(content))
97
+ return [];
98
+ return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_use');
99
+ }
100
+ function extractToolResults(content) {
101
+ if (!Array.isArray(content))
102
+ return [];
103
+ return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_result');
104
+ }
105
+ function extractReasoningBlocks(content) {
106
+ if (!Array.isArray(content))
107
+ return [];
108
+ return content.filter((block) => block && typeof block === 'object' && block.type === 'reasoning');
109
+ }
110
+ function isoFromUnknownTimestamp(value, fallback) {
111
+ const candidate = value instanceof Date ? value : new Date(value ?? '');
112
+ if (!Number.isNaN(candidate.getTime()))
113
+ return candidate.toISOString();
114
+ return fallback.toISOString();
115
+ }
116
+ function isToolResultOnlyContent(content) {
117
+ return Array.isArray(content)
118
+ && content.length > 0
119
+ && content.every((block) => block && typeof block === 'object' && block.type === 'tool_result');
120
+ }
121
+ function isPromptUserMessage(message) {
122
+ if (!message || message.role !== 'user' || message.isMeta)
123
+ return false;
124
+ const { content } = message;
125
+ if (typeof content === 'string')
126
+ return content.trim().length > 0;
127
+ if (!Array.isArray(content))
128
+ return false;
129
+ if (isToolResultOnlyContent(content))
130
+ return false;
131
+ return content.length > 0;
132
+ }
133
+ const GATEWAY_INPUT_METADATA_BLOCK_RE = /Conversation info \(untrusted metadata\):\s*```json\s*([\s\S]*?)\s*```\s*/i;
134
+ function runtimeVersionAttributes(version) {
135
+ if (version === undefined || version === null)
136
+ return {};
137
+ const normalized = String(version).trim();
138
+ if (!normalized)
139
+ return {};
140
+ return {
141
+ 'agentic.runtime.version': normalized,
142
+ };
143
+ }
144
+ function extractGatewayInputMetadata(text) {
145
+ if (!text)
146
+ return { input: '', attributes: {} };
147
+ const match = text.match(GATEWAY_INPUT_METADATA_BLOCK_RE);
148
+ if (!match)
149
+ return { input: text.trim(), attributes: {} };
150
+ const attributes = {};
151
+ const metadataRaw = match[1];
152
+ try {
153
+ const parsed = JSON.parse(metadataRaw);
154
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
155
+ attributes['agentic.input.metadata'] = JSON.stringify(parsed);
156
+ if (parsed.message_id !== undefined)
157
+ attributes['agentic.input.message_id'] = String(parsed.message_id);
158
+ if (parsed.sender !== undefined)
159
+ attributes['agentic.input.sender'] = String(parsed.sender);
160
+ }
161
+ }
162
+ catch {
163
+ // ignore malformed metadata JSON and only strip wrapper text
164
+ }
165
+ const input = text.slice(match.index + match[0].length).trim() || text.trim();
166
+ return { input, attributes };
167
+ }
168
+ function normalizeUserInput(content) {
169
+ const text = extractText(content).trim();
170
+ if (text)
171
+ return extractGatewayInputMetadata(text);
172
+ if (Array.isArray(content))
173
+ return { input: JSON.stringify(content), attributes: {} };
174
+ if (typeof content === 'string')
175
+ return extractGatewayInputMetadata(content);
176
+ return { input: '', attributes: {} };
177
+ }
178
+ function extractToolResultText(block, message) {
179
+ const raw = block?.content;
180
+ if (typeof raw === 'string' && raw.trim())
181
+ return raw;
182
+ if (Array.isArray(raw)) {
183
+ const text = extractText(raw).trim();
184
+ if (text)
185
+ return text;
186
+ return JSON.stringify(raw);
187
+ }
188
+ if (raw && typeof raw === 'object')
189
+ return JSON.stringify(raw);
190
+ const stdout = message?.toolUseResult?.stdout;
191
+ if (typeof stdout === 'string' && stdout.length > 0)
192
+ return stdout;
193
+ const stderr = message?.toolUseResult?.stderr;
194
+ if (typeof stderr === 'string' && stderr.length > 0)
195
+ return stderr;
196
+ return '';
197
+ }
198
+ function parseSlashCommand(text) {
199
+ const m = text.match(/<command-name>\s*\/([^<\s]+)\s*<\/command-name>/i);
200
+ if (m) {
201
+ const argsMatch = text.match(/<command-args>([\s\S]*?)<\/command-args>/i);
202
+ return { name: m[1].trim(), args: argsMatch ? argsMatch[1].trim() : '' };
203
+ }
204
+ // fallback for plain slash commands like "/compact please summarize"
205
+ const plain = String(text ?? '').trim().match(/^\/([a-zA-Z0-9._:-]+)(?:\s+([\s\S]*))?$/);
206
+ if (!plain)
207
+ return null;
208
+ return {
209
+ name: plain[1].trim(),
210
+ args: plain[2] ? plain[2].trim() : '',
211
+ };
212
+ }
213
+ function isMcpToolName(name) {
214
+ const n = String(name || '').toLowerCase();
215
+ return n === 'mcp' || n.startsWith('mcp__') || n.startsWith('mcp-');
216
+ }
217
+ function hashText(text) {
218
+ return createHash('sha256').update(String(text ?? '')).digest('hex');
219
+ }
220
+ function lineCount(text) {
221
+ const s = String(text ?? '');
222
+ if (!s)
223
+ return 0;
224
+ return s.split(/\r?\n/).length;
225
+ }
226
+ function tokenSet(text) {
227
+ const tokens = String(text ?? '').trim().split(/\s+/).filter(Boolean);
228
+ return new Set(tokens);
229
+ }
230
+ function similarityScore(a, b) {
231
+ if (a === b)
232
+ return 1;
233
+ const setA = tokenSet(a);
234
+ const setB = tokenSet(b);
235
+ if (setA.size === 0 && setB.size === 0)
236
+ return 1;
237
+ if (setA.size === 0 || setB.size === 0)
238
+ return 0;
239
+ let intersection = 0;
240
+ for (const token of setA) {
241
+ if (setB.has(token))
242
+ intersection += 1;
243
+ }
244
+ const union = setA.size + setB.size - intersection;
245
+ if (union === 0)
246
+ return 1;
247
+ return Number((intersection / union).toFixed(6));
248
+ }
249
+ function actorHeuristic(messages) {
250
+ const hasSidechainSignal = messages.some((m) => m?.isSidechain === true || (typeof m?.agentId === 'string' && m.agentId.trim().length > 0));
251
+ if (hasSidechainSignal)
252
+ return { role: 'unknown', confidence: 0.6 };
253
+ return { role: 'main', confidence: 0.95 };
254
+ }
255
+ function createTurn(messages, turnId, projectId, sessionId, runtime) {
256
+ const user = messages.find(isPromptUserMessage) ?? messages.find((m) => m.role === 'user' && !m.isMeta) ?? messages[0];
257
+ const assistantsRaw = messages.filter((m) => m.role === 'assistant');
258
+ const assistantOrder = [];
259
+ const assistantLatest = new Map();
260
+ for (let i = 0; i < assistantsRaw.length; i += 1) {
261
+ const msg = assistantsRaw[i];
262
+ const key = msg.messageId ? `id:${msg.messageId}` : `idx:${i}`;
263
+ if (!assistantLatest.has(key))
264
+ assistantOrder.push(key);
265
+ assistantLatest.set(key, msg);
266
+ }
267
+ const assistants = assistantOrder.map((key) => assistantLatest.get(key)).filter(Boolean);
268
+ const start = user?.timestamp ?? messages[0]?.timestamp ?? new Date();
269
+ const end = messages[messages.length - 1]?.timestamp ?? start;
270
+ const output = assistants.map((m) => extractText(m.content)).filter(Boolean).join('\n');
271
+ const runtimeVersion = [...messages]
272
+ .map((m) => String(m.runtimeVersion ?? '').trim())
273
+ .filter(Boolean)
274
+ .at(-1);
275
+ const runtimeAttrs = runtimeVersionAttributes(runtimeVersion);
276
+ const normalizedInput = normalizeUserInput(user?.content);
277
+ const totalUsage = {};
278
+ let latestModel;
279
+ for (const msg of assistants) {
280
+ if (msg.model)
281
+ latestModel = msg.model;
282
+ addUsage(totalUsage, msg.usage);
283
+ }
284
+ const usage = Object.keys(totalUsage).length ? totalUsage : undefined;
285
+ const actor = actorHeuristic(messages);
286
+ const events = [
287
+ {
288
+ runtime,
289
+ projectId,
290
+ sessionId,
291
+ turnId,
292
+ category: 'turn',
293
+ name: `${runtime} - Turn ${turnId}`,
294
+ startedAt: start.toISOString(),
295
+ endedAt: end.toISOString(),
296
+ input: normalizedInput.input,
297
+ output,
298
+ attributes: {
299
+ 'agentic.event.category': 'turn',
300
+ 'langfuse.observation.type': 'agent',
301
+ 'gen_ai.operation.name': 'invoke_agent',
302
+ ...runtimeAttrs,
303
+ ...modelAttributes(latestModel),
304
+ 'agentic.actor.role': actor.role,
305
+ 'agentic.actor.role_confidence': actor.confidence,
306
+ ...normalizedInput.attributes,
307
+ ...usageAttributes(usage),
308
+ },
309
+ },
310
+ ];
311
+ const resultByToolId = new Map();
312
+ for (const msg of messages) {
313
+ if (msg.role !== 'user')
314
+ continue;
315
+ const resultAt = isoFromUnknownTimestamp(msg.timestamp, end);
316
+ for (const tr of extractToolResults(msg.content)) {
317
+ const toolUseId = String(tr.tool_use_id ?? tr.toolUseId ?? '');
318
+ if (!toolUseId)
319
+ continue;
320
+ const content = extractToolResultText(tr, msg);
321
+ if (!resultByToolId.has(toolUseId) || !resultByToolId.get(toolUseId)?.content) {
322
+ resultByToolId.set(toolUseId, { content, endedAt: resultAt });
323
+ }
324
+ }
325
+ if (msg.sourceToolUseId) {
326
+ const fallback = extractToolResultText({}, msg);
327
+ if (fallback
328
+ && (!resultByToolId.has(msg.sourceToolUseId) || !resultByToolId.get(msg.sourceToolUseId)?.content)) {
329
+ resultByToolId.set(msg.sourceToolUseId, { content: fallback, endedAt: resultAt });
330
+ }
331
+ }
332
+ }
333
+ if (user?.role === 'user') {
334
+ const slash = parseSlashCommand(extractText(user.content));
335
+ if (slash) {
336
+ const isMcp = slash.name.toLowerCase() === 'mcp' || slash.name.toLowerCase().startsWith('mcp:');
337
+ events.push({
338
+ runtime,
339
+ projectId,
340
+ sessionId,
341
+ turnId,
342
+ category: isMcp ? 'mcp' : 'agent_command',
343
+ name: isMcp ? 'MCP Slash Command' : `Agent Command: /${slash.name}`,
344
+ startedAt: start.toISOString(),
345
+ endedAt: start.toISOString(),
346
+ input: extractText(user.content),
347
+ output: '',
348
+ attributes: {
349
+ 'agentic.event.category': isMcp ? 'mcp' : 'agent_command',
350
+ 'langfuse.observation.type': isMcp ? 'tool' : 'event',
351
+ ...runtimeAttrs,
352
+ 'agentic.command.name': slash.name,
353
+ 'agentic.command.args': slash.args,
354
+ 'gen_ai.operation.name': isMcp ? 'execute_tool' : 'invoke_agent',
355
+ },
356
+ });
357
+ }
358
+ }
359
+ for (const assistant of assistants) {
360
+ const reasoningBlocks = extractReasoningBlocks(assistant.content);
361
+ for (const reasoning of reasoningBlocks) {
362
+ const reasoningText = String(reasoning?.text ?? '').trim();
363
+ if (!reasoningText)
364
+ continue;
365
+ const reasoningAt = isoFromUnknownTimestamp(reasoning?.timestamp, assistant.timestamp);
366
+ events.push({
367
+ runtime,
368
+ projectId,
369
+ sessionId,
370
+ turnId,
371
+ category: 'reasoning',
372
+ name: 'Assistant Reasoning',
373
+ startedAt: reasoningAt,
374
+ endedAt: reasoningAt,
375
+ input: '',
376
+ output: reasoningText,
377
+ attributes: {
378
+ 'agentic.event.category': 'reasoning',
379
+ 'langfuse.observation.type': 'span',
380
+ ...runtimeAttrs,
381
+ 'gen_ai.operation.name': 'invoke_agent',
382
+ ...modelAttributes(assistant.model),
383
+ },
384
+ });
385
+ }
386
+ const toolUses = extractToolUses(assistant.content);
387
+ for (const tu of toolUses) {
388
+ const toolName = String(tu.name ?? '');
389
+ const toolId = String(tu.id ?? '');
390
+ const toolInput = tu.input ?? {};
391
+ const toolResult = resultByToolId.get(toolId);
392
+ const toolOutput = toolResult?.content ?? '';
393
+ const t = assistant.timestamp.toISOString();
394
+ const toolEndedAt = toolResult?.endedAt ?? t;
395
+ if (toolName === 'Bash') {
396
+ const commandLine = String(toolInput.command ?? '');
397
+ events.push({
398
+ runtime,
399
+ projectId,
400
+ sessionId,
401
+ turnId,
402
+ category: 'shell_command',
403
+ name: 'Tool: Bash',
404
+ startedAt: t,
405
+ endedAt: toolEndedAt,
406
+ input: commandLine,
407
+ output: toolOutput,
408
+ attributes: {
409
+ 'agentic.event.category': 'shell_command',
410
+ 'langfuse.observation.type': 'tool',
411
+ ...runtimeAttrs,
412
+ 'process.command_line': commandLine,
413
+ 'gen_ai.tool.name': 'Bash',
414
+ 'gen_ai.tool.call.id': toolId,
415
+ 'gen_ai.operation.name': 'execute_tool',
416
+ ...modelAttributes(assistant.model),
417
+ ...usageAttributes(assistant.usage),
418
+ },
419
+ });
420
+ continue;
421
+ }
422
+ if (isMcpToolName(toolName)) {
423
+ const serverName = toolName.startsWith('mcp__') ? toolName.split('__')[1] : undefined;
424
+ events.push({
425
+ runtime,
426
+ projectId,
427
+ sessionId,
428
+ turnId,
429
+ category: 'mcp',
430
+ name: `Tool: ${toolName}`,
431
+ startedAt: t,
432
+ endedAt: toolEndedAt,
433
+ input: JSON.stringify(toolInput),
434
+ output: toolOutput,
435
+ attributes: {
436
+ 'agentic.event.category': 'mcp',
437
+ 'langfuse.observation.type': 'tool',
438
+ ...runtimeAttrs,
439
+ 'gen_ai.tool.name': toolName,
440
+ 'mcp.request.id': toolId,
441
+ 'gen_ai.operation.name': 'execute_tool',
442
+ ...(serverName ? { 'agentic.mcp.server.name': serverName } : {}),
443
+ ...modelAttributes(assistant.model),
444
+ ...usageAttributes(assistant.usage),
445
+ },
446
+ });
447
+ continue;
448
+ }
449
+ if (toolName === 'Task') {
450
+ events.push({
451
+ runtime,
452
+ projectId,
453
+ sessionId,
454
+ turnId,
455
+ category: 'agent_task',
456
+ name: 'Tool: Task',
457
+ startedAt: t,
458
+ endedAt: toolEndedAt,
459
+ input: JSON.stringify(toolInput),
460
+ output: toolOutput,
461
+ attributes: {
462
+ 'agentic.event.category': 'agent_task',
463
+ 'langfuse.observation.type': 'agent',
464
+ ...runtimeAttrs,
465
+ 'gen_ai.tool.name': 'Task',
466
+ 'gen_ai.tool.call.id': toolId,
467
+ 'gen_ai.operation.name': 'invoke_agent',
468
+ ...modelAttributes(assistant.model),
469
+ ...usageAttributes(assistant.usage),
470
+ },
471
+ });
472
+ continue;
473
+ }
474
+ if (toolName) {
475
+ events.push({
476
+ runtime,
477
+ projectId,
478
+ sessionId,
479
+ turnId,
480
+ category: 'tool',
481
+ name: `Tool: ${toolName}`,
482
+ startedAt: t,
483
+ endedAt: toolEndedAt,
484
+ input: JSON.stringify(toolInput),
485
+ output: toolOutput,
486
+ attributes: {
487
+ 'agentic.event.category': 'tool',
488
+ 'langfuse.observation.type': 'tool',
489
+ ...runtimeAttrs,
490
+ 'gen_ai.tool.name': toolName,
491
+ 'gen_ai.tool.call.id': toolId,
492
+ 'gen_ai.operation.name': 'execute_tool',
493
+ ...modelAttributes(assistant.model),
494
+ ...usageAttributes(assistant.usage),
495
+ },
496
+ });
497
+ }
498
+ }
499
+ }
500
+ const turnInput = String(events[0].input ?? '').trim();
501
+ const turnOutput = String(events[0].output ?? '').trim();
502
+ if (!turnInput && !turnOutput && events.length === 1) {
503
+ return [];
504
+ }
505
+ events[0].attributes['agentic.subagent.calls'] = events.filter((e) => e.category === 'agent_task').length;
506
+ return events;
507
+ }
508
+ export function groupByTurns(messages) {
509
+ const turns = [];
510
+ let current = [];
511
+ for (const msg of messages) {
512
+ if (isPromptUserMessage(msg)) {
513
+ if (current.length > 0)
514
+ turns.push(current);
515
+ current = [msg];
516
+ continue;
517
+ }
518
+ if (current.length > 0)
519
+ current.push(msg);
520
+ }
521
+ if (current.length > 0)
522
+ turns.push(current);
523
+ return turns;
524
+ }
525
+ export function normalizeTranscriptMessages({ runtime, projectId, sessionId, messages }) {
526
+ const turns = groupByTurns(messages);
527
+ const events = [];
528
+ let previousInput = '';
529
+ let previousHash = '';
530
+ let hasPreviousTurn = false;
531
+ for (let i = 0; i < turns.length; i += 1) {
532
+ const turnEvents = createTurn(turns[i], `turn-${i + 1}`, projectId, sessionId, runtime);
533
+ if (!turnEvents.length)
534
+ continue;
535
+ const turnEvent = turnEvents[0];
536
+ const input = String(turnEvent.input ?? '');
537
+ const inputHash = hashText(input);
538
+ if (!hasPreviousTurn) {
539
+ turnEvent.attributes['agentic.turn.input.hash'] = inputHash;
540
+ turnEvent.attributes['agentic.turn.input.prev_hash'] = '';
541
+ turnEvent.attributes['agentic.turn.diff.char_delta'] = 0;
542
+ turnEvent.attributes['agentic.turn.diff.line_delta'] = 0;
543
+ turnEvent.attributes['agentic.turn.diff.similarity'] = 1;
544
+ turnEvent.attributes['agentic.turn.diff.changed'] = false;
545
+ hasPreviousTurn = true;
546
+ }
547
+ else {
548
+ turnEvent.attributes['agentic.turn.input.hash'] = inputHash;
549
+ turnEvent.attributes['agentic.turn.input.prev_hash'] = previousHash;
550
+ turnEvent.attributes['agentic.turn.diff.char_delta'] = input.length - previousInput.length;
551
+ turnEvent.attributes['agentic.turn.diff.line_delta'] = lineCount(input) - lineCount(previousInput);
552
+ turnEvent.attributes['agentic.turn.diff.similarity'] = similarityScore(previousInput, input);
553
+ turnEvent.attributes['agentic.turn.diff.changed'] = inputHash !== previousHash;
554
+ }
555
+ previousInput = input;
556
+ previousHash = inputHash;
557
+ events.push(...turnEvents);
558
+ }
559
+ return events;
560
+ }
561
+ export function parseProjectIdFromTranscriptPath(transcriptPath, marker) {
562
+ if (!transcriptPath)
563
+ return undefined;
564
+ const normalized = transcriptPath.replace(/\\/g, '/');
565
+ const idx = normalized.indexOf(marker);
566
+ if (idx === -1)
567
+ return undefined;
568
+ const rest = normalized.slice(idx + marker.length);
569
+ return rest.split('/')[0] || undefined;
570
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@bububuger/spanory",
3
+ "version": "0.1.14",
4
+ "description": "Spanory CLI for cross-runtime agent observability",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Bububuger/spanory.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "bin": {
19
+ "spanory": "dist/index.js"
20
+ },
21
+ "scripts": {
22
+ "check": "tsc -p tsconfig.runtime.json --noEmit",
23
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.runtime.json",
24
+ "test": "vitest run test/unit",
25
+ "test:bdd": "npm run build && vitest run test/bdd",
26
+ "test:golden:update": "npm run build && node test/fixtures/golden/otlp/update-golden.mjs && node test/fixtures/golden/codex/update-golden.mjs",
27
+ "build:bundle": "npm run --workspace @bububuger/backend-langfuse build && npm run --workspace @bububuger/otlp-core build && npm run build && mkdir -p build && esbuild dist/index.js --bundle --platform=node --format=cjs --target=node18 --outfile=build/spanory.cjs",
28
+ "build:bin:macos-arm64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-macos-arm64 --output ../../dist/spanory-macos-arm64",
29
+ "build:bin:macos-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-macos-x64 --output ../../dist/spanory-macos-x64",
30
+ "build:bin:linux-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-linux-x64 --output ../../dist/spanory-linux-x64",
31
+ "build:bin:win-x64": "npm run build:bundle && pkg build/spanory.cjs --targets node18-win-x64 --output ../../dist/spanory-win-x64.exe"
32
+ },
33
+ "dependencies": {
34
+ "commander": "^14.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "esbuild": "^0.25.10",
38
+ "pkg": "^5.8.1",
39
+ "vitest": "^3.2.4"
40
+ }
41
+ }