hackhours 0.1.0 → 0.1.1

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 CHANGED
@@ -28,6 +28,7 @@ hackhours init
28
28
  ```bash
29
29
  hackhours start
30
30
  hackhours stop
31
+ hackhours status
31
32
  ```
32
33
 
33
34
  ## Reports
@@ -70,4 +71,4 @@ npm test
70
71
 
71
72
  ## Notes
72
73
  - `hackhours start` spawns a detached background process.
73
- - Use `hackhours stop` to end tracking cleanly.
74
+ - Use `hackhours stop` to end tracking cleanly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hackhours",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Offline local-first coding activity tracker",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +0,0 @@
1
- import { ChronoCollections } from "../storage/chrono.js";
2
- export type Summary = {
3
- totalTimeMs: number;
4
- filesEdited: Map<string, number>;
5
- languages: Map<string, number>;
6
- projects: Map<string, number>;
7
- activityByHour: number[];
8
- activityByDay: Map<string, number>;
9
- };
10
- export declare const buildSummary: (collections: ChronoCollections, from: number, to: number, idleMinutes: number) => Promise<Summary>;
@@ -1,56 +0,0 @@
1
- import { getEventsInRange, getSessionsInRange } from "../storage/queries.js";
2
- import { minutesToMs, toDateKey } from "../utils/time.js";
3
- const addToMap = (map, key, value) => {
4
- map.set(key, (map.get(key) ?? 0) + value);
5
- };
6
- export const buildSummary = async (collections, from, to, idleMinutes) => {
7
- const events = await getEventsInRange(collections, from, to);
8
- const sessions = await getSessionsInRange(collections, from, to);
9
- const idleMs = minutesToMs(idleMinutes);
10
- const now = Date.now();
11
- const sessionsById = new Map(sessions.map((s) => [s.sessionId, s]));
12
- const eventsBySession = new Map();
13
- for (const event of events) {
14
- if (!eventsBySession.has(event.sessionId)) {
15
- eventsBySession.set(event.sessionId, []);
16
- }
17
- eventsBySession.get(event.sessionId)?.push(event);
18
- }
19
- const summary = {
20
- totalTimeMs: 0,
21
- filesEdited: new Map(),
22
- languages: new Map(),
23
- projects: new Map(),
24
- activityByHour: Array.from({ length: 24 }, () => 0),
25
- activityByDay: new Map(),
26
- };
27
- for (const session of sessions) {
28
- const sessionEnd = session.endTimestamp ?? now;
29
- const overlapStart = Math.max(session.startTimestamp, from);
30
- const overlapEnd = Math.min(sessionEnd, to);
31
- if (overlapEnd > overlapStart) {
32
- summary.totalTimeMs += overlapEnd - overlapStart;
33
- }
34
- }
35
- for (const [sessionId, list] of eventsBySession.entries()) {
36
- const session = sessionsById.get(sessionId);
37
- const sessionEnd = session?.endTimestamp ?? now;
38
- const sorted = [...list].sort((a, b) => a.timestamp - b.timestamp);
39
- for (let i = 0; i < sorted.length; i += 1) {
40
- const current = sorted[i];
41
- const next = sorted[i + 1];
42
- const nextTs = Math.min(next?.timestamp ?? sessionEnd, to);
43
- const duration = Math.max(0, Math.min(nextTs - current.timestamp, idleMs));
44
- if (duration <= 0)
45
- continue;
46
- addToMap(summary.languages, current.language, duration);
47
- addToMap(summary.projects, current.project, duration);
48
- addToMap(summary.filesEdited, current.filePath, duration);
49
- const eventDate = new Date(current.timestamp);
50
- const hour = eventDate.getHours();
51
- summary.activityByHour[hour] += duration;
52
- addToMap(summary.activityByDay, toDateKey(eventDate), duration);
53
- }
54
- }
55
- return summary;
56
- };
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/cli.js DELETED
@@ -1,274 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import chalk from "chalk";
4
- import Table from "cli-table3";
5
- import * as asciichart from "asciichart";
6
- import { spawn } from "node:child_process";
7
- import path from "node:path";
8
- import process from "node:process";
9
- import { loadConfig, promptInitConfig, saveConfig, } from "./config/config.js";
10
- import { openChrono } from "./storage/chrono.js";
11
- import { buildSummary } from "./analytics/queries.js";
12
- import { runWatcher } from "./tracker/watcher.js";
13
- import { readState, writeState, clearState } from "./utils/state.js";
14
- import { endOfDay, formatDuration, parseDateKey, startOfDay } from "./utils/time.js";
15
- const program = new Command();
16
- const formatBar = (value, total, size = 16) => {
17
- if (total <= 0)
18
- return " ".repeat(size);
19
- const filled = Math.round((value / total) * size);
20
- return `${"█".repeat(filled)}${" ".repeat(size - filled)}`;
21
- };
22
- const printSummary = (title, summary) => {
23
- const total = summary.totalTimeMs;
24
- console.log(chalk.bold(`\n${title}`));
25
- console.log(`${chalk.cyan("Total:")} ${formatDuration(total)}`);
26
- console.log(`${chalk.cyan("Files edited:")} ${summary.filesEdited.size}`);
27
- const langEntries = [...summary.languages.entries()].sort((a, b) => b[1] - a[1]);
28
- if (langEntries.length > 0) {
29
- console.log(`\n${chalk.cyan("Languages")}`);
30
- const langTable = new Table({
31
- head: ["Language", "Time", "%"],
32
- });
33
- for (const [lang, duration] of langEntries) {
34
- langTable.push([lang, formatDuration(duration), `${Math.round((duration / total) * 100)}%`]);
35
- }
36
- console.log(langTable.toString());
37
- }
38
- console.log(`\n${chalk.cyan("Activity")}`);
39
- const series = summary.activityByHour.map((ms) => Math.round(ms / 60000));
40
- if (series.some((v) => v > 0)) {
41
- console.log(asciichart.plot(series, { height: 8 }));
42
- }
43
- else {
44
- console.log("No activity recorded.");
45
- }
46
- };
47
- const printBreakdown = (title, map, total) => {
48
- console.log(chalk.bold(`\n${title}`));
49
- const entries = [...map.entries()].sort((a, b) => b[1] - a[1]);
50
- if (entries.length === 0) {
51
- console.log("No activity recorded.");
52
- return;
53
- }
54
- const table = new Table({ head: ["Name", "Time", "%", ""] });
55
- for (const [name, duration] of entries) {
56
- const percent = total > 0 ? duration / total : 0;
57
- table.push([name, formatDuration(duration), `${Math.round(percent * 100)}%`, formatBar(duration, total)]);
58
- }
59
- console.log(table.toString());
60
- };
61
- const parseRange = (from, to) => {
62
- if (!from || !to) {
63
- throw new Error("Both --from and --to are required.");
64
- }
65
- const fromDate = startOfDay(parseDateKey(from));
66
- const toDate = endOfDay(parseDateKey(to));
67
- return { from: fromDate.getTime(), to: toDate.getTime() };
68
- };
69
- program
70
- .name("hackhours")
71
- .description("Offline local-first coding activity tracker")
72
- .version("0.1.0");
73
- program
74
- .command("init")
75
- .description("Initialize HackHours configuration")
76
- .action(async () => {
77
- const config = await promptInitConfig();
78
- saveConfig(config);
79
- console.log(chalk.green("Saved configuration to ~/.hackhours/config.json"));
80
- });
81
- program
82
- .command("start")
83
- .description("Start background tracking")
84
- .action(async () => {
85
- const existing = readState();
86
- if (existing) {
87
- try {
88
- process.kill(existing.pid, 0);
89
- console.log(chalk.yellow("HackHours is already running."));
90
- return;
91
- }
92
- catch {
93
- clearState();
94
- }
95
- }
96
- const node = process.execPath;
97
- const cli = path.resolve(process.argv[1]);
98
- const child = spawn(node, [cli, "daemon"], {
99
- detached: true,
100
- stdio: "ignore",
101
- });
102
- child.unref();
103
- writeState({ pid: child.pid ?? 0, startedAt: Date.now() });
104
- console.log(chalk.green("HackHours tracking started."));
105
- });
106
- program
107
- .command("stop")
108
- .description("Stop background tracking")
109
- .action(() => {
110
- const state = readState();
111
- if (!state) {
112
- console.log(chalk.yellow("HackHours is not running."));
113
- return;
114
- }
115
- try {
116
- process.kill(state.pid);
117
- clearState();
118
- console.log(chalk.green("HackHours tracking stopped."));
119
- }
120
- catch {
121
- clearState();
122
- console.log(chalk.yellow("HackHours was not running."));
123
- }
124
- });
125
- program
126
- .command("daemon")
127
- .description("Run watcher in foreground (internal)")
128
- .action(async () => {
129
- const config = loadConfig();
130
- const { collections } = await openChrono(config.dataDir);
131
- const watcher = await runWatcher(config, collections);
132
- const shutdown = async () => {
133
- await watcher.stop();
134
- process.exit(0);
135
- };
136
- process.on("SIGINT", shutdown);
137
- process.on("SIGTERM", shutdown);
138
- });
139
- program
140
- .command("today")
141
- .description("Show today's summary")
142
- .action(async () => {
143
- const config = loadConfig();
144
- const { collections } = await openChrono(config.dataDir);
145
- const now = new Date();
146
- const from = startOfDay(now).getTime();
147
- const to = endOfDay(now).getTime();
148
- const summary = await buildSummary(collections, from, to, config.idleMinutes);
149
- printSummary("HackHours – Today", summary);
150
- });
151
- program
152
- .command("week")
153
- .description("Show weekly summary")
154
- .action(async () => {
155
- const config = loadConfig();
156
- const { collections } = await openChrono(config.dataDir);
157
- const now = new Date();
158
- const fromDate = new Date(now);
159
- fromDate.setDate(fromDate.getDate() - 6);
160
- const from = startOfDay(fromDate).getTime();
161
- const to = endOfDay(now).getTime();
162
- const summary = await buildSummary(collections, from, to, config.idleMinutes);
163
- printSummary("HackHours – Last 7 Days", summary);
164
- });
165
- program
166
- .command("month")
167
- .description("Show monthly summary")
168
- .action(async () => {
169
- const config = loadConfig();
170
- const { collections } = await openChrono(config.dataDir);
171
- const now = new Date();
172
- const fromDate = new Date(now);
173
- fromDate.setDate(fromDate.getDate() - 29);
174
- const from = startOfDay(fromDate).getTime();
175
- const to = endOfDay(now).getTime();
176
- const summary = await buildSummary(collections, from, to, config.idleMinutes);
177
- printSummary("HackHours – Last 30 Days", summary);
178
- });
179
- program
180
- .command("stats")
181
- .description("Custom stats for a given range")
182
- .requiredOption("--from <date>", "Start date (YYYY-MM-DD)")
183
- .requiredOption("--to <date>", "End date (YYYY-MM-DD)")
184
- .action(async (options) => {
185
- const config = loadConfig();
186
- const { collections } = await openChrono(config.dataDir);
187
- const range = parseRange(options.from, options.to);
188
- const summary = await buildSummary(collections, range.from, range.to, config.idleMinutes);
189
- printSummary(`HackHours – ${options.from} to ${options.to}`, summary);
190
- });
191
- program
192
- .command("languages")
193
- .description("Language breakdown")
194
- .option("--from <date>", "Start date (YYYY-MM-DD)")
195
- .option("--to <date>", "End date (YYYY-MM-DD)")
196
- .action(async (options) => {
197
- const config = loadConfig();
198
- const { collections } = await openChrono(config.dataDir);
199
- let from;
200
- let to;
201
- if (options.from && options.to) {
202
- const range = parseRange(options.from, options.to);
203
- from = range.from;
204
- to = range.to;
205
- }
206
- else {
207
- const now = new Date();
208
- const fromDate = new Date(now);
209
- fromDate.setDate(fromDate.getDate() - 6);
210
- from = startOfDay(fromDate).getTime();
211
- to = endOfDay(now).getTime();
212
- }
213
- const summary = await buildSummary(collections, from, to, config.idleMinutes);
214
- printBreakdown("Languages", summary.languages, summary.totalTimeMs);
215
- });
216
- program
217
- .command("projects")
218
- .description("Project breakdown")
219
- .option("--from <date>", "Start date (YYYY-MM-DD)")
220
- .option("--to <date>", "End date (YYYY-MM-DD)")
221
- .action(async (options) => {
222
- const config = loadConfig();
223
- const { collections } = await openChrono(config.dataDir);
224
- let from;
225
- let to;
226
- if (options.from && options.to) {
227
- const range = parseRange(options.from, options.to);
228
- from = range.from;
229
- to = range.to;
230
- }
231
- else {
232
- const now = new Date();
233
- const fromDate = new Date(now);
234
- fromDate.setDate(fromDate.getDate() - 6);
235
- from = startOfDay(fromDate).getTime();
236
- to = endOfDay(now).getTime();
237
- }
238
- const summary = await buildSummary(collections, from, to, config.idleMinutes);
239
- printBreakdown("Projects", summary.projects, summary.totalTimeMs);
240
- });
241
- program
242
- .command("files")
243
- .description("File breakdown (top 10)")
244
- .option("--from <date>", "Start date (YYYY-MM-DD)")
245
- .option("--to <date>", "End date (YYYY-MM-DD)")
246
- .action(async (options) => {
247
- const config = loadConfig();
248
- const { collections } = await openChrono(config.dataDir);
249
- let from;
250
- let to;
251
- if (options.from && options.to) {
252
- const range = parseRange(options.from, options.to);
253
- from = range.from;
254
- to = range.to;
255
- }
256
- else {
257
- const now = new Date();
258
- const fromDate = new Date(now);
259
- fromDate.setDate(fromDate.getDate() - 6);
260
- from = startOfDay(fromDate).getTime();
261
- to = endOfDay(now).getTime();
262
- }
263
- const summary = await buildSummary(collections, from, to, config.idleMinutes);
264
- const top = [...summary.filesEdited.entries()]
265
- .sort((a, b) => b[1] - a[1])
266
- .slice(0, 10);
267
- const total = summary.totalTimeMs;
268
- const table = new Table({ head: ["File", "Time", "%"] });
269
- for (const [file, duration] of top) {
270
- table.push([file, formatDuration(duration), `${Math.round((duration / total) * 100)}%`]);
271
- }
272
- console.log(table.toString());
273
- });
274
- program.parseAsync(process.argv);
@@ -1,11 +0,0 @@
1
- export type HackHoursConfig = {
2
- directories: string[];
3
- idleMinutes: number;
4
- exclude: string[];
5
- dataDir: string;
6
- };
7
- export declare const getDefaultConfig: () => HackHoursConfig;
8
- export declare const ensureConfigDir: (configPath?: string) => void;
9
- export declare const loadConfig: (configPath?: string) => HackHoursConfig;
10
- export declare const saveConfig: (config: HackHoursConfig, configPath?: string) => void;
11
- export declare const promptInitConfig: () => Promise<HackHoursConfig>;
@@ -1,77 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import inquirer from "inquirer";
5
- import { DEFAULT_CONFIG_PATH, DEFAULT_DATA_DIR, DEFAULT_EXCLUDE, DEFAULT_IDLE_MINUTES, } from "./defaults.js";
6
- export const getDefaultConfig = () => ({
7
- directories: [process.cwd()],
8
- idleMinutes: DEFAULT_IDLE_MINUTES,
9
- exclude: DEFAULT_EXCLUDE,
10
- dataDir: DEFAULT_DATA_DIR,
11
- });
12
- export const ensureConfigDir = (configPath = DEFAULT_CONFIG_PATH) => {
13
- const dir = path.dirname(configPath);
14
- if (!fs.existsSync(dir)) {
15
- fs.mkdirSync(dir, { recursive: true });
16
- }
17
- };
18
- export const loadConfig = (configPath = DEFAULT_CONFIG_PATH) => {
19
- if (!fs.existsSync(configPath)) {
20
- return getDefaultConfig();
21
- }
22
- const raw = fs.readFileSync(configPath, "utf-8");
23
- const parsed = JSON.parse(raw);
24
- return {
25
- ...getDefaultConfig(),
26
- ...parsed,
27
- };
28
- };
29
- export const saveConfig = (config, configPath = DEFAULT_CONFIG_PATH) => {
30
- ensureConfigDir(configPath);
31
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
32
- };
33
- export const promptInitConfig = async () => {
34
- const defaultConfig = getDefaultConfig();
35
- const answers = await inquirer.prompt([
36
- {
37
- type: "input",
38
- name: "directories",
39
- message: "Which directories should HackHours track? (comma-separated)",
40
- default: defaultConfig.directories.join(", "),
41
- filter: (input) => input
42
- .split(",")
43
- .map((v) => v.trim())
44
- .filter(Boolean),
45
- },
46
- {
47
- type: "number",
48
- name: "idleMinutes",
49
- message: "Idle timeout in minutes",
50
- default: defaultConfig.idleMinutes,
51
- },
52
- {
53
- type: "input",
54
- name: "exclude",
55
- message: "Exclude globs (comma-separated)",
56
- default: defaultConfig.exclude.join(", "),
57
- filter: (input) => input
58
- .split(",")
59
- .map((v) => v.trim())
60
- .filter(Boolean),
61
- },
62
- {
63
- type: "input",
64
- name: "dataDir",
65
- message: "Data directory",
66
- default: defaultConfig.dataDir,
67
- filter: (input) => input.trim(),
68
- },
69
- ]);
70
- const expanded = answers.dataDir.replace(/^~\//, `${os.homedir()}/`);
71
- return {
72
- directories: answers.directories,
73
- idleMinutes: answers.idleMinutes,
74
- exclude: answers.exclude,
75
- dataDir: expanded,
76
- };
77
- };
@@ -1,4 +0,0 @@
1
- export declare const DEFAULT_IDLE_MINUTES = 2;
2
- export declare const DEFAULT_CONFIG_PATH: string;
3
- export declare const DEFAULT_DATA_DIR: string;
4
- export declare const DEFAULT_EXCLUDE: string[];
@@ -1,17 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- export const DEFAULT_IDLE_MINUTES = 2;
4
- export const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".hackhours", "config.json");
5
- export const DEFAULT_DATA_DIR = path.join(os.homedir(), ".hackhours", "data");
6
- export const DEFAULT_EXCLUDE = [
7
- "**/node_modules/**",
8
- "**/.git/**",
9
- "**/dist/**",
10
- "**/build/**",
11
- "**/.next/**",
12
- "**/.turbo/**",
13
- "**/.cache/**",
14
- "**/.idea/**",
15
- "**/.vscode/**",
16
- "**/*.log",
17
- ];
package/dist/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./config/config.js";
2
- export * from "./storage/chrono.js";
3
- export * from "./tracker/watcher.js";
4
- export * from "./analytics/queries.js";
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- export * from "./config/config.js";
2
- export * from "./storage/chrono.js";
3
- export * from "./tracker/watcher.js";
4
- export * from "./analytics/queries.js";
@@ -1,11 +0,0 @@
1
- import { AggregateDoc, EventDoc, SessionDoc } from "./schema.js";
2
- export type ChronoCollections = {
3
- sessions: any;
4
- events: any;
5
- aggregates: any;
6
- };
7
- export declare const openChrono: (dataDir: string) => Promise<{
8
- db: any;
9
- collections: ChronoCollections;
10
- }>;
11
- export type { AggregateDoc, EventDoc, SessionDoc };
@@ -1,37 +0,0 @@
1
- import fs from "node:fs";
2
- import ChronoDB from "chronodb";
3
- import { aggregateSchema, eventSchema, sessionSchema, } from "./schema.js";
4
- export const openChrono = async (dataDir) => {
5
- fs.mkdirSync(dataDir, { recursive: true });
6
- let db;
7
- try {
8
- db = await ChronoDB.open({ path: dataDir, cloudSync: false });
9
- }
10
- catch (error) {
11
- const prev = process.cwd();
12
- process.chdir(dataDir);
13
- try {
14
- db = await ChronoDB.open({ cloudSync: false });
15
- }
16
- catch (inner) {
17
- process.chdir(prev);
18
- throw inner;
19
- }
20
- }
21
- const sessions = db.col("sessions", {
22
- schema: sessionSchema,
23
- indexes: ["sessionId", "startTimestamp", "endTimestamp"],
24
- });
25
- const events = db.col("events", {
26
- schema: eventSchema,
27
- indexes: ["timestamp", "language", "project", "sessionId"],
28
- });
29
- const aggregates = db.col("aggregates", {
30
- schema: aggregateSchema,
31
- indexes: ["date"],
32
- });
33
- return {
34
- db,
35
- collections: { sessions, events, aggregates },
36
- };
37
- };
@@ -1,4 +0,0 @@
1
- import { ChronoCollections, EventDoc, SessionDoc } from "./chrono.js";
2
- export declare const getSessionsInRange: (collections: ChronoCollections, from: number, to: number) => Promise<SessionDoc[]>;
3
- export declare const getEventsInRange: (collections: ChronoCollections, from: number, to: number) => Promise<EventDoc[]>;
4
- export declare const getAggregateByDate: (collections: ChronoCollections, date: string) => Promise<any>;
@@ -1,14 +0,0 @@
1
- export const getSessionsInRange = async (collections, from, to) => {
2
- const all = (await collections.sessions.getAll());
3
- return all.filter((s) => {
4
- const end = s.endTimestamp ?? s.startTimestamp;
5
- return s.startTimestamp <= to && end >= from;
6
- });
7
- };
8
- export const getEventsInRange = async (collections, from, to) => {
9
- const all = (await collections.events.getAll());
10
- return all.filter((e) => e.timestamp >= from && e.timestamp <= to);
11
- };
12
- export const getAggregateByDate = async (collections, date) => {
13
- return collections.aggregates.getOne({ date });
14
- };
@@ -1,77 +0,0 @@
1
- export type SessionDoc = {
2
- sessionId: string;
3
- startTimestamp: number;
4
- endTimestamp: number | null;
5
- durationMs: number;
6
- };
7
- export type EventDoc = {
8
- timestamp: number;
9
- filePath: string;
10
- language: string;
11
- project: string;
12
- sessionId: string;
13
- };
14
- export type AggregateDoc = {
15
- date: string;
16
- totalTimeMs: number;
17
- filesEdited: string;
18
- languagesUsed: string;
19
- };
20
- export declare const sessionSchema: {
21
- sessionId: {
22
- type: string;
23
- distinct: boolean;
24
- };
25
- startTimestamp: {
26
- type: string;
27
- important: boolean;
28
- };
29
- endTimestamp: {
30
- type: string;
31
- nullable: boolean;
32
- };
33
- durationMs: {
34
- type: string;
35
- default: number;
36
- };
37
- };
38
- export declare const eventSchema: {
39
- timestamp: {
40
- type: string;
41
- important: boolean;
42
- };
43
- filePath: {
44
- type: string;
45
- important: boolean;
46
- };
47
- language: {
48
- type: string;
49
- important: boolean;
50
- };
51
- project: {
52
- type: string;
53
- important: boolean;
54
- };
55
- sessionId: {
56
- type: string;
57
- important: boolean;
58
- };
59
- };
60
- export declare const aggregateSchema: {
61
- date: {
62
- type: string;
63
- distinct: boolean;
64
- };
65
- totalTimeMs: {
66
- type: string;
67
- default: number;
68
- };
69
- filesEdited: {
70
- type: string;
71
- default: string;
72
- };
73
- languagesUsed: {
74
- type: string;
75
- default: string;
76
- };
77
- };
@@ -1,19 +0,0 @@
1
- export const sessionSchema = {
2
- sessionId: { type: "string", distinct: true },
3
- startTimestamp: { type: "number", important: true },
4
- endTimestamp: { type: "number", nullable: true },
5
- durationMs: { type: "number", default: 0 },
6
- };
7
- export const eventSchema = {
8
- timestamp: { type: "number", important: true },
9
- filePath: { type: "string", important: true },
10
- language: { type: "string", important: true },
11
- project: { type: "string", important: true },
12
- sessionId: { type: "string", important: true },
13
- };
14
- export const aggregateSchema = {
15
- date: { type: "string", distinct: true },
16
- totalTimeMs: { type: "number", default: 0 },
17
- filesEdited: { type: "string", default: "[]" },
18
- languagesUsed: { type: "string", default: "[]" },
19
- };
@@ -1,14 +0,0 @@
1
- import { HackHoursConfig } from "../config/config.js";
2
- import { ChronoCollections } from "../storage/chrono.js";
3
- export declare class SessionManager {
4
- private active;
5
- private idleMs;
6
- private config;
7
- private collections;
8
- constructor(config: HackHoursConfig, collections: ChronoCollections);
9
- handleActivity(filePath: string): Promise<void>;
10
- checkIdle(): Promise<void>;
11
- stop(): Promise<void>;
12
- private startSession;
13
- private endSession;
14
- }
@@ -1,78 +0,0 @@
1
- import crypto from "node:crypto";
2
- import path from "node:path";
3
- import { detectLanguage } from "../utils/language.js";
4
- import { resolveProjectRoot } from "../utils/project.js";
5
- import { minutesToMs, toDateKey } from "../utils/time.js";
6
- export class SessionManager {
7
- constructor(config, collections) {
8
- this.active = null;
9
- this.config = config;
10
- this.collections = collections;
11
- this.idleMs = minutesToMs(config.idleMinutes);
12
- }
13
- async handleActivity(filePath) {
14
- const now = Date.now();
15
- if (!this.active) {
16
- this.active = await this.startSession(now);
17
- }
18
- this.active.lastActivity = now;
19
- const language = detectLanguage(filePath);
20
- const projectRoot = resolveProjectRoot(filePath, this.config.directories);
21
- await this.collections.events.add({
22
- timestamp: now,
23
- filePath: path.resolve(filePath),
24
- language,
25
- project: projectRoot,
26
- sessionId: this.active.sessionId,
27
- });
28
- }
29
- async checkIdle() {
30
- if (!this.active)
31
- return;
32
- const now = Date.now();
33
- if (now - this.active.lastActivity > this.idleMs) {
34
- await this.endSession(this.active, this.active.lastActivity);
35
- this.active = null;
36
- }
37
- }
38
- async stop() {
39
- if (!this.active)
40
- return;
41
- await this.endSession(this.active, this.active.lastActivity);
42
- this.active = null;
43
- }
44
- async startSession(startTimestamp) {
45
- const sessionId = crypto.randomUUID();
46
- await this.collections.sessions.add({
47
- sessionId,
48
- startTimestamp,
49
- endTimestamp: null,
50
- durationMs: 0,
51
- });
52
- return {
53
- sessionId,
54
- startTimestamp,
55
- lastActivity: startTimestamp,
56
- };
57
- }
58
- async endSession(session, endTimestamp) {
59
- const durationMs = Math.max(0, endTimestamp - session.startTimestamp);
60
- await this.collections.sessions.updateMany({ sessionId: session.sessionId }, {
61
- endTimestamp,
62
- durationMs,
63
- });
64
- const dateKey = toDateKey(new Date(endTimestamp));
65
- const existing = await this.collections.aggregates.getOne({ date: dateKey });
66
- if (!existing) {
67
- await this.collections.aggregates.add({
68
- date: dateKey,
69
- totalTimeMs: durationMs,
70
- filesEdited: "[]",
71
- languagesUsed: "[]",
72
- });
73
- }
74
- else {
75
- await this.collections.aggregates.updateMany({ date: dateKey }, { totalTimeMs: (existing.totalTimeMs ?? 0) + durationMs });
76
- }
77
- }
78
- }
@@ -1,6 +0,0 @@
1
- import { HackHoursConfig } from "../config/config.js";
2
- import { ChronoCollections } from "../storage/chrono.js";
3
- export type WatcherHandle = {
4
- stop: () => Promise<void>;
5
- };
6
- export declare const runWatcher: (config: HackHoursConfig, collections: ChronoCollections) => Promise<WatcherHandle>;
@@ -1,25 +0,0 @@
1
- import chokidar from "chokidar";
2
- import { SessionManager } from "./session.js";
3
- export const runWatcher = async (config, collections) => {
4
- const sessionManager = new SessionManager(config, collections);
5
- const watcher = chokidar.watch(config.directories, {
6
- ignored: config.exclude,
7
- ignoreInitial: true,
8
- persistent: true,
9
- });
10
- const onActivity = (filePath) => {
11
- sessionManager.handleActivity(filePath).catch(() => undefined);
12
- };
13
- watcher.on("add", onActivity);
14
- watcher.on("change", onActivity);
15
- const interval = setInterval(() => {
16
- sessionManager.checkIdle().catch(() => undefined);
17
- }, 10000);
18
- return {
19
- stop: async () => {
20
- clearInterval(interval);
21
- await watcher.close();
22
- await sessionManager.stop();
23
- },
24
- };
25
- };
@@ -1 +0,0 @@
1
- export declare const detectLanguage: (filePath: string) => string;
@@ -1,42 +0,0 @@
1
- import path from "node:path";
2
- const EXTENSION_MAP = {
3
- ".ts": "TypeScript",
4
- ".tsx": "TypeScript",
5
- ".js": "JavaScript",
6
- ".jsx": "JavaScript",
7
- ".mjs": "JavaScript",
8
- ".cjs": "JavaScript",
9
- ".py": "Python",
10
- ".go": "Go",
11
- ".rs": "Rust",
12
- ".java": "Java",
13
- ".kt": "Kotlin",
14
- ".c": "C",
15
- ".h": "C",
16
- ".cpp": "C++",
17
- ".hpp": "C++",
18
- ".cs": "C#",
19
- ".rb": "Ruby",
20
- ".php": "PHP",
21
- ".swift": "Swift",
22
- ".dart": "Dart",
23
- ".lua": "Lua",
24
- ".sh": "Shell",
25
- ".bash": "Shell",
26
- ".zsh": "Shell",
27
- ".ps1": "PowerShell",
28
- ".json": "JSON",
29
- ".yml": "YAML",
30
- ".yaml": "YAML",
31
- ".toml": "TOML",
32
- ".md": "Markdown",
33
- ".html": "HTML",
34
- ".css": "CSS",
35
- ".scss": "SCSS",
36
- ".less": "LESS",
37
- ".sql": "SQL",
38
- };
39
- export const detectLanguage = (filePath) => {
40
- const ext = path.extname(filePath).toLowerCase();
41
- return EXTENSION_MAP[ext] ?? "Other";
42
- };
@@ -1,2 +0,0 @@
1
- export declare const resolveProjectRoot: (filePath: string, directories: string[]) => string;
2
- export declare const formatProjectName: (projectRoot: string) => string;
@@ -1,10 +0,0 @@
1
- import path from "node:path";
2
- export const resolveProjectRoot = (filePath, directories) => {
3
- const normalized = path.resolve(filePath);
4
- const match = directories
5
- .map((dir) => path.resolve(dir))
6
- .filter((dir) => normalized.startsWith(dir))
7
- .sort((a, b) => b.length - a.length)[0];
8
- return match ?? path.dirname(normalized);
9
- };
10
- export const formatProjectName = (projectRoot) => path.basename(projectRoot);
@@ -1,8 +0,0 @@
1
- export type DaemonState = {
2
- pid: number;
3
- startedAt: number;
4
- };
5
- export declare const statePath: () => string;
6
- export declare const readState: () => DaemonState | null;
7
- export declare const writeState: (state: DaemonState) => void;
8
- export declare const clearState: () => void;
@@ -1,25 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- export const statePath = () => path.join(os.homedir(), ".hackhours", "state.json");
5
- export const readState = () => {
6
- const file = statePath();
7
- if (!fs.existsSync(file))
8
- return null;
9
- const raw = fs.readFileSync(file, "utf-8");
10
- return JSON.parse(raw);
11
- };
12
- export const writeState = (state) => {
13
- const file = statePath();
14
- const dir = path.dirname(file);
15
- if (!fs.existsSync(dir)) {
16
- fs.mkdirSync(dir, { recursive: true });
17
- }
18
- fs.writeFileSync(file, JSON.stringify(state, null, 2), "utf-8");
19
- };
20
- export const clearState = () => {
21
- const file = statePath();
22
- if (fs.existsSync(file)) {
23
- fs.unlinkSync(file);
24
- }
25
- };
@@ -1,7 +0,0 @@
1
- export declare const minutesToMs: (minutes: number) => number;
2
- export declare const startOfDay: (date: Date) => Date;
3
- export declare const endOfDay: (date: Date) => Date;
4
- export declare const toDateKey: (date: Date) => string;
5
- export declare const parseDateKey: (value: string) => Date;
6
- export declare const formatDuration: (ms: number) => string;
7
- export declare const formatPercent: (value: number) => string;
@@ -1,35 +0,0 @@
1
- export const minutesToMs = (minutes) => minutes * 60 * 1000;
2
- export const startOfDay = (date) => {
3
- const d = new Date(date);
4
- d.setHours(0, 0, 0, 0);
5
- return d;
6
- };
7
- export const endOfDay = (date) => {
8
- const d = new Date(date);
9
- d.setHours(23, 59, 59, 999);
10
- return d;
11
- };
12
- export const toDateKey = (date) => {
13
- const year = date.getFullYear();
14
- const month = `${date.getMonth() + 1}`.padStart(2, "0");
15
- const day = `${date.getDate()}`.padStart(2, "0");
16
- return `${year}-${month}-${day}`;
17
- };
18
- export const parseDateKey = (value) => {
19
- const [year, month, day] = value.split("-").map(Number);
20
- if (!year || !month || !day) {
21
- throw new Error(`Invalid date: ${value}`);
22
- }
23
- return new Date(year, month - 1, day);
24
- };
25
- export const formatDuration = (ms) => {
26
- if (ms <= 0)
27
- return "0m";
28
- const totalMinutes = Math.round(ms / 60000);
29
- const hours = Math.floor(totalMinutes / 60);
30
- const minutes = totalMinutes % 60;
31
- if (hours === 0)
32
- return `${minutes}m`;
33
- return `${hours}h ${minutes}m`;
34
- };
35
- export const formatPercent = (value) => `${(value * 100).toFixed(0)}%`;