@basou/cli 0.57.1 → 0.59.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/program.js CHANGED
@@ -12,10 +12,12 @@ import {
12
12
  acquireLock,
13
13
  appendChainedEventLocked,
14
14
  assertBasouRootSafe,
15
+ assertSessionDirSafe,
15
16
  basouPaths,
16
17
  EVENT_SCHEMA_VERSION,
17
18
  enumerateApprovals,
18
19
  findErrorCode,
20
+ inspectSessionEntry,
19
21
  isLazyExpired,
20
22
  LOCAL_CLI_EVENT_SOURCE,
21
23
  linkYamlFile,
@@ -123,6 +125,8 @@ function printSessionSkip(sid, reason) {
123
125
  const short = shortSessionId(sid);
124
126
  if (reason === "events_jsonl_unreadable") {
125
127
  console.error(`Warning: skipped suspect check for ${short}: events.jsonl unreadable`);
128
+ } else if (reason === "session_dir_not_directory") {
129
+ console.error(`Skipped ${sid}: ${reason}`);
126
130
  } else {
127
131
  console.error(`Skipped ${short}: ${reason}`);
128
132
  }
@@ -139,6 +143,9 @@ function printSessionListSkip(sid, reason) {
139
143
  case "events_jsonl_unreadable":
140
144
  console.error(`Warning: skipped suspect check for ${short}: events.jsonl unreadable`);
141
145
  break;
146
+ case "session_dir_not_directory":
147
+ console.error(`Skipped ${sid}: not a directory (a symlink or a file is not followed)`);
148
+ break;
142
149
  }
143
150
  }
144
151
  function printTaskSkip(taskId, reason) {
@@ -257,13 +264,20 @@ async function doRunApprovalShow(idInput, options, ctx) {
257
264
  if (loaded === null) {
258
265
  throw new Error(`Approval not found: ${idInput}`);
259
266
  }
260
- const sessionDir = join(paths.sessions, loaded.approval.session_id);
267
+ const sessionId = loaded.approval.session_id;
268
+ const entry = await inspectSessionEntry(paths, sessionId);
261
269
  const relatedEvents = [];
262
- for await (const ev of replayEvents(sessionDir, {
263
- onWarning: (w) => printReplayWarning(w, loaded.approval.session_id)
264
- })) {
265
- if (isApprovalEvent(ev) && ev.approval_id === id) {
266
- relatedEvents.push(ev);
270
+ if (entry === "symlink" || entry === "not_a_directory") {
271
+ console.error(
272
+ `Warning: session ${sessionId} is not a directory (a symlink or a file is not followed); its events are not shown`
273
+ );
274
+ } else {
275
+ for await (const ev of replayEvents(join(paths.sessions, sessionId), {
276
+ onWarning: (w) => printReplayWarning(w, sessionId)
277
+ })) {
278
+ if (isApprovalEvent(ev) && ev.approval_id === id) {
279
+ relatedEvents.push(ev);
280
+ }
267
281
  }
268
282
  }
269
283
  const now = /* @__PURE__ */ new Date();
@@ -342,6 +356,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
342
356
  const eventId = prefixedUlid("evt");
343
357
  const sessionLock = await acquireLock(paths, "session", approval.session_id);
344
358
  try {
359
+ await assertSessionDirSafe(paths, approval.session_id);
345
360
  const sessionDir = join(paths.sessions, approval.session_id);
346
361
  for await (const ev of replayEvents(sessionDir, {
347
362
  onWarning: (w) => printReplayWarning(w, approval.session_id)
@@ -2062,6 +2077,7 @@ import { join as join6 } from "path";
2062
2077
  import {
2063
2078
  acquireLock as acquireLock3,
2064
2079
  assertBasouRootSafe as assertBasouRootSafe4,
2080
+ assertSessionStoreSafe,
2065
2081
  basouPaths as basouPaths6,
2066
2082
  ChildProcessRunner,
2067
2083
  appendChainedEvent as coreAppendChainedEvent,
@@ -2100,6 +2116,7 @@ async function runExec(command, args, options, ctx = {}) {
2100
2116
  const paths = basouPaths6(repoRoot);
2101
2117
  await assertBasouRootSafe4(paths.root);
2102
2118
  const manifest = await readManifest3(paths);
2119
+ await assertSessionStoreSafe(paths);
2103
2120
  const sessionId = prefixedUlid3("ses");
2104
2121
  const sessionDir = join6(paths.sessions, sessionId);
2105
2122
  await mkdir(sessionDir, { recursive: true });
@@ -2914,6 +2931,7 @@ import { createInterface } from "readline";
2914
2931
  import {
2915
2932
  AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
2916
2933
  assertBasouRootSafe as assertBasouRootSafe6,
2934
+ assertSessionStoreSafe as assertSessionStoreSafe2,
2917
2935
  basouPaths as basouPaths8,
2918
2936
  CLAUDE_IMPORT_SOURCE,
2919
2937
  CODEX_IMPORT_SOURCE,
@@ -2921,7 +2939,7 @@ import {
2921
2939
  claudeTranscriptToImportPayload,
2922
2940
  codexRolloutToImportPayload,
2923
2941
  displayPath as displayPath2,
2924
- enumerateSessionDirs,
2942
+ enumerateSessionEntries,
2925
2943
  findErrorCode as findErrorCode5,
2926
2944
  importSessionFromJson,
2927
2945
  observedFilesOf,
@@ -3085,7 +3103,12 @@ async function resolveImportTarget(ctx) {
3085
3103
  return { repositoryRoot, paths, manifest };
3086
3104
  }
3087
3105
  async function importDerivedSessions(paths, manifest, options, sourceKind, candidates, projectPaths, boundaryDeclared) {
3088
- const existingByExternalId = await loadExistingByExternalId(paths, sourceKind);
3106
+ await assertSessionStoreSafe2(paths);
3107
+ const { byExternalId: existingByExternalId, notDirectories } = await loadExistingByExternalId(
3108
+ paths,
3109
+ sourceKind
3110
+ );
3111
+ let notDirectoriesWarned = false;
3089
3112
  const seenThisRun = /* @__PURE__ */ new Set();
3090
3113
  const crossProjectCheck = boundaryDeclared;
3091
3114
  const crossProject = [];
@@ -3177,6 +3200,13 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
3177
3200
  }
3178
3201
  counts.replaced++;
3179
3202
  }
3203
+ if (priors.length === 0 && notDirectories.length > 0 && !notDirectoriesWarned) {
3204
+ notDirectoriesWarned = true;
3205
+ const one = notDirectories.length === 1;
3206
+ console.error(
3207
+ `Import: ${notDirectories.join(", ")} ${one ? "is not a directory" : "are not directories"} (a symlink or a file is not followed), so ${one ? "it was" : "they were"} not checked for an earlier import; a session imported there may be imported again`
3208
+ );
3209
+ }
3180
3210
  const result = await importSessionFromJson(paths, manifest, payload, {
3181
3211
  dryRun: options.dryRun === true
3182
3212
  });
@@ -3253,10 +3283,11 @@ async function loadExistingByExternalId(paths, sourceKind) {
3253
3283
  else list.push(prior);
3254
3284
  };
3255
3285
  let sessionIds;
3286
+ let notDirectories;
3256
3287
  try {
3257
- sessionIds = await enumerateSessionDirs(paths);
3288
+ ({ dirs: sessionIds, notDirectories } = await enumerateSessionEntries(paths));
3258
3289
  } catch {
3259
- return byExternalId;
3290
+ return { byExternalId, notDirectories: [] };
3260
3291
  }
3261
3292
  for (const sessionId of sessionIds) {
3262
3293
  let session;
@@ -3277,7 +3308,7 @@ async function loadExistingByExternalId(paths, sourceKind) {
3277
3308
  const match = typeof label === "string" ? label.match(/^claude-code import (\S+)$/) : null;
3278
3309
  if (match?.[1] !== void 0) add(match[1], prior);
3279
3310
  }
3280
- return byExternalId;
3311
+ return { byExternalId, notDirectories };
3281
3312
  }
3282
3313
  async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
3283
3314
  if (options.session !== void 0) {
@@ -9557,6 +9588,7 @@ import { join as join14 } from "path";
9557
9588
  import {
9558
9589
  acquireLock as acquireLock5,
9559
9590
  assertBasouRootSafe as assertBasouRootSafe12,
9591
+ assertSessionStoreSafe as assertSessionStoreSafe3,
9560
9592
  basouPaths as basouPaths17,
9561
9593
  ChildProcessRunner as ChildProcessRunner2,
9562
9594
  claudeCodeAdapterMetadata,
@@ -9632,6 +9664,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
9632
9664
  const paths = basouPaths17(repoRoot);
9633
9665
  await assertBasouRootSafe12(paths.root);
9634
9666
  const manifest = await readManifest10(paths);
9667
+ await assertSessionStoreSafe3(paths);
9635
9668
  const sessionId = prefixedUlid4("ses");
9636
9669
  const sessionDir = join14(paths.sessions, sessionId);
9637
9670
  await mkdir2(sessionDir, { recursive: true });
@@ -10021,7 +10054,7 @@ import {
10021
10054
  basouPaths as basouPaths18,
10022
10055
  displayPath as displayPath3,
10023
10056
  EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION6,
10024
- enumerateSessionDirs as enumerateSessionDirs2,
10057
+ enumerateSessionEntries as enumerateSessionEntries2,
10025
10058
  findErrorCode as findErrorCode12,
10026
10059
  importSessionFromJson as importSessionFromJson2,
10027
10060
  LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE4,
@@ -10653,7 +10686,7 @@ async function doRunSessionRechain(options, ctx) {
10653
10686
  const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
10654
10687
  const paths = basouPaths18(repositoryRoot);
10655
10688
  await assertWorkspaceInitialized11(paths.root);
10656
- const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
10689
+ const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateAllSessionNames(paths);
10657
10690
  const dryRun = options.dryRun === true;
10658
10691
  const rows = [];
10659
10692
  for (const sessionId of sessionIds) {
@@ -10674,7 +10707,9 @@ async function doRunSessionRechain(options, ctx) {
10674
10707
  rows.push({ session_id: sessionId, status: "skipped", reason: outcome.reason });
10675
10708
  }
10676
10709
  }
10677
- const tamperedCount = rows.filter((r) => r.reason === "tampered").length;
10710
+ const tamperedCount = rows.filter(
10711
+ (r) => r.reason === "tampered" || r.reason === "symlink" || r.reason === "not_a_directory"
10712
+ ).length;
10678
10713
  const errorCount = rows.filter((r) => r.status === "error").length;
10679
10714
  if (options.json === true) {
10680
10715
  console.log(JSON.stringify(rows, null, 2));
@@ -10697,11 +10732,19 @@ function renderRechainRow(row, dryRun) {
10697
10732
  case "rechained":
10698
10733
  return `${dryRun ? "would rechain" : "rechained"} (${row.event_count} events)`;
10699
10734
  case "skipped":
10700
- return row.reason === "tampered" ? "skipped (TAMPERED \u2014 inspect with 'basou verify')" : `skipped (${row.reason})`;
10735
+ if (row.reason === "tampered") return "skipped (TAMPERED \u2014 inspect with 'basou verify')";
10736
+ if (row.reason === "symlink" || row.reason === "not_a_directory") {
10737
+ return `skipped (${row.reason === "symlink" ? "SYMLINK" : "NOT A DIRECTORY"} \u2014 not followed; inspect with 'basou verify')`;
10738
+ }
10739
+ return `skipped (${row.reason})`;
10701
10740
  case "error":
10702
10741
  return `error (${row.message})`;
10703
10742
  }
10704
10743
  }
10744
+ async function enumerateAllSessionNames(paths) {
10745
+ const { dirs, notDirectories } = await enumerateSessionEntries2(paths);
10746
+ return [...dirs, ...notDirectories].sort();
10747
+ }
10705
10748
 
10706
10749
  // src/commands/stats.ts
10707
10750
  import {
@@ -12049,7 +12092,7 @@ function maxLen3(values, floor) {
12049
12092
  import {
12050
12093
  assertBasouRootSafe as assertBasouRootSafe17,
12051
12094
  basouPaths as basouPaths22,
12052
- enumerateSessionDirs as enumerateSessionDirs3,
12095
+ enumerateSessionEntries as enumerateSessionEntries3,
12053
12096
  findErrorCode as findErrorCode16,
12054
12097
  resolveRepositoryRoot as resolveRepositoryRoot12,
12055
12098
  resolveSessionId as resolveSessionId5,
@@ -12083,7 +12126,7 @@ async function doRunVerify(options, ctx) {
12083
12126
  const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
12084
12127
  const paths = basouPaths22(repositoryRoot);
12085
12128
  await assertWorkspaceInitialized14(paths.root);
12086
- const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
12129
+ const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session, { allowNotDirectory: true })] : await enumerateAllSessionNames2(paths);
12087
12130
  const rows = [];
12088
12131
  for (const sessionId of sessionIds) {
12089
12132
  const verdict = await verifyEventsChain(paths, sessionId);
@@ -12130,6 +12173,10 @@ function renderVerdict(row) {
12130
12173
  return "empty";
12131
12174
  }
12132
12175
  }
12176
+ async function enumerateAllSessionNames2(paths) {
12177
+ const { dirs, notDirectories } = await enumerateSessionEntries3(paths);
12178
+ return [...dirs, ...notDirectories].sort();
12179
+ }
12133
12180
  async function resolveRepositoryRootForVerify(cwd) {
12134
12181
  try {
12135
12182
  return await resolveRepositoryRoot12(cwd);
@@ -12667,6 +12714,7 @@ import {
12667
12714
  computeWorkStats as computeWorkStats2,
12668
12715
  enumerateApprovals as enumerateApprovals2,
12669
12716
  findErrorCode as findErrorCode17,
12717
+ inspectSessionEntry as inspectSessionEntry2,
12670
12718
  isLazyExpired as isLazyExpired2,
12671
12719
  loadApproval as loadApproval2,
12672
12720
  loadSessionEntries as loadSessionEntries4,
@@ -12678,6 +12726,7 @@ import {
12678
12726
  readTaskFile as readTaskFile2,
12679
12727
  renderDecisions as renderDecisions3,
12680
12728
  renderHandoff as renderHandoff3,
12729
+ SessionIdSchema,
12681
12730
  summarizeOrientation,
12682
12731
  tryRemoteUrl
12683
12732
  } from "@basou/core";
@@ -13741,6 +13790,16 @@ async function sessionsList(ws, nowProvider) {
13741
13790
  return { sessions };
13742
13791
  }
13743
13792
  async function sessionDetail(ws, sessionId) {
13793
+ if (!SessionIdSchema.safeParse(sessionId).success) {
13794
+ throw new HttpError(404, "Session not found");
13795
+ }
13796
+ const entry = await inspectSessionEntry2(ws.paths, sessionId);
13797
+ if (entry === "symlink" || entry === "not_a_directory") {
13798
+ throw new HttpError(
13799
+ 404,
13800
+ `Session ${sessionId} is not a directory; a symlink or a file there is not followed`
13801
+ );
13802
+ }
13744
13803
  let session;
13745
13804
  try {
13746
13805
  session = await readSessionYaml3(ws.paths, sessionId);
@@ -14123,7 +14182,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
14123
14182
  function readBuildStamp() {
14124
14183
  if (false) return void 0;
14125
14184
  try {
14126
- return JSON.parse('{"version":"0.57.1","commit":"0b14831","committedAt":"2026-10-01T11:07:24+09:00"}');
14185
+ return JSON.parse('{"version":"0.59.0","commit":"431fe03","committedAt":"2026-10-01T20:32:13+09:00"}');
14127
14186
  } catch {
14128
14187
  return void 0;
14129
14188
  }