Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ structured, typed tools.
`get_session`, `search_sessions`, `get_top_models`, `get_top_providers`,
`get_weekly_activity`

`get_overview` accepts an optional `directory` string — when provided, returns
aggregate stats for only that directory. Omit for all directories.

Register with OpenCode by adding to `.opencode/opencode.jsonc` in any project:

```json
Expand Down
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"compile": "deno compile --allow-read --allow-write --allow-env --allow-ffi --output ocv main.ts",
"compile-all": "deno compile --allow-read --allow-write --allow-env --allow-ffi --output ocv-x86_64-linux --target x86_64-unknown-linux-gnu main.ts && deno compile --allow-read --allow-write --allow-env --allow-ffi --output ocv-x86_64-macos --target x86_64-apple-darwin main.ts && deno compile --allow-read --allow-write --allow-env --allow-ffi --output ocv-aarch64-macos --target aarch64-apple-darwin main.ts && deno compile --allow-read --allow-write --allow-env --allow-ffi --output ocv-aarch64-linux --target aarch64-unknown-linux-gnu main.ts",
"start": "deno run --allow-read --allow-write --allow-env --allow-ffi --allow-net main.ts",
"check": "deno check main.ts"
"check": "deno check main.ts",
"test": "deno test --allow-read --allow-write --allow-env --allow-ffi"
},
"imports": {
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7",
Expand Down
14 changes: 11 additions & 3 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 29 additions & 4 deletions lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,12 @@ export async function getDbStats(
/**
* Group sessions by directory returning aggregate stats per directory.
*/
export function getDirectoryOverview(db: Database): DirectoryOverviewRow[] {
return convertRow(
db.prepare(`
export function getDirectoryOverview(
db: Database,
directory?: string,
): DirectoryOverviewRow[] {
const sql = directory
? `
SELECT
directory,
COUNT(*) AS total,
Expand All @@ -287,9 +290,31 @@ export function getDirectoryOverview(db: Database): DirectoryOverviewRow[] {
COALESCE(SUM(cost), 0) AS cost,
MAX(time_created) AS last_active
FROM session
WHERE directory = ?
GROUP BY directory
ORDER BY total DESC
`).all(),
`
: `
SELECT
directory,
COUNT(*) AS total,
SUM(CASE WHEN time_archived IS NULL THEN 1 ELSE 0 END) AS active,
SUM(CASE WHEN time_archived IS NOT NULL THEN 1 ELSE 0 END) AS archived,
SUM(CASE WHEN parent_id IS NULL THEN 1 ELSE 0 END) AS main_count,
SUM(CASE WHEN parent_id IS NOT NULL THEN 1 ELSE 0 END) AS sub_count,
COALESCE(SUM(tokens_input), 0) AS tokens_input,
COALESCE(SUM(tokens_output), 0) AS tokens_output,
COALESCE(SUM(tokens_reasoning), 0) AS tokens_reasoning,
COALESCE(SUM(tokens_cache_read), 0) AS tokens_cache_read,
COALESCE(SUM(tokens_cache_write), 0) AS tokens_cache_write,
COALESCE(SUM(cost), 0) AS cost,
MAX(time_created) AS last_active
FROM session
GROUP BY directory
ORDER BY total DESC
`;
return convertRow(
directory ? db.prepare(sql).all(directory) : db.prepare(sql).all(),
) as DirectoryOverviewRow[];
}

Expand Down
155 changes: 155 additions & 0 deletions lib/db_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// deno-lint-ignore-file no-import-prefix

import { Database } from "@db/sqlite";
import { assertAlmostEquals, assertEquals } from "jsr:@std/assert@^1.0.0";
import { getDirectoryOverview } from "./db.ts";

function createTestDb(): Database {
const db = new Database(":memory:", { int64: true });
db.exec(`
CREATE TABLE session (
id TEXT PRIMARY KEY,
directory TEXT NOT NULL,
parent_id TEXT,
time_archived INTEGER,
tokens_input INTEGER DEFAULT 0,
tokens_output INTEGER DEFAULT 0,
tokens_reasoning INTEGER DEFAULT 0,
tokens_cache_read INTEGER DEFAULT 0,
tokens_cache_write INTEGER DEFAULT 0,
cost REAL DEFAULT 0,
time_created INTEGER
)
`);

// dir-a: 2 sessions (1 main + 1 sub), both active
db.prepare(
`INSERT INTO session (id, directory, parent_id, time_archived, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, cost, time_created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
"s1",
"dir-a",
null,
null,
100,
50,
10,
20,
5,
0.0015,
1000,
);

db.prepare(
`INSERT INTO session (id, directory, parent_id, time_archived, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, cost, time_created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
"s2",
"dir-a",
"s1",
null,
200,
100,
20,
40,
10,
0.003,
2000,
);

// dir-b: 1 session (main), archived
db.prepare(
`INSERT INTO session (id, directory, parent_id, time_archived, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, cost, time_created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
"s3",
"dir-b",
null,
5000,
50,
25,
5,
10,
2,
0.00075,
3000,
);

return db;
}

Deno.test("returns all directories when no filter", () => {
const db = createTestDb();
try {
const rows = getDirectoryOverview(db);

assertEquals(rows.length, 2);

const row0 = rows[0];
assertEquals(row0.directory, "dir-a");
assertEquals(row0.total, 2);
assertEquals(row0.active, 2);
assertEquals(row0.archived, 0);
assertEquals(row0.main_count, 1);
assertEquals(row0.sub_count, 1);
assertEquals(row0.tokens_input, 300);
assertEquals(row0.tokens_output, 150);
assertEquals(row0.tokens_reasoning, 30);
assertEquals(row0.tokens_cache_read, 60);
assertEquals(row0.tokens_cache_write, 15);
assertAlmostEquals(row0.cost, 0.0045);
assertEquals(row0.last_active, 2000);

const row1 = rows[1];
assertEquals(row1.directory, "dir-b");
assertEquals(row1.total, 1);
assertEquals(row1.active, 0);
assertEquals(row1.archived, 1);
assertEquals(row1.main_count, 1);
assertEquals(row1.sub_count, 0);
assertEquals(row1.tokens_input, 50);
assertEquals(row1.tokens_output, 25);
assertEquals(row1.tokens_reasoning, 5);
assertEquals(row1.tokens_cache_read, 10);
assertEquals(row1.tokens_cache_write, 2);
assertAlmostEquals(row1.cost, 0.00075);
assertEquals(row1.last_active, 3000);
} finally {
db.close();
}
});

Deno.test("filters to one directory when filter provided", () => {
const db = createTestDb();
try {
const rows = getDirectoryOverview(db, "dir-a");

assertEquals(rows.length, 1);
const row = rows[0];
assertEquals(row.directory, "dir-a");
assertEquals(row.total, 2);
assertEquals(row.active, 2);
assertEquals(row.archived, 0);
assertEquals(row.main_count, 1);
assertEquals(row.sub_count, 1);
assertEquals(row.tokens_input, 300);
assertEquals(row.tokens_output, 150);
assertEquals(row.tokens_reasoning, 30);
assertEquals(row.tokens_cache_read, 60);
assertEquals(row.tokens_cache_write, 15);
assertAlmostEquals(row.cost, 0.0045);
assertEquals(row.last_active, 2000);
} finally {
db.close();
}
});

Deno.test("returns empty for non-existent directory", () => {
const db = createTestDb();
try {
const rows = getDirectoryOverview(db, "__nonexistent__");
assertEquals(rows.length, 0);
} finally {
db.close();
}
});
12 changes: 10 additions & 2 deletions lib/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ export function createMcpServer(dbPath: string): Server {
description: "Get per-directory session overview with aggregate stats",
inputSchema: {
type: "object",
properties: {},
properties: {
directory: {
type: "string",
description: "Optional. Filter to one directory. Omit for all.",
},
},
required: [],
},
},
Expand Down Expand Up @@ -153,7 +158,10 @@ export function createMcpServer(dbPath: string): Server {
result = await getDbStats(db, dbPath);
break;
case "get_overview":
result = getDirectoryOverview(db);
result = getDirectoryOverview(
db,
typeof args?.directory === "string" ? args.directory : undefined,
);
break;
case "list_sessions":
if (!args?.directory || typeof args.directory !== "string") {
Expand Down
2 changes: 1 addition & 1 deletion version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '1.4.0';
export const VERSION = "1.4.0";