@respan/cli 0.5.2 → 0.6.0
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/commands/auth/login.js +2 -2
- package/dist/commands/integrate/claude-code.js +9 -28
- package/dist/commands/integrate/codex-cli.d.ts +8 -0
- package/dist/commands/integrate/codex-cli.js +123 -64
- package/dist/commands/integrate/gemini-cli.js +99 -53
- package/dist/hooks/claude-code.cjs +951 -0
- package/dist/hooks/claude-code.d.ts +1 -0
- package/dist/hooks/claude-code.js +641 -0
- package/dist/hooks/codex-cli.cjs +782 -0
- package/dist/hooks/codex-cli.d.ts +1 -0
- package/dist/hooks/codex-cli.js +457 -0
- package/dist/hooks/gemini-cli.cjs +825 -0
- package/dist/hooks/gemini-cli.d.ts +1 -0
- package/dist/hooks/gemini-cli.js +561 -0
- package/dist/hooks/shared.d.ts +82 -0
- package/dist/hooks/shared.js +461 -0
- package/dist/lib/integrate.d.ts +5 -1
- package/dist/lib/integrate.js +6 -2
- package/oclif.manifest.json +815 -814
- package/package.json +7 -3
- package/dist/assets/hook.py +0 -1052
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/hooks/gemini-cli.ts
|
|
26
|
+
var fs2 = __toESM(require("node:fs"), 1);
|
|
27
|
+
var os2 = __toESM(require("node:os"), 1);
|
|
28
|
+
var path2 = __toESM(require("node:path"), 1);
|
|
29
|
+
var import_node_child_process = require("node:child_process");
|
|
30
|
+
|
|
31
|
+
// src/hooks/shared.ts
|
|
32
|
+
var fs = __toESM(require("node:fs"), 1);
|
|
33
|
+
var os = __toESM(require("node:os"), 1);
|
|
34
|
+
var path = __toESM(require("node:path"), 1);
|
|
35
|
+
function resolveTracesEndpoint(baseUrl) {
|
|
36
|
+
const DEFAULT = "https://api.respan.ai/api/v2/traces";
|
|
37
|
+
if (!baseUrl) return DEFAULT;
|
|
38
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
39
|
+
if (normalized.endsWith("/api")) return `${normalized}/v2/traces`;
|
|
40
|
+
return `${normalized}/api/v2/traces`;
|
|
41
|
+
}
|
|
42
|
+
var resolveTracingIngestEndpoint = resolveTracesEndpoint;
|
|
43
|
+
var _logFile = null;
|
|
44
|
+
var _debug = false;
|
|
45
|
+
function initLogging(logFile, debug2) {
|
|
46
|
+
_logFile = logFile;
|
|
47
|
+
_debug = debug2;
|
|
48
|
+
const dir = path.dirname(logFile);
|
|
49
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
function log(level, message) {
|
|
52
|
+
if (!_logFile) return;
|
|
53
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
54
|
+
fs.appendFileSync(_logFile, `${ts} [${level}] ${message}
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
function debug(message) {
|
|
58
|
+
if (_debug) log("DEBUG", message);
|
|
59
|
+
}
|
|
60
|
+
var DEFAULT_BASE_URL = "https://api.respan.ai/api";
|
|
61
|
+
function resolveCredentials() {
|
|
62
|
+
let apiKey = process.env.RESPAN_API_KEY ?? "";
|
|
63
|
+
let baseUrl = process.env.RESPAN_BASE_URL ?? DEFAULT_BASE_URL;
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
const credsFile = path.join(os.homedir(), ".respan", "credentials.json");
|
|
66
|
+
if (fs.existsSync(credsFile)) {
|
|
67
|
+
try {
|
|
68
|
+
const creds = JSON.parse(fs.readFileSync(credsFile, "utf-8"));
|
|
69
|
+
const configFile = path.join(os.homedir(), ".respan", "config.json");
|
|
70
|
+
let profile = "default";
|
|
71
|
+
if (fs.existsSync(configFile)) {
|
|
72
|
+
const cfg = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
73
|
+
profile = cfg.activeProfile ?? "default";
|
|
74
|
+
}
|
|
75
|
+
const cred = creds[profile] ?? {};
|
|
76
|
+
apiKey = cred.apiKey ?? cred.accessToken ?? "";
|
|
77
|
+
if (!baseUrl || baseUrl === DEFAULT_BASE_URL) {
|
|
78
|
+
baseUrl = cred.baseUrl ?? baseUrl;
|
|
79
|
+
}
|
|
80
|
+
if (baseUrl && !baseUrl.replace(/\/+$/, "").endsWith("/api")) {
|
|
81
|
+
baseUrl = baseUrl.replace(/\/+$/, "") + "/api";
|
|
82
|
+
}
|
|
83
|
+
if (apiKey) {
|
|
84
|
+
debug(`Using API key from credentials.json (profile: ${profile})`);
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
debug(`Failed to read credentials.json: ${e}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!apiKey) return null;
|
|
92
|
+
return { apiKey, baseUrl };
|
|
93
|
+
}
|
|
94
|
+
var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
95
|
+
"customer_id",
|
|
96
|
+
"span_name",
|
|
97
|
+
"workflow_name",
|
|
98
|
+
"base_url",
|
|
99
|
+
"project_id"
|
|
100
|
+
]);
|
|
101
|
+
function loadRespanConfig(configPath) {
|
|
102
|
+
if (!fs.existsSync(configPath)) {
|
|
103
|
+
return { fields: {}, properties: {} };
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
107
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
108
|
+
return { fields: {}, properties: {} };
|
|
109
|
+
}
|
|
110
|
+
const fields = {};
|
|
111
|
+
const properties = {};
|
|
112
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
113
|
+
if (KNOWN_CONFIG_KEYS.has(k)) {
|
|
114
|
+
fields[k] = String(v);
|
|
115
|
+
} else {
|
|
116
|
+
properties[k] = String(v);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { fields, properties };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
debug(`Failed to load config from ${configPath}: ${e}`);
|
|
122
|
+
return { fields: {}, properties: {} };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function acquireLock(lockPath, timeoutMs = 5e3) {
|
|
126
|
+
const deadline = Date.now() + timeoutMs;
|
|
127
|
+
while (Date.now() < deadline) {
|
|
128
|
+
try {
|
|
129
|
+
fs.mkdirSync(lockPath);
|
|
130
|
+
return () => {
|
|
131
|
+
try {
|
|
132
|
+
fs.rmdirSync(lockPath);
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
} catch {
|
|
137
|
+
const waitMs = Math.min(100, deadline - Date.now());
|
|
138
|
+
if (waitMs > 0) {
|
|
139
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
debug("Could not acquire lock within timeout, proceeding without lock");
|
|
144
|
+
return () => {
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function nowISO() {
|
|
148
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
149
|
+
}
|
|
150
|
+
function parseTimestamp(ts) {
|
|
151
|
+
try {
|
|
152
|
+
const d = new Date(ts);
|
|
153
|
+
return isNaN(d.getTime()) ? null : d;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function latencySeconds(start, end) {
|
|
159
|
+
const s = parseTimestamp(start);
|
|
160
|
+
const e = parseTimestamp(end);
|
|
161
|
+
if (s && e) return (e.getTime() - s.getTime()) / 1e3;
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
function truncate(text, maxChars = 4e3) {
|
|
165
|
+
if (text.length <= maxChars) return text;
|
|
166
|
+
return text.slice(0, maxChars) + "\n... (truncated)";
|
|
167
|
+
}
|
|
168
|
+
function addDefaultsToAll(spans) {
|
|
169
|
+
return spans;
|
|
170
|
+
}
|
|
171
|
+
function resolveSpanFields(config, defaults) {
|
|
172
|
+
const fields = config?.fields ?? {};
|
|
173
|
+
return {
|
|
174
|
+
workflowName: process.env.RESPAN_WORKFLOW_NAME ?? fields.workflow_name ?? defaults.workflowName,
|
|
175
|
+
spanName: process.env.RESPAN_SPAN_NAME ?? fields.span_name ?? defaults.spanName,
|
|
176
|
+
customerId: process.env.RESPAN_CUSTOMER_ID ?? fields.customer_id ?? ""
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function buildMetadata(config, base = {}) {
|
|
180
|
+
const metadata = { ...base };
|
|
181
|
+
if (config?.properties) {
|
|
182
|
+
Object.assign(metadata, config.properties);
|
|
183
|
+
}
|
|
184
|
+
const envMetadata = process.env.RESPAN_METADATA;
|
|
185
|
+
if (envMetadata) {
|
|
186
|
+
try {
|
|
187
|
+
const extra = JSON.parse(envMetadata);
|
|
188
|
+
if (typeof extra === "object" && extra !== null) {
|
|
189
|
+
Object.assign(metadata, extra);
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return metadata;
|
|
195
|
+
}
|
|
196
|
+
function toOtlpValue(value) {
|
|
197
|
+
if (value === null || value === void 0) return null;
|
|
198
|
+
if (typeof value === "string") return { stringValue: value };
|
|
199
|
+
if (typeof value === "number") {
|
|
200
|
+
return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
|
|
201
|
+
}
|
|
202
|
+
if (typeof value === "boolean") return { boolValue: value };
|
|
203
|
+
if (Array.isArray(value)) {
|
|
204
|
+
const values = value.map(toOtlpValue).filter(Boolean);
|
|
205
|
+
return { arrayValue: { values } };
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === "object") {
|
|
208
|
+
const values = Object.entries(value).map(([k, v]) => {
|
|
209
|
+
const converted = toOtlpValue(v);
|
|
210
|
+
return converted ? { key: k, value: converted } : null;
|
|
211
|
+
}).filter(Boolean);
|
|
212
|
+
return { kvlistValue: { values } };
|
|
213
|
+
}
|
|
214
|
+
return { stringValue: String(value) };
|
|
215
|
+
}
|
|
216
|
+
function toOtlpAttributes(attrs) {
|
|
217
|
+
const result = [];
|
|
218
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
219
|
+
if (value === null || value === void 0) continue;
|
|
220
|
+
const converted = toOtlpValue(value);
|
|
221
|
+
if (converted) result.push({ key, value: converted });
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
function isoToNanos(iso) {
|
|
226
|
+
const d = new Date(iso);
|
|
227
|
+
if (isNaN(d.getTime())) return "0";
|
|
228
|
+
return String(BigInt(d.getTime()) * 1000000n);
|
|
229
|
+
}
|
|
230
|
+
function stringToTraceId(s) {
|
|
231
|
+
let hash = 0;
|
|
232
|
+
for (let i = 0; i < s.length; i++) {
|
|
233
|
+
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
|
|
234
|
+
}
|
|
235
|
+
let hash2 = 0;
|
|
236
|
+
for (let i = s.length - 1; i >= 0; i--) {
|
|
237
|
+
hash2 = (hash2 << 7) - hash2 + s.charCodeAt(i) | 0;
|
|
238
|
+
}
|
|
239
|
+
const hex1 = (hash >>> 0).toString(16).padStart(8, "0");
|
|
240
|
+
const hex2 = (hash2 >>> 0).toString(16).padStart(8, "0");
|
|
241
|
+
const hex3 = (s.length * 2654435761 >>> 0).toString(16).padStart(8, "0");
|
|
242
|
+
const hex4 = ((hash ^ hash2) >>> 0).toString(16).padStart(8, "0");
|
|
243
|
+
return (hex1 + hex2 + hex3 + hex4).slice(0, 32);
|
|
244
|
+
}
|
|
245
|
+
function stringToSpanId(s) {
|
|
246
|
+
let hash = 0;
|
|
247
|
+
for (let i = 0; i < s.length; i++) {
|
|
248
|
+
hash = (hash << 5) - hash + s.charCodeAt(i) | 0;
|
|
249
|
+
}
|
|
250
|
+
const hex1 = (hash >>> 0).toString(16).padStart(8, "0");
|
|
251
|
+
let hash2 = 0;
|
|
252
|
+
for (let i = s.length - 1; i >= 0; i--) {
|
|
253
|
+
hash2 = (hash2 << 3) - hash2 + s.charCodeAt(i) | 0;
|
|
254
|
+
}
|
|
255
|
+
const hex2 = (hash2 >>> 0).toString(16).padStart(8, "0");
|
|
256
|
+
return (hex1 + hex2).slice(0, 16);
|
|
257
|
+
}
|
|
258
|
+
function toOtlpPayload(spans) {
|
|
259
|
+
const otlpSpans = spans.map((span) => {
|
|
260
|
+
const attrs = {};
|
|
261
|
+
if (span.thread_identifier) attrs["respan.threads.thread_identifier"] = span.thread_identifier;
|
|
262
|
+
if (span.customer_identifier) attrs["respan.customer_params.customer_identifier"] = span.customer_identifier;
|
|
263
|
+
if (span.span_workflow_name) attrs["traceloop.workflow.name"] = span.span_workflow_name;
|
|
264
|
+
if (span.span_path) attrs["traceloop.entity.path"] = span.span_path;
|
|
265
|
+
const isRoot = !span.span_parent_id;
|
|
266
|
+
const isLlm = span.span_name.includes(".chat");
|
|
267
|
+
const isTool = span.span_name.startsWith("Tool:");
|
|
268
|
+
const isThinking = span.span_name.startsWith("Thinking") || span.span_name === "Reasoning";
|
|
269
|
+
if (isLlm) {
|
|
270
|
+
attrs["traceloop.span.kind"] = "task";
|
|
271
|
+
attrs["llm.request.type"] = "chat";
|
|
272
|
+
} else if (isTool) {
|
|
273
|
+
attrs["traceloop.span.kind"] = "tool";
|
|
274
|
+
} else if (isRoot) {
|
|
275
|
+
attrs["traceloop.span.kind"] = "workflow";
|
|
276
|
+
} else if (isThinking) {
|
|
277
|
+
attrs["traceloop.span.kind"] = "task";
|
|
278
|
+
}
|
|
279
|
+
if (span.model) attrs["gen_ai.request.model"] = span.model;
|
|
280
|
+
if (span.provider_id) attrs["gen_ai.system"] = span.provider_id;
|
|
281
|
+
if (span.prompt_tokens !== void 0) attrs["gen_ai.usage.prompt_tokens"] = span.prompt_tokens;
|
|
282
|
+
if (span.completion_tokens !== void 0) attrs["gen_ai.usage.completion_tokens"] = span.completion_tokens;
|
|
283
|
+
if (span.total_tokens !== void 0) attrs["llm.usage.total_tokens"] = span.total_tokens;
|
|
284
|
+
if (span.input) attrs["traceloop.entity.input"] = span.input;
|
|
285
|
+
if (span.output) attrs["traceloop.entity.output"] = span.output;
|
|
286
|
+
if (span.metadata && Object.keys(span.metadata).length > 0) {
|
|
287
|
+
attrs["respan.metadata"] = JSON.stringify(span.metadata);
|
|
288
|
+
}
|
|
289
|
+
attrs["respan.entity.log_method"] = "ts_tracing";
|
|
290
|
+
const startNanos = isoToNanos(span.start_time);
|
|
291
|
+
let endNanos = isoToNanos(span.timestamp);
|
|
292
|
+
if (startNanos === endNanos && span.latency && span.latency > 0) {
|
|
293
|
+
const startMs = new Date(span.start_time).getTime();
|
|
294
|
+
endNanos = String(BigInt(Math.round(startMs + span.latency * 1e3)) * 1000000n);
|
|
295
|
+
}
|
|
296
|
+
const otlpSpan = {
|
|
297
|
+
traceId: stringToTraceId(span.trace_unique_id),
|
|
298
|
+
spanId: stringToSpanId(span.span_unique_id),
|
|
299
|
+
name: span.span_name,
|
|
300
|
+
kind: isLlm ? 3 : 1,
|
|
301
|
+
// 3=CLIENT (LLM calls), 1=INTERNAL
|
|
302
|
+
startTimeUnixNano: startNanos,
|
|
303
|
+
endTimeUnixNano: endNanos,
|
|
304
|
+
attributes: toOtlpAttributes(attrs),
|
|
305
|
+
status: { code: 1 }
|
|
306
|
+
// STATUS_CODE_OK
|
|
307
|
+
};
|
|
308
|
+
if (span.span_parent_id) {
|
|
309
|
+
otlpSpan.parentSpanId = stringToSpanId(span.span_parent_id);
|
|
310
|
+
}
|
|
311
|
+
return otlpSpan;
|
|
312
|
+
});
|
|
313
|
+
return {
|
|
314
|
+
resourceSpans: [{
|
|
315
|
+
resource: {
|
|
316
|
+
attributes: toOtlpAttributes({
|
|
317
|
+
"service.name": "respan-cli-hooks"
|
|
318
|
+
})
|
|
319
|
+
},
|
|
320
|
+
scopeSpans: [{
|
|
321
|
+
scope: { name: "respan-cli-hooks", version: "0.5.3" },
|
|
322
|
+
spans: otlpSpans
|
|
323
|
+
}]
|
|
324
|
+
}]
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/hooks/gemini-cli.ts
|
|
329
|
+
var STATE_DIR = path2.join(os2.homedir(), ".gemini", "state");
|
|
330
|
+
var LOG_FILE = path2.join(STATE_DIR, "respan_hook.log");
|
|
331
|
+
var LOCK_PATH = path2.join(STATE_DIR, "respan_hook.lock");
|
|
332
|
+
var DEBUG_MODE = (process.env.GEMINI_RESPAN_DEBUG ?? "").toLowerCase() === "true";
|
|
333
|
+
var MAX_CHARS = parseInt(process.env.GEMINI_RESPAN_MAX_CHARS ?? "4000", 10) || 4e3;
|
|
334
|
+
var SEND_DELAY = parseInt(process.env.GEMINI_RESPAN_SEND_DELAY ?? "10", 10) || 10;
|
|
335
|
+
initLogging(LOG_FILE, DEBUG_MODE);
|
|
336
|
+
var TOOL_DISPLAY_NAMES = {
|
|
337
|
+
read_file: "File Read",
|
|
338
|
+
read_many_files: "File Read",
|
|
339
|
+
write_file: "File Write",
|
|
340
|
+
list_directory: "Directory List",
|
|
341
|
+
run_shell_command: "Shell",
|
|
342
|
+
google_web_search: "Web Search",
|
|
343
|
+
web_fetch: "Web Fetch",
|
|
344
|
+
glob: "Find Files",
|
|
345
|
+
grep_search: "Search Text",
|
|
346
|
+
search_file_content: "Search Text",
|
|
347
|
+
replace: "File Edit",
|
|
348
|
+
save_memory: "Memory",
|
|
349
|
+
write_todos: "Todos",
|
|
350
|
+
get_internal_docs: "Docs"
|
|
351
|
+
};
|
|
352
|
+
function toolDisplayName(name) {
|
|
353
|
+
return TOOL_DISPLAY_NAMES[name] ?? (name || "Unknown");
|
|
354
|
+
}
|
|
355
|
+
function formatToolInput(toolName, args) {
|
|
356
|
+
if (!args) return "";
|
|
357
|
+
const a = args;
|
|
358
|
+
if (toolName === "run_shell_command" && typeof a === "object") {
|
|
359
|
+
const cmd = String(a.command ?? "");
|
|
360
|
+
const dir = String(a.dir_path ?? "");
|
|
361
|
+
return truncate(dir ? `[${dir}] Command: ${cmd}` : `Command: ${cmd}`, MAX_CHARS);
|
|
362
|
+
}
|
|
363
|
+
if (["read_file", "write_file"].includes(toolName) && typeof a === "object")
|
|
364
|
+
return truncate(String(a.file_path ?? JSON.stringify(a)), MAX_CHARS);
|
|
365
|
+
if (toolName === "read_many_files" && typeof a === "object")
|
|
366
|
+
return truncate(String(a.include ?? JSON.stringify(a)), MAX_CHARS);
|
|
367
|
+
if (toolName === "google_web_search" && typeof a === "object")
|
|
368
|
+
return truncate(`Query: ${a.query ?? String(a)}`, MAX_CHARS);
|
|
369
|
+
if (["glob", "grep_search", "search_file_content"].includes(toolName) && typeof a === "object")
|
|
370
|
+
return truncate(String(a.pattern ?? JSON.stringify(a)), MAX_CHARS);
|
|
371
|
+
if (toolName === "replace" && typeof a === "object") {
|
|
372
|
+
const fp = String(a.file_path ?? "");
|
|
373
|
+
const old = String(a.old_string ?? "");
|
|
374
|
+
if (fp && old) return truncate(`${fp}: ${JSON.stringify(old)} \u2192 ...`, MAX_CHARS);
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
return truncate(JSON.stringify(args, null, 2), MAX_CHARS);
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
return truncate(String(args), MAX_CHARS);
|
|
381
|
+
}
|
|
382
|
+
function statePath(sessionId) {
|
|
383
|
+
const safeId = sessionId.replace(/[/\\]/g, "_").slice(0, 64);
|
|
384
|
+
return path2.join(STATE_DIR, `respan_stream_${safeId}.json`);
|
|
385
|
+
}
|
|
386
|
+
function loadStreamState(sessionId) {
|
|
387
|
+
const p = statePath(sessionId);
|
|
388
|
+
if (fs2.existsSync(p)) {
|
|
389
|
+
try {
|
|
390
|
+
return JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { accumulated_text: "", last_tokens: 0, first_chunk_time: "" };
|
|
395
|
+
}
|
|
396
|
+
function saveStreamState(sessionId, state) {
|
|
397
|
+
const p = statePath(sessionId);
|
|
398
|
+
fs2.mkdirSync(path2.dirname(p), { recursive: true });
|
|
399
|
+
const tmp = p + ".tmp." + process.pid;
|
|
400
|
+
try {
|
|
401
|
+
fs2.writeFileSync(tmp, JSON.stringify(state));
|
|
402
|
+
fs2.renameSync(tmp, p);
|
|
403
|
+
} catch {
|
|
404
|
+
try {
|
|
405
|
+
fs2.unlinkSync(tmp);
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
fs2.writeFileSync(p, JSON.stringify(state));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function clearStreamState(sessionId) {
|
|
412
|
+
try {
|
|
413
|
+
fs2.unlinkSync(statePath(sessionId));
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function extractMessages(hookData) {
|
|
418
|
+
const llmReq = hookData.llm_request ?? {};
|
|
419
|
+
const messages = llmReq.messages ?? [];
|
|
420
|
+
return messages.map((msg) => ({
|
|
421
|
+
role: String(msg.role ?? "user") === "model" ? "assistant" : String(msg.role ?? "user"),
|
|
422
|
+
content: truncate(String(msg.content ?? ""), MAX_CHARS)
|
|
423
|
+
}));
|
|
424
|
+
}
|
|
425
|
+
function detectModel(hookData) {
|
|
426
|
+
const override = process.env.RESPAN_GEMINI_MODEL;
|
|
427
|
+
if (override) return override;
|
|
428
|
+
const llmReq = hookData.llm_request ?? {};
|
|
429
|
+
return String(llmReq.model ?? "") || "gemini-cli";
|
|
430
|
+
}
|
|
431
|
+
function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens) {
|
|
432
|
+
const spans = [];
|
|
433
|
+
const sessionId = String(hookData.session_id ?? "");
|
|
434
|
+
const model = detectModel(hookData);
|
|
435
|
+
const now = nowISO();
|
|
436
|
+
const endTime = String(hookData.timestamp ?? "") || now;
|
|
437
|
+
const beginTime = startTimeIso || endTime;
|
|
438
|
+
const lat = latencySeconds(beginTime, endTime);
|
|
439
|
+
const promptMessages = extractMessages(hookData);
|
|
440
|
+
const completionMessage = { role: "assistant", content: truncate(outputText, MAX_CHARS) };
|
|
441
|
+
const { workflowName, spanName, customerId } = resolveSpanFields(config, {
|
|
442
|
+
workflowName: "gemini-cli",
|
|
443
|
+
spanName: "gemini-cli"
|
|
444
|
+
});
|
|
445
|
+
const safeId = sessionId.replace(/[/\\]/g, "_").slice(0, 50);
|
|
446
|
+
const traceUniqueId = `gcli_${safeId}`;
|
|
447
|
+
const rootSpanId = `gcli_${safeId}_root`;
|
|
448
|
+
const threadId = `gcli_${sessionId}`;
|
|
449
|
+
const llmReq = hookData.llm_request ?? {};
|
|
450
|
+
const reqConfig = llmReq.config ?? {};
|
|
451
|
+
const baseMeta = { source: "gemini-cli" };
|
|
452
|
+
if (toolTurns > 0) baseMeta.tool_turns = toolTurns;
|
|
453
|
+
if (thoughtsTokens > 0) baseMeta.reasoning_tokens = thoughtsTokens;
|
|
454
|
+
const metadata = buildMetadata(config, baseMeta);
|
|
455
|
+
spans.push({
|
|
456
|
+
trace_unique_id: traceUniqueId,
|
|
457
|
+
thread_identifier: threadId,
|
|
458
|
+
customer_identifier: customerId,
|
|
459
|
+
span_unique_id: rootSpanId,
|
|
460
|
+
span_name: spanName,
|
|
461
|
+
span_workflow_name: workflowName,
|
|
462
|
+
model,
|
|
463
|
+
provider_id: "",
|
|
464
|
+
span_path: "",
|
|
465
|
+
input: promptMessages.length ? JSON.stringify(promptMessages) : "",
|
|
466
|
+
output: JSON.stringify(completionMessage),
|
|
467
|
+
timestamp: endTime,
|
|
468
|
+
start_time: beginTime,
|
|
469
|
+
metadata,
|
|
470
|
+
...lat !== void 0 ? { latency: lat } : {}
|
|
471
|
+
});
|
|
472
|
+
const genSpan = {
|
|
473
|
+
trace_unique_id: traceUniqueId,
|
|
474
|
+
span_unique_id: `gcli_${safeId}_gen`,
|
|
475
|
+
span_parent_id: rootSpanId,
|
|
476
|
+
span_name: "gemini.chat",
|
|
477
|
+
span_workflow_name: workflowName,
|
|
478
|
+
span_path: "gemini_chat",
|
|
479
|
+
model,
|
|
480
|
+
provider_id: "google",
|
|
481
|
+
metadata: {},
|
|
482
|
+
input: promptMessages.length ? JSON.stringify(promptMessages) : "",
|
|
483
|
+
output: JSON.stringify(completionMessage),
|
|
484
|
+
timestamp: endTime,
|
|
485
|
+
start_time: beginTime,
|
|
486
|
+
prompt_tokens: tokens.prompt_tokens,
|
|
487
|
+
completion_tokens: tokens.completion_tokens,
|
|
488
|
+
total_tokens: tokens.total_tokens,
|
|
489
|
+
...lat !== void 0 ? { latency: lat } : {}
|
|
490
|
+
};
|
|
491
|
+
if (reqConfig.temperature != null) genSpan.temperature = reqConfig.temperature;
|
|
492
|
+
if (reqConfig.maxOutputTokens != null) genSpan.max_tokens = reqConfig.maxOutputTokens;
|
|
493
|
+
spans.push(genSpan);
|
|
494
|
+
if (thoughtsTokens > 0) {
|
|
495
|
+
spans.push({
|
|
496
|
+
trace_unique_id: traceUniqueId,
|
|
497
|
+
span_unique_id: `gcli_${safeId}_reasoning`,
|
|
498
|
+
span_parent_id: rootSpanId,
|
|
499
|
+
span_name: "Reasoning",
|
|
500
|
+
span_workflow_name: workflowName,
|
|
501
|
+
span_path: "reasoning",
|
|
502
|
+
provider_id: "",
|
|
503
|
+
metadata: { reasoning_tokens: thoughtsTokens },
|
|
504
|
+
input: "",
|
|
505
|
+
output: `[Reasoning: ${thoughtsTokens} tokens]`,
|
|
506
|
+
timestamp: endTime,
|
|
507
|
+
start_time: beginTime
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
for (let i = 0; i < toolTurns; i++) {
|
|
511
|
+
const detail = toolDetails[i] ?? null;
|
|
512
|
+
const toolName = detail?.name ?? "";
|
|
513
|
+
const toolArgs = detail?.args ?? detail?.input ?? {};
|
|
514
|
+
const toolOutput = detail?.output ?? "";
|
|
515
|
+
const displayName = toolName ? toolDisplayName(toolName) : `Call ${i + 1}`;
|
|
516
|
+
const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
|
|
517
|
+
const toolMeta = {};
|
|
518
|
+
if (toolName) toolMeta.tool_name = toolName;
|
|
519
|
+
if (detail?.error) toolMeta.error = detail.error;
|
|
520
|
+
const toolStart = detail?.start_time ?? beginTime;
|
|
521
|
+
const toolEnd = detail?.end_time ?? endTime;
|
|
522
|
+
const toolLat = latencySeconds(toolStart, toolEnd);
|
|
523
|
+
spans.push({
|
|
524
|
+
trace_unique_id: traceUniqueId,
|
|
525
|
+
span_unique_id: `gcli_${safeId}_tool_${i + 1}`,
|
|
526
|
+
span_parent_id: rootSpanId,
|
|
527
|
+
span_name: `Tool: ${displayName}`,
|
|
528
|
+
span_workflow_name: workflowName,
|
|
529
|
+
span_path: toolName ? `tool_${toolName}` : "tool_call",
|
|
530
|
+
provider_id: "",
|
|
531
|
+
metadata: toolMeta,
|
|
532
|
+
input: toolInputStr,
|
|
533
|
+
output: truncate(toolOutput, MAX_CHARS),
|
|
534
|
+
timestamp: toolEnd,
|
|
535
|
+
start_time: toolStart,
|
|
536
|
+
...toolLat !== void 0 ? { latency: toolLat } : {}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return addDefaultsToAll(spans);
|
|
540
|
+
}
|
|
541
|
+
function sendSpansDetached(spans, apiKey, baseUrl) {
|
|
542
|
+
const url = resolveTracingIngestEndpoint(baseUrl);
|
|
543
|
+
debug(`Sending ${spans.length} span(s) to ${url}: ${spans.map((s) => s.span_name).join(", ")}`);
|
|
544
|
+
if (DEBUG_MODE) {
|
|
545
|
+
const debugFile = path2.join(STATE_DIR, "respan_last_payload.json");
|
|
546
|
+
fs2.writeFileSync(debugFile, JSON.stringify(spans, null, 2));
|
|
547
|
+
}
|
|
548
|
+
const payloadFile = path2.join(STATE_DIR, `respan_send_${process.pid}.json`);
|
|
549
|
+
fs2.writeFileSync(payloadFile, JSON.stringify(toOtlpPayload(spans)));
|
|
550
|
+
const senderScript = `
|
|
551
|
+
const fs = require('fs');
|
|
552
|
+
const pf = ${JSON.stringify(payloadFile)};
|
|
553
|
+
try {
|
|
554
|
+
const data = fs.readFileSync(pf);
|
|
555
|
+
(async () => {
|
|
556
|
+
for (let i = 0; i < 3; i++) {
|
|
557
|
+
try {
|
|
558
|
+
const r = await fetch(${JSON.stringify(url)}, {
|
|
559
|
+
method: 'POST',
|
|
560
|
+
headers: {
|
|
561
|
+
'Content-Type': 'application/json',
|
|
562
|
+
'X-Respan-Dogfood': '1',
|
|
563
|
+
'Authorization': 'Bearer ' + process.env.RESPAN_API_KEY,
|
|
564
|
+
},
|
|
565
|
+
body: data,
|
|
566
|
+
signal: AbortSignal.timeout(30000),
|
|
567
|
+
});
|
|
568
|
+
if (r.status < 500) break;
|
|
569
|
+
if (i < 2) await new Promise(r => setTimeout(r, 1000));
|
|
570
|
+
} catch(e) {
|
|
571
|
+
if (i < 2) await new Promise(r => setTimeout(r, 1000));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
})().finally(() => { try { fs.unlinkSync(pf); } catch {} });
|
|
575
|
+
} catch(e) { try { fs.unlinkSync(pf); } catch {} }
|
|
576
|
+
`;
|
|
577
|
+
const env = { ...process.env, RESPAN_API_KEY: apiKey };
|
|
578
|
+
try {
|
|
579
|
+
const child = (0, import_node_child_process.execFile)("node", ["-e", senderScript], {
|
|
580
|
+
env,
|
|
581
|
+
stdio: "ignore",
|
|
582
|
+
detached: true
|
|
583
|
+
});
|
|
584
|
+
child.unref();
|
|
585
|
+
debug("Launched sender subprocess");
|
|
586
|
+
} catch (e) {
|
|
587
|
+
log("ERROR", `Failed to launch sender: ${e}`);
|
|
588
|
+
try {
|
|
589
|
+
fs2.unlinkSync(payloadFile);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function launchDelayedSend(sessionId, sendVersion, spans, apiKey, baseUrl) {
|
|
595
|
+
const payloadFile = path2.join(STATE_DIR, `respan_delayed_${process.pid}.json`);
|
|
596
|
+
fs2.writeFileSync(payloadFile, JSON.stringify(toOtlpPayload(spans)));
|
|
597
|
+
const stateFile = statePath(sessionId);
|
|
598
|
+
const url = resolveTracingIngestEndpoint(baseUrl);
|
|
599
|
+
const script = `
|
|
600
|
+
const fs = require('fs');
|
|
601
|
+
setTimeout(async () => {
|
|
602
|
+
const sf = ${JSON.stringify(stateFile)};
|
|
603
|
+
const pf = ${JSON.stringify(payloadFile)};
|
|
604
|
+
try {
|
|
605
|
+
if (!fs.existsSync(sf)) { fs.unlinkSync(pf); process.exit(0); }
|
|
606
|
+
const state = JSON.parse(fs.readFileSync(sf, 'utf-8'));
|
|
607
|
+
if (state.send_version !== ${sendVersion}) { fs.unlinkSync(pf); process.exit(0); }
|
|
608
|
+
const data = fs.readFileSync(pf);
|
|
609
|
+
for (let i = 0; i < 3; i++) {
|
|
610
|
+
try {
|
|
611
|
+
const r = await fetch(${JSON.stringify(url)}, {
|
|
612
|
+
method: 'POST',
|
|
613
|
+
headers: {
|
|
614
|
+
'Content-Type': 'application/json',
|
|
615
|
+
'X-Respan-Dogfood': '1',
|
|
616
|
+
'Authorization': 'Bearer ' + process.env.RESPAN_API_KEY,
|
|
617
|
+
},
|
|
618
|
+
body: data,
|
|
619
|
+
signal: AbortSignal.timeout(30000),
|
|
620
|
+
});
|
|
621
|
+
if (r.status < 500) break;
|
|
622
|
+
if (i < 2) await new Promise(r => setTimeout(r, 1000));
|
|
623
|
+
} catch(e) { if (i < 2) await new Promise(r => setTimeout(r, 1000)); }
|
|
624
|
+
}
|
|
625
|
+
try { fs.unlinkSync(sf); } catch {}
|
|
626
|
+
try { fs.unlinkSync(pf); } catch {}
|
|
627
|
+
} catch(e) { try { fs.unlinkSync(pf); } catch {} }
|
|
628
|
+
}, ${SEND_DELAY * 1e3});
|
|
629
|
+
`;
|
|
630
|
+
const env = { ...process.env, RESPAN_API_KEY: apiKey };
|
|
631
|
+
try {
|
|
632
|
+
const child = (0, import_node_child_process.execFile)("node", ["-e", script], {
|
|
633
|
+
env,
|
|
634
|
+
stdio: "ignore",
|
|
635
|
+
detached: true
|
|
636
|
+
});
|
|
637
|
+
child.unref();
|
|
638
|
+
debug(`Launched delayed sender (version=${sendVersion}, delay=${SEND_DELAY}s)`);
|
|
639
|
+
} catch (e) {
|
|
640
|
+
log("ERROR", `Failed to launch delayed sender: ${e}`);
|
|
641
|
+
try {
|
|
642
|
+
fs2.unlinkSync(payloadFile);
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function processBeforeTool(hookData) {
|
|
648
|
+
const sessionId = String(hookData.session_id ?? "unknown");
|
|
649
|
+
const toolName = String(hookData.tool_name ?? "");
|
|
650
|
+
const toolInput = hookData.tool_input ?? {};
|
|
651
|
+
debug(`BeforeTool: ${toolName}`);
|
|
652
|
+
const state = loadStreamState(sessionId);
|
|
653
|
+
const pending = state.pending_tools ?? [];
|
|
654
|
+
pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
|
|
655
|
+
state.pending_tools = pending;
|
|
656
|
+
saveStreamState(sessionId, state);
|
|
657
|
+
process.stdout.write("{}\n");
|
|
658
|
+
}
|
|
659
|
+
function processAfterTool(hookData) {
|
|
660
|
+
const sessionId = String(hookData.session_id ?? "unknown");
|
|
661
|
+
const toolName = String(hookData.tool_name ?? "");
|
|
662
|
+
const toolResponse = hookData.tool_response ?? {};
|
|
663
|
+
const output = String(toolResponse.llmContent ?? "");
|
|
664
|
+
const error = toolResponse.error ? String(toolResponse.error) : void 0;
|
|
665
|
+
debug(`AfterTool: ${toolName}, output_len=${output.length}, error=${error}`);
|
|
666
|
+
const state = loadStreamState(sessionId);
|
|
667
|
+
const pending = state.pending_tools ?? [];
|
|
668
|
+
const completed = state.tool_details ?? [];
|
|
669
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
670
|
+
if (pending[i].name === toolName) {
|
|
671
|
+
const detail = pending.splice(i, 1)[0];
|
|
672
|
+
detail.output = output;
|
|
673
|
+
detail.end_time = nowISO();
|
|
674
|
+
if (error) detail.error = error;
|
|
675
|
+
completed.push(detail);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
state.pending_tools = pending;
|
|
680
|
+
state.tool_details = completed;
|
|
681
|
+
saveStreamState(sessionId, state);
|
|
682
|
+
process.stdout.write("{}\n");
|
|
683
|
+
}
|
|
684
|
+
function processChunk(hookData) {
|
|
685
|
+
const sessionId = String(hookData.session_id ?? "unknown");
|
|
686
|
+
const llmResp = hookData.llm_response ?? {};
|
|
687
|
+
const chunkText = String(llmResp.text ?? "") || "";
|
|
688
|
+
const usage = llmResp.usageMetadata ?? {};
|
|
689
|
+
const completionTokens = Number(usage.candidatesTokenCount ?? 0);
|
|
690
|
+
const thoughtsTokens = Number(usage.thoughtsTokenCount ?? 0);
|
|
691
|
+
const candidates = llmResp.candidates ?? [];
|
|
692
|
+
let finishReason = "";
|
|
693
|
+
let hasToolCall = false;
|
|
694
|
+
const chunkToolDetails = [];
|
|
695
|
+
if (candidates.length > 0 && typeof candidates[0] === "object") {
|
|
696
|
+
finishReason = String(candidates[0].finishReason ?? "");
|
|
697
|
+
const content = candidates[0].content ?? {};
|
|
698
|
+
if (typeof content === "object") {
|
|
699
|
+
for (const part of content.parts ?? []) {
|
|
700
|
+
if (typeof part !== "object") continue;
|
|
701
|
+
const fc = part.functionCall ?? part.toolCall;
|
|
702
|
+
if (fc) {
|
|
703
|
+
hasToolCall = true;
|
|
704
|
+
if (typeof fc === "object") {
|
|
705
|
+
chunkToolDetails.push({
|
|
706
|
+
name: String(fc.name ?? ""),
|
|
707
|
+
args: fc.args ?? {}
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const messages = hookData.llm_request?.messages ?? [];
|
|
715
|
+
const currentMsgCount = messages.length;
|
|
716
|
+
let state = loadStreamState(sessionId);
|
|
717
|
+
const isFinished = ["STOP", "MAX_TOKENS", "SAFETY"].includes(finishReason);
|
|
718
|
+
const savedMsgCount = state.msg_count ?? 0;
|
|
719
|
+
let toolCallDetected = false;
|
|
720
|
+
if (savedMsgCount > 0 && currentMsgCount > savedMsgCount) {
|
|
721
|
+
const newMsgs = messages.slice(savedMsgCount);
|
|
722
|
+
const hasNewUserMsg = newMsgs.some((m) => m.role === "user");
|
|
723
|
+
if (hasNewUserMsg) {
|
|
724
|
+
debug(`New user message detected (msgs ${savedMsgCount} \u2192 ${currentMsgCount}), starting fresh turn`);
|
|
725
|
+
clearStreamState(sessionId);
|
|
726
|
+
state = { accumulated_text: "", last_tokens: 0, first_chunk_time: "" };
|
|
727
|
+
} else {
|
|
728
|
+
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
729
|
+
state.send_version = (state.send_version ?? 0) + 1;
|
|
730
|
+
toolCallDetected = true;
|
|
731
|
+
debug(`Tool call detected via msg_count (${savedMsgCount} \u2192 ${currentMsgCount}), tool_turns=${state.tool_turns}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
state.msg_count = currentMsgCount;
|
|
735
|
+
if (chunkText) {
|
|
736
|
+
if (!state.first_chunk_time) state.first_chunk_time = nowISO();
|
|
737
|
+
state.accumulated_text += chunkText;
|
|
738
|
+
state.last_tokens = completionTokens || state.last_tokens;
|
|
739
|
+
if (thoughtsTokens > 0) state.thoughts_tokens = thoughtsTokens;
|
|
740
|
+
saveStreamState(sessionId, state);
|
|
741
|
+
debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
|
|
742
|
+
}
|
|
743
|
+
const isToolTurn = hasToolCall || ["TOOL_CALLS", "FUNCTION_CALL", "TOOL_USE"].includes(finishReason);
|
|
744
|
+
if (isToolTurn) {
|
|
745
|
+
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
746
|
+
state.send_version = (state.send_version ?? 0) + 1;
|
|
747
|
+
if (chunkToolDetails.length) {
|
|
748
|
+
state.tool_details = [...state.tool_details ?? [], ...chunkToolDetails];
|
|
749
|
+
}
|
|
750
|
+
saveStreamState(sessionId, state);
|
|
751
|
+
debug(`Tool call via response parts (finish=${finishReason}), tool_turns=${state.tool_turns}`);
|
|
752
|
+
process.stdout.write("{}\n");
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const hasNewText = state.accumulated_text.length > (state.last_send_text_len ?? 0);
|
|
756
|
+
const shouldSend = (!toolCallDetected || isFinished) && hasNewText && state.accumulated_text && (!chunkText || isFinished);
|
|
757
|
+
process.stdout.write("{}\n");
|
|
758
|
+
if (!shouldSend) {
|
|
759
|
+
if (toolCallDetected) saveStreamState(sessionId, state);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const creds = resolveCredentials();
|
|
763
|
+
if (!creds) {
|
|
764
|
+
log("ERROR", "No API key found. Run: respan auth login");
|
|
765
|
+
clearStreamState(sessionId);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const finalPrompt = Number(usage.promptTokenCount ?? 0);
|
|
769
|
+
const finalCompletion = completionTokens || state.last_tokens;
|
|
770
|
+
const finalTotal = Number(usage.totalTokenCount ?? 0) || finalPrompt + finalCompletion;
|
|
771
|
+
const tok = { prompt_tokens: finalPrompt, completion_tokens: finalCompletion, total_tokens: finalTotal };
|
|
772
|
+
const config = loadRespanConfig(path2.join(os2.homedir(), ".gemini", "respan.json"));
|
|
773
|
+
const spans = buildSpans(
|
|
774
|
+
hookData,
|
|
775
|
+
state.accumulated_text,
|
|
776
|
+
tok,
|
|
777
|
+
config,
|
|
778
|
+
state.first_chunk_time || void 0,
|
|
779
|
+
state.tool_turns ?? 0,
|
|
780
|
+
state.tool_details ?? [],
|
|
781
|
+
state.thoughts_tokens ?? 0
|
|
782
|
+
);
|
|
783
|
+
if (isFinished && chunkText) {
|
|
784
|
+
debug(`Immediate send (text+STOP, tool_turns=${state.tool_turns ?? 0}), ${state.accumulated_text.length} chars`);
|
|
785
|
+
sendSpansDetached(spans, creds.apiKey, creds.baseUrl);
|
|
786
|
+
clearStreamState(sessionId);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
state.send_version = (state.send_version ?? 0) + 1;
|
|
790
|
+
state.last_send_text_len = state.accumulated_text.length;
|
|
791
|
+
saveStreamState(sessionId, state);
|
|
792
|
+
debug(`Delayed send (version=${state.send_version}, delay=${SEND_DELAY}s), ${state.accumulated_text.length} chars`);
|
|
793
|
+
launchDelayedSend(sessionId, state.send_version, spans, creds.apiKey, creds.baseUrl);
|
|
794
|
+
}
|
|
795
|
+
function main() {
|
|
796
|
+
try {
|
|
797
|
+
const raw = fs2.readFileSync(0, "utf-8");
|
|
798
|
+
if (!raw.trim()) {
|
|
799
|
+
process.stdout.write("{}\n");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const hookData = JSON.parse(raw);
|
|
803
|
+
const event = String(hookData.hook_event_name ?? "");
|
|
804
|
+
const unlock = acquireLock(LOCK_PATH);
|
|
805
|
+
try {
|
|
806
|
+
if (event === "BeforeTool") {
|
|
807
|
+
processBeforeTool(hookData);
|
|
808
|
+
} else if (event === "AfterTool") {
|
|
809
|
+
processAfterTool(hookData);
|
|
810
|
+
} else {
|
|
811
|
+
processChunk(hookData);
|
|
812
|
+
}
|
|
813
|
+
} finally {
|
|
814
|
+
unlock?.();
|
|
815
|
+
}
|
|
816
|
+
} catch (e) {
|
|
817
|
+
if (e instanceof SyntaxError) {
|
|
818
|
+
log("ERROR", `Invalid JSON from stdin: ${e}`);
|
|
819
|
+
} else {
|
|
820
|
+
log("ERROR", `Hook error: ${e}`);
|
|
821
|
+
}
|
|
822
|
+
process.stdout.write("{}\n");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
main();
|