@respan/cli 0.17.0 → 0.17.2

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/claude-code.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;
@@ -44,13 +66,13 @@ var _debug = false;
44
66
  function initLogging(logFile, debug2) {
45
67
  _logFile = logFile;
46
68
  _debug = debug2;
47
- const dir = path.dirname(logFile);
48
- fs.mkdirSync(dir, { recursive: true });
69
+ const dir = path2.dirname(logFile);
70
+ fs2.mkdirSync(dir, { recursive: true });
49
71
  }
50
72
  function log(level, message) {
51
73
  if (!_logFile) return;
52
74
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
53
- fs.appendFileSync(_logFile, `${ts} [${level}] ${message}
75
+ fs2.appendFileSync(_logFile, `${ts} [${level}] ${message}
54
76
  `);
55
77
  }
56
78
  function debug(message) {
@@ -61,14 +83,15 @@ function resolveCredentials() {
61
83
  let apiKey = process.env.RESPAN_API_KEY ?? "";
62
84
  let baseUrl = process.env.RESPAN_BASE_URL ?? DEFAULT_BASE_URL;
63
85
  if (!apiKey) {
64
- const credsFile = path.join(os.homedir(), ".respan", "credentials.json");
65
- if (fs.existsSync(credsFile)) {
86
+ const credsFile = path2.join(os2.homedir(), ".respan", "credentials.json");
87
+ if (fs2.existsSync(credsFile)) {
88
+ tightenCredentialPermissions();
66
89
  try {
67
- const creds = JSON.parse(fs.readFileSync(credsFile, "utf-8"));
68
- const configFile = path.join(os.homedir(), ".respan", "config.json");
90
+ const creds = JSON.parse(fs2.readFileSync(credsFile, "utf-8"));
91
+ const configFile = path2.join(os2.homedir(), ".respan", "config.json");
69
92
  let profile = "default";
70
- if (fs.existsSync(configFile)) {
71
- const cfg = JSON.parse(fs.readFileSync(configFile, "utf-8"));
93
+ if (fs2.existsSync(configFile)) {
94
+ const cfg = JSON.parse(fs2.readFileSync(configFile, "utf-8"));
72
95
  profile = cfg.activeProfile ?? "default";
73
96
  }
74
97
  const cred = creds[profile] ?? {};
@@ -98,11 +121,11 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
98
121
  "project_id"
99
122
  ]);
100
123
  function loadRespanConfig(configPath) {
101
- if (!fs.existsSync(configPath)) {
124
+ if (!fs2.existsSync(configPath)) {
102
125
  return { fields: {}, properties: {} };
103
126
  }
104
127
  try {
105
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
128
+ const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
106
129
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
107
130
  return { fields: {}, properties: {} };
108
131
  }
@@ -122,36 +145,36 @@ function loadRespanConfig(configPath) {
122
145
  }
123
146
  }
124
147
  function loadState(statePath) {
125
- if (!fs.existsSync(statePath)) return {};
148
+ if (!fs2.existsSync(statePath)) return {};
126
149
  try {
127
- return JSON.parse(fs.readFileSync(statePath, "utf-8"));
150
+ return JSON.parse(fs2.readFileSync(statePath, "utf-8"));
128
151
  } catch {
129
152
  return {};
130
153
  }
131
154
  }
132
155
  function saveState(statePath, state) {
133
- const dir = path.dirname(statePath);
134
- fs.mkdirSync(dir, { recursive: true });
156
+ const dir = path2.dirname(statePath);
157
+ fs2.mkdirSync(dir, { recursive: true });
135
158
  const tmpPath = statePath + ".tmp." + process.pid;
136
159
  try {
137
- fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
138
- fs.renameSync(tmpPath, statePath);
160
+ fs2.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
161
+ fs2.renameSync(tmpPath, statePath);
139
162
  } catch (e) {
140
163
  try {
141
- fs.unlinkSync(tmpPath);
164
+ fs2.unlinkSync(tmpPath);
142
165
  } catch {
143
166
  }
144
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
167
+ fs2.writeFileSync(statePath, JSON.stringify(state, null, 2));
145
168
  }
146
169
  }
147
170
  function acquireLock(lockPath, timeoutMs = 5e3) {
148
171
  const deadline = Date.now() + timeoutMs;
149
172
  while (Date.now() < deadline) {
150
173
  try {
151
- fs.mkdirSync(lockPath);
174
+ fs2.mkdirSync(lockPath);
152
175
  return () => {
153
176
  try {
154
- fs.rmdirSync(lockPath);
177
+ fs2.rmdirSync(lockPath);
155
178
  } catch {
156
179
  }
157
180
  };
@@ -360,9 +383,9 @@ async function sendSpans(spans, apiKey, baseUrl, context) {
360
383
  const spanNames = spans.map((s) => s.span_name);
361
384
  debug(`Sending ${spans.length} spans (${body.length} bytes) to ${url} for ${context}: ${spanNames.join(", ")}`);
362
385
  if (_debug) {
363
- const debugDir = _logFile ? path.dirname(_logFile) : os.tmpdir();
364
- const debugFile = path.join(debugDir, `respan_spans_${context.replace(/\s+/g, "_")}.json`);
365
- fs.writeFileSync(debugFile, body);
386
+ const debugDir = _logFile ? path2.dirname(_logFile) : os2.tmpdir();
387
+ const debugFile = path2.join(debugDir, `respan_spans_${context.replace(/\s+/g, "_")}.json`);
388
+ fs2.writeFileSync(debugFile, body);
366
389
  debug(`Dumped OTLP payload to ${debugFile}`);
367
390
  }
368
391
  for (let attempt = 0; attempt < 3; attempt++) {
@@ -403,10 +426,10 @@ function sleep(ms) {
403
426
  }
404
427
 
405
428
  // src/hooks/claude-code.ts
406
- var STATE_DIR = path2.join(os2.homedir(), ".claude", "state");
407
- var LOG_FILE = path2.join(STATE_DIR, "respan_hook.log");
408
- var STATE_FILE = path2.join(STATE_DIR, "respan_state.json");
409
- var LOCK_PATH = path2.join(STATE_DIR, "respan_hook.lock");
429
+ var STATE_DIR = path3.join(os3.homedir(), ".claude", "state");
430
+ var LOG_FILE = path3.join(STATE_DIR, "respan_hook.log");
431
+ var STATE_FILE = path3.join(STATE_DIR, "respan_state.json");
432
+ var LOCK_PATH = path3.join(STATE_DIR, "respan_hook.lock");
410
433
  var DEBUG_MODE = (process.env.CC_RESPAN_DEBUG ?? "").toLowerCase() === "true";
411
434
  var MAX_CHARS = parseInt(process.env.CC_RESPAN_MAX_CHARS ?? "4000", 10) || 4e3;
412
435
  initLogging(LOG_FILE, DEBUG_MODE);
@@ -730,7 +753,7 @@ function processTranscript(sessionId, transcriptFile, state, apiKey, baseUrl, co
730
753
  const sessionState = state[sessionId] ?? {};
731
754
  const lastLine = Number(sessionState.last_line ?? 0);
732
755
  const turnCount = Number(sessionState.turn_count ?? 0);
733
- const content = fs2.readFileSync(transcriptFile, "utf-8");
756
+ const content = fs3.readFileSync(transcriptFile, "utf-8");
734
757
  const lines = content.trim().split("\n");
735
758
  const totalLines = lines.length;
736
759
  if (lastLine >= totalLines) {
@@ -831,13 +854,13 @@ function processTranscript(sessionId, transcriptFile, state, apiKey, baseUrl, co
831
854
  function readStdinPayload() {
832
855
  if (process.stdin.isTTY) return null;
833
856
  try {
834
- const raw = fs2.readFileSync(0, "utf-8");
857
+ const raw = fs3.readFileSync(0, "utf-8");
835
858
  if (!raw.trim()) return null;
836
859
  const payload = JSON.parse(raw);
837
860
  const sessionId = String(payload.session_id ?? "");
838
861
  const transcriptPath = String(payload.transcript_path ?? "");
839
862
  if (!sessionId || !transcriptPath) return null;
840
- if (!fs2.existsSync(transcriptPath)) return null;
863
+ if (!fs3.existsSync(transcriptPath)) return null;
841
864
  debug(`Got transcript from stdin: session=${sessionId}, path=${transcriptPath}`);
842
865
  return { sessionId, transcriptPath };
843
866
  } catch {
@@ -845,17 +868,17 @@ function readStdinPayload() {
845
868
  }
846
869
  }
847
870
  function findLatestTranscript() {
848
- const projectsDir = path2.join(os2.homedir(), ".claude", "projects");
849
- if (!fs2.existsSync(projectsDir)) return null;
871
+ const projectsDir = path3.join(os3.homedir(), ".claude", "projects");
872
+ if (!fs3.existsSync(projectsDir)) return null;
850
873
  let latestFile = null;
851
874
  let latestMtime = 0;
852
- for (const projEntry of fs2.readdirSync(projectsDir)) {
853
- const projDir = path2.join(projectsDir, projEntry);
854
- if (!fs2.statSync(projDir).isDirectory()) continue;
855
- for (const file of fs2.readdirSync(projDir)) {
875
+ for (const projEntry of fs3.readdirSync(projectsDir)) {
876
+ const projDir = path3.join(projectsDir, projEntry);
877
+ if (!fs3.statSync(projDir).isDirectory()) continue;
878
+ for (const file of fs3.readdirSync(projDir)) {
856
879
  if (!file.endsWith(".jsonl")) continue;
857
- const full = path2.join(projDir, file);
858
- const mtime = fs2.statSync(full).mtimeMs;
880
+ const full = path3.join(projDir, file);
881
+ const mtime = fs3.statSync(full).mtimeMs;
859
882
  if (mtime > latestMtime) {
860
883
  latestMtime = mtime;
861
884
  latestFile = full;
@@ -864,10 +887,10 @@ function findLatestTranscript() {
864
887
  }
865
888
  if (!latestFile) return null;
866
889
  try {
867
- const firstLine = fs2.readFileSync(latestFile, "utf-8").split("\n")[0];
890
+ const firstLine = fs3.readFileSync(latestFile, "utf-8").split("\n")[0];
868
891
  if (!firstLine) return null;
869
892
  const firstMsg = JSON.parse(firstLine);
870
- const sessionId = String(firstMsg.sessionId ?? path2.basename(latestFile, ".jsonl"));
893
+ const sessionId = String(firstMsg.sessionId ?? path3.basename(latestFile, ".jsonl"));
871
894
  return { sessionId, transcriptPath: latestFile };
872
895
  } catch {
873
896
  return null;
@@ -886,7 +909,7 @@ async function mainWorker() {
886
909
  debug(`Processing session: ${sessionId}`);
887
910
  let config = null;
888
911
  try {
889
- const content = fs2.readFileSync(transcriptPath, "utf-8");
912
+ const content = fs3.readFileSync(transcriptPath, "utf-8");
890
913
  const lines = content.split("\n");
891
914
  let cwd = "";
892
915
  for (const line of lines.slice(0, 5)) {
@@ -901,7 +924,7 @@ async function mainWorker() {
901
924
  }
902
925
  }
903
926
  if (cwd) {
904
- config = loadRespanConfig(path2.join(cwd, ".claude", "respan.json"));
927
+ config = loadRespanConfig(path3.join(cwd, ".claude", "respan.json"));
905
928
  }
906
929
  } catch {
907
930
  }
@@ -23,15 +23,37 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
 
25
25
  // src/hooks/codex-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;
@@ -44,13 +66,13 @@ var _debug = false;
44
66
  function initLogging(logFile, debug2) {
45
67
  _logFile = logFile;
46
68
  _debug = debug2;
47
- const dir = path.dirname(logFile);
48
- fs.mkdirSync(dir, { recursive: true });
69
+ const dir = path2.dirname(logFile);
70
+ fs2.mkdirSync(dir, { recursive: true });
49
71
  }
50
72
  function log(level, message) {
51
73
  if (!_logFile) return;
52
74
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
53
- fs.appendFileSync(_logFile, `${ts} [${level}] ${message}
75
+ fs2.appendFileSync(_logFile, `${ts} [${level}] ${message}
54
76
  `);
55
77
  }
56
78
  function debug(message) {
@@ -61,14 +83,15 @@ function resolveCredentials() {
61
83
  let apiKey = process.env.RESPAN_API_KEY ?? "";
62
84
  let baseUrl = process.env.RESPAN_BASE_URL ?? DEFAULT_BASE_URL;
63
85
  if (!apiKey) {
64
- const credsFile = path.join(os.homedir(), ".respan", "credentials.json");
65
- if (fs.existsSync(credsFile)) {
86
+ const credsFile = path2.join(os2.homedir(), ".respan", "credentials.json");
87
+ if (fs2.existsSync(credsFile)) {
88
+ tightenCredentialPermissions();
66
89
  try {
67
- const creds = JSON.parse(fs.readFileSync(credsFile, "utf-8"));
68
- const configFile = path.join(os.homedir(), ".respan", "config.json");
90
+ const creds = JSON.parse(fs2.readFileSync(credsFile, "utf-8"));
91
+ const configFile = path2.join(os2.homedir(), ".respan", "config.json");
69
92
  let profile = "default";
70
- if (fs.existsSync(configFile)) {
71
- const cfg = JSON.parse(fs.readFileSync(configFile, "utf-8"));
93
+ if (fs2.existsSync(configFile)) {
94
+ const cfg = JSON.parse(fs2.readFileSync(configFile, "utf-8"));
72
95
  profile = cfg.activeProfile ?? "default";
73
96
  }
74
97
  const cred = creds[profile] ?? {};
@@ -98,11 +121,11 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
98
121
  "project_id"
99
122
  ]);
100
123
  function loadRespanConfig(configPath) {
101
- if (!fs.existsSync(configPath)) {
124
+ if (!fs2.existsSync(configPath)) {
102
125
  return { fields: {}, properties: {} };
103
126
  }
104
127
  try {
105
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
128
+ const raw = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
106
129
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
107
130
  return { fields: {}, properties: {} };
108
131
  }
@@ -122,36 +145,36 @@ function loadRespanConfig(configPath) {
122
145
  }
123
146
  }
124
147
  function loadState(statePath) {
125
- if (!fs.existsSync(statePath)) return {};
148
+ if (!fs2.existsSync(statePath)) return {};
126
149
  try {
127
- return JSON.parse(fs.readFileSync(statePath, "utf-8"));
150
+ return JSON.parse(fs2.readFileSync(statePath, "utf-8"));
128
151
  } catch {
129
152
  return {};
130
153
  }
131
154
  }
132
155
  function saveState(statePath, state) {
133
- const dir = path.dirname(statePath);
134
- fs.mkdirSync(dir, { recursive: true });
156
+ const dir = path2.dirname(statePath);
157
+ fs2.mkdirSync(dir, { recursive: true });
135
158
  const tmpPath = statePath + ".tmp." + process.pid;
136
159
  try {
137
- fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
138
- fs.renameSync(tmpPath, statePath);
160
+ fs2.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
161
+ fs2.renameSync(tmpPath, statePath);
139
162
  } catch (e) {
140
163
  try {
141
- fs.unlinkSync(tmpPath);
164
+ fs2.unlinkSync(tmpPath);
142
165
  } catch {
143
166
  }
144
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
167
+ fs2.writeFileSync(statePath, JSON.stringify(state, null, 2));
145
168
  }
146
169
  }
147
170
  function acquireLock(lockPath, timeoutMs = 5e3) {
148
171
  const deadline = Date.now() + timeoutMs;
149
172
  while (Date.now() < deadline) {
150
173
  try {
151
- fs.mkdirSync(lockPath);
174
+ fs2.mkdirSync(lockPath);
152
175
  return () => {
153
176
  try {
154
- fs.rmdirSync(lockPath);
177
+ fs2.rmdirSync(lockPath);
155
178
  } catch {
156
179
  }
157
180
  };
@@ -360,9 +383,9 @@ async function sendSpans(spans, apiKey, baseUrl, context) {
360
383
  const spanNames = spans.map((s) => s.span_name);
361
384
  debug(`Sending ${spans.length} spans (${body.length} bytes) to ${url} for ${context}: ${spanNames.join(", ")}`);
362
385
  if (_debug) {
363
- const debugDir = _logFile ? path.dirname(_logFile) : os.tmpdir();
364
- const debugFile = path.join(debugDir, `respan_spans_${context.replace(/\s+/g, "_")}.json`);
365
- fs.writeFileSync(debugFile, body);
386
+ const debugDir = _logFile ? path2.dirname(_logFile) : os2.tmpdir();
387
+ const debugFile = path2.join(debugDir, `respan_spans_${context.replace(/\s+/g, "_")}.json`);
388
+ fs2.writeFileSync(debugFile, body);
366
389
  debug(`Dumped OTLP payload to ${debugFile}`);
367
390
  }
368
391
  for (let attempt = 0; attempt < 3; attempt++) {
@@ -403,10 +426,10 @@ function sleep(ms) {
403
426
  }
404
427
 
405
428
  // src/hooks/codex-cli.ts
406
- var STATE_DIR = path2.join(os2.homedir(), ".codex", "state");
407
- var LOG_FILE = path2.join(STATE_DIR, "respan_hook.log");
408
- var STATE_FILE = path2.join(STATE_DIR, "respan_state.json");
409
- var LOCK_PATH = path2.join(STATE_DIR, "respan_hook.lock");
429
+ var STATE_DIR = path3.join(os3.homedir(), ".codex", "state");
430
+ var LOG_FILE = path3.join(STATE_DIR, "respan_hook.log");
431
+ var STATE_FILE = path3.join(STATE_DIR, "respan_state.json");
432
+ var LOCK_PATH = path3.join(STATE_DIR, "respan_hook.lock");
410
433
  var DEBUG_MODE = (process.env.CODEX_RESPAN_DEBUG ?? "").toLowerCase() === "true";
411
434
  var MAX_CHARS = parseInt(process.env.CODEX_RESPAN_MAX_CHARS ?? "4000", 10) || 4e3;
412
435
  initLogging(LOG_FILE, DEBUG_MODE);
@@ -656,13 +679,13 @@ function createSpans(sessionId, turnNum, turn, config) {
656
679
  return addDefaultsToAll(spans);
657
680
  }
658
681
  function findSessionFile(sessionId) {
659
- const sessionsDir = path2.join(os2.homedir(), ".codex", "sessions");
660
- if (!fs2.existsSync(sessionsDir)) return null;
682
+ const sessionsDir = path3.join(os3.homedir(), ".codex", "sessions");
683
+ if (!fs3.existsSync(sessionsDir)) return null;
661
684
  const walk = (dir) => {
662
- const entries = fs2.readdirSync(dir).sort().reverse();
685
+ const entries = fs3.readdirSync(dir).sort().reverse();
663
686
  for (const entry of entries) {
664
- const full = path2.join(dir, entry);
665
- if (fs2.statSync(full).isDirectory()) {
687
+ const full = path3.join(dir, entry);
688
+ if (fs3.statSync(full).isDirectory()) {
666
689
  const result = walk(full);
667
690
  if (result) return result;
668
691
  } else if (entry.endsWith(".jsonl") && entry.includes(sessionId)) {
@@ -674,14 +697,14 @@ function findSessionFile(sessionId) {
674
697
  return walk(sessionsDir);
675
698
  }
676
699
  function findLatestSessionFile() {
677
- const sessionsDir = path2.join(os2.homedir(), ".codex", "sessions");
678
- if (!fs2.existsSync(sessionsDir)) return null;
700
+ const sessionsDir = path3.join(os3.homedir(), ".codex", "sessions");
701
+ if (!fs3.existsSync(sessionsDir)) return null;
679
702
  let latestFile = null;
680
703
  let latestMtime = 0;
681
704
  const walk = (dir) => {
682
- for (const entry of fs2.readdirSync(dir)) {
683
- const full = path2.join(dir, entry);
684
- const stat = fs2.statSync(full);
705
+ for (const entry of fs3.readdirSync(dir)) {
706
+ const full = path3.join(dir, entry);
707
+ const stat = fs3.statSync(full);
685
708
  if (stat.isDirectory()) walk(full);
686
709
  else if (entry.endsWith(".jsonl") && stat.mtimeMs > latestMtime) {
687
710
  latestMtime = stat.mtimeMs;
@@ -692,11 +715,11 @@ function findLatestSessionFile() {
692
715
  walk(sessionsDir);
693
716
  if (!latestFile) return null;
694
717
  try {
695
- const firstLine = fs2.readFileSync(latestFile, "utf-8").split("\n")[0];
718
+ const firstLine = fs3.readFileSync(latestFile, "utf-8").split("\n")[0];
696
719
  if (!firstLine) return null;
697
720
  const firstMsg = JSON.parse(firstLine);
698
721
  const payload = firstMsg.payload ?? {};
699
- const sessionId = String(payload.id ?? path2.basename(latestFile, ".jsonl"));
722
+ const sessionId = String(payload.id ?? path3.basename(latestFile, ".jsonl"));
700
723
  return { sessionId, sessionFile: latestFile };
701
724
  } catch {
702
725
  return null;
@@ -713,7 +736,7 @@ async function mainWorker() {
713
736
  log("ERROR", "No API key");
714
737
  return;
715
738
  }
716
- const config = cwd ? loadRespanConfig(path2.join(cwd, ".codex", "respan.json")) : null;
739
+ const config = cwd ? loadRespanConfig(path3.join(cwd, ".codex", "respan.json")) : null;
717
740
  const maxAttempts = 3;
718
741
  let turns = 0;
719
742
  try {
@@ -723,7 +746,7 @@ async function mainWorker() {
723
746
  const state = loadState(STATE_FILE);
724
747
  const sessionState = state[sessionId] ?? {};
725
748
  const lastTurnCount = Number(sessionState.turn_count ?? 0);
726
- const lines = fs2.readFileSync(sessionFile, "utf-8").trim().split("\n");
749
+ const lines = fs3.readFileSync(sessionFile, "utf-8").trim().split("\n");
727
750
  const events = parseSession(lines);
728
751
  const allTurns = extractTurns(events);
729
752
  if (allTurns.length > lastTurnCount) {
@@ -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], {
@@ -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
- fallback_models?: string[];
41
40
  load_balance_models?: Record<string, unknown>[];
42
41
  tools?: Record<string, unknown>[];
43
42
  tool_choice?: string | Record<string, unknown>;
@@ -49,9 +49,6 @@ export const DEMO_PROMPTS = [
49
49
  "variables": {
50
50
  "customer_message": ""
51
51
  },
52
- "fallback_models": [
53
- "openai/gpt-5.4"
54
- ],
55
52
  "tools": [
56
53
  {
57
54
  "type": "function",
@@ -914,16 +914,14 @@
914
914
  "set.js"
915
915
  ]
916
916
  },
917
- "datasets:create-span": {
917
+ "demo:clear": {
918
918
  "aliases": [],
919
- "args": {
920
- "dataset-id": {
921
- "description": "Dataset ID",
922
- "name": "dataset-id",
923
- "required": true
924
- }
925
- },
926
- "description": "Create a span in a dataset",
919
+ "args": {},
920
+ "description": "Delete the sample prompts, datasets, and evaluators created by `respan demo`. Demo traces are left in place (each run is timestamped history). Idempotent — safe to re-run.",
921
+ "examples": [
922
+ "<%= config.bin %> demo clear",
923
+ "<%= config.bin %> demo clear --yes"
924
+ ],
927
925
  "flags": {
928
926
  "api-key": {
929
927
  "description": "API key (env: RESPAN_API_KEY)",
@@ -966,18 +964,17 @@
966
964
  "allowNo": false,
967
965
  "type": "boolean"
968
966
  },
969
- "body": {
970
- "description": "Span body as JSON string",
971
- "name": "body",
972
- "required": true,
973
- "hasDynamicHelp": false,
974
- "multiple": false,
975
- "type": "option"
967
+ "yes": {
968
+ "char": "y",
969
+ "description": "Skip the confirmation prompt",
970
+ "name": "yes",
971
+ "allowNo": false,
972
+ "type": "boolean"
976
973
  }
977
974
  },
978
975
  "hasDynamicHelp": false,
979
976
  "hiddenAliases": [],
980
- "id": "datasets:create-span",
977
+ "id": "demo:clear",
981
978
  "pluginAlias": "@respan/cli",
982
979
  "pluginName": "@respan/cli",
983
980
  "pluginType": "core",
@@ -987,14 +984,17 @@
987
984
  "relativePath": [
988
985
  "dist",
989
986
  "commands",
990
- "datasets",
991
- "create-span.js"
987
+ "demo",
988
+ "clear.js"
992
989
  ]
993
990
  },
994
- "datasets:create": {
991
+ "demo": {
995
992
  "aliases": [],
996
993
  "args": {},
997
- "description": "Create a new dataset",
994
+ "description": "Seed your account with sample data (prompts and traces) so you have something to explore. Idempotent — safe to re-run.",
995
+ "examples": [
996
+ "<%= config.bin %> demo"
997
+ ],
998
998
  "flags": {
999
999
  "api-key": {
1000
1000
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1036,26 +1036,11 @@
1036
1036
  "name": "verbose",
1037
1037
  "allowNo": false,
1038
1038
  "type": "boolean"
1039
- },
1040
- "name": {
1041
- "description": "Dataset name",
1042
- "name": "name",
1043
- "required": true,
1044
- "hasDynamicHelp": false,
1045
- "multiple": false,
1046
- "type": "option"
1047
- },
1048
- "description": {
1049
- "description": "Dataset description",
1050
- "name": "description",
1051
- "hasDynamicHelp": false,
1052
- "multiple": false,
1053
- "type": "option"
1054
1039
  }
1055
1040
  },
1056
1041
  "hasDynamicHelp": false,
1057
1042
  "hiddenAliases": [],
1058
- "id": "datasets:create",
1043
+ "id": "demo",
1059
1044
  "pluginAlias": "@respan/cli",
1060
1045
  "pluginName": "@respan/cli",
1061
1046
  "pluginType": "core",
@@ -1065,25 +1050,20 @@
1065
1050
  "relativePath": [
1066
1051
  "dist",
1067
1052
  "commands",
1068
- "datasets",
1069
- "create.js"
1053
+ "demo",
1054
+ "index.js"
1070
1055
  ]
1071
1056
  },
1072
- "datasets:get-span": {
1057
+ "datasets:create-span": {
1073
1058
  "aliases": [],
1074
1059
  "args": {
1075
1060
  "dataset-id": {
1076
1061
  "description": "Dataset ID",
1077
1062
  "name": "dataset-id",
1078
1063
  "required": true
1079
- },
1080
- "span-id": {
1081
- "description": "Span ID",
1082
- "name": "span-id",
1083
- "required": true
1084
1064
  }
1085
1065
  },
1086
- "description": "Get a specific span from a dataset",
1066
+ "description": "Create a span in a dataset",
1087
1067
  "flags": {
1088
1068
  "api-key": {
1089
1069
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1125,11 +1105,19 @@
1125
1105
  "name": "verbose",
1126
1106
  "allowNo": false,
1127
1107
  "type": "boolean"
1108
+ },
1109
+ "body": {
1110
+ "description": "Span body as JSON string",
1111
+ "name": "body",
1112
+ "required": true,
1113
+ "hasDynamicHelp": false,
1114
+ "multiple": false,
1115
+ "type": "option"
1128
1116
  }
1129
1117
  },
1130
1118
  "hasDynamicHelp": false,
1131
1119
  "hiddenAliases": [],
1132
- "id": "datasets:get-span",
1120
+ "id": "datasets:create-span",
1133
1121
  "pluginAlias": "@respan/cli",
1134
1122
  "pluginName": "@respan/cli",
1135
1123
  "pluginType": "core",
@@ -1140,19 +1128,13 @@
1140
1128
  "dist",
1141
1129
  "commands",
1142
1130
  "datasets",
1143
- "get-span.js"
1131
+ "create-span.js"
1144
1132
  ]
1145
1133
  },
1146
- "datasets:get": {
1134
+ "datasets:create": {
1147
1135
  "aliases": [],
1148
- "args": {
1149
- "id": {
1150
- "description": "Dataset ID",
1151
- "name": "id",
1152
- "required": true
1153
- }
1154
- },
1155
- "description": "Get a specific dataset",
1136
+ "args": {},
1137
+ "description": "Create a new dataset",
1156
1138
  "flags": {
1157
1139
  "api-key": {
1158
1140
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1194,11 +1176,26 @@
1194
1176
  "name": "verbose",
1195
1177
  "allowNo": false,
1196
1178
  "type": "boolean"
1179
+ },
1180
+ "name": {
1181
+ "description": "Dataset name",
1182
+ "name": "name",
1183
+ "required": true,
1184
+ "hasDynamicHelp": false,
1185
+ "multiple": false,
1186
+ "type": "option"
1187
+ },
1188
+ "description": {
1189
+ "description": "Dataset description",
1190
+ "name": "description",
1191
+ "hasDynamicHelp": false,
1192
+ "multiple": false,
1193
+ "type": "option"
1197
1194
  }
1198
1195
  },
1199
1196
  "hasDynamicHelp": false,
1200
1197
  "hiddenAliases": [],
1201
- "id": "datasets:get",
1198
+ "id": "datasets:create",
1202
1199
  "pluginAlias": "@respan/cli",
1203
1200
  "pluginName": "@respan/cli",
1204
1201
  "pluginType": "core",
@@ -1209,13 +1206,24 @@
1209
1206
  "dist",
1210
1207
  "commands",
1211
1208
  "datasets",
1212
- "get.js"
1209
+ "create.js"
1213
1210
  ]
1214
1211
  },
1215
- "datasets:list": {
1212
+ "datasets:get-span": {
1216
1213
  "aliases": [],
1217
- "args": {},
1218
- "description": "List datasets",
1214
+ "args": {
1215
+ "dataset-id": {
1216
+ "description": "Dataset ID",
1217
+ "name": "dataset-id",
1218
+ "required": true
1219
+ },
1220
+ "span-id": {
1221
+ "description": "Span ID",
1222
+ "name": "span-id",
1223
+ "required": true
1224
+ }
1225
+ },
1226
+ "description": "Get a specific span from a dataset",
1219
1227
  "flags": {
1220
1228
  "api-key": {
1221
1229
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1257,27 +1265,11 @@
1257
1265
  "name": "verbose",
1258
1266
  "allowNo": false,
1259
1267
  "type": "boolean"
1260
- },
1261
- "limit": {
1262
- "description": "Number of results per page",
1263
- "name": "limit",
1264
- "default": 50,
1265
- "hasDynamicHelp": false,
1266
- "multiple": false,
1267
- "type": "option"
1268
- },
1269
- "page": {
1270
- "description": "Page number",
1271
- "name": "page",
1272
- "default": 1,
1273
- "hasDynamicHelp": false,
1274
- "multiple": false,
1275
- "type": "option"
1276
1268
  }
1277
1269
  },
1278
1270
  "hasDynamicHelp": false,
1279
1271
  "hiddenAliases": [],
1280
- "id": "datasets:list",
1272
+ "id": "datasets:get-span",
1281
1273
  "pluginAlias": "@respan/cli",
1282
1274
  "pluginName": "@respan/cli",
1283
1275
  "pluginType": "core",
@@ -1288,19 +1280,19 @@
1288
1280
  "dist",
1289
1281
  "commands",
1290
1282
  "datasets",
1291
- "list.js"
1283
+ "get-span.js"
1292
1284
  ]
1293
1285
  },
1294
- "datasets:spans": {
1286
+ "datasets:get": {
1295
1287
  "aliases": [],
1296
1288
  "args": {
1297
- "dataset-id": {
1289
+ "id": {
1298
1290
  "description": "Dataset ID",
1299
- "name": "dataset-id",
1291
+ "name": "id",
1300
1292
  "required": true
1301
1293
  }
1302
1294
  },
1303
- "description": "List spans in a dataset",
1295
+ "description": "Get a specific dataset",
1304
1296
  "flags": {
1305
1297
  "api-key": {
1306
1298
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1346,7 +1338,7 @@
1346
1338
  },
1347
1339
  "hasDynamicHelp": false,
1348
1340
  "hiddenAliases": [],
1349
- "id": "datasets:spans",
1341
+ "id": "datasets:get",
1350
1342
  "pluginAlias": "@respan/cli",
1351
1343
  "pluginName": "@respan/cli",
1352
1344
  "pluginType": "core",
@@ -1357,19 +1349,13 @@
1357
1349
  "dist",
1358
1350
  "commands",
1359
1351
  "datasets",
1360
- "spans.js"
1352
+ "get.js"
1361
1353
  ]
1362
1354
  },
1363
- "datasets:update": {
1355
+ "datasets:list": {
1364
1356
  "aliases": [],
1365
- "args": {
1366
- "id": {
1367
- "description": "Dataset ID",
1368
- "name": "id",
1369
- "required": true
1370
- }
1371
- },
1372
- "description": "Update a dataset",
1357
+ "args": {},
1358
+ "description": "List datasets",
1373
1359
  "flags": {
1374
1360
  "api-key": {
1375
1361
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1412,16 +1398,18 @@
1412
1398
  "allowNo": false,
1413
1399
  "type": "boolean"
1414
1400
  },
1415
- "name": {
1416
- "description": "Dataset name",
1417
- "name": "name",
1401
+ "limit": {
1402
+ "description": "Number of results per page",
1403
+ "name": "limit",
1404
+ "default": 50,
1418
1405
  "hasDynamicHelp": false,
1419
1406
  "multiple": false,
1420
1407
  "type": "option"
1421
1408
  },
1422
- "description": {
1423
- "description": "Dataset description",
1424
- "name": "description",
1409
+ "page": {
1410
+ "description": "Page number",
1411
+ "name": "page",
1412
+ "default": 1,
1425
1413
  "hasDynamicHelp": false,
1426
1414
  "multiple": false,
1427
1415
  "type": "option"
@@ -1429,7 +1417,7 @@
1429
1417
  },
1430
1418
  "hasDynamicHelp": false,
1431
1419
  "hiddenAliases": [],
1432
- "id": "datasets:update",
1420
+ "id": "datasets:list",
1433
1421
  "pluginAlias": "@respan/cli",
1434
1422
  "pluginName": "@respan/cli",
1435
1423
  "pluginType": "core",
@@ -1440,17 +1428,19 @@
1440
1428
  "dist",
1441
1429
  "commands",
1442
1430
  "datasets",
1443
- "update.js"
1431
+ "list.js"
1444
1432
  ]
1445
1433
  },
1446
- "demo:clear": {
1434
+ "datasets:spans": {
1447
1435
  "aliases": [],
1448
- "args": {},
1449
- "description": "Delete the sample prompts, datasets, and evaluators created by `respan demo`. Demo traces are left in place (each run is timestamped history). Idempotent — safe to re-run.",
1450
- "examples": [
1451
- "<%= config.bin %> demo clear",
1452
- "<%= config.bin %> demo clear --yes"
1453
- ],
1436
+ "args": {
1437
+ "dataset-id": {
1438
+ "description": "Dataset ID",
1439
+ "name": "dataset-id",
1440
+ "required": true
1441
+ }
1442
+ },
1443
+ "description": "List spans in a dataset",
1454
1444
  "flags": {
1455
1445
  "api-key": {
1456
1446
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1492,18 +1482,11 @@
1492
1482
  "name": "verbose",
1493
1483
  "allowNo": false,
1494
1484
  "type": "boolean"
1495
- },
1496
- "yes": {
1497
- "char": "y",
1498
- "description": "Skip the confirmation prompt",
1499
- "name": "yes",
1500
- "allowNo": false,
1501
- "type": "boolean"
1502
1485
  }
1503
1486
  },
1504
1487
  "hasDynamicHelp": false,
1505
1488
  "hiddenAliases": [],
1506
- "id": "demo:clear",
1489
+ "id": "datasets:spans",
1507
1490
  "pluginAlias": "@respan/cli",
1508
1491
  "pluginName": "@respan/cli",
1509
1492
  "pluginType": "core",
@@ -1513,17 +1496,20 @@
1513
1496
  "relativePath": [
1514
1497
  "dist",
1515
1498
  "commands",
1516
- "demo",
1517
- "clear.js"
1499
+ "datasets",
1500
+ "spans.js"
1518
1501
  ]
1519
1502
  },
1520
- "demo": {
1503
+ "datasets:update": {
1521
1504
  "aliases": [],
1522
- "args": {},
1523
- "description": "Seed your account with sample data (prompts and traces) so you have something to explore. Idempotent — safe to re-run.",
1524
- "examples": [
1525
- "<%= config.bin %> demo"
1526
- ],
1505
+ "args": {
1506
+ "id": {
1507
+ "description": "Dataset ID",
1508
+ "name": "id",
1509
+ "required": true
1510
+ }
1511
+ },
1512
+ "description": "Update a dataset",
1527
1513
  "flags": {
1528
1514
  "api-key": {
1529
1515
  "description": "API key (env: RESPAN_API_KEY)",
@@ -1565,11 +1551,25 @@
1565
1551
  "name": "verbose",
1566
1552
  "allowNo": false,
1567
1553
  "type": "boolean"
1554
+ },
1555
+ "name": {
1556
+ "description": "Dataset name",
1557
+ "name": "name",
1558
+ "hasDynamicHelp": false,
1559
+ "multiple": false,
1560
+ "type": "option"
1561
+ },
1562
+ "description": {
1563
+ "description": "Dataset description",
1564
+ "name": "description",
1565
+ "hasDynamicHelp": false,
1566
+ "multiple": false,
1567
+ "type": "option"
1568
1568
  }
1569
1569
  },
1570
1570
  "hasDynamicHelp": false,
1571
1571
  "hiddenAliases": [],
1572
- "id": "demo",
1572
+ "id": "datasets:update",
1573
1573
  "pluginAlias": "@respan/cli",
1574
1574
  "pluginName": "@respan/cli",
1575
1575
  "pluginType": "core",
@@ -1579,8 +1579,8 @@
1579
1579
  "relativePath": [
1580
1580
  "dist",
1581
1581
  "commands",
1582
- "demo",
1583
- "index.js"
1582
+ "datasets",
1583
+ "update.js"
1584
1584
  ]
1585
1585
  },
1586
1586
  "eval": {
@@ -4285,5 +4285,5 @@
4285
4285
  ]
4286
4286
  }
4287
4287
  },
4288
- "version": "0.17.0"
4288
+ "version": "0.17.2"
4289
4289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@respan/cli",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Respan CLI - manage your LLM observability from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",