codesession-cli 1.0.0 → 1.2.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/README.md +179 -119
- package/dist/agents.d.ts +151 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +305 -0
- package/dist/agents.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -2
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +210 -50
- package/dist/index.js.map +1 -1
- package/package.json +30 -5
- package/skills/codesession/SKILL.md +97 -0
- package/src/db.ts +0 -239
- package/src/formatters.ts +0 -152
- package/src/git.ts +0 -44
- package/src/index.ts +0 -206
- package/src/types.ts +0 -49
- package/src/watcher.ts +0 -52
- package/test.ts +0 -4
- package/tsconfig.json +0 -20
package/src/db.ts
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import Database from 'better-sqlite3';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
5
|
-
import { Session, FileChange, Commit, AIUsage, SessionStats } from './types';
|
|
6
|
-
|
|
7
|
-
const DB_DIR = join(homedir(), '.devsession');
|
|
8
|
-
const DB_PATH = join(DB_DIR, 'sessions.db');
|
|
9
|
-
|
|
10
|
-
if (!existsSync(DB_DIR)) {
|
|
11
|
-
mkdirSync(DB_DIR, { recursive: true });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const db = new Database(DB_PATH);
|
|
15
|
-
|
|
16
|
-
// Initialize database
|
|
17
|
-
db.exec(`
|
|
18
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
19
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
20
|
-
name TEXT NOT NULL,
|
|
21
|
-
start_time TEXT NOT NULL,
|
|
22
|
-
end_time TEXT,
|
|
23
|
-
duration INTEGER,
|
|
24
|
-
working_directory TEXT NOT NULL,
|
|
25
|
-
files_changed INTEGER DEFAULT 0,
|
|
26
|
-
commits INTEGER DEFAULT 0,
|
|
27
|
-
ai_cost REAL DEFAULT 0,
|
|
28
|
-
ai_tokens INTEGER DEFAULT 0,
|
|
29
|
-
notes TEXT,
|
|
30
|
-
status TEXT DEFAULT 'active'
|
|
31
|
-
)
|
|
32
|
-
`);
|
|
33
|
-
|
|
34
|
-
db.exec(`
|
|
35
|
-
CREATE TABLE IF NOT EXISTS file_changes (
|
|
36
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
-
session_id INTEGER NOT NULL,
|
|
38
|
-
file_path TEXT NOT NULL,
|
|
39
|
-
change_type TEXT NOT NULL,
|
|
40
|
-
timestamp TEXT NOT NULL,
|
|
41
|
-
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
42
|
-
)
|
|
43
|
-
`);
|
|
44
|
-
|
|
45
|
-
db.exec(`
|
|
46
|
-
CREATE TABLE IF NOT EXISTS commits (
|
|
47
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
|
-
session_id INTEGER NOT NULL,
|
|
49
|
-
hash TEXT NOT NULL,
|
|
50
|
-
message TEXT NOT NULL,
|
|
51
|
-
timestamp TEXT NOT NULL,
|
|
52
|
-
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
53
|
-
)
|
|
54
|
-
`);
|
|
55
|
-
|
|
56
|
-
db.exec(`
|
|
57
|
-
CREATE TABLE IF NOT EXISTS ai_usage (
|
|
58
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
-
session_id INTEGER NOT NULL,
|
|
60
|
-
provider TEXT NOT NULL,
|
|
61
|
-
model TEXT NOT NULL,
|
|
62
|
-
tokens INTEGER NOT NULL,
|
|
63
|
-
cost REAL NOT NULL,
|
|
64
|
-
timestamp TEXT NOT NULL,
|
|
65
|
-
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
66
|
-
)
|
|
67
|
-
`);
|
|
68
|
-
|
|
69
|
-
export function createSession(session: Omit<Session, 'id'>): number {
|
|
70
|
-
const stmt = db.prepare(`
|
|
71
|
-
INSERT INTO sessions (name, start_time, working_directory, status)
|
|
72
|
-
VALUES (?, ?, ?, ?)
|
|
73
|
-
`);
|
|
74
|
-
const result = stmt.run(session.name, session.startTime, session.workingDirectory, 'active');
|
|
75
|
-
return result.lastInsertRowid as number;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function getActiveSession(): Session | null {
|
|
79
|
-
const stmt = db.prepare('SELECT * FROM sessions WHERE status = ? ORDER BY id DESC LIMIT 1');
|
|
80
|
-
const row = stmt.get('active') as any;
|
|
81
|
-
if (!row) return null;
|
|
82
|
-
return mapSession(row);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function endSession(sessionId: number, endTime: string, notes?: string): void {
|
|
86
|
-
const session = getSession(sessionId);
|
|
87
|
-
if (!session) return;
|
|
88
|
-
|
|
89
|
-
const duration = Math.floor((new Date(endTime).getTime() - new Date(session.startTime).getTime()) / 1000);
|
|
90
|
-
|
|
91
|
-
const stmt = db.prepare(`
|
|
92
|
-
UPDATE sessions
|
|
93
|
-
SET end_time = ?, duration = ?, status = ?, notes = ?
|
|
94
|
-
WHERE id = ?
|
|
95
|
-
`);
|
|
96
|
-
stmt.run(endTime, duration, 'completed', notes || null, sessionId);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function getSession(sessionId: number): Session | null {
|
|
100
|
-
const stmt = db.prepare('SELECT * FROM sessions WHERE id = ?');
|
|
101
|
-
const row = stmt.get(sessionId) as any;
|
|
102
|
-
return row ? mapSession(row) : null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function getSessions(limit = 10): Session[] {
|
|
106
|
-
const stmt = db.prepare('SELECT * FROM sessions ORDER BY start_time DESC LIMIT ?');
|
|
107
|
-
const rows = stmt.all(limit) as any[];
|
|
108
|
-
return rows.map(mapSession);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function addFileChange(change: Omit<FileChange, 'id'>): void {
|
|
112
|
-
const stmt = db.prepare(`
|
|
113
|
-
INSERT INTO file_changes (session_id, file_path, change_type, timestamp)
|
|
114
|
-
VALUES (?, ?, ?, ?)
|
|
115
|
-
`);
|
|
116
|
-
stmt.run(change.sessionId, change.filePath, change.changeType, change.timestamp);
|
|
117
|
-
|
|
118
|
-
// Update session files count
|
|
119
|
-
const countStmt = db.prepare('SELECT COUNT(DISTINCT file_path) as count FROM file_changes WHERE session_id = ?');
|
|
120
|
-
const result = countStmt.get(change.sessionId) as any;
|
|
121
|
-
|
|
122
|
-
const updateStmt = db.prepare('UPDATE sessions SET files_changed = ? WHERE id = ?');
|
|
123
|
-
updateStmt.run(result.count, change.sessionId);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function addCommit(commit: Omit<Commit, 'id'>): void {
|
|
127
|
-
const stmt = db.prepare(`
|
|
128
|
-
INSERT INTO commits (session_id, hash, message, timestamp)
|
|
129
|
-
VALUES (?, ?, ?, ?)
|
|
130
|
-
`);
|
|
131
|
-
stmt.run(commit.sessionId, commit.hash, commit.message, commit.timestamp);
|
|
132
|
-
|
|
133
|
-
// Update session commits count
|
|
134
|
-
const countStmt = db.prepare('SELECT COUNT(*) as count FROM commits WHERE session_id = ?');
|
|
135
|
-
const result = countStmt.get(commit.sessionId) as any;
|
|
136
|
-
|
|
137
|
-
const updateStmt = db.prepare('UPDATE sessions SET commits = ? WHERE id = ?');
|
|
138
|
-
updateStmt.run(result.count, commit.sessionId);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function addAIUsage(usage: Omit<AIUsage, 'id'>): void {
|
|
142
|
-
const stmt = db.prepare(`
|
|
143
|
-
INSERT INTO ai_usage (session_id, provider, model, tokens, cost, timestamp)
|
|
144
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
145
|
-
`);
|
|
146
|
-
stmt.run(usage.sessionId, usage.provider, usage.model, usage.tokens, usage.cost, usage.timestamp);
|
|
147
|
-
|
|
148
|
-
// Update session AI totals
|
|
149
|
-
const sumStmt = db.prepare(`
|
|
150
|
-
SELECT SUM(cost) as total_cost, SUM(tokens) as total_tokens
|
|
151
|
-
FROM ai_usage WHERE session_id = ?
|
|
152
|
-
`);
|
|
153
|
-
const result = sumStmt.get(usage.sessionId) as any;
|
|
154
|
-
|
|
155
|
-
const updateStmt = db.prepare('UPDATE sessions SET ai_cost = ?, ai_tokens = ? WHERE id = ?');
|
|
156
|
-
updateStmt.run(result.total_cost || 0, result.total_tokens || 0, usage.sessionId);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function getFileChanges(sessionId: number): FileChange[] {
|
|
160
|
-
const stmt = db.prepare('SELECT * FROM file_changes WHERE session_id = ? ORDER BY timestamp');
|
|
161
|
-
const rows = stmt.all(sessionId) as any[];
|
|
162
|
-
return rows.map((row) => ({
|
|
163
|
-
id: row.id,
|
|
164
|
-
sessionId: row.session_id,
|
|
165
|
-
filePath: row.file_path,
|
|
166
|
-
changeType: row.change_type,
|
|
167
|
-
timestamp: row.timestamp,
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function getCommits(sessionId: number): Commit[] {
|
|
172
|
-
const stmt = db.prepare('SELECT * FROM commits WHERE session_id = ? ORDER BY timestamp');
|
|
173
|
-
const rows = stmt.all(sessionId) as any[];
|
|
174
|
-
return rows.map((row) => ({
|
|
175
|
-
id: row.id,
|
|
176
|
-
sessionId: row.session_id,
|
|
177
|
-
hash: row.hash,
|
|
178
|
-
message: row.message,
|
|
179
|
-
timestamp: row.timestamp,
|
|
180
|
-
}));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function getAIUsage(sessionId: number): AIUsage[] {
|
|
184
|
-
const stmt = db.prepare('SELECT * FROM ai_usage WHERE session_id = ? ORDER BY timestamp');
|
|
185
|
-
const rows = stmt.all(sessionId) as any[];
|
|
186
|
-
return rows.map((row) => ({
|
|
187
|
-
id: row.id,
|
|
188
|
-
sessionId: row.session_id,
|
|
189
|
-
provider: row.provider,
|
|
190
|
-
model: row.model,
|
|
191
|
-
tokens: row.tokens,
|
|
192
|
-
cost: row.cost,
|
|
193
|
-
timestamp: row.timestamp,
|
|
194
|
-
}));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function getStats(): SessionStats {
|
|
198
|
-
const stmt = db.prepare(`
|
|
199
|
-
SELECT
|
|
200
|
-
COUNT(*) as total,
|
|
201
|
-
SUM(duration) as total_time,
|
|
202
|
-
SUM(files_changed) as total_files,
|
|
203
|
-
SUM(commits) as total_commits,
|
|
204
|
-
SUM(ai_cost) as total_cost,
|
|
205
|
-
AVG(duration) as avg_time
|
|
206
|
-
FROM sessions WHERE status = 'completed'
|
|
207
|
-
`);
|
|
208
|
-
const result = stmt.get() as any;
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
totalSessions: result.total || 0,
|
|
212
|
-
totalTime: result.total_time || 0,
|
|
213
|
-
totalFiles: result.total_files || 0,
|
|
214
|
-
totalCommits: result.total_commits || 0,
|
|
215
|
-
totalAICost: result.total_cost || 0,
|
|
216
|
-
avgSessionTime: result.avg_time || 0,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function mapSession(row: any): Session {
|
|
221
|
-
return {
|
|
222
|
-
id: row.id,
|
|
223
|
-
name: row.name,
|
|
224
|
-
startTime: row.start_time,
|
|
225
|
-
endTime: row.end_time,
|
|
226
|
-
duration: row.duration,
|
|
227
|
-
workingDirectory: row.working_directory,
|
|
228
|
-
filesChanged: row.files_changed,
|
|
229
|
-
commits: row.commits,
|
|
230
|
-
aiCost: row.ai_cost,
|
|
231
|
-
aiTokens: row.ai_tokens,
|
|
232
|
-
notes: row.notes,
|
|
233
|
-
status: row.status,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function closeDb(): void {
|
|
238
|
-
db.close();
|
|
239
|
-
}
|
package/src/formatters.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import Table from 'cli-table3';
|
|
3
|
-
import { Session, SessionStats, FileChange, Commit } from './types';
|
|
4
|
-
import { formatDistanceToNow, format } from 'date-fns';
|
|
5
|
-
|
|
6
|
-
export function formatDuration(seconds: number): string {
|
|
7
|
-
const hours = Math.floor(seconds / 3600);
|
|
8
|
-
const minutes = Math.floor((seconds % 3600) / 60);
|
|
9
|
-
|
|
10
|
-
if (hours > 0) {
|
|
11
|
-
return `${hours}h ${minutes}m`;
|
|
12
|
-
}
|
|
13
|
-
return `${minutes}m`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function formatCost(cost: number): string {
|
|
17
|
-
return `$${cost.toFixed(2)}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function displaySession(session: Session): void {
|
|
21
|
-
console.log(chalk.bold.cyan(`\nSession: ${session.name}\n`));
|
|
22
|
-
|
|
23
|
-
const table = new Table({
|
|
24
|
-
head: [chalk.cyan.bold('Metric'), chalk.cyan.bold('Value')],
|
|
25
|
-
style: { head: [], border: [] },
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
table.push(
|
|
29
|
-
['Status', session.status === 'active' ? chalk.green('Active') : chalk.gray('Completed')],
|
|
30
|
-
['Started', format(new Date(session.startTime), 'MMM dd, yyyy HH:mm')],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
if (session.endTime) {
|
|
34
|
-
table.push(['Ended', format(new Date(session.endTime), 'MMM dd, yyyy HH:mm')]);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (session.duration) {
|
|
38
|
-
table.push(['Duration', chalk.white(formatDuration(session.duration))]);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
table.push(
|
|
42
|
-
['Files Changed', chalk.white(session.filesChanged.toString())],
|
|
43
|
-
['Commits', chalk.white(session.commits.toString())],
|
|
44
|
-
['AI Tokens', chalk.white(session.aiTokens.toLocaleString())],
|
|
45
|
-
['AI Cost', chalk.yellow(formatCost(session.aiCost))],
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
if (session.notes) {
|
|
49
|
-
table.push(['Notes', chalk.gray(session.notes)]);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log(table.toString());
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function displaySessions(sessions: Session[]): void {
|
|
56
|
-
if (sessions.length === 0) {
|
|
57
|
-
console.log(chalk.yellow('\nNo sessions found.\n'));
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const table = new Table({
|
|
62
|
-
head: [
|
|
63
|
-
chalk.cyan.bold('ID'),
|
|
64
|
-
chalk.cyan.bold('Name'),
|
|
65
|
-
chalk.cyan.bold('Started'),
|
|
66
|
-
chalk.cyan.bold('Duration'),
|
|
67
|
-
chalk.cyan.bold('Files'),
|
|
68
|
-
chalk.cyan.bold('Commits'),
|
|
69
|
-
chalk.cyan.bold('AI Cost'),
|
|
70
|
-
],
|
|
71
|
-
style: { head: [], border: [] },
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
for (const session of sessions) {
|
|
75
|
-
table.push([
|
|
76
|
-
chalk.gray(`#${session.id}`),
|
|
77
|
-
session.status === 'active' ? chalk.green(session.name) : chalk.white(session.name),
|
|
78
|
-
formatDistanceToNow(new Date(session.startTime), { addSuffix: true }),
|
|
79
|
-
session.duration ? formatDuration(session.duration) : chalk.gray('ongoing'),
|
|
80
|
-
session.filesChanged.toString(),
|
|
81
|
-
session.commits.toString(),
|
|
82
|
-
chalk.yellow(formatCost(session.aiCost)),
|
|
83
|
-
]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.log('\n' + table.toString() + '\n');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function displayStats(stats: SessionStats): void {
|
|
90
|
-
console.log(chalk.bold.cyan('\nOverall Stats\n'));
|
|
91
|
-
|
|
92
|
-
const table = new Table({
|
|
93
|
-
head: [chalk.cyan.bold('Metric'), chalk.cyan.bold('Value')],
|
|
94
|
-
style: { head: [], border: [] },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
table.push(
|
|
98
|
-
['Total Sessions', chalk.white(stats.totalSessions.toLocaleString())],
|
|
99
|
-
['Total Time', chalk.white(formatDuration(stats.totalTime))],
|
|
100
|
-
['Average Session', chalk.white(formatDuration(stats.avgSessionTime))],
|
|
101
|
-
['Files Changed', chalk.white(stats.totalFiles.toLocaleString())],
|
|
102
|
-
['Commits', chalk.white(stats.totalCommits.toLocaleString())],
|
|
103
|
-
['Total AI Cost', chalk.yellow(formatCost(stats.totalAICost))],
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
console.log(table.toString() + '\n');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function displayFileChanges(changes: FileChange[]): void {
|
|
110
|
-
if (changes.length === 0) return;
|
|
111
|
-
|
|
112
|
-
console.log(chalk.bold.cyan('\nFile Changes\n'));
|
|
113
|
-
|
|
114
|
-
const table = new Table({
|
|
115
|
-
head: [chalk.cyan.bold('Type'), chalk.cyan.bold('File'), chalk.cyan.bold('Time')],
|
|
116
|
-
style: { head: [], border: [] },
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
for (const change of changes) {
|
|
120
|
-
const typeColor = change.changeType === 'created' ? chalk.green :
|
|
121
|
-
change.changeType === 'modified' ? chalk.yellow : chalk.red;
|
|
122
|
-
|
|
123
|
-
table.push([
|
|
124
|
-
typeColor(change.changeType),
|
|
125
|
-
change.filePath,
|
|
126
|
-
formatDistanceToNow(new Date(change.timestamp), { addSuffix: true }),
|
|
127
|
-
]);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log(table.toString() + '\n');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function displayCommits(commits: Commit[]): void {
|
|
134
|
-
if (commits.length === 0) return;
|
|
135
|
-
|
|
136
|
-
console.log(chalk.bold.cyan('\nCommits\n'));
|
|
137
|
-
|
|
138
|
-
const table = new Table({
|
|
139
|
-
head: [chalk.cyan.bold('Hash'), chalk.cyan.bold('Message'), chalk.cyan.bold('Time')],
|
|
140
|
-
style: { head: [], border: [] },
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
for (const commit of commits) {
|
|
144
|
-
table.push([
|
|
145
|
-
chalk.gray(commit.hash),
|
|
146
|
-
commit.message,
|
|
147
|
-
formatDistanceToNow(new Date(commit.timestamp), { addSuffix: true }),
|
|
148
|
-
]);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
console.log(table.toString() + '\n');
|
|
152
|
-
}
|
package/src/git.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { simpleGit, SimpleGit } from 'simple-git';
|
|
2
|
-
import { addCommit } from './db';
|
|
3
|
-
|
|
4
|
-
let git: SimpleGit;
|
|
5
|
-
let lastCommitHash: string | null = null;
|
|
6
|
-
|
|
7
|
-
export function initGit(cwd: string): void {
|
|
8
|
-
git = simpleGit(cwd);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function checkForNewCommits(sessionId: number): Promise<void> {
|
|
12
|
-
if (!git) return;
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const log = await git.log({ maxCount: 1 });
|
|
16
|
-
if (log.latest && log.latest.hash !== lastCommitHash) {
|
|
17
|
-
lastCommitHash = log.latest.hash;
|
|
18
|
-
|
|
19
|
-
addCommit({
|
|
20
|
-
sessionId,
|
|
21
|
-
hash: log.latest.hash.substring(0, 7),
|
|
22
|
-
message: log.latest.message,
|
|
23
|
-
timestamp: new Date().toISOString(),
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
} catch (error) {
|
|
27
|
-
// Not a git repo or no commits yet
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function getGitInfo(): Promise<{ branch: string; hasChanges: boolean } | null> {
|
|
32
|
-
if (!git) return null;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
|
|
36
|
-
const status = await git.status();
|
|
37
|
-
return {
|
|
38
|
-
branch: branch.trim(),
|
|
39
|
-
hasChanges: !status.isClean(),
|
|
40
|
-
};
|
|
41
|
-
} catch (error) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import {
|
|
6
|
-
createSession,
|
|
7
|
-
getActiveSession,
|
|
8
|
-
endSession,
|
|
9
|
-
getSession,
|
|
10
|
-
getSessions,
|
|
11
|
-
getStats,
|
|
12
|
-
getFileChanges,
|
|
13
|
-
getCommits,
|
|
14
|
-
addAIUsage
|
|
15
|
-
} from './db';
|
|
16
|
-
import { initGit, checkForNewCommits, getGitInfo } from './git';
|
|
17
|
-
import { startWatcher, stopWatcher } from './watcher';
|
|
18
|
-
import {
|
|
19
|
-
displaySession,
|
|
20
|
-
displaySessions,
|
|
21
|
-
displayStats,
|
|
22
|
-
displayFileChanges,
|
|
23
|
-
displayCommits
|
|
24
|
-
} from './formatters';
|
|
25
|
-
|
|
26
|
-
const program = new Command();
|
|
27
|
-
|
|
28
|
-
program
|
|
29
|
-
.name('devsession')
|
|
30
|
-
.description('Track your AI coding sessions: time, files, commits, AI costs')
|
|
31
|
-
.version('1.0.0');
|
|
32
|
-
|
|
33
|
-
// Start command
|
|
34
|
-
program
|
|
35
|
-
.command('start')
|
|
36
|
-
.description('Start a new coding session')
|
|
37
|
-
.argument('<name>', 'Session name')
|
|
38
|
-
.action(async (name: string) => {
|
|
39
|
-
const active = getActiveSession();
|
|
40
|
-
if (active) {
|
|
41
|
-
console.log(chalk.yellow(`\nSession "${active.name}" is already active.`));
|
|
42
|
-
console.log(chalk.gray('End it with: ds end\n'));
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const cwd = process.cwd();
|
|
47
|
-
const sessionId = createSession({
|
|
48
|
-
name,
|
|
49
|
-
startTime: new Date().toISOString(),
|
|
50
|
-
workingDirectory: cwd,
|
|
51
|
-
filesChanged: 0,
|
|
52
|
-
commits: 0,
|
|
53
|
-
aiCost: 0,
|
|
54
|
-
aiTokens: 0,
|
|
55
|
-
status: 'active',
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Initialize git tracking
|
|
59
|
-
initGit(cwd);
|
|
60
|
-
|
|
61
|
-
// Start file watcher
|
|
62
|
-
startWatcher(sessionId, cwd);
|
|
63
|
-
|
|
64
|
-
// Check for commits every 10 seconds
|
|
65
|
-
const gitInterval = setInterval(async () => {
|
|
66
|
-
await checkForNewCommits(sessionId);
|
|
67
|
-
}, 10000);
|
|
68
|
-
|
|
69
|
-
// Store interval ID
|
|
70
|
-
(global as any).gitInterval = gitInterval;
|
|
71
|
-
|
|
72
|
-
const gitInfo = await getGitInfo();
|
|
73
|
-
|
|
74
|
-
console.log(chalk.green(`\n✓ Session started: ${name}`));
|
|
75
|
-
if (gitInfo) {
|
|
76
|
-
console.log(chalk.gray(` Branch: ${gitInfo.branch}`));
|
|
77
|
-
}
|
|
78
|
-
console.log(chalk.gray(` Directory: ${cwd}`));
|
|
79
|
-
console.log(chalk.gray('\n Tracking: files, commits, AI usage'));
|
|
80
|
-
console.log(chalk.gray(' End with: ds end\n'));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// End command
|
|
84
|
-
program
|
|
85
|
-
.command('end')
|
|
86
|
-
.description('End the active session')
|
|
87
|
-
.option('-n, --notes <notes>', 'Session notes')
|
|
88
|
-
.action((options) => {
|
|
89
|
-
const session = getActiveSession();
|
|
90
|
-
if (!session) {
|
|
91
|
-
console.log(chalk.yellow('\nNo active session.\n'));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Stop tracking
|
|
96
|
-
stopWatcher();
|
|
97
|
-
if ((global as any).gitInterval) {
|
|
98
|
-
clearInterval((global as any).gitInterval);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
endSession(session.id!, new Date().toISOString(), options.notes);
|
|
102
|
-
|
|
103
|
-
const updated = getSession(session.id!);
|
|
104
|
-
if (updated) {
|
|
105
|
-
console.log(chalk.green('\n✓ Session ended\n'));
|
|
106
|
-
displaySession(updated);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Show command
|
|
111
|
-
program
|
|
112
|
-
.command('show')
|
|
113
|
-
.description('Show session details')
|
|
114
|
-
.argument('[id]', 'Session ID (defaults to last session)')
|
|
115
|
-
.option('--files', 'Show file changes')
|
|
116
|
-
.option('--commits', 'Show commits')
|
|
117
|
-
.action((id: string | undefined, options) => {
|
|
118
|
-
let session;
|
|
119
|
-
|
|
120
|
-
if (id) {
|
|
121
|
-
session = getSession(parseInt(id));
|
|
122
|
-
} else {
|
|
123
|
-
const sessions = getSessions(1);
|
|
124
|
-
session = sessions[0];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!session) {
|
|
128
|
-
console.log(chalk.yellow('\nSession not found.\n'));
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
displaySession(session);
|
|
133
|
-
|
|
134
|
-
if (options.files) {
|
|
135
|
-
const files = getFileChanges(session.id!);
|
|
136
|
-
displayFileChanges(files);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (options.commits) {
|
|
140
|
-
const commits = getCommits(session.id!);
|
|
141
|
-
displayCommits(commits);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// List command
|
|
146
|
-
program
|
|
147
|
-
.command('list')
|
|
148
|
-
.alias('ls')
|
|
149
|
-
.description('List recent sessions')
|
|
150
|
-
.option('-l, --limit <number>', 'Number of sessions to show', parseInt, 10)
|
|
151
|
-
.action((options) => {
|
|
152
|
-
const sessions = getSessions(options.limit);
|
|
153
|
-
displaySessions(sessions);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Stats command
|
|
157
|
-
program
|
|
158
|
-
.command('stats')
|
|
159
|
-
.description('Show overall statistics')
|
|
160
|
-
.action(() => {
|
|
161
|
-
const stats = getStats();
|
|
162
|
-
displayStats(stats);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Log AI usage command
|
|
166
|
-
program
|
|
167
|
-
.command('log-ai')
|
|
168
|
-
.description('Log AI usage for active session')
|
|
169
|
-
.requiredOption('-p, --provider <provider>', 'AI provider')
|
|
170
|
-
.requiredOption('-m, --model <model>', 'Model name')
|
|
171
|
-
.requiredOption('-t, --tokens <tokens>', 'Total tokens', parseInt)
|
|
172
|
-
.requiredOption('-c, --cost <cost>', 'Cost in dollars', parseFloat)
|
|
173
|
-
.action((options) => {
|
|
174
|
-
const session = getActiveSession();
|
|
175
|
-
if (!session) {
|
|
176
|
-
console.log(chalk.yellow('\nNo active session. Start one with: ds start <name>\n'));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
addAIUsage({
|
|
181
|
-
sessionId: session.id!,
|
|
182
|
-
provider: options.provider,
|
|
183
|
-
model: options.model,
|
|
184
|
-
tokens: options.tokens,
|
|
185
|
-
cost: options.cost,
|
|
186
|
-
timestamp: new Date().toISOString(),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
console.log(chalk.green(`\n✓ Logged AI usage: ${options.tokens.toLocaleString()} tokens, $${options.cost.toFixed(2)}\n`));
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Status command
|
|
193
|
-
program
|
|
194
|
-
.command('status')
|
|
195
|
-
.description('Show active session status')
|
|
196
|
-
.action(() => {
|
|
197
|
-
const session = getActiveSession();
|
|
198
|
-
if (!session) {
|
|
199
|
-
console.log(chalk.yellow('\nNo active session.\n'));
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
displaySession(session);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
program.parse();
|
package/src/types.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export interface Session {
|
|
2
|
-
id?: number;
|
|
3
|
-
name: string;
|
|
4
|
-
startTime: string;
|
|
5
|
-
endTime?: string;
|
|
6
|
-
duration?: number; // in seconds
|
|
7
|
-
workingDirectory: string;
|
|
8
|
-
filesChanged: number;
|
|
9
|
-
commits: number;
|
|
10
|
-
aiCost: number;
|
|
11
|
-
aiTokens: number;
|
|
12
|
-
notes?: string;
|
|
13
|
-
status: 'active' | 'completed';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface FileChange {
|
|
17
|
-
id?: number;
|
|
18
|
-
sessionId: number;
|
|
19
|
-
filePath: string;
|
|
20
|
-
changeType: 'created' | 'modified' | 'deleted';
|
|
21
|
-
timestamp: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface Commit {
|
|
25
|
-
id?: number;
|
|
26
|
-
sessionId: number;
|
|
27
|
-
hash: string;
|
|
28
|
-
message: string;
|
|
29
|
-
timestamp: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AIUsage {
|
|
33
|
-
id?: number;
|
|
34
|
-
sessionId: number;
|
|
35
|
-
provider: string;
|
|
36
|
-
model: string;
|
|
37
|
-
tokens: number;
|
|
38
|
-
cost: number;
|
|
39
|
-
timestamp: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface SessionStats {
|
|
43
|
-
totalSessions: number;
|
|
44
|
-
totalTime: number;
|
|
45
|
-
totalFiles: number;
|
|
46
|
-
totalCommits: number;
|
|
47
|
-
totalAICost: number;
|
|
48
|
-
avgSessionTime: number;
|
|
49
|
-
}
|