codesession-cli 1.0.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.
@@ -0,0 +1,152 @@
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 ADDED
@@ -0,0 +1,44 @@
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 ADDED
@@ -0,0 +1,206 @@
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 ADDED
@@ -0,0 +1,49 @@
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
+ }
package/src/watcher.ts ADDED
@@ -0,0 +1,52 @@
1
+ import chokidar from 'chokidar';
2
+ import { relative } from 'path';
3
+ import { addFileChange } from './db';
4
+
5
+ let watcher: chokidar.FSWatcher | null = null;
6
+ const changedFiles = new Set<string>();
7
+
8
+ export function startWatcher(sessionId: number, cwd: string): void {
9
+ if (watcher) return;
10
+
11
+ watcher = chokidar.watch(cwd, {
12
+ ignored: /(^|[\/\\])\..|(node_modules|dist|build|\.git)/,
13
+ persistent: true,
14
+ ignoreInitial: true,
15
+ });
16
+
17
+ watcher
18
+ .on('add', (path) => handleChange(sessionId, path, cwd, 'created'))
19
+ .on('change', (path) => handleChange(sessionId, path, cwd, 'modified'))
20
+ .on('unlink', (path) => handleChange(sessionId, path, cwd, 'deleted'));
21
+ }
22
+
23
+ export function stopWatcher(): void {
24
+ if (watcher) {
25
+ watcher.close();
26
+ watcher = null;
27
+ changedFiles.clear();
28
+ }
29
+ }
30
+
31
+ function handleChange(
32
+ sessionId: number,
33
+ path: string,
34
+ cwd: string,
35
+ changeType: 'created' | 'modified' | 'deleted'
36
+ ): void {
37
+ const relativePath = relative(cwd, path);
38
+
39
+ // Deduplicate rapid changes to same file
40
+ const key = `${relativePath}-${changeType}`;
41
+ if (changedFiles.has(key)) return;
42
+
43
+ changedFiles.add(key);
44
+ setTimeout(() => changedFiles.delete(key), 1000);
45
+
46
+ addFileChange({
47
+ sessionId,
48
+ filePath: relativePath,
49
+ changeType,
50
+ timestamp: new Date().toISOString(),
51
+ });
52
+ }
package/test.ts ADDED
@@ -0,0 +1,4 @@
1
+ // Test file to trigger file watcher
2
+ export function hello() {
3
+ return 'Hello from test!';
4
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "moduleResolution": "node"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }