@respan/cli 0.17.1 → 0.17.3
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/hooks/claude-code.cjs +69 -46
- package/dist/hooks/codex-cli.cjs +68 -45
- package/dist/hooks/gemini-cli.cjs +67 -44
- package/dist/hooks/shared.d.ts +0 -1
- package/dist/hooks/shared.js +3 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +35 -7
- package/dist/lib/demo/prompt-fixtures.d.ts +0 -1
- package/oclif.manifest.json +142 -142
- package/package.json +1 -1
|
@@ -23,15 +23,37 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
|
|
25
25
|
// src/hooks/gemini-cli.ts
|
|
26
|
+
var fs3 = __toESM(require("node:fs"), 1);
|
|
27
|
+
var os3 = __toESM(require("node:os"), 1);
|
|
28
|
+
var path3 = __toESM(require("node:path"), 1);
|
|
29
|
+
var import_node_child_process = require("node:child_process");
|
|
30
|
+
|
|
31
|
+
// src/hooks/shared.ts
|
|
26
32
|
var fs2 = __toESM(require("node:fs"), 1);
|
|
27
33
|
var os2 = __toESM(require("node:os"), 1);
|
|
28
34
|
var path2 = __toESM(require("node:path"), 1);
|
|
29
|
-
var import_node_child_process = require("node:child_process");
|
|
30
35
|
|
|
31
|
-
// src/
|
|
36
|
+
// src/lib/config.ts
|
|
32
37
|
var fs = __toESM(require("node:fs"), 1);
|
|
33
|
-
var os = __toESM(require("node:os"), 1);
|
|
34
38
|
var path = __toESM(require("node:path"), 1);
|
|
39
|
+
var os = __toESM(require("node:os"), 1);
|
|
40
|
+
var CONFIG_DIR = path.join(os.homedir(), ".respan");
|
|
41
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
42
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
43
|
+
function restrictToOwner(target) {
|
|
44
|
+
if (process.platform === "win32") return;
|
|
45
|
+
try {
|
|
46
|
+
const mode = fs.statSync(target).mode & 511;
|
|
47
|
+
if (mode & 63) fs.chmodSync(target, mode & 448);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function tightenCredentialPermissions() {
|
|
52
|
+
restrictToOwner(CONFIG_DIR);
|
|
53
|
+
restrictToOwner(CREDENTIALS_FILE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/hooks/shared.ts
|
|
35
57
|
function resolveTracesEndpoint(baseUrl) {
|
|
36
58
|
const DEFAULT = "https://api.respan.ai/api/v2/traces";
|
|
37
59
|
if (!baseUrl) return DEFAULT;
|
|
@@ -45,13 +67,13 @@ var _debug = false;
|
|
|
45
67
|
function initLogging(logFile, debug2) {
|
|
46
68
|
_logFile = logFile;
|
|
47
69
|
_debug = debug2;
|
|
48
|
-
const dir =
|
|
49
|
-
|
|
70
|
+
const dir = path2.dirname(logFile);
|
|
71
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
50
72
|
}
|
|
51
73
|
function log(level, message) {
|
|
52
74
|
if (!_logFile) return;
|
|
53
75
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
54
|
-
|
|
76
|
+
fs2.appendFileSync(_logFile, `${ts} [${level}] ${message}
|
|
55
77
|
`);
|
|
56
78
|
}
|
|
57
79
|
function debug(message) {
|
|
@@ -62,14 +84,15 @@ function resolveCredentials() {
|
|
|
62
84
|
let apiKey = process.env.RESPAN_API_KEY ?? "";
|
|
63
85
|
let baseUrl = process.env.RESPAN_BASE_URL ?? DEFAULT_BASE_URL;
|
|
64
86
|
if (!apiKey) {
|
|
65
|
-
const credsFile =
|
|
66
|
-
if (
|
|
87
|
+
const credsFile = path2.join(os2.homedir(), ".respan", "credentials.json");
|
|
88
|
+
if (fs2.existsSync(credsFile)) {
|
|
89
|
+
tightenCredentialPermissions();
|
|
67
90
|
try {
|
|
68
|
-
const creds = JSON.parse(
|
|
69
|
-
const configFile =
|
|
91
|
+
const creds = JSON.parse(fs2.readFileSync(credsFile, "utf-8"));
|
|
92
|
+
const configFile = path2.join(os2.homedir(), ".respan", "config.json");
|
|
70
93
|
let profile = "default";
|
|
71
|
-
if (
|
|
72
|
-
const cfg = JSON.parse(
|
|
94
|
+
if (fs2.existsSync(configFile)) {
|
|
95
|
+
const cfg = JSON.parse(fs2.readFileSync(configFile, "utf-8"));
|
|
73
96
|
profile = cfg.activeProfile ?? "default";
|
|
74
97
|
}
|
|
75
98
|
const cred = creds[profile] ?? {};
|
|
@@ -99,11 +122,11 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
99
122
|
"project_id"
|
|
100
123
|
]);
|
|
101
124
|
function loadRespanConfig(configPath) {
|
|
102
|
-
if (!
|
|
125
|
+
if (!fs2.existsSync(configPath)) {
|
|
103
126
|
return { fields: {}, properties: {} };
|
|
104
127
|
}
|
|
105
128
|
try {
|
|
106
|
-
const raw = JSON.parse(
|
|
129
|
+
const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
107
130
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
108
131
|
return { fields: {}, properties: {} };
|
|
109
132
|
}
|
|
@@ -126,10 +149,10 @@ function acquireLock(lockPath, timeoutMs = 5e3) {
|
|
|
126
149
|
const deadline = Date.now() + timeoutMs;
|
|
127
150
|
while (Date.now() < deadline) {
|
|
128
151
|
try {
|
|
129
|
-
|
|
152
|
+
fs2.mkdirSync(lockPath);
|
|
130
153
|
return () => {
|
|
131
154
|
try {
|
|
132
|
-
|
|
155
|
+
fs2.rmdirSync(lockPath);
|
|
133
156
|
} catch {
|
|
134
157
|
}
|
|
135
158
|
};
|
|
@@ -327,9 +350,9 @@ function toOtlpPayload(spans) {
|
|
|
327
350
|
}
|
|
328
351
|
|
|
329
352
|
// src/hooks/gemini-cli.ts
|
|
330
|
-
var STATE_DIR =
|
|
331
|
-
var LOG_FILE =
|
|
332
|
-
var LOCK_PATH =
|
|
353
|
+
var STATE_DIR = path3.join(os3.homedir(), ".gemini", "state");
|
|
354
|
+
var LOG_FILE = path3.join(STATE_DIR, "respan_hook.log");
|
|
355
|
+
var LOCK_PATH = path3.join(STATE_DIR, "respan_hook.lock");
|
|
333
356
|
var DEBUG_MODE = (process.env.GEMINI_RESPAN_DEBUG ?? "").toLowerCase() === "true";
|
|
334
357
|
var MAX_CHARS = parseInt(process.env.GEMINI_RESPAN_MAX_CHARS ?? "4000", 10) || 4e3;
|
|
335
358
|
var SEND_DELAY = parseInt(process.env.GEMINI_RESPAN_SEND_DELAY ?? "10", 10) || 10;
|
|
@@ -382,13 +405,13 @@ function formatToolInput(toolName, args) {
|
|
|
382
405
|
}
|
|
383
406
|
function statePath(sessionId) {
|
|
384
407
|
const safeId = sessionId.replace(/[/\\]/g, "_").slice(0, 64);
|
|
385
|
-
return
|
|
408
|
+
return path3.join(STATE_DIR, `respan_stream_${safeId}.json`);
|
|
386
409
|
}
|
|
387
410
|
function loadStreamState(sessionId) {
|
|
388
411
|
const p = statePath(sessionId);
|
|
389
|
-
if (
|
|
412
|
+
if (fs3.existsSync(p)) {
|
|
390
413
|
try {
|
|
391
|
-
return JSON.parse(
|
|
414
|
+
return JSON.parse(fs3.readFileSync(p, "utf-8"));
|
|
392
415
|
} catch {
|
|
393
416
|
}
|
|
394
417
|
}
|
|
@@ -396,22 +419,22 @@ function loadStreamState(sessionId) {
|
|
|
396
419
|
}
|
|
397
420
|
function saveStreamState(sessionId, state) {
|
|
398
421
|
const p = statePath(sessionId);
|
|
399
|
-
|
|
422
|
+
fs3.mkdirSync(path3.dirname(p), { recursive: true });
|
|
400
423
|
const tmp = p + ".tmp." + process.pid;
|
|
401
424
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
425
|
+
fs3.writeFileSync(tmp, JSON.stringify(state));
|
|
426
|
+
fs3.renameSync(tmp, p);
|
|
404
427
|
} catch {
|
|
405
428
|
try {
|
|
406
|
-
|
|
429
|
+
fs3.unlinkSync(tmp);
|
|
407
430
|
} catch {
|
|
408
431
|
}
|
|
409
|
-
|
|
432
|
+
fs3.writeFileSync(p, JSON.stringify(state));
|
|
410
433
|
}
|
|
411
434
|
}
|
|
412
435
|
function clearStreamState(sessionId) {
|
|
413
436
|
try {
|
|
414
|
-
|
|
437
|
+
fs3.unlinkSync(statePath(sessionId));
|
|
415
438
|
} catch {
|
|
416
439
|
}
|
|
417
440
|
}
|
|
@@ -582,11 +605,11 @@ function sendSpansDetached(spans, apiKey, baseUrl) {
|
|
|
582
605
|
const url = resolveTracingIngestEndpoint(baseUrl);
|
|
583
606
|
debug(`Sending ${spans.length} span(s) to ${url}: ${spans.map((s) => s.span_name).join(", ")}`);
|
|
584
607
|
if (DEBUG_MODE) {
|
|
585
|
-
const debugFile =
|
|
586
|
-
|
|
608
|
+
const debugFile = path3.join(STATE_DIR, "respan_last_payload.json");
|
|
609
|
+
fs3.writeFileSync(debugFile, JSON.stringify(spans, null, 2));
|
|
587
610
|
}
|
|
588
|
-
const payloadFile =
|
|
589
|
-
|
|
611
|
+
const payloadFile = path3.join(STATE_DIR, `respan_send_${process.pid}.json`);
|
|
612
|
+
fs3.writeFileSync(payloadFile, JSON.stringify(toOtlpPayload(spans)));
|
|
590
613
|
const senderScript = `
|
|
591
614
|
const fs = require('fs');
|
|
592
615
|
const pf = ${JSON.stringify(payloadFile)};
|
|
@@ -626,14 +649,14 @@ try {
|
|
|
626
649
|
} catch (e) {
|
|
627
650
|
log("ERROR", `Failed to launch sender: ${e}`);
|
|
628
651
|
try {
|
|
629
|
-
|
|
652
|
+
fs3.unlinkSync(payloadFile);
|
|
630
653
|
} catch {
|
|
631
654
|
}
|
|
632
655
|
}
|
|
633
656
|
}
|
|
634
657
|
function launchDelayedSend(sessionId, sendVersion, spans, apiKey, baseUrl) {
|
|
635
|
-
const payloadFile =
|
|
636
|
-
|
|
658
|
+
const payloadFile = path3.join(STATE_DIR, `respan_delayed_${process.pid}.json`);
|
|
659
|
+
fs3.writeFileSync(payloadFile, JSON.stringify(toOtlpPayload(spans)));
|
|
637
660
|
const stateFile = statePath(sessionId);
|
|
638
661
|
const url = resolveTracingIngestEndpoint(baseUrl);
|
|
639
662
|
const script = `
|
|
@@ -679,7 +702,7 @@ setTimeout(async () => {
|
|
|
679
702
|
} catch (e) {
|
|
680
703
|
log("ERROR", `Failed to launch delayed sender: ${e}`);
|
|
681
704
|
try {
|
|
682
|
-
|
|
705
|
+
fs3.unlinkSync(payloadFile);
|
|
683
706
|
} catch {
|
|
684
707
|
}
|
|
685
708
|
}
|
|
@@ -825,7 +848,7 @@ function processChunk(hookData) {
|
|
|
825
848
|
const finalCompletion = completionTokens || state.last_tokens;
|
|
826
849
|
const finalTotal = Number(usage.totalTokenCount ?? 0) || finalPrompt + finalCompletion;
|
|
827
850
|
const tok = { prompt_tokens: finalPrompt, completion_tokens: finalCompletion, total_tokens: finalTotal };
|
|
828
|
-
const config = loadRespanConfig(
|
|
851
|
+
const config = loadRespanConfig(path3.join(os3.homedir(), ".gemini", "respan.json"));
|
|
829
852
|
const spans = buildSpans(
|
|
830
853
|
hookData,
|
|
831
854
|
state.accumulated_text,
|
|
@@ -852,8 +875,8 @@ function processChunk(hookData) {
|
|
|
852
875
|
}
|
|
853
876
|
function processChunkInWorker(dataFile) {
|
|
854
877
|
try {
|
|
855
|
-
const raw =
|
|
856
|
-
|
|
878
|
+
const raw = fs3.readFileSync(dataFile, "utf-8");
|
|
879
|
+
fs3.unlinkSync(dataFile);
|
|
857
880
|
if (!raw.trim()) return;
|
|
858
881
|
const hookData = JSON.parse(raw);
|
|
859
882
|
const unlock = acquireLock(LOCK_PATH);
|
|
@@ -865,7 +888,7 @@ function processChunkInWorker(dataFile) {
|
|
|
865
888
|
} catch (e) {
|
|
866
889
|
log("ERROR", `Worker error: ${e}`);
|
|
867
890
|
try {
|
|
868
|
-
|
|
891
|
+
fs3.unlinkSync(dataFile);
|
|
869
892
|
} catch {
|
|
870
893
|
}
|
|
871
894
|
}
|
|
@@ -878,7 +901,7 @@ function main() {
|
|
|
878
901
|
}
|
|
879
902
|
let raw = "";
|
|
880
903
|
try {
|
|
881
|
-
raw =
|
|
904
|
+
raw = fs3.readFileSync(0, "utf-8");
|
|
882
905
|
} catch {
|
|
883
906
|
}
|
|
884
907
|
process.stdout.write("{}\n");
|
|
@@ -897,9 +920,9 @@ function main() {
|
|
|
897
920
|
unlock?.();
|
|
898
921
|
}
|
|
899
922
|
} else {
|
|
900
|
-
const dataFile =
|
|
901
|
-
|
|
902
|
-
|
|
923
|
+
const dataFile = path3.join(STATE_DIR, `respan_chunk_${process.pid}.json`);
|
|
924
|
+
fs3.mkdirSync(STATE_DIR, { recursive: true });
|
|
925
|
+
fs3.writeFileSync(dataFile, raw);
|
|
903
926
|
try {
|
|
904
927
|
const scriptPath = __filename || process.argv[1];
|
|
905
928
|
const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
|
package/dist/hooks/shared.d.ts
CHANGED
package/dist/hooks/shared.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
+
import { tightenCredentialPermissions } from '../lib/config.js';
|
|
10
11
|
// Resolve the v2/traces endpoint (OTLP JSON format)
|
|
11
12
|
export function resolveTracesEndpoint(baseUrl) {
|
|
12
13
|
const DEFAULT = 'https://api.respan.ai/api/v2/traces';
|
|
@@ -46,6 +47,8 @@ export function resolveCredentials() {
|
|
|
46
47
|
if (!apiKey) {
|
|
47
48
|
const credsFile = path.join(os.homedir(), '.respan', 'credentials.json');
|
|
48
49
|
if (fs.existsSync(credsFile)) {
|
|
50
|
+
// Hook-only users may never run the CLI again after logging in
|
|
51
|
+
tightenCredentialPermissions();
|
|
49
52
|
try {
|
|
50
53
|
const creds = JSON.parse(fs.readFileSync(credsFile, 'utf-8'));
|
|
51
54
|
const configFile = path.join(os.homedir(), '.respan', 'config.json');
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface Config {
|
|
|
15
15
|
activeProfile?: string;
|
|
16
16
|
defaults?: Record<string, string>;
|
|
17
17
|
}
|
|
18
|
+
export declare function tightenCredentialPermissions(): void;
|
|
18
19
|
export declare function getAllCredentials(): Record<string, Credential>;
|
|
19
20
|
export declare function getCredential(profile?: string): Credential | undefined;
|
|
20
21
|
export declare function setCredential(profile: string, credential: Credential): void;
|
package/dist/lib/config.js
CHANGED
|
@@ -4,11 +4,27 @@ import * as os from 'node:os';
|
|
|
4
4
|
const CONFIG_DIR = path.join(os.homedir(), '.respan');
|
|
5
5
|
const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json');
|
|
6
6
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const CONFIG_DIR_MODE = 0o700;
|
|
8
|
+
const CREDENTIALS_FILE_MODE = 0o600;
|
|
9
|
+
// Removes group and other access, keeping the owner's bits. Best-effort: a path
|
|
10
|
+
// we can't chmod (another owner, read-only mount) must stay usable, and Windows
|
|
11
|
+
// doesn't map these modes to ACLs.
|
|
12
|
+
function restrictToOwner(target) {
|
|
13
|
+
if (process.platform === 'win32')
|
|
14
|
+
return;
|
|
15
|
+
try {
|
|
16
|
+
const mode = fs.statSync(target).mode & 0o777;
|
|
17
|
+
if (mode & 0o077)
|
|
18
|
+
fs.chmodSync(target, mode & 0o700);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Missing, or not ours to change
|
|
10
22
|
}
|
|
11
23
|
}
|
|
24
|
+
function ensureConfigDir() {
|
|
25
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
26
|
+
restrictToOwner(CONFIG_DIR);
|
|
27
|
+
}
|
|
12
28
|
function readJson(filePath, fallback) {
|
|
13
29
|
try {
|
|
14
30
|
if (!fs.existsSync(filePath))
|
|
@@ -20,12 +36,24 @@ function readJson(filePath, fallback) {
|
|
|
20
36
|
return fallback;
|
|
21
37
|
}
|
|
22
38
|
}
|
|
23
|
-
function writeJson(filePath, data) {
|
|
39
|
+
function writeJson(filePath, data, mode = 0o666) {
|
|
24
40
|
ensureConfigDir();
|
|
25
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
41
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf-8', mode });
|
|
26
42
|
}
|
|
27
43
|
// --- Credentials ---
|
|
44
|
+
// Tightens files written by older CLI versions, which used the default modes.
|
|
45
|
+
export function tightenCredentialPermissions() {
|
|
46
|
+
restrictToOwner(CONFIG_DIR);
|
|
47
|
+
restrictToOwner(CREDENTIALS_FILE);
|
|
48
|
+
}
|
|
49
|
+
function writeCredentials(creds) {
|
|
50
|
+
// writeFileSync's mode only applies when it creates the file, so tighten an
|
|
51
|
+
// existing one first: new secrets must not land in a world-readable file.
|
|
52
|
+
restrictToOwner(CREDENTIALS_FILE);
|
|
53
|
+
writeJson(CREDENTIALS_FILE, creds, CREDENTIALS_FILE_MODE);
|
|
54
|
+
}
|
|
28
55
|
export function getAllCredentials() {
|
|
56
|
+
tightenCredentialPermissions();
|
|
29
57
|
return readJson(CREDENTIALS_FILE, {});
|
|
30
58
|
}
|
|
31
59
|
export function getCredential(profile) {
|
|
@@ -36,7 +64,7 @@ export function getCredential(profile) {
|
|
|
36
64
|
export function setCredential(profile, credential) {
|
|
37
65
|
const creds = getAllCredentials();
|
|
38
66
|
creds[profile] = credential;
|
|
39
|
-
|
|
67
|
+
writeCredentials(creds);
|
|
40
68
|
// Set active profile if this is the first credential
|
|
41
69
|
const config = getConfig();
|
|
42
70
|
if (!config.activeProfile) {
|
|
@@ -46,7 +74,7 @@ export function setCredential(profile, credential) {
|
|
|
46
74
|
export function deleteCredential(profile) {
|
|
47
75
|
const creds = getAllCredentials();
|
|
48
76
|
delete creds[profile];
|
|
49
|
-
|
|
77
|
+
writeCredentials(creds);
|
|
50
78
|
}
|
|
51
79
|
// --- Config ---
|
|
52
80
|
export function getConfig() {
|
|
@@ -37,7 +37,6 @@ export interface DemoPromptVersion {
|
|
|
37
37
|
stream?: boolean;
|
|
38
38
|
thinking?: Record<string, unknown>;
|
|
39
39
|
variables?: Record<string, unknown>;
|
|
40
|
-
load_balance_models?: Record<string, unknown>[];
|
|
41
40
|
tools?: Record<string, unknown>[];
|
|
42
41
|
tool_choice?: string | Record<string, unknown>;
|
|
43
42
|
response_format?: Record<string, unknown>;
|