Sign In

session-intelligence-cli

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

session-intelligence-cli - npm Package Compare versions

Comparing version
0.2.0
to
0.2.1
+2
-1
package.json
{
"name": "session-intelligence-cli",
"version": "0.2.0",
"version": "0.2.1",
"description": "Real-time dashboard for AI coding sessions — track activity, git changes, token usage, and session history across Claude Code and Cursor",

@@ -31,2 +31,3 @@ "type": "module",

"dist",
"!dist/__tests__",
"README.md"

@@ -33,0 +34,0 @@ ],

import { describe, it, expect, vi, beforeEach } from "vitest";
import { execSync } from "node:child_process";
import { readGitState, readGitDiffStats } from "../git.js";
vi.mock("node:child_process", () => ({
execSync: vi.fn(),
}));
const mockExecSync = vi.mocked(execSync);
beforeEach(() => {
mockExecSync.mockReset();
});
describe("readGitState", () => {
it("returns null when not in a git repo", () => {
mockExecSync.mockReturnValue("");
expect(readGitState("/tmp")).toBeNull();
});
it("returns null when git command throws", () => {
mockExecSync.mockImplementation(() => { throw new Error("not a git repo"); });
expect(readGitState("/tmp")).toBeNull();
});
it("parses git state correctly", () => {
mockExecSync.mockImplementation((cmd) => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --abbrev-ref"))
return "feat/login";
if (cmdStr.includes("git log -1"))
return "abc123\nfix login redirect";
if (cmdStr.includes("git status --porcelain"))
return " M src/login.ts";
return "";
});
const state = readGitState("/project");
expect(state).toEqual({
branch: "feat/login",
lastCommitSha: "abc123",
lastCommitMessage: "fix login redirect",
hasUncommittedChanges: true,
});
});
it("reports no uncommitted changes when porcelain is empty", () => {
mockExecSync.mockImplementation((cmd) => {
const cmdStr = String(cmd);
if (cmdStr.includes("rev-parse --abbrev-ref"))
return "main";
if (cmdStr.includes("git log -1"))
return "def456\ninitial commit";
if (cmdStr.includes("git status --porcelain"))
return "";
return "";
});
const state = readGitState("/project");
expect(state).not.toBeNull();
expect(state.hasUncommittedChanges).toBe(false);
});
});
describe("readGitDiffStats", () => {
it("parses shortstat output with all fields", () => {
mockExecSync.mockReturnValue(" 3 files changed, 45 insertions(+), 12 deletions(-)");
const stats = readGitDiffStats("/project");
expect(stats).toEqual({
filesChanged: 3,
insertions: 45,
deletions: 12,
});
});
it("handles insertions-only shortstat", () => {
mockExecSync.mockReturnValue(" 1 file changed, 10 insertions(+)");
const stats = readGitDiffStats("/project");
expect(stats).toEqual({
filesChanged: 1,
insertions: 10,
deletions: 0,
});
});
it("handles deletions-only shortstat", () => {
mockExecSync.mockReturnValue(" 2 files changed, 5 deletions(-)");
const stats = readGitDiffStats("/project");
expect(stats).toEqual({
filesChanged: 2,
insertions: 0,
deletions: 5,
});
});
it("returns zeros when no diff output", () => {
mockExecSync.mockReturnValue("");
const stats = readGitDiffStats("/project");
expect(stats).toEqual({
filesChanged: 0,
insertions: 0,
deletions: 0,
});
});
});
import { describe, it, expect } from "vitest";
import { buildCursorPayload, CURSOR_EVENT_MAP } from "../report-cursor.js";
describe("CURSOR_EVENT_MAP", () => {
it("maps all expected event types", () => {
expect(CURSOR_EVENT_MAP["session-start"]).toBe("SessionStart");
expect(CURSOR_EVENT_MAP["session-end"]).toBe("SessionEnd");
expect(CURSOR_EVENT_MAP["prompt"]).toBe("UserPromptSubmit");
expect(CURSOR_EVENT_MAP["tool-use"]).toBe("PostToolUse");
expect(CURSOR_EVENT_MAP["file-edit"]).toBe("PostToolUse");
expect(CURSOR_EVENT_MAP["stop"]).toBe("Stop");
expect(CURSOR_EVENT_MAP["subagent-stop"]).toBe("SubagentStop");
expect(CURSOR_EVENT_MAP["shell"]).toBe("PostToolUse");
});
});
describe("buildCursorPayload", () => {
it("maps conversation_id to session_id", () => {
const payload = buildCursorPayload("session-start", {
conversation_id: "conv-123",
}, "/home/user/project");
expect(payload.session_id).toBe("conv-123");
});
it("always sets source to cursor", () => {
const payload = buildCursorPayload("session-start", {}, "/tmp");
expect(payload.source).toBe("cursor");
});
it("uses provided cwd", () => {
const payload = buildCursorPayload("session-start", {}, "/home/user/project");
expect(payload.cwd).toBe("/home/user/project");
});
it("defaults session_id to empty string when missing", () => {
const payload = buildCursorPayload("session-start", {}, "/tmp");
expect(payload.session_id).toBe("");
});
it("passes through unknown event types", () => {
const payload = buildCursorPayload("unknown-event", {}, "/tmp");
expect(payload.hook_event_name).toBe("unknown-event");
});
describe("session-start", () => {
it("produces a SessionStart payload", () => {
const payload = buildCursorPayload("session-start", {
conversation_id: "abc",
}, "/home/user/project");
expect(payload).toEqual({
session_id: "abc",
cwd: "/home/user/project",
hook_event_name: "SessionStart",
source: "cursor",
});
});
});
describe("prompt", () => {
it("extracts prompt from input field", () => {
const payload = buildCursorPayload("prompt", {
conversation_id: "abc",
input: "fix the bug",
}, "/tmp");
expect(payload.hook_event_name).toBe("UserPromptSubmit");
expect(payload.prompt).toBe("fix the bug");
});
it("falls back to prompt field when input is missing", () => {
const payload = buildCursorPayload("prompt", {
conversation_id: "abc",
prompt: "fallback prompt",
}, "/tmp");
expect(payload.prompt).toBe("fallback prompt");
});
});
describe("tool-use", () => {
it("passes through tool_name and tool_input", () => {
const payload = buildCursorPayload("tool-use", {
conversation_id: "abc",
tool_name: "Read",
tool_input: { file_path: "/tmp/foo.ts" },
}, "/tmp");
expect(payload.hook_event_name).toBe("PostToolUse");
expect(payload.tool_name).toBe("Read");
expect(payload.tool_input).toEqual({ file_path: "/tmp/foo.ts" });
});
});
describe("file-edit", () => {
it("normalizes to Edit tool with file_path", () => {
const payload = buildCursorPayload("file-edit", {
conversation_id: "abc",
file_path: "/home/user/project/src/index.ts",
edits: [{ old_string: "foo", new_string: "bar" }],
}, "/tmp");
expect(payload.hook_event_name).toBe("PostToolUse");
expect(payload.tool_name).toBe("Edit");
expect(payload.tool_input).toEqual({ file_path: "/home/user/project/src/index.ts" });
});
it("omits tool_input when file_path is missing", () => {
const payload = buildCursorPayload("file-edit", {
conversation_id: "abc",
}, "/tmp");
expect(payload.tool_name).toBe("Edit");
expect(payload.tool_input).toBeUndefined();
});
});
describe("shell", () => {
it("normalizes to Bash tool with command", () => {
const payload = buildCursorPayload("shell", {
conversation_id: "abc",
command: "npm test",
}, "/tmp");
expect(payload.hook_event_name).toBe("PostToolUse");
expect(payload.tool_name).toBe("Bash");
expect(payload.tool_input).toEqual({ command: "npm test" });
});
it("omits tool_input when command is missing", () => {
const payload = buildCursorPayload("shell", {
conversation_id: "abc",
}, "/tmp");
expect(payload.tool_name).toBe("Bash");
expect(payload.tool_input).toBeUndefined();
});
});
describe("subagent-stop", () => {
it("extracts agent_type", () => {
const payload = buildCursorPayload("subagent-stop", {
conversation_id: "abc",
agent_type: "task",
}, "/tmp");
expect(payload.hook_event_name).toBe("SubagentStop");
expect(payload.agent_type).toBe("task");
});
});
describe("stop", () => {
it("produces a Stop payload", () => {
const payload = buildCursorPayload("stop", {
conversation_id: "abc",
}, "/tmp");
expect(payload.hook_event_name).toBe("Stop");
expect(payload.source).toBe("cursor");
});
});
describe("session-end", () => {
it("produces a SessionEnd payload without usage", () => {
const payload = buildCursorPayload("session-end", {
conversation_id: "abc",
}, "/tmp");
expect(payload.hook_event_name).toBe("SessionEnd");
expect(payload.usage).toBeUndefined();
});
});
});
import { describe, it, expect } from "vitest";
import { buildClaudePayload, CLAUDE_EVENT_MAP } from "../report.js";
describe("CLAUDE_EVENT_MAP", () => {
it("maps all expected event types", () => {
expect(CLAUDE_EVENT_MAP["session-start"]).toBe("SessionStart");
expect(CLAUDE_EVENT_MAP["stop"]).toBe("Stop");
expect(CLAUDE_EVENT_MAP["session-end"]).toBe("SessionEnd");
expect(CLAUDE_EVENT_MAP["tool-use"]).toBe("PostToolUse");
expect(CLAUDE_EVENT_MAP["notification"]).toBe("Notification");
expect(CLAUDE_EVENT_MAP["subagent-stop"]).toBe("SubagentStop");
});
});
describe("buildClaudePayload", () => {
it("maps session_id directly", () => {
const payload = buildClaudePayload("session-start", {
session_id: "sess-123",
}, "/home/user/project");
expect(payload.session_id).toBe("sess-123");
});
it("always sets source to claude-code", () => {
const payload = buildClaudePayload("session-start", {}, "/tmp");
expect(payload.source).toBe("claude-code");
});
it("defaults session_id to empty string when missing", () => {
const payload = buildClaudePayload("session-start", {}, "/tmp");
expect(payload.session_id).toBe("");
});
it("passes through model, tool_name, tool_input", () => {
const payload = buildClaudePayload("tool-use", {
session_id: "sess-1",
model: "claude-sonnet-4-5-20250514",
tool_name: "Edit",
tool_input: { file_path: "/tmp/foo.ts" },
}, "/tmp");
expect(payload.hook_event_name).toBe("PostToolUse");
expect(payload.model).toBe("claude-sonnet-4-5-20250514");
expect(payload.tool_name).toBe("Edit");
expect(payload.tool_input).toEqual({ file_path: "/tmp/foo.ts" });
});
it("passes through unknown event types", () => {
const payload = buildClaudePayload("unknown-event", {}, "/tmp");
expect(payload.hook_event_name).toBe("unknown-event");
});
it("produces correct session-start payload", () => {
const payload = buildClaudePayload("session-start", {
session_id: "sess-abc",
model: "claude-opus-4-6",
}, "/home/user/project");
expect(payload).toEqual({
session_id: "sess-abc",
cwd: "/home/user/project",
hook_event_name: "SessionStart",
source: "claude-code",
model: "claude-opus-4-6",
tool_name: undefined,
tool_input: undefined,
});
});
});
import { describe, it, expect } from "vitest";
import { writeFileSync, mkdirSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { parseTranscript, discoverTranscripts } from "../transcript.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const FIXTURE_PATH = join(__dirname, "fixtures", "transcript-full.jsonl");
function writeTempTranscript(lines) {
const dir = join(tmpdir(), `si-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(dir, { recursive: true });
const filePath = join(dir, "transcript.jsonl");
writeFileSync(filePath, lines.map((l) => JSON.stringify(l)).join("\n"));
return filePath;
}
function cleanup(filePath) {
rmSync(dirname(filePath), { recursive: true, force: true });
}
describe("parseTranscript", () => {
it("returns null for missing file", () => {
expect(parseTranscript("/nonexistent/path.jsonl")).toBeNull();
});
it("returns null for empty file", () => {
const fp = writeTempTranscript([]);
try {
expect(parseTranscript(fp)).toBeNull();
}
finally {
cleanup(fp);
}
});
it("returns null when fewer than 2 events are parsed", () => {
const fp = writeTempTranscript([
{
type: "user",
sessionId: "sess-1",
cwd: "/project",
timestamp: "2026-01-01T10:00:00Z",
message: { content: "hello" },
},
]);
try {
expect(parseTranscript(fp)).toBeNull();
}
finally {
cleanup(fp);
}
});
describe("with full realistic fixture", () => {
it("extracts session metadata", () => {
const result = parseTranscript(FIXTURE_PATH);
expect(result).not.toBeNull();
expect(result.session_id).toBe("test-session-001");
expect(result.cwd).toBe("/Users/testuser/projects/my-app");
expect(result.git_branch).toBe("feat/auth");
expect(result.model).toBe("claude-sonnet-4-5-20250514");
});
it("tracks correct time range", () => {
const result = parseTranscript(FIXTURE_PATH);
// First timestamped event is the progress at 10:00:01, last is the final progress at 10:01:10.500
expect(result.started_at).toBeLessThanOrEqual(new Date("2026-03-10T10:00:03.000Z").getTime());
expect(result.ended_at).toBeGreaterThanOrEqual(new Date("2026-03-10T10:01:10.000Z").getTime());
});
it("accumulates token usage across all assistant messages", () => {
const result = parseTranscript(FIXTURE_PATH);
// Sum of all input_tokens across assistant messages: 50+50+50+100+150+200+250+300+350 = 1500
expect(result.usage.input_tokens).toBe(1500);
// Sum of output_tokens: 10+20+30+25+40+35+15+20+15 = 210
expect(result.usage.output_tokens).toBe(210);
});
it("accumulates cache tokens using cache_read_input_tokens field", () => {
const result = parseTranscript(FIXTURE_PATH);
// cache_read_input_tokens: 500+500+500+2000+2500+3000+3500+4000+4500 = 21000
expect(result.usage.cache_read_tokens).toBe(21000);
// cache_creation_input_tokens: 1000+1000+1000+0+0+0+0+0+0 = 3000
expect(result.usage.cache_creation_tokens).toBe(3000);
});
it("skips file-history-snapshot entries", () => {
const result = parseTranscript(FIXTURE_PATH);
const hasSnapshot = result.events.some((e) => e.event_type === "file-history-snapshot");
expect(hasSnapshot).toBe(false);
});
it("skips progress entries", () => {
const result = parseTranscript(FIXTURE_PATH);
const hasProgress = result.events.some((e) => e.event_type === "progress");
expect(hasProgress).toBe(false);
});
it("skips system entries", () => {
const result = parseTranscript(FIXTURE_PATH);
const hasSystem = result.events.some((e) => e.event_type === "system");
expect(hasSystem).toBe(false);
});
it("extracts user prompts with string content", () => {
const result = parseTranscript(FIXTURE_PATH);
const prompts = result.events.filter((e) => e.event_type === "UserPromptSubmit" && e.prompt !== null);
expect(prompts.some((p) => p.prompt === "add authentication to the API endpoints")).toBe(true);
});
it("extracts user prompts with multi-block array content", () => {
const result = parseTranscript(FIXTURE_PATH);
const prompts = result.events.filter((e) => e.event_type === "UserPromptSubmit" && e.prompt !== null);
expect(prompts.some((p) => p.prompt === "looks good, can you also add rate limiting?")).toBe(true);
});
it("extracts Read tool with relativized file path", () => {
const result = parseTranscript(FIXTURE_PATH);
const readEvent = result.events.find((e) => e.tool_name === "Read");
expect(readEvent).toBeDefined();
expect(readEvent.file_paths).toEqual(["src/api/routes.ts"]);
});
it("extracts Edit tool with relativized file path", () => {
const result = parseTranscript(FIXTURE_PATH);
const editEvent = result.events.find((e) => e.tool_name === "Edit");
expect(editEvent).toBeDefined();
expect(editEvent.file_paths).toEqual(["src/api/routes.ts"]);
});
it("extracts Write tool with relativized file path", () => {
const result = parseTranscript(FIXTURE_PATH);
const writeEvent = result.events.find((e) => e.tool_name === "Write");
expect(writeEvent).toBeDefined();
expect(writeEvent.file_paths).toEqual(["src/middleware/auth.ts"]);
});
it("extracts Grep tool with relativized path", () => {
const result = parseTranscript(FIXTURE_PATH);
const grepEvent = result.events.find((e) => e.tool_name === "Grep");
expect(grepEvent).toBeDefined();
expect(grepEvent.file_paths).toEqual(["src"]);
});
it("extracts Glob tool with relativized path", () => {
const result = parseTranscript(FIXTURE_PATH);
const globEvent = result.events.find((e) => e.tool_name === "Glob");
expect(globEvent).toBeDefined();
expect(globEvent.file_paths).toEqual(["src/middleware"]);
});
it("extracts Bash command", () => {
const result = parseTranscript(FIXTURE_PATH);
const bashEvent = result.events.find((e) => e.tool_name === "Bash");
expect(bashEvent).toBeDefined();
expect(bashEvent.command).toBe("npm test");
});
it("does not extract events from thinking-only assistant blocks", () => {
const result = parseTranscript(FIXTURE_PATH);
// thinking blocks should not produce tool events
const thinkingTools = result.events.filter((e) => e.tool_name === "thinking");
expect(thinkingTools).toHaveLength(0);
});
it("does not extract events from text-only assistant blocks", () => {
const result = parseTranscript(FIXTURE_PATH);
// text-only assistant messages should not produce tool events
// but tool_use blocks from the same message chain should
const textTools = result.events.filter((e) => e.tool_name === "text");
expect(textTools).toHaveLength(0);
});
});
it("truncates prompts to 500 characters", () => {
const longPrompt = "x".repeat(600);
const fp = writeTempTranscript([
{
type: "user",
sessionId: "sess-1",
cwd: "/project",
timestamp: "2026-01-01T10:00:00Z",
message: { content: longPrompt },
},
{
type: "assistant",
timestamp: "2026-01-01T10:00:05Z",
message: {
model: "claude-sonnet-4-5-20250514",
content: [{ type: "tool_use", name: "Read", input: { file_path: "/project/foo.ts" } }],
},
},
]);
try {
const result = parseTranscript(fp);
const prompt = result.events.find((e) => e.prompt !== null);
expect(prompt.prompt.length).toBe(500);
}
finally {
cleanup(fp);
}
});
it("handles malformed JSON lines gracefully", () => {
const dir = join(tmpdir(), `si-test-malformed-${Date.now()}`);
mkdirSync(dir, { recursive: true });
const fp = join(dir, "transcript.jsonl");
const content = [
'{"type":"user","sessionId":"s1","cwd":"/p","timestamp":"2026-01-01T10:00:00Z","message":{"content":"hello"}}',
"this is not json",
"",
'{"type":"assistant","timestamp":"2026-01-01T10:00:05Z","message":{"model":"claude-sonnet-4-5-20250514","content":[{"type":"tool_use","name":"Read","input":{"file_path":"/p/foo.ts"}}]}}',
].join("\n");
writeFileSync(fp, content);
try {
const result = parseTranscript(fp);
expect(result).not.toBeNull();
expect(result.events.length).toBeGreaterThanOrEqual(2);
}
finally {
rmSync(dir, { recursive: true, force: true });
}
});
it("handles file paths not under cwd", () => {
const fp = writeTempTranscript([
{
type: "user",
sessionId: "sess-1",
cwd: "/project",
timestamp: "2026-01-01T10:00:00Z",
message: { content: "read this" },
},
{
type: "assistant",
timestamp: "2026-01-01T10:00:05Z",
message: {
model: "claude-sonnet-4-5-20250514",
content: [
{ type: "tool_use", name: "Read", input: { file_path: "/other/path/file.ts" } },
],
},
},
]);
try {
const result = parseTranscript(fp);
const readEvent = result.events.find((e) => e.tool_name === "Read");
expect(readEvent.file_paths).toEqual(["/other/path/file.ts"]);
}
finally {
cleanup(fp);
}
});
});
describe("discoverTranscripts", () => {
it("returns empty array for nonexistent directory", () => {
expect(discoverTranscripts("/nonexistent/path")).toEqual([]);
});
it("discovers jsonl files in project subdirectories", () => {
const base = join(tmpdir(), `si-discover-${Date.now()}`);
const projectDir = join(base, "my-project");
mkdirSync(projectDir, { recursive: true });
writeFileSync(join(projectDir, "session-1.jsonl"), "{}");
writeFileSync(join(projectDir, "session-2.jsonl"), "{}");
writeFileSync(join(projectDir, "notes.txt"), "not a transcript");
try {
const results = discoverTranscripts(base);
expect(results).toHaveLength(2);
expect(results.every((r) => r.endsWith(".jsonl"))).toBe(true);
}
finally {
rmSync(base, { recursive: true, force: true });
}
});
it("ignores non-directory entries", () => {
const base = join(tmpdir(), `si-discover-file-${Date.now()}`);
mkdirSync(base, { recursive: true });
writeFileSync(join(base, "not-a-dir.jsonl"), "{}");
try {
const results = discoverTranscripts(base);
expect(results).toHaveLength(0);
}
finally {
rmSync(base, { recursive: true, force: true });
}
});
});