@basestream/cli 0.2.6 → 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 +119 -81
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -252,8 +252,9 @@ var CLAUDE_DIR = path2.join(os2.homedir(), ".claude");
252
252
  var CLAUDE_SETTINGS_PATH = path2.join(CLAUDE_DIR, "settings.json");
253
253
  var CLAUDE_SKILLS_PATH = path2.join(CLAUDE_DIR, "skills", "bs-summarize.md");
254
254
  var CLAUDE_MD_PATH = path2.join(CLAUDE_DIR, "CLAUDE.md");
255
- var HOOK_COMMAND = "npx @basestream/cli _hook-stop";
255
+ var HOOK_COMMAND = "npx --yes @basestream/cli@latest _hook-stop";
256
256
  var HOOK_MARKER = "_hook-stop";
257
+ var HOOK_TIMEOUT = 60;
257
258
  var CLAUDE_MD_MARKER = "<!-- basestream -->";
258
259
  var CLAUDE_MD_RULE = `${CLAUDE_MD_MARKER}
259
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.
@@ -276,6 +277,11 @@ Do not just append \u2014 synthesize everything into a single cohesive summary.
276
277
 
277
278
  Write the result to \`~/.basestream/sessions/\${CLAUDE_SESSION_ID}-summary.md\`, overwriting the file.
278
279
  `;
280
+ var BASESTREAM_DIR2 = path2.join(os2.homedir(), ".basestream");
281
+ var REQUIRED_PERMISSIONS = [
282
+ `Write(${BASESTREAM_DIR2}/**)`,
283
+ `Read(${BASESTREAM_DIR2}/**)`
284
+ ];
279
285
  function injectClaudeMdRule() {
280
286
  let existing = "";
281
287
  if (fs2.existsSync(CLAUDE_MD_PATH)) {
@@ -313,6 +319,33 @@ function detectClaudeCode() {
313
319
  return null;
314
320
  }
315
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
+ }
316
349
  function injectClaudeCodeHook() {
317
350
  const settingsDir = path2.dirname(CLAUDE_SETTINGS_PATH);
318
351
  fs2.mkdirSync(settingsDir, { recursive: true });
@@ -329,52 +362,37 @@ function injectClaudeCodeHook() {
329
362
  const hooks = settings.hooks;
330
363
  if (Array.isArray(hooks.Stop)) {
331
364
  const existing = hooks.Stop;
332
- const alreadyInstalled = existing.some(
365
+ const ourEntryIndex = existing.findIndex(
333
366
  (entry) => entry.hooks?.some((h) => h.command?.includes(HOOK_MARKER))
334
367
  );
335
- if (alreadyInstalled) {
336
- check("Claude Code hook already installed");
337
- } else {
338
- hooks.Stop.push({
339
- matcher: "*",
340
- hooks: [
341
- {
342
- type: "command",
343
- command: HOOK_COMMAND,
344
- timeout: 30
345
- }
346
- ]
347
- });
348
- }
349
- } else {
350
- hooks.Stop = [
351
- {
352
- matcher: "*",
353
- hooks: [
354
- {
355
- type: "command",
356
- command: HOOK_COMMAND,
357
- timeout: 30
358
- }
359
- ]
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;
360
375
  }
361
- ];
362
- }
363
- if (!settings.permissions || typeof settings.permissions !== "object") {
364
- settings.permissions = {};
365
- }
366
- const permissions = settings.permissions;
367
- if (!Array.isArray(permissions.allow)) {
368
- 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
+ }
369
382
  }
370
- const allow = permissions.allow;
371
- const SKILL_PERMISSIONS2 = [
372
- `Write(${os2.homedir()}/.basestream/sessions/**)`,
373
- `Read(${os2.homedir()}/.basestream/sessions/**)`
374
- ];
375
- for (const rule of SKILL_PERMISSIONS2) {
376
- if (!allow.includes(rule)) allow.push(rule);
383
+ if (!Array.isArray(hooks.Stop)) {
384
+ hooks.Stop = [];
377
385
  }
386
+ hooks.Stop.push({
387
+ matcher: "*",
388
+ hooks: [
389
+ {
390
+ type: "command",
391
+ command: HOOK_COMMAND,
392
+ timeout: HOOK_TIMEOUT
393
+ }
394
+ ]
395
+ });
378
396
  fs2.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
379
397
  check("Injected tracking hook into ~/.claude/settings.json");
380
398
  }
@@ -388,6 +406,7 @@ async function init() {
388
406
  }
389
407
  console.log();
390
408
  injectClaudeCodeHook();
409
+ injectPermissions();
391
410
  installSkill();
392
411
  injectClaudeMdRule();
393
412
  ensureDirs();
@@ -395,9 +414,11 @@ async function init() {
395
414
  console.log();
396
415
  await login();
397
416
  console.log();
398
- console.log(
399
- ` ${c.dim("That's it. Work normally \u2014 every session is now tracked.")}`
400
- );
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.")}`);
401
422
  console.log();
402
423
  }
403
424
 
@@ -631,24 +652,34 @@ function categorizeWork(acc) {
631
652
  const files = Array.from(acc.filesWritten);
632
653
  const fileCount = files.length;
633
654
  let category = WorkCategory.OTHER;
634
- const testFiles = files.filter(
635
- (f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__")
636
- );
637
- const docFiles = files.filter(
638
- (f) => f.endsWith(".md") || f.includes("/docs/")
639
- );
640
- const configFiles = files.filter(
641
- (f) => f.includes("Dockerfile") || f.includes(".yml") || f.includes(".yaml") || f.includes("ci")
642
- );
643
- if (testFiles.length > fileCount / 2) category = WorkCategory.TESTING;
644
- else if (docFiles.length > fileCount / 2) category = WorkCategory.DOCS;
645
- else if (configFiles.length > fileCount / 2) category = WorkCategory.DEVOPS;
646
- 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
+ }
647
680
  let complexity = Complexity.LOW;
648
- if (fileCount === 0) complexity = Complexity.LOW;
649
- else if (fileCount <= 2) complexity = Complexity.LOW;
650
- else if (fileCount <= 6) complexity = Complexity.MEDIUM;
651
- 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;
652
683
  return { category, complexity };
653
684
  }
654
685
  function readSkillSummary(sessionId) {
@@ -708,24 +739,28 @@ function flushToBuffer(acc) {
708
739
  function buildSummary(acc, category) {
709
740
  const fileCount = acc.filesWritten.size;
710
741
  const commitCount = acc.commitShas.size;
742
+ const toolCount = acc.toolCalls.length;
743
+ const project = acc.projectName || "unknown project";
711
744
  const parts = [];
712
745
  if (category !== WorkCategory.OTHER) {
713
746
  parts.push(category.toLowerCase());
714
747
  }
715
748
  if (fileCount > 0) {
716
- parts.push(
717
- `${fileCount} file${fileCount !== 1 ? "s" : ""} modified`
718
- );
749
+ parts.push(`${fileCount} file${fileCount !== 1 ? "s" : ""} modified`);
719
750
  }
720
751
  if (commitCount > 0) {
721
- parts.push(
722
- `${commitCount} commit${commitCount !== 1 ? "s" : ""}`
723
- );
752
+ parts.push(`${commitCount} commit${commitCount !== 1 ? "s" : ""}`);
724
753
  }
725
- if (acc.projectName) {
726
- parts.push(`in ${acc.projectName}`);
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
+ }
727
761
  }
728
- return parts.length > 0 ? parts.join(", ") : `Claude Code session in ${acc.projectName || "unknown project"}`;
762
+ parts.push(`in ${project}`);
763
+ return parts.join(", ");
729
764
  }
730
765
  function makeLogger(sessionId) {
731
766
  ensureDirs();
@@ -786,13 +821,15 @@ async function hookStop() {
786
821
  };
787
822
  log("new session");
788
823
  }
824
+ acc.turns = 0;
825
+ acc.toolCalls = [];
789
826
  if (transcript_path) {
790
827
  acc = analyzeTranscript(transcript_path, acc);
791
828
  }
792
829
  acc.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
793
830
  log(`turns=${acc.turns} files=${acc.filesWritten.size} commits=${acc.commitShas.size}`);
794
831
  writeSessionAccumulator(acc);
795
- 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) {
796
833
  flushToBuffer(acc);
797
834
  const config = readConfig();
798
835
  if (config?.apiKey) {
@@ -822,11 +859,12 @@ var CLAUDE_SETTINGS_PATH2 = path6.join(CLAUDE_DIR2, "settings.json");
822
859
  var CLAUDE_SKILLS_PATH2 = path6.join(CLAUDE_DIR2, "skills", "bs-summarize.md");
823
860
  var CLAUDE_MD_PATH2 = path6.join(CLAUDE_DIR2, "CLAUDE.md");
824
861
  var HOOK_MARKER2 = "_hook-stop";
825
- var CLAUDE_MD_MARKER2 = "<!-- basestream -->";
826
- var SKILL_PERMISSIONS = [
827
- `Write(${os4.homedir()}/.basestream/sessions/**)`,
828
- `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}/**)`
829
866
  ];
867
+ var CLAUDE_MD_BLOCK_RE = /\n?<!-- basestream -->[\s\S]*?<!-- \/basestream -->\n?/;
830
868
  function removeClaudeCodeHook() {
831
869
  if (!fs6.existsSync(CLAUDE_SETTINGS_PATH2)) {
832
870
  warn("No Claude Code settings found \u2014 nothing to remove");
@@ -856,7 +894,7 @@ function removeClaudeCodeHook() {
856
894
  if (permissions && Array.isArray(permissions.allow)) {
857
895
  const before = permissions.allow.length;
858
896
  permissions.allow = permissions.allow.filter(
859
- (rule) => !SKILL_PERMISSIONS.includes(rule)
897
+ (p) => !REQUIRED_PERMISSIONS2.includes(p)
860
898
  );
861
899
  if (permissions.allow.length !== before) {
862
900
  if (permissions.allow.length === 0) delete permissions.allow;
@@ -879,9 +917,9 @@ function removeSkill() {
879
917
  }
880
918
  function removeClaudeMdRule() {
881
919
  if (!fs6.existsSync(CLAUDE_MD_PATH2)) return;
882
- const existing = fs6.readFileSync(CLAUDE_MD_PATH2, "utf-8");
883
- if (!existing.includes(CLAUDE_MD_MARKER2)) return;
884
- 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();
885
923
  fs6.writeFileSync(CLAUDE_MD_PATH2, updated ? updated + "\n" : "");
886
924
  check("Removed Basestream rules from ~/.claude/CLAUDE.md");
887
925
  }
@@ -925,7 +963,7 @@ async function main() {
925
963
  process.exit(0);
926
964
  }
927
965
  if (command === "--version" || command === "-v") {
928
- console.log(true ? "0.2.6" : "dev");
966
+ console.log(true ? "0.2.7" : "dev");
929
967
  process.exit(0);
930
968
  }
931
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.6"
21
+ "version": "0.2.7"
22
22
  }