@basestream/cli 0.2.5 → 0.2.7

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +160 -84
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -14,7 +14,7 @@ import fs from "node:fs";
14
14
  import path from "node:path";
15
15
  import os from "node:os";
16
16
  function ensureDirs() {
17
- for (const dir of [BASESTREAM_DIR, BUFFER_DIR, SESSION_DIR]) {
17
+ for (const dir of [BASESTREAM_DIR, BUFFER_DIR, SESSION_DIR, LOG_DIR]) {
18
18
  fs.mkdirSync(dir, { recursive: true });
19
19
  }
20
20
  }
@@ -34,7 +34,7 @@ function writeConfig(config) {
34
34
  ensureDirs();
35
35
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
36
36
  }
37
- var BASESTREAM_DIR, BUFFER_DIR, CONFIG_FILE, SESSION_DIR;
37
+ var BASESTREAM_DIR, BUFFER_DIR, CONFIG_FILE, SESSION_DIR, LOG_DIR, LOG_RETENTION_DAYS;
38
38
  var init_config = __esm({
39
39
  "src/cli/config.ts"() {
40
40
  "use strict";
@@ -42,6 +42,8 @@ var init_config = __esm({
42
42
  BUFFER_DIR = path.join(BASESTREAM_DIR, "buffer");
43
43
  CONFIG_FILE = path.join(BASESTREAM_DIR, "config.json");
44
44
  SESSION_DIR = path.join(BASESTREAM_DIR, "sessions");
45
+ LOG_DIR = path.join(BASESTREAM_DIR, "logs");
46
+ LOG_RETENTION_DAYS = 7;
45
47
  }
46
48
  });
47
49
 
@@ -250,8 +252,9 @@ var CLAUDE_DIR = path2.join(os2.homedir(), ".claude");
250
252
  var CLAUDE_SETTINGS_PATH = path2.join(CLAUDE_DIR, "settings.json");
251
253
  var CLAUDE_SKILLS_PATH = path2.join(CLAUDE_DIR, "skills", "bs-summarize.md");
252
254
  var CLAUDE_MD_PATH = path2.join(CLAUDE_DIR, "CLAUDE.md");
253
- var HOOK_COMMAND = "npx @basestream/cli _hook-stop";
255
+ var HOOK_COMMAND = "npx --yes @basestream/cli@latest _hook-stop";
254
256
  var HOOK_MARKER = "_hook-stop";
257
+ var HOOK_TIMEOUT = 60;
255
258
  var CLAUDE_MD_MARKER = "<!-- basestream -->";
256
259
  var CLAUDE_MD_RULE = `${CLAUDE_MD_MARKER}
257
260
  - Before starting any non-trivial task, if the reason or context behind the work isn't already clear, ask: "What's the context for this?" Skip this for small tasks, typo fixes, or anything self-explanatory.
@@ -274,6 +277,11 @@ Do not just append \u2014 synthesize everything into a single cohesive summary.
274
277
 
275
278
  Write the result to \`~/.basestream/sessions/\${CLAUDE_SESSION_ID}-summary.md\`, overwriting the file.
276
279
  `;
280
+ var BASESTREAM_DIR2 = path2.join(os2.homedir(), ".basestream");
281
+ var REQUIRED_PERMISSIONS = [
282
+ `Write(${BASESTREAM_DIR2}/**)`,
283
+ `Read(${BASESTREAM_DIR2}/**)`
284
+ ];
277
285
  function injectClaudeMdRule() {
278
286
  let existing = "";
279
287
  if (fs2.existsSync(CLAUDE_MD_PATH)) {
@@ -311,6 +319,33 @@ function detectClaudeCode() {
311
319
  return null;
312
320
  }
313
321
  }
322
+ function injectPermissions() {
323
+ const settingsDir = path2.dirname(CLAUDE_SETTINGS_PATH);
324
+ fs2.mkdirSync(settingsDir, { recursive: true });
325
+ let settings = {};
326
+ if (fs2.existsSync(CLAUDE_SETTINGS_PATH)) {
327
+ try {
328
+ settings = JSON.parse(fs2.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
329
+ } catch {
330
+ }
331
+ }
332
+ if (!settings.permissions || typeof settings.permissions !== "object") {
333
+ settings.permissions = {};
334
+ }
335
+ const permissions = settings.permissions;
336
+ if (!Array.isArray(permissions.allow)) {
337
+ permissions.allow = [];
338
+ }
339
+ const allow = permissions.allow;
340
+ const missing = REQUIRED_PERMISSIONS.filter((p) => !allow.includes(p));
341
+ if (missing.length === 0) {
342
+ check("Claude Code permissions already configured");
343
+ return;
344
+ }
345
+ allow.push(...missing);
346
+ fs2.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
347
+ check("Added ~/.basestream/** permissions to ~/.claude/settings.json");
348
+ }
314
349
  function injectClaudeCodeHook() {
315
350
  const settingsDir = path2.dirname(CLAUDE_SETTINGS_PATH);
316
351
  fs2.mkdirSync(settingsDir, { recursive: true });
@@ -327,52 +362,37 @@ function injectClaudeCodeHook() {
327
362
  const hooks = settings.hooks;
328
363
  if (Array.isArray(hooks.Stop)) {
329
364
  const existing = hooks.Stop;
330
- const alreadyInstalled = existing.some(
365
+ const ourEntryIndex = existing.findIndex(
331
366
  (entry) => entry.hooks?.some((h) => h.command?.includes(HOOK_MARKER))
332
367
  );
333
- if (alreadyInstalled) {
334
- check("Claude Code hook already installed");
335
- } else {
336
- hooks.Stop.push({
337
- matcher: "*",
338
- hooks: [
339
- {
340
- type: "command",
341
- command: HOOK_COMMAND,
342
- timeout: 30
343
- }
344
- ]
345
- });
346
- }
347
- } else {
348
- hooks.Stop = [
349
- {
350
- matcher: "*",
351
- hooks: [
352
- {
353
- type: "command",
354
- command: HOOK_COMMAND,
355
- timeout: 30
356
- }
357
- ]
368
+ if (ourEntryIndex !== -1) {
369
+ const entry = existing[ourEntryIndex];
370
+ const hookIndex = entry.hooks.findIndex((h) => h.command?.includes(HOOK_MARKER));
371
+ const hook = entry.hooks[hookIndex];
372
+ if (hook.command === HOOK_COMMAND && hook.timeout === HOOK_TIMEOUT) {
373
+ check("Claude Code hook already installed");
374
+ return;
358
375
  }
359
- ];
360
- }
361
- if (!settings.permissions || typeof settings.permissions !== "object") {
362
- settings.permissions = {};
363
- }
364
- const permissions = settings.permissions;
365
- if (!Array.isArray(permissions.allow)) {
366
- permissions.allow = [];
376
+ hook.command = HOOK_COMMAND;
377
+ hook.timeout = HOOK_TIMEOUT;
378
+ fs2.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
379
+ check("Updated Claude Code hook to latest command");
380
+ return;
381
+ }
367
382
  }
368
- const allow = permissions.allow;
369
- const SKILL_PERMISSIONS2 = [
370
- `Write(${os2.homedir()}/.basestream/sessions/**)`,
371
- `Read(${os2.homedir()}/.basestream/sessions/**)`
372
- ];
373
- for (const rule of SKILL_PERMISSIONS2) {
374
- if (!allow.includes(rule)) allow.push(rule);
383
+ if (!Array.isArray(hooks.Stop)) {
384
+ hooks.Stop = [];
375
385
  }
386
+ hooks.Stop.push({
387
+ matcher: "*",
388
+ hooks: [
389
+ {
390
+ type: "command",
391
+ command: HOOK_COMMAND,
392
+ timeout: HOOK_TIMEOUT
393
+ }
394
+ ]
395
+ });
376
396
  fs2.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
377
397
  check("Injected tracking hook into ~/.claude/settings.json");
378
398
  }
@@ -386,6 +406,7 @@ async function init() {
386
406
  }
387
407
  console.log();
388
408
  injectClaudeCodeHook();
409
+ injectPermissions();
389
410
  installSkill();
390
411
  injectClaudeMdRule();
391
412
  ensureDirs();
@@ -393,9 +414,11 @@ async function init() {
393
414
  console.log();
394
415
  await login();
395
416
  console.log();
396
- console.log(
397
- ` ${c.dim("That's it. Work normally \u2014 every session is now tracked.")}`
398
- );
417
+ console.log(` ${c.dim("That's it. One last step:")}`);
418
+ console.log();
419
+ console.log(` ${c.bold("Restart Claude Code")} for the hook to take effect.`);
420
+ console.log();
421
+ console.log(` ${c.dim("After that, every session is automatically tracked.")}`);
399
422
  console.log();
400
423
  }
401
424
 
@@ -629,24 +652,34 @@ function categorizeWork(acc) {
629
652
  const files = Array.from(acc.filesWritten);
630
653
  const fileCount = files.length;
631
654
  let category = WorkCategory.OTHER;
632
- const testFiles = files.filter(
633
- (f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__")
634
- );
635
- const docFiles = files.filter(
636
- (f) => f.endsWith(".md") || f.includes("/docs/")
637
- );
638
- const configFiles = files.filter(
639
- (f) => f.includes("Dockerfile") || f.includes(".yml") || f.includes(".yaml") || f.includes("ci")
640
- );
641
- if (testFiles.length > fileCount / 2) category = WorkCategory.TESTING;
642
- else if (docFiles.length > fileCount / 2) category = WorkCategory.DOCS;
643
- else if (configFiles.length > fileCount / 2) category = WorkCategory.DEVOPS;
644
- else if (fileCount > 0) category = WorkCategory.FEATURE;
655
+ if (fileCount > 0) {
656
+ const testFiles = files.filter(
657
+ (f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__")
658
+ );
659
+ const docFiles = files.filter(
660
+ (f) => f.endsWith(".md") || f.includes("/docs/")
661
+ );
662
+ const configFiles = files.filter(
663
+ (f) => f.includes("Dockerfile") || f.includes(".yml") || f.includes(".yaml") || f.includes("ci")
664
+ );
665
+ if (testFiles.length > fileCount / 2) category = WorkCategory.TESTING;
666
+ else if (docFiles.length > fileCount / 2) category = WorkCategory.DOCS;
667
+ else if (configFiles.length > fileCount / 2) category = WorkCategory.DEVOPS;
668
+ else category = WorkCategory.FEATURE;
669
+ } else {
670
+ const tools = acc.toolCalls.map((t) => t.tool);
671
+ const writeTools = tools.filter(
672
+ (t) => ["Write", "Edit", "NotebookEdit"].includes(t)
673
+ );
674
+ const readTools = tools.filter(
675
+ (t) => ["Read", "Grep", "Glob", "WebFetch", "WebSearch"].includes(t)
676
+ );
677
+ if (writeTools.length > 0) category = WorkCategory.FEATURE;
678
+ else if (readTools.length > tools.length / 2) category = WorkCategory.REFACTOR;
679
+ }
645
680
  let complexity = Complexity.LOW;
646
- if (fileCount === 0) complexity = Complexity.LOW;
647
- else if (fileCount <= 2) complexity = Complexity.LOW;
648
- else if (fileCount <= 6) complexity = Complexity.MEDIUM;
649
- else complexity = Complexity.HIGH;
681
+ if (fileCount >= 7 || acc.toolCalls.length >= 30) complexity = Complexity.HIGH;
682
+ else if (fileCount >= 3 || acc.toolCalls.length >= 10) complexity = Complexity.MEDIUM;
650
683
  return { category, complexity };
651
684
  }
652
685
  function readSkillSummary(sessionId) {
@@ -706,24 +739,52 @@ function flushToBuffer(acc) {
706
739
  function buildSummary(acc, category) {
707
740
  const fileCount = acc.filesWritten.size;
708
741
  const commitCount = acc.commitShas.size;
742
+ const toolCount = acc.toolCalls.length;
743
+ const project = acc.projectName || "unknown project";
709
744
  const parts = [];
710
745
  if (category !== WorkCategory.OTHER) {
711
746
  parts.push(category.toLowerCase());
712
747
  }
713
748
  if (fileCount > 0) {
714
- parts.push(
715
- `${fileCount} file${fileCount !== 1 ? "s" : ""} modified`
716
- );
749
+ parts.push(`${fileCount} file${fileCount !== 1 ? "s" : ""} modified`);
717
750
  }
718
751
  if (commitCount > 0) {
719
- parts.push(
720
- `${commitCount} commit${commitCount !== 1 ? "s" : ""}`
721
- );
752
+ parts.push(`${commitCount} commit${commitCount !== 1 ? "s" : ""}`);
753
+ }
754
+ if (fileCount === 0 && commitCount === 0) {
755
+ if (toolCount > 0) {
756
+ const uniqueTools = [...new Set(acc.toolCalls.map((t) => t.tool))];
757
+ parts.push(`${acc.turns} turn${acc.turns !== 1 ? "s" : ""}, ${toolCount} tool call${toolCount !== 1 ? "s" : ""} (${uniqueTools.slice(0, 3).join(", ")})`);
758
+ } else {
759
+ parts.push(`${acc.turns} turn${acc.turns !== 1 ? "s" : ""} Q&A`);
760
+ }
722
761
  }
723
- if (acc.projectName) {
724
- parts.push(`in ${acc.projectName}`);
762
+ parts.push(`in ${project}`);
763
+ return parts.join(", ");
764
+ }
765
+ function makeLogger(sessionId) {
766
+ ensureDirs();
767
+ const logFile = path5.join(LOG_DIR, `${sessionId}.log`);
768
+ return (...args) => {
769
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.join(" ")}
770
+ `;
771
+ try {
772
+ fs5.appendFileSync(logFile, line);
773
+ } catch {
774
+ }
775
+ };
776
+ }
777
+ function pruneOldLogs() {
778
+ try {
779
+ const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
780
+ for (const file of fs5.readdirSync(LOG_DIR)) {
781
+ if (!file.endsWith(".log")) continue;
782
+ const full = path5.join(LOG_DIR, file);
783
+ const stat = fs5.statSync(full);
784
+ if (stat.mtimeMs < cutoff) fs5.unlinkSync(full);
785
+ }
786
+ } catch {
725
787
  }
726
- return parts.length > 0 ? parts.join(", ") : `Claude Code session in ${acc.projectName || "unknown project"}`;
727
788
  }
728
789
  async function hookStop() {
729
790
  let payload;
@@ -739,6 +800,9 @@ async function hookStop() {
739
800
  const { session_id, transcript_path, cwd } = payload;
740
801
  if (!session_id) process.exit(0);
741
802
  ensureDirs();
803
+ pruneOldLogs();
804
+ const log = makeLogger(session_id);
805
+ log("hook fired", "cwd=" + cwd);
742
806
  let acc = readSessionAccumulator(session_id);
743
807
  if (!acc) {
744
808
  const gitInfo = extractGitInfo(cwd);
@@ -755,22 +819,33 @@ async function hookStop() {
755
819
  gitRepo: gitInfo.repo,
756
820
  projectName: gitInfo.projectName
757
821
  };
822
+ log("new session");
758
823
  }
824
+ acc.turns = 0;
825
+ acc.toolCalls = [];
759
826
  if (transcript_path) {
760
827
  acc = analyzeTranscript(transcript_path, acc);
761
828
  }
762
829
  acc.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
830
+ log(`turns=${acc.turns} files=${acc.filesWritten.size} commits=${acc.commitShas.size}`);
763
831
  writeSessionAccumulator(acc);
764
- if (acc.filesWritten.size > 0 || acc.commitShas.size > 0 || acc.turns >= 3) {
832
+ if (acc.filesWritten.size > 0 || acc.commitShas.size > 0 || acc.turns >= 1) {
765
833
  flushToBuffer(acc);
766
834
  const config = readConfig();
767
835
  if (config?.apiKey) {
768
836
  try {
769
837
  const { syncEntries: syncEntries2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
770
838
  await syncEntries2(config);
771
- } catch {
839
+ log("synced");
840
+ process.stdout.write(JSON.stringify({ systemMessage: "basestream synced" }) + "\n");
841
+ } catch (e) {
842
+ log("sync error:", String(e));
772
843
  }
844
+ } else {
845
+ log("no apiKey, skipping sync");
773
846
  }
847
+ } else {
848
+ log("nothing to flush");
774
849
  }
775
850
  }
776
851
 
@@ -784,11 +859,12 @@ var CLAUDE_SETTINGS_PATH2 = path6.join(CLAUDE_DIR2, "settings.json");
784
859
  var CLAUDE_SKILLS_PATH2 = path6.join(CLAUDE_DIR2, "skills", "bs-summarize.md");
785
860
  var CLAUDE_MD_PATH2 = path6.join(CLAUDE_DIR2, "CLAUDE.md");
786
861
  var HOOK_MARKER2 = "_hook-stop";
787
- var CLAUDE_MD_MARKER2 = "<!-- basestream -->";
788
- var SKILL_PERMISSIONS = [
789
- `Write(${os4.homedir()}/.basestream/sessions/**)`,
790
- `Read(${os4.homedir()}/.basestream/sessions/**)`
862
+ var BASESTREAM_DIR3 = path6.join(os4.homedir(), ".basestream");
863
+ var REQUIRED_PERMISSIONS2 = [
864
+ `Write(${BASESTREAM_DIR3}/**)`,
865
+ `Read(${BASESTREAM_DIR3}/**)`
791
866
  ];
867
+ var CLAUDE_MD_BLOCK_RE = /\n?<!-- basestream -->[\s\S]*?<!-- \/basestream -->\n?/;
792
868
  function removeClaudeCodeHook() {
793
869
  if (!fs6.existsSync(CLAUDE_SETTINGS_PATH2)) {
794
870
  warn("No Claude Code settings found \u2014 nothing to remove");
@@ -818,7 +894,7 @@ function removeClaudeCodeHook() {
818
894
  if (permissions && Array.isArray(permissions.allow)) {
819
895
  const before = permissions.allow.length;
820
896
  permissions.allow = permissions.allow.filter(
821
- (rule) => !SKILL_PERMISSIONS.includes(rule)
897
+ (p) => !REQUIRED_PERMISSIONS2.includes(p)
822
898
  );
823
899
  if (permissions.allow.length !== before) {
824
900
  if (permissions.allow.length === 0) delete permissions.allow;
@@ -841,9 +917,9 @@ function removeSkill() {
841
917
  }
842
918
  function removeClaudeMdRule() {
843
919
  if (!fs6.existsSync(CLAUDE_MD_PATH2)) return;
844
- const existing = fs6.readFileSync(CLAUDE_MD_PATH2, "utf-8");
845
- if (!existing.includes(CLAUDE_MD_MARKER2)) return;
846
- const updated = existing.replace(/\n\n<!-- basestream -->[\s\S]*?<!-- \/basestream -->\n?/, "").replace(/<!-- basestream -->[\s\S]*?<!-- \/basestream -->\n?/, "").trimEnd();
920
+ const content = fs6.readFileSync(CLAUDE_MD_PATH2, "utf-8");
921
+ if (!CLAUDE_MD_BLOCK_RE.test(content)) return;
922
+ const updated = content.replace(CLAUDE_MD_BLOCK_RE, "\n").trimEnd();
847
923
  fs6.writeFileSync(CLAUDE_MD_PATH2, updated ? updated + "\n" : "");
848
924
  check("Removed Basestream rules from ~/.claude/CLAUDE.md");
849
925
  }
@@ -887,7 +963,7 @@ async function main() {
887
963
  process.exit(0);
888
964
  }
889
965
  if (command === "--version" || command === "-v") {
890
- console.log(true ? "0.2.5" : "dev");
966
+ console.log(true ? "0.2.7" : "dev");
891
967
  process.exit(0);
892
968
  }
893
969
  switch (command || "init") {
package/package.json CHANGED
@@ -18,5 +18,5 @@
18
18
  "unlink:cli": "npm unlink -g @basestream/cli"
19
19
  },
20
20
  "type": "module",
21
- "version": "0.2.5"
21
+ "version": "0.2.7"
22
22
  }