@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.
- package/dist/cli.mjs +160 -84
- 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
|
|
365
|
+
const ourEntryIndex = existing.findIndex(
|
|
331
366
|
(entry) => entry.hooks?.some((h) => h.command?.includes(HOOK_MARKER))
|
|
332
367
|
);
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
hooks
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
647
|
-
else if (fileCount
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
|
|
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 >=
|
|
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
|
-
|
|
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
|
|
788
|
-
var
|
|
789
|
-
`Write(${
|
|
790
|
-
`Read(${
|
|
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
|
-
(
|
|
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
|
|
845
|
-
if (!
|
|
846
|
-
const updated =
|
|
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.
|
|
966
|
+
console.log(true ? "0.2.7" : "dev");
|
|
891
967
|
process.exit(0);
|
|
892
968
|
}
|
|
893
969
|
switch (command || "init") {
|
package/package.json
CHANGED