session-intelligence-cli 0.1.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/api.d.ts +1 -0
- package/dist/api.js +19 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +16 -0
- package/dist/git.d.ts +13 -0
- package/dist/git.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/report.d.ts +1 -0
- package/dist/report.js +86 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +61 -0
- package/dist/status.d.ts +1 -0
- package/dist/status.js +63 -0
- package/package.json +29 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sendEvent(payload: Record<string, unknown>): Promise<void>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readConfig } from "./config.js";
|
|
2
|
+
export async function sendEvent(payload) {
|
|
3
|
+
const config = readConfig();
|
|
4
|
+
if (!config)
|
|
5
|
+
return;
|
|
6
|
+
try {
|
|
7
|
+
await fetch(`${config.serverUrl}/api/events`, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify(payload),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Silent — don't block Claude Code
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".session-intelligence");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function readConfig() {
|
|
7
|
+
if (!existsSync(CONFIG_FILE))
|
|
8
|
+
return null;
|
|
9
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
export function writeConfig(config) {
|
|
13
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
14
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
15
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
16
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface GitState {
|
|
2
|
+
branch: string;
|
|
3
|
+
lastCommitSha: string;
|
|
4
|
+
lastCommitMessage: string;
|
|
5
|
+
hasUncommittedChanges: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface GitDiffStats {
|
|
8
|
+
filesChanged: number;
|
|
9
|
+
insertions: number;
|
|
10
|
+
deletions: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function readGitState(cwd: string): GitState | null;
|
|
13
|
+
export declare function readGitDiffStats(cwd: string): GitDiffStats;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
function run(cmd, cwd) {
|
|
3
|
+
try {
|
|
4
|
+
return execSync(cmd, { cwd, encoding: "utf-8", timeout: 5000 }).trim();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function readGitState(cwd) {
|
|
11
|
+
const branch = run("git rev-parse --abbrev-ref HEAD", cwd);
|
|
12
|
+
if (!branch)
|
|
13
|
+
return null;
|
|
14
|
+
const logLine = run("git log -1 --format=%H%n%s", cwd);
|
|
15
|
+
const [sha = "", ...msgParts] = logLine.split("\n");
|
|
16
|
+
return {
|
|
17
|
+
branch,
|
|
18
|
+
lastCommitSha: sha,
|
|
19
|
+
lastCommitMessage: msgParts.join("\n"),
|
|
20
|
+
hasUncommittedChanges: run("git status --porcelain", cwd).length > 0,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function readGitDiffStats(cwd) {
|
|
24
|
+
const stat = run("git diff --cached --shortstat", cwd) || run("git diff HEAD~1 --shortstat", cwd);
|
|
25
|
+
const files = stat.match(/(\d+) file/);
|
|
26
|
+
const ins = stat.match(/(\d+) insertion/);
|
|
27
|
+
const del = stat.match(/(\d+) deletion/);
|
|
28
|
+
return {
|
|
29
|
+
filesChanged: files ? parseInt(files[1]) : 0,
|
|
30
|
+
insertions: ins ? parseInt(ins[1]) : 0,
|
|
31
|
+
deletions: del ? parseInt(del[1]) : 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { report } from "./report.js";
|
|
3
|
+
import { setup } from "./setup.js";
|
|
4
|
+
import { status } from "./status.js";
|
|
5
|
+
const [, , command, ...args] = process.argv;
|
|
6
|
+
switch (command) {
|
|
7
|
+
case "report":
|
|
8
|
+
report(args[0] ?? "unknown").catch(() => process.exit(0));
|
|
9
|
+
break;
|
|
10
|
+
case "setup":
|
|
11
|
+
setup().catch((e) => {
|
|
12
|
+
console.error(e);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
15
|
+
break;
|
|
16
|
+
case "status":
|
|
17
|
+
status().catch((e) => {
|
|
18
|
+
console.error(e);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
break;
|
|
22
|
+
default:
|
|
23
|
+
console.log("Usage: session-intelligence <setup|report|status> [args]");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function report(eventType: string): Promise<void>;
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readGitState, readGitDiffStats } from "./git.js";
|
|
3
|
+
import { sendEvent } from "./api.js";
|
|
4
|
+
const EVENT_MAP = {
|
|
5
|
+
"session-start": "SessionStart",
|
|
6
|
+
"stop": "Stop",
|
|
7
|
+
"session-end": "SessionEnd",
|
|
8
|
+
"tool-use": "PostToolUse",
|
|
9
|
+
"notification": "Notification",
|
|
10
|
+
"subagent-stop": "SubagentStop",
|
|
11
|
+
};
|
|
12
|
+
function parseTranscriptTokens(transcriptPath) {
|
|
13
|
+
const zero = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_creation_tokens: 0 };
|
|
14
|
+
try {
|
|
15
|
+
const content = readFileSync(transcriptPath, "utf-8");
|
|
16
|
+
for (const line of content.split("\n")) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
const entry = JSON.parse(trimmed);
|
|
22
|
+
if (entry.type !== "assistant" || !entry.message?.usage)
|
|
23
|
+
continue;
|
|
24
|
+
const u = entry.message.usage;
|
|
25
|
+
zero.input_tokens += u.input_tokens ?? 0;
|
|
26
|
+
zero.output_tokens += u.output_tokens ?? 0;
|
|
27
|
+
zero.cache_read_tokens += u.cache_read_input_tokens ?? 0;
|
|
28
|
+
zero.cache_creation_tokens += u.cache_creation_input_tokens ?? 0;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// skip malformed lines
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// file not found or unreadable
|
|
37
|
+
}
|
|
38
|
+
return zero;
|
|
39
|
+
}
|
|
40
|
+
export async function report(eventType) {
|
|
41
|
+
let stdinData = {};
|
|
42
|
+
try {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
for await (const chunk of process.stdin) {
|
|
45
|
+
chunks.push(chunk);
|
|
46
|
+
}
|
|
47
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
48
|
+
if (raw)
|
|
49
|
+
stdinData = JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// No stdin or invalid JSON
|
|
53
|
+
}
|
|
54
|
+
const sessionId = stdinData.session_id ?? "";
|
|
55
|
+
const cwd = stdinData.cwd ?? process.cwd();
|
|
56
|
+
const payload = {
|
|
57
|
+
session_id: sessionId,
|
|
58
|
+
cwd,
|
|
59
|
+
hook_event_name: EVENT_MAP[eventType] ?? eventType,
|
|
60
|
+
model: stdinData.model,
|
|
61
|
+
tool_name: stdinData.tool_name,
|
|
62
|
+
};
|
|
63
|
+
if (eventType === "session-end") {
|
|
64
|
+
const transcriptPath = stdinData.transcript_path;
|
|
65
|
+
if (transcriptPath) {
|
|
66
|
+
payload.usage = parseTranscriptTokens(transcriptPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const git = readGitState(cwd);
|
|
70
|
+
if (git) {
|
|
71
|
+
const gitPayload = {
|
|
72
|
+
branch: git.branch,
|
|
73
|
+
lastCommitSha: git.lastCommitSha,
|
|
74
|
+
lastCommitMessage: git.lastCommitMessage,
|
|
75
|
+
hasUncommittedChanges: git.hasUncommittedChanges,
|
|
76
|
+
};
|
|
77
|
+
if (eventType === "stop" || eventType === "session-end") {
|
|
78
|
+
const stats = readGitDiffStats(cwd);
|
|
79
|
+
gitPayload.filesChanged = stats.filesChanged;
|
|
80
|
+
gitPayload.insertions = stats.insertions;
|
|
81
|
+
gitPayload.deletions = stats.deletions;
|
|
82
|
+
}
|
|
83
|
+
payload.git = gitPayload;
|
|
84
|
+
}
|
|
85
|
+
await sendEvent(payload);
|
|
86
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function setup(): Promise<void>;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { writeConfig } from "./config.js";
|
|
6
|
+
function prompt(question) {
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
rl.question(question, (answer) => {
|
|
10
|
+
rl.close();
|
|
11
|
+
resolve(answer.trim());
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const HOOKS_CONFIG = {
|
|
16
|
+
SessionStart: [{ type: "command", command: "npx session-intelligence-cli report session-start" }],
|
|
17
|
+
Stop: [{ type: "command", command: "npx session-intelligence-cli report stop" }],
|
|
18
|
+
PostToolUse: [{ type: "command", command: "npx session-intelligence-cli report tool-use", timeout: 3000 }],
|
|
19
|
+
Notification: [{ type: "command", command: "npx session-intelligence-cli report notification" }],
|
|
20
|
+
SubagentStop: [{ type: "command", command: "npx session-intelligence-cli report subagent-stop", timeout: 3000 }],
|
|
21
|
+
};
|
|
22
|
+
export async function setup() {
|
|
23
|
+
console.log("\n Session Intelligence — Setup\n");
|
|
24
|
+
const serverUrl = await prompt(" Server URL (e.g. https://your-app.vercel.app): ");
|
|
25
|
+
const apiKey = await prompt(" API Key: ");
|
|
26
|
+
writeConfig({ apiKey, serverUrl });
|
|
27
|
+
console.log("\n Config saved to ~/.session-intelligence/config.json");
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${serverUrl}/api/health`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new Error(`HTTP ${res.status}`);
|
|
35
|
+
console.log(" Server connection verified");
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.warn(` Warning: Could not reach server (${e}). Check URL and try again.`);
|
|
39
|
+
}
|
|
40
|
+
const claudeSettingsPath = join(homedir(), ".claude", "settings.json");
|
|
41
|
+
let settings = {};
|
|
42
|
+
if (existsSync(claudeSettingsPath)) {
|
|
43
|
+
settings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
const existingHooks = (settings.hooks ?? {});
|
|
46
|
+
for (const [event, handlers] of Object.entries(HOOKS_CONFIG)) {
|
|
47
|
+
const existing = existingHooks[event] ?? [];
|
|
48
|
+
const alreadyInstalled = existing.some((h) => typeof h === "object" &&
|
|
49
|
+
h !== null &&
|
|
50
|
+
"command" in h &&
|
|
51
|
+
h.command.includes("session-intelligence"));
|
|
52
|
+
if (!alreadyInstalled) {
|
|
53
|
+
existingHooks[event] = [...existing, ...handlers];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
settings.hooks = existingHooks;
|
|
57
|
+
mkdirSync(dirname(claudeSettingsPath), { recursive: true });
|
|
58
|
+
writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));
|
|
59
|
+
console.log(" Claude Code hooks installed to ~/.claude/settings.json");
|
|
60
|
+
console.log("\n Done! Your next Claude Code session will start reporting.\n");
|
|
61
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function status(): Promise<void>;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readConfig } from "./config.js";
|
|
2
|
+
const STATUS_DISPLAY = {
|
|
3
|
+
thinking: { icon: "\u25CF", label: "thinking", color: "\x1b[32m" },
|
|
4
|
+
needs_input: { icon: "\u25D0", label: "needs_input", color: "\x1b[33m" },
|
|
5
|
+
idle: { icon: "\u25CB", label: "idle", color: "\x1b[90m" },
|
|
6
|
+
archived: { icon: "\u25CB", label: "archived", color: "\x1b[90m" },
|
|
7
|
+
};
|
|
8
|
+
const RESET = "\x1b[0m";
|
|
9
|
+
const BOLD = "\x1b[1m";
|
|
10
|
+
const DIM = "\x1b[2m";
|
|
11
|
+
function timeAgo(timestamp) {
|
|
12
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
13
|
+
if (seconds < 60)
|
|
14
|
+
return `${seconds}s ago`;
|
|
15
|
+
const minutes = Math.floor(seconds / 60);
|
|
16
|
+
if (minutes < 60)
|
|
17
|
+
return `${minutes}m ago`;
|
|
18
|
+
const hours = Math.floor(minutes / 60);
|
|
19
|
+
if (hours < 24)
|
|
20
|
+
return `${hours}h ago`;
|
|
21
|
+
const days = Math.floor(hours / 24);
|
|
22
|
+
return `${days}d ago`;
|
|
23
|
+
}
|
|
24
|
+
export async function status() {
|
|
25
|
+
const config = readConfig();
|
|
26
|
+
if (!config) {
|
|
27
|
+
console.error("Not configured. Run: session-intelligence setup");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
let data;
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${config.serverUrl}/api/sessions`, {
|
|
33
|
+
headers: { Authorization: `Bearer ${config.apiKey}` },
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
console.error(`Server returned ${res.status}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
data = (await res.json());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
console.error("Could not reach server");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const { sessions, limited } = data;
|
|
46
|
+
console.log(`${BOLD}Sessions (${sessions.length})${RESET}`);
|
|
47
|
+
console.log();
|
|
48
|
+
if (sessions.length === 0) {
|
|
49
|
+
console.log(` ${DIM}No active sessions${RESET}`);
|
|
50
|
+
console.log();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const s of sessions) {
|
|
54
|
+
const display = STATUS_DISPLAY[s.status] ?? STATUS_DISPLAY.idle;
|
|
55
|
+
const branch = s.gitBranch ? `${DIM}${s.gitBranch}${RESET}` : "";
|
|
56
|
+
const ago = timeAgo(s.lastEventAt);
|
|
57
|
+
console.log(` ${display.color}${display.icon} ${display.label}${RESET} ${s.project} ${branch} ${DIM}${ago}${RESET}`);
|
|
58
|
+
}
|
|
59
|
+
if (limited) {
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(`${DIM}More sessions available — upgrade to Pro${RESET}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "session-intelligence-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI plugin for Session Intelligence — hooks into Claude Code to track sessions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/0xleal/personal-dashboard.git",
|
|
10
|
+
"directory": "cli"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"session-intelligence": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
}
|
|
29
|
+
}
|