@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.
@@ -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/hooks/shared.ts
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 = path.dirname(logFile);
49
- fs.mkdirSync(dir, { recursive: true });
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
- fs.appendFileSync(_logFile, `${ts} [${level}] ${message}
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 = path.join(os.homedir(), ".respan", "credentials.json");
66
- if (fs.existsSync(credsFile)) {
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(fs.readFileSync(credsFile, "utf-8"));
69
- const configFile = path.join(os.homedir(), ".respan", "config.json");
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 (fs.existsSync(configFile)) {
72
- const cfg = JSON.parse(fs.readFileSync(configFile, "utf-8"));
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 (!fs.existsSync(configPath)) {
125
+ if (!fs2.existsSync(configPath)) {
103
126
  return { fields: {}, properties: {} };
104
127
  }
105
128
  try {
106
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
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
- fs.mkdirSync(lockPath);
152
+ fs2.mkdirSync(lockPath);
130
153
  return () => {
131
154
  try {
132
- fs.rmdirSync(lockPath);
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 = path2.join(os2.homedir(), ".gemini", "state");
331
- var LOG_FILE = path2.join(STATE_DIR, "respan_hook.log");
332
- var LOCK_PATH = path2.join(STATE_DIR, "respan_hook.lock");
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 path2.join(STATE_DIR, `respan_stream_${safeId}.json`);
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 (fs2.existsSync(p)) {
412
+ if (fs3.existsSync(p)) {
390
413
  try {
391
- return JSON.parse(fs2.readFileSync(p, "utf-8"));
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
- fs2.mkdirSync(path2.dirname(p), { recursive: true });
422
+ fs3.mkdirSync(path3.dirname(p), { recursive: true });
400
423
  const tmp = p + ".tmp." + process.pid;
401
424
  try {
402
- fs2.writeFileSync(tmp, JSON.stringify(state));
403
- fs2.renameSync(tmp, p);
425
+ fs3.writeFileSync(tmp, JSON.stringify(state));
426
+ fs3.renameSync(tmp, p);
404
427
  } catch {
405
428
  try {
406
- fs2.unlinkSync(tmp);
429
+ fs3.unlinkSync(tmp);
407
430
  } catch {
408
431
  }
409
- fs2.writeFileSync(p, JSON.stringify(state));
432
+ fs3.writeFileSync(p, JSON.stringify(state));
410
433
  }
411
434
  }
412
435
  function clearStreamState(sessionId) {
413
436
  try {
414
- fs2.unlinkSync(statePath(sessionId));
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 = path2.join(STATE_DIR, "respan_last_payload.json");
586
- fs2.writeFileSync(debugFile, JSON.stringify(spans, null, 2));
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 = path2.join(STATE_DIR, `respan_send_${process.pid}.json`);
589
- fs2.writeFileSync(payloadFile, JSON.stringify(toOtlpPayload(spans)));
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
- fs2.unlinkSync(payloadFile);
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 = path2.join(STATE_DIR, `respan_delayed_${process.pid}.json`);
636
- fs2.writeFileSync(payloadFile, JSON.stringify(toOtlpPayload(spans)));
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
- fs2.unlinkSync(payloadFile);
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(path2.join(os2.homedir(), ".gemini", "respan.json"));
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 = fs2.readFileSync(dataFile, "utf-8");
856
- fs2.unlinkSync(dataFile);
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
- fs2.unlinkSync(dataFile);
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 = fs2.readFileSync(0, "utf-8");
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 = path2.join(STATE_DIR, `respan_chunk_${process.pid}.json`);
901
- fs2.mkdirSync(STATE_DIR, { recursive: true });
902
- fs2.writeFileSync(dataFile, raw);
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], {
@@ -34,7 +34,6 @@ export interface SpanData {
34
34
  completion_message?: Record<string, unknown> | null;
35
35
  warnings?: string;
36
36
  encoding_format?: string;
37
- disable_fallback?: boolean;
38
37
  respan_params?: Record<string, unknown>;
39
38
  field_name?: string;
40
39
  delimiter?: string;
@@ -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');
@@ -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;
@@ -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
- function ensureConfigDir() {
8
- if (!fs.existsSync(CONFIG_DIR)) {
9
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
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
- writeJson(CREDENTIALS_FILE, creds);
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
- writeJson(CREDENTIALS_FILE, creds);
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>;