wormclaude 1.0.19 → 1.0.21

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/dist/api.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { loadStored, DEFAULT_BASE_URL } from './auth.js';
5
5
  import { StreamingToolCallParser } from './streamparser.js';
6
6
  import { safeJsonStringify } from './safejson.js';
7
+ import { getTrace, getSession } from './telemetry.js';
7
8
  export function loadConfig() {
8
9
  const stored = loadStored();
9
10
  return {
@@ -124,6 +125,7 @@ export async function* streamChat(messages, tools, config, signal) {
124
125
  headers: {
125
126
  'Content-Type': 'application/json',
126
127
  ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
128
+ ...(getTrace() ? { 'X-WC-Trace': getTrace(), 'X-WC-Session': getSession() } : {}),
127
129
  },
128
130
  body: JSON.stringify(body),
129
131
  signal,
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import React, { useState, useRef, useEffect } from 'react';
3
- import { render, Box, Text, useApp, useInput } from 'ink';
3
+ import { render, Box, Text, Static, useApp, useInput } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import * as path from 'node:path';
6
6
  import { theme, VERSION } from './theme.js';
7
7
  import { loadConfig, streamChat, fetchAccount } from './api.js';
8
+ import { newTrace, flushTelemetry } from './telemetry.js';
8
9
  import { tier } from './program.js';
9
10
  import { allToolSchemas, executeToolCalls, executeTool, toolLabel, setToolConfig } from './tools.js';
10
11
  import { sanitizeError, sanitizeOutput } from './errorsan.js';
@@ -84,18 +85,10 @@ const _initHistory = () => {
84
85
  sys.push({ role: 'system', content: _memCtx });
85
86
  return sys;
86
87
  };
87
- // FULLSCREEN (alternate screen) — WormClaude'un yöntemi: tüm ekranı ink yönetir,
88
- // scrollback YOK, resize'da HER ŞEY yeniden çizilir → sarmalanma/kaskad olmaz.
89
- // ?1049h alt-screen · ?1007h alternate-scroll (fare tekerleği → ok tuşu; seçim bozulmaz)
90
- try {
91
- process.stdout.write('\x1b[?1049h\x1b[?1007h\x1b[2J\x1b[H');
92
- }
93
- catch { }
94
- const _leaveAlt = () => { try {
95
- process.stdout.write('\x1b[?1007l\x1b[?1049l');
96
- }
97
- catch { } };
98
- process.on('exit', _leaveAlt);
88
+ // NORMAL buffer (alt-screen YOK) — transcript <Static> ile gerçek terminal scrollback'ine
89
+ // basılır. Böylece terminalin doğal kaydırması (fare tekerleği / Shift+PageUp) ve metin
90
+ // seçip kopyalama tam çalışır. Eskiden ?1049h alt-screen kullanılıyordu; o, scrollback'i ve
91
+ // kopyalamayı engelliyordu — kaldırıldı.
99
92
  function useDimensions() {
100
93
  const [d, setD] = useState({
101
94
  cols: process.stdout.columns || 80,
@@ -447,25 +440,9 @@ function App() {
447
440
  else if (key.downArrow)
448
441
  setCmdSel((s) => (s + 1) % n);
449
442
  });
450
- // Geçmişte kaydırma: PageUp/PageDown her zaman; boş input'ta ok tuşları (fare
451
- // tekerleği alternate-scroll ile ok tuşu yollar tekerlekle kaydırma).
452
- useInput((_inp, key) => {
453
- if (!started || perm || ask || lang === null)
454
- return;
455
- // satır-temelli; üst sınır render'da off=min(scroll,maxScroll) ile kırpılır
456
- if (key.pageUp)
457
- setScroll((s) => s + 10);
458
- else if (key.pageDown)
459
- setScroll((s) => Math.max(0, s - 10));
460
- else if (input === '' && !busy) {
461
- if (key.upArrow)
462
- setScroll((s) => s + 3);
463
- else if (key.downArrow)
464
- setScroll((s) => Math.max(0, s - 3));
465
- }
466
- });
467
- // Yeni mesaj gelince en alta dön
468
- useEffect(() => { setScroll(0); }, [items.length]);
443
+ // NOT: Transcript artık <Static> ile terminal scrollback'ine basılıyor; kaydırma
444
+ // terminalin kendi işi (fare tekerleği / Shift+PageUp). Tuşları YAKALAMIYORUZ ki
445
+ // doğal kaydırma + metin seçip kopyalama bozulmasın.
469
446
  // Başlangıçta güven seviyesini (Doğrulanmış Araştırmacı Programı) çek → StatusLine rozeti
470
447
  useEffect(() => {
471
448
  if (!started)
@@ -518,6 +495,7 @@ function App() {
518
495
  const ac = new AbortController();
519
496
  abortRef.current = ac;
520
497
  setBusy(true);
498
+ newTrace(); // observability: bu tur için yeni dağıtık-trace id (gateway + tool span'ları birleşir)
521
499
  push({ kind: 'user', text: displayText ?? userText });
522
500
  historyRef.current = [...historyRef.current, { role: 'user', content: userText }];
523
501
  let iter = 0;
@@ -675,6 +653,8 @@ function App() {
675
653
  // Oto-hafıza: eşik geçildiyse arka planda hafızayı güncelle
676
654
  if (shouldExtract(historyRef.current))
677
655
  triggerMemory(historyRef.current, config);
656
+ // Observability: bu turun CLI span'larını (tool/skill/agent) gateway'e yolla — best-effort.
657
+ flushTelemetry(config.baseUrl, config.apiKey);
678
658
  }
679
659
  async function onSubmit(value) {
680
660
  let v = value.trim();
@@ -852,33 +832,12 @@ function App() {
852
832
  React.createElement(Box, { flexGrow: 1 }),
853
833
  React.createElement(StatusLine, { model: config.model, ctxTokens: 0 })));
854
834
  }
855
- return (React.createElement(Box, { flexDirection: "column", height: rows, width: cols },
856
- React.createElement(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden" }, (() => {
857
- // Satır-temelli kaydırma (Claude tarzı): tüm öğeleri satıra çevir, pencere göster
858
- const all = buildLines(items, cols);
859
- const reserve = busy ? 4 : 6;
860
- const avail = Math.max(4, rows - reserve - (streaming ? 3 : 0) - 2); // -2: göstergeler
861
- const maxScroll = Math.max(0, all.length - avail);
862
- const off = Math.min(scroll, maxScroll); // alttan kaç satır yukarı
863
- const startLine = Math.max(0, all.length - avail - off);
864
- const view = all.slice(startLine, startLine + avail);
865
- return (React.createElement(React.Fragment, null,
866
- startLine > 0 ? React.createElement(Text, { color: theme.greyDim },
867
- " \u2191 ",
868
- startLine,
869
- " sat\u0131r \u00B7 PageUp") : null,
870
- view.map((segs, i) => (React.createElement(Text, { key: i }, segs.length === 0
871
- ? ' '
872
- : segs.map((s, j) => (React.createElement(Text, { key: j, color: s.dim ? theme.greyDim : s.color, bold: s.bold }, linkify(s.text))))))),
873
- off > 0 ? React.createElement(Text, { color: theme.greyDim },
874
- " \u2193 ",
875
- off,
876
- " sat\u0131r \u00B7 PageDown / Son") : null,
877
- streaming && off === 0 ? (React.createElement(Box, { flexDirection: "row" },
878
- React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
879
- React.createElement(Box, { flexDirection: "column" },
880
- React.createElement(Text, { color: theme.white }, streaming)))) : null));
881
- })()),
835
+ return (React.createElement(Box, { flexDirection: "column", width: cols },
836
+ React.createElement(Static, { items: items }, (item, i) => React.createElement(RenderItem, { key: i, item: item })),
837
+ streaming ? (React.createElement(Box, { marginTop: 1, flexDirection: "row" },
838
+ React.createElement(Text, { color: theme.redBright, bold: true }, "\u23FA "),
839
+ React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
840
+ React.createElement(MarkdownDisplay, { text: streaming })))) : null,
882
841
  perm ? (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.red, paddingX: 1, marginTop: 1 },
883
842
  React.createElement(Text, { color: theme.redBright, bold: true }, t('perm.title', perm.label)),
884
843
  permMode === 'feedback' ? (React.createElement(React.Fragment, null,
@@ -0,0 +1,132 @@
1
+ // Bağımlılıksız terminal sözdizimi vurgusu (lowlight/highlight.js yerine hafif tokenizer).
2
+ // Kod bloklarını dile göre token'lara böler; her token'a tema rengi atar. Asla patlamaz —
3
+ // tanımadığı dilde ortak kurallarla (string/sayı/yorum/anahtar kelime) renklendirir.
4
+ import { theme } from './theme.js';
5
+ // Dil bazlı anahtar kelime setleri (ortak + spesifik).
6
+ const KW = {
7
+ js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'new', 'class', 'extends', 'super', 'this', 'typeof', 'instanceof', 'in', 'of', 'await', 'async', 'yield', 'try', 'catch', 'finally', 'throw', 'import', 'export', 'from', 'default', 'delete', 'void', 'null', 'undefined', 'true', 'false'],
8
+ ts: ['interface', 'type', 'enum', 'implements', 'public', 'private', 'protected', 'readonly', 'abstract', 'namespace', 'declare', 'as', 'keyof', 'infer', 'satisfies'],
9
+ py: ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'break', 'continue', 'import', 'from', 'as', 'with', 'try', 'except', 'finally', 'raise', 'lambda', 'yield', 'global', 'nonlocal', 'pass', 'assert', 'del', 'in', 'is', 'not', 'and', 'or', 'None', 'True', 'False', 'self', 'async', 'await'],
10
+ bash: ['if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'do', 'done', 'case', 'esac', 'function', 'in', 'return', 'export', 'local', 'echo', 'cd', 'source', 'set', 'unset'],
11
+ go: ['func', 'package', 'import', 'var', 'const', 'type', 'struct', 'interface', 'map', 'chan', 'go', 'defer', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'select', 'break', 'continue', 'nil', 'true', 'false'],
12
+ rust: ['fn', 'let', 'mut', 'const', 'struct', 'enum', 'impl', 'trait', 'pub', 'use', 'mod', 'match', 'if', 'else', 'for', 'while', 'loop', 'return', 'self', 'Self', 'where', 'async', 'await', 'move', 'ref', 'true', 'false'],
13
+ java: ['public', 'private', 'protected', 'class', 'interface', 'extends', 'implements', 'static', 'final', 'void', 'new', 'return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'this', 'super', 'null', 'true', 'false'],
14
+ css: ['important', 'inherit', 'initial', 'auto', 'none', 'flex', 'grid', 'block', 'inline'],
15
+ };
16
+ function langKeywords(lang) {
17
+ const l = (lang || '').toLowerCase();
18
+ const base = [];
19
+ if (l === 'ts' || l === 'tsx' || l === 'typescript')
20
+ base.push(...KW.js, ...KW.ts);
21
+ else if (l === 'js' || l === 'jsx' || l === 'javascript' || l === 'json' || l === 'node')
22
+ base.push(...KW.js);
23
+ else if (l === 'py' || l === 'python')
24
+ base.push(...KW.py);
25
+ else if (l === 'sh' || l === 'bash' || l === 'shell' || l === 'zsh')
26
+ base.push(...KW.bash);
27
+ else if (l === 'go' || l === 'golang')
28
+ base.push(...KW.go);
29
+ else if (l === 'rs' || l === 'rust')
30
+ base.push(...KW.rust);
31
+ else if (l === 'java')
32
+ base.push(...KW.java);
33
+ else if (l === 'css' || l === 'scss')
34
+ base.push(...KW.css);
35
+ else
36
+ base.push(...KW.js, ...KW.py); // bilinmeyen dil → en yaygın iki set
37
+ return new Set(base);
38
+ }
39
+ const lineComment = (lang) => {
40
+ const l = (lang || '').toLowerCase();
41
+ if (['py', 'python', 'sh', 'bash', 'shell', 'zsh', 'yaml', 'yml', 'rb', 'ruby', 'toml'].includes(l))
42
+ return '#';
43
+ return '//';
44
+ };
45
+ // Bir satırı token'lara böl. Basit ama sağlam bir durum makinesi.
46
+ function tokenizeLine(line, kw, lc) {
47
+ const toks = [];
48
+ let i = 0;
49
+ const n = line.length;
50
+ const push = (text, color) => { if (text)
51
+ toks.push({ text, color }); };
52
+ while (i < n) {
53
+ const ch = line[i];
54
+ // satır yorumu (// veya #)
55
+ if (line.startsWith(lc, i)) {
56
+ push(line.slice(i), theme.synComment);
57
+ break;
58
+ }
59
+ // dizgi: " ' `
60
+ if (ch === '"' || ch === "'" || ch === '`') {
61
+ let j = i + 1;
62
+ while (j < n && line[j] !== ch) {
63
+ if (line[j] === '\\')
64
+ j++;
65
+ j++;
66
+ }
67
+ push(line.slice(i, Math.min(j + 1, n)), theme.synString);
68
+ i = j + 1;
69
+ continue;
70
+ }
71
+ // sayı
72
+ if (/[0-9]/.test(ch) && (i === 0 || !/[A-Za-z_]/.test(line[i - 1]))) {
73
+ let j = i;
74
+ while (j < n && /[0-9a-fA-FxX._]/.test(line[j]))
75
+ j++;
76
+ push(line.slice(i, j), theme.synNumber);
77
+ i = j;
78
+ continue;
79
+ }
80
+ // tanımlayıcı / anahtar kelime
81
+ if (/[A-Za-z_$@]/.test(ch)) {
82
+ let j = i;
83
+ while (j < n && /[A-Za-z0-9_$]/.test(line[j]))
84
+ j++;
85
+ const word = line.slice(i, j);
86
+ const after = line[j];
87
+ if (kw.has(word))
88
+ push(word, theme.synKeyword);
89
+ else if (after === '(')
90
+ push(word, theme.synFunc); // fonksiyon çağrısı
91
+ else if (/^[A-Z]/.test(word))
92
+ push(word, theme.synType); // Tip/Sınıf
93
+ else
94
+ push(word); // düz
95
+ i = j;
96
+ continue;
97
+ }
98
+ // tek karakter (operatör/noktalama) — düz
99
+ push(ch);
100
+ i++;
101
+ }
102
+ return toks;
103
+ }
104
+ /** Kodu satır-satır token dizilerine çevirir. Her satır Tok[]. */
105
+ export function highlight(code, lang = '') {
106
+ try {
107
+ const kw = langKeywords(lang);
108
+ const lc = lineComment(lang);
109
+ let blockComment = false; // /* ... */ için
110
+ return code.split('\n').map((line) => {
111
+ // çok-satırlı /* */ yorum bloğu (basit destek)
112
+ if (blockComment) {
113
+ const end = line.indexOf('*/');
114
+ if (end === -1)
115
+ return [{ text: line, color: theme.synComment }];
116
+ blockComment = false;
117
+ const rest = tokenizeLine(line.slice(end + 2), kw, lc);
118
+ return [{ text: line.slice(0, end + 2), color: theme.synComment }, ...rest];
119
+ }
120
+ const start = line.indexOf('/*');
121
+ if (start !== -1 && line.indexOf('*/', start) === -1) {
122
+ blockComment = true;
123
+ const pre = tokenizeLine(line.slice(0, start), kw, lc);
124
+ return [...pre, { text: line.slice(start), color: theme.synComment }];
125
+ }
126
+ return tokenizeLine(line, kw, lc);
127
+ });
128
+ }
129
+ catch {
130
+ return code.split('\n').map((l) => [{ text: l, color: theme.synString }]);
131
+ }
132
+ }
package/dist/markdown.js CHANGED
@@ -4,6 +4,18 @@ import React from 'react';
4
4
  import { Text, Box } from 'ink';
5
5
  import stringWidth from 'string-width';
6
6
  import { theme } from './theme.js';
7
+ import { highlight } from './highlight.js';
8
+ // Kod bloğunu sözdizimi-vurgulu render eder (sol kenarda ince kırmızı çubuk + dil etiketi).
9
+ function CodeBlock({ code, lang }) {
10
+ const hl = highlight(code.join('\n'), lang);
11
+ return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
12
+ lang ? React.createElement(Text, { color: theme.greyDim },
13
+ ' ',
14
+ lang) : null,
15
+ hl.map((toks, i) => (React.createElement(Box, { key: i },
16
+ React.createElement(Text, { color: theme.greyDim }, "\u2502 "),
17
+ React.createElement(Text, null, toks.length ? toks.map((tk, j) => React.createElement(Text, { key: j, color: tk.color }, tk.text)) : ' '))))));
18
+ }
7
19
  // ── Inline markdown: **bold** *italik* ~~strike~~ `code` [text](url) <u>u</u> url ──
8
20
  const INLINE_RE = /(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|<u>.*?<\/u>|https?:\/\/\S+)/g;
9
21
  export function RenderInline({ text }) {
@@ -122,7 +134,7 @@ export function MarkdownDisplay({ text, width }) {
122
134
  const blocks = [];
123
135
  let key = 0;
124
136
  const push = (n) => blocks.push(React.createElement(React.Fragment, { key: key++ }, n));
125
- let inCode = false, codeFence = '', codeLines = [];
137
+ let inCode = false, codeFence = '', codeLang = '', codeLines = [];
126
138
  let inTable = false, tHeaders = [], tRows = [];
127
139
  const flushTable = () => { if (tHeaders.length)
128
140
  push(React.createElement(Table, { headers: tHeaders, rows: tRows, width: W })); inTable = false; tHeaders = []; tRows = []; };
@@ -137,9 +149,10 @@ export function MarkdownDisplay({ text, width }) {
137
149
  if (inCode) {
138
150
  const fm = line.match(fenceRe);
139
151
  if (fm && fm[1][0] === codeFence[0] && fm[1].length >= codeFence.length) {
140
- push(React.createElement(Box, { flexDirection: "column", marginY: 1 }, codeLines.map((c, i) => React.createElement(Text, { key: i, color: theme.green }, c))));
152
+ push(React.createElement(CodeBlock, { code: codeLines, lang: codeLang }));
141
153
  inCode = false;
142
154
  codeFence = '';
155
+ codeLang = '';
143
156
  codeLines = [];
144
157
  }
145
158
  else
@@ -153,6 +166,7 @@ export function MarkdownDisplay({ text, width }) {
153
166
  flushTable();
154
167
  inCode = true;
155
168
  codeFence = fm[1];
169
+ codeLang = (fm[2] || '').trim();
156
170
  return;
157
171
  }
158
172
  if (rm && !inTable) {
@@ -214,7 +228,7 @@ export function MarkdownDisplay({ text, width }) {
214
228
  React.createElement(RenderInline, { text: line })));
215
229
  });
216
230
  if (inCode)
217
- push(React.createElement(Box, { flexDirection: "column", marginY: 1 }, codeLines.map((c, i) => React.createElement(Text, { key: i, color: theme.green }, c))));
231
+ push(React.createElement(CodeBlock, { code: codeLines, lang: codeLang }));
218
232
  if (inTable)
219
233
  flushTable();
220
234
  return React.createElement(Box, { flexDirection: "column" }, blocks);
@@ -0,0 +1,37 @@
1
+ // Observability — istemci-tarafı dağıtık tracing. CLI kendi span'larını (tool/skill/agent/memory)
2
+ // gateway'e POST /v1/telemetry ile yollar; gateway kendi span'larını AYNI trace_id ile yazar →
3
+ // admin panelinde birleşik waterfall. trace_id ayrıca X-WC-Trace header'ı ile her isteğe gider.
4
+ let currentTrace = '';
5
+ let buffer = [];
6
+ const sessionId = 's-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
7
+ const rid = () => Math.random().toString(36).slice(2, 8);
8
+ /** Yeni tur başında çağrılır — yeni trace_id üretir, tamponu temizler. */
9
+ export function newTrace() {
10
+ currentTrace = 't-' + Date.now().toString(36) + rid();
11
+ buffer = [];
12
+ return currentTrace;
13
+ }
14
+ export function getTrace() { return currentTrace; }
15
+ export function getSession() { return sessionId; }
16
+ /** Bir span ekle (tampona). type: tool_called/tool_failed/skill_selected/agent_spawned/memory_summary_created… */
17
+ export function emit(type, attrs = {}, duration_ms = 0, status = 'ok') {
18
+ if (!currentTrace)
19
+ return;
20
+ buffer.push({ type, span_id: rid(), duration_ms: Math.round(duration_ms), status, ts: Date.now() / 1000, attrs });
21
+ }
22
+ /** Tampondaki span'ları gateway'e gönder (tur sonunda). Hata sessizce yutulur. */
23
+ export async function flushTelemetry(baseUrl, apiKey) {
24
+ if (!currentTrace || buffer.length === 0)
25
+ return;
26
+ const events = buffer;
27
+ buffer = [];
28
+ try {
29
+ await fetch(`${baseUrl}/telemetry`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
32
+ body: JSON.stringify({ trace_id: currentTrace, session_id: sessionId, events }),
33
+ signal: AbortSignal.timeout(5000),
34
+ });
35
+ }
36
+ catch { /* telemetri best-effort — kullanıcıyı etkilemez */ }
37
+ }
package/dist/theme.js CHANGED
@@ -7,5 +7,13 @@ export const theme = {
7
7
  greyDim: '#666666',
8
8
  green: '#4ade80',
9
9
  errorRed: '#ff6b6b',
10
+ // ── Sözdizimi vurgusu (kod blokları) ──
11
+ synKeyword: '#ff5c5c', // anahtar kelimeler (if, function, def…) — marka kırmızısı
12
+ synString: '#4ade80', // dizgiler "…" '…' `…`
13
+ synNumber: '#f0b429', // sayılar
14
+ synComment: '#666666', // yorumlar
15
+ synFunc: '#4fc3f7', // fonksiyon/metot adları
16
+ synType: '#a78bfa', // tip/sınıf adları, sabitler
17
+ synProp: '#e0e0e0', // özellik/anahtar adları
10
18
  };
11
- export const VERSION = '1.0.19';
19
+ export const VERSION = '1.0.21';
package/dist/tools.js CHANGED
@@ -10,6 +10,7 @@ import { loadConfig } from './api.js';
10
10
  import { runAgentLoop } from './agent.js';
11
11
  import { resolveSubagent, subagentTypesHint } from './subagents.js';
12
12
  import { saveMemoryFact } from './memory.js';
13
+ import { emit as emitSpan } from './telemetry.js';
13
14
  import { tasks } from './tasks.js';
14
15
  import { getMcpToolSchemas, callMcpTool } from './mcp.js';
15
16
  import { getSkills, getSkill, buildSkillPrompt } from './skills.js';
@@ -707,7 +708,9 @@ export async function executeToolCalls(calls, hooks) {
707
708
  while (p < b.items.length) {
708
709
  const { c, i } = b.items[p++];
709
710
  hooks?.onStart?.(c, i);
711
+ const _t = Date.now();
710
712
  results[i] = await execOne(c, hooks);
713
+ emitSpan(results[i].ok ? 'tool_called' : 'tool_failed', { tool: c.name }, Date.now() - _t, results[i].ok ? 'ok' : 'error');
711
714
  hooks?.onResult?.(c, i, results[i]);
712
715
  }
713
716
  };
@@ -716,7 +719,9 @@ export async function executeToolCalls(calls, hooks) {
716
719
  else {
717
720
  for (const { c, i } of b.items) {
718
721
  hooks?.onStart?.(c, i);
722
+ const _t = Date.now();
719
723
  results[i] = await execOne(c, hooks);
724
+ emitSpan(results[i].ok ? 'tool_called' : 'tool_failed', { tool: c.name }, Date.now() - _t, results[i].ok ? 'ok' : 'error');
720
725
  hooks?.onResult?.(c, i, results[i]);
721
726
  }
722
727
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {