@charan6924/ducky 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.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # 🦆 ducky
2
+
3
+ A CLI tool that passively monitors a developer's local environment to capture signals about AI coding assistant usage during a session.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build
10
+ npm link
11
+ ```
12
+
13
+ After `npm link`, the `ducky` command is available globally.
14
+
15
+ ## Usage
16
+
17
+ ```
18
+ ducky start — begin tracking AI usage in the current directory
19
+ ducky stop — stop tracking and generate ducky-report.json
20
+ ```
21
+
22
+ Tracking runs as a detached background process. The session file is written to `ducky.session.json` every 5 seconds. When stopped, a `ducky-report.json` is generated in the project root.
23
+
24
+ ## How It Works
25
+
26
+ Ducky runs 7 watchers concurrently as a background daemon:
27
+
28
+ | Tracker | Signal Captured | Method |
29
+ |---|---|---|
30
+ | **Process Watcher** | AI-related processes running on the system | Polls `ps aux` every 5s, matches against known AI tool names |
31
+ | **Window Watcher** | Active window title and app name | Polls `osascript` every 5s, checks if the frontmost window matches AI tool patterns |
32
+ | **Filesystem Watcher** | File changes in the project directory | Uses `fs.watch` recursively, logs all file change events |
33
+ | **File Pattern Watcher** | AI-specific file artifacts (.aider, .copilot, CLAUDE.md, etc.) | Scans the project tree every 60s for known AI tool marker files |
34
+ | **Shell History Watcher** | AI commands in shell history | Reads `~/.zsh_history` on start, scans for AI-related commands |
35
+ | **Git Log Watcher** | Git commits with AI-generated messages | Runs `git log`, flags commits containing AI-typical patterns |
36
+ | **Network Watcher** | Network connections to known AI API endpoints | Runs `lsof -i` every 10s, matches against known AI API domains |
37
+
38
+ All tracking is **passive** — it only reads system state and never interferes with the developer's workflow. All data stays **local** — nothing is sent to any external service.
39
+
40
+ ## Report Format
41
+
42
+ `ducky-report.json` contains:
43
+
44
+ - **metadata**: Session start/end time, duration in ms, project directory
45
+ - **tracking**: Per-tracker data with all captured signals
46
+
47
+ ---
48
+
49
+ ## Writeup
50
+
51
+ ### 1. Tracking Approach
52
+
53
+ Ducky focuses on breadth of signal. The seven trackers cover seven distinct attack surfaces:
54
+
55
+ - **Process snapshots** reveal which AI tools are actively running (e.g., Claude Code as a terminal process, Cursor as an Electron app).
56
+ - **Window titles** capture the *context* of AI use — what file is the developer editing when they invoke an AI assistant?
57
+ - **Filesystem changes** log *what* the AI is writing — new files, modified files, deletion patterns.
58
+ - **File pattern scanning** detects AI tool presence even when tools aren't actively running (a `.copilot` config, a `CLAUDE.md` instruction file, a `.aider` file).
59
+ - **Shell history** captures the developer's *interaction style* — do they craft detailed prompts? Do they chain AI calls? Do they use AI to write git commits?
60
+ - **Git log analysis** flags commits that have AI-generated messages — a strong signal of AI-assisted development.
61
+ - **Network connections** reveal which AI *services* are being used (Anthropic API, OpenAI, GitHub Copilot), distinguishing between local and cloud AI tools.
62
+
63
+ The design philosophy is defense-in-depth for signal: any single tracker can be evaded, but seven independent sensors make it very difficult to use AI tools without leaving a trace.
64
+
65
+ ### 2. Why AI Usage Tracking Matters
66
+
67
+ Tracking AI usage evaluates a developer's ability beyond what traditional assessments capture:
68
+
69
+ - **Tool fluency** — Top AI users aren't prompting blindly; they know when to use AI and when to write code themselves. Usage patterns reveal judgment.
70
+ - **Workflow integration** — Does the developer use AI as a crutch (always-on chat, accepting all suggestions) or as a force multiplier (targeted prompts, reviewing outputs critically)?
71
+ - **Burst patterns** — Rapid-fire AI interactions suggest exploration/learning. Long, deliberate prompts suggest experienced orchestration. The ratio between AI interaction and human review is telling.
72
+ - **Attribution awareness** — Developers who mark AI-generated commits vs. those who don't reveals intellectual honesty and understanding of code ownership.
73
+
74
+ Traditional assessments (resumes, interviews, whiteboard coding) don't capture how someone *actually* builds software day-to-day. AI usage signals are a window into genuine engineering habits.
75
+
76
+ ### 3. Limitations & Extensions
77
+
78
+ **Current Limitations:**
79
+
80
+ - **macOS-only** — Several trackers (osascript for windows, lsof for network) are macOS-specific. Linux/Windows support would require platform-specific alternatives.
81
+ - **No editor plugin** — Tracking happens at the OS level, not inside the editor. An IDE extension could capture inline completions (Copilot, TabNine) that leave no process or window trace.
82
+ - **Shell history only on start** — Currently snapshots `.zsh_history` once at startup. A continuous tail would capture commands issued during the session.
83
+ - **False positives** — Substring matching on process names and window titles can trigger on non-AI tools. A deny-list or confidence scoring would improve accuracy.
84
+
85
+ **Desired Extensions (no constraints):**
86
+
87
+ 1. **Editor plugin (VS Code, JetBrains)** — Track inline completion acceptance/rejection rates, prompt lengths, file-level attribution. This is the richest signal and what I'd prioritize first.
88
+ 2. **Clipboard monitoring** — AI tools frequently copy-paste code into editors. Tracking clipboard origin and destination would catch AI use invisible to process/window watchers.
89
+ 3. **Browser extension** — Capture ChatGPT/Claude.ai web interactions, prompt content, code snippet copy events. This would catch web-based AI usage that leaves no local process trace.
90
+ 4. **Keystroke dynamics** — Measure paste vs. type ratios. AI-generated code is pasted, not typed. High paste-to-type ratio is a strong heuristic.
91
+ 5. **Prompt caching analysis** — Monitor terminal scrollback and editor temporary files to reconstruct prompt context. This would reveal *how* the developer structures prompts, not just *that* they used AI.
92
+ 6. **AI-powered classification** — Use an LLM to classify code as AI-generated vs. human-written based on style, comment patterns, naming conventions. This would catch AI use even when no known tool name appears.
@@ -0,0 +1 @@
1
+ export declare function start(): void;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.start = start;
4
+ const child_process_1 = require("child_process");
5
+ const path_1 = require("path");
6
+ const fs_1 = require("fs");
7
+ function start() {
8
+ const pidPath = '.ducky.pid';
9
+ if ((0, fs_1.existsSync)(pidPath)) {
10
+ const existingPid = Number((0, fs_1.readFileSync)(pidPath, 'utf-8'));
11
+ try {
12
+ // if kill with 0 signal works, process is running
13
+ process.kill(existingPid, 0);
14
+ console.log('ducky is already tracking (pid ' + existingPid + ')');
15
+ return;
16
+ }
17
+ catch {
18
+ // process not running — stale PID file, clean it up
19
+ (0, fs_1.unlinkSync)(pidPath);
20
+ }
21
+ }
22
+ const child = (0, child_process_1.fork)((0, path_1.join)(__dirname, '../daemon.js'), [], {
23
+ stdio: 'ignore',
24
+ detached: true,
25
+ });
26
+ child.unref();
27
+ (0, fs_1.writeFileSync)(pidPath, String(child.pid));
28
+ console.log('ducky started (pid ' + child.pid + ')');
29
+ process.exit(0);
30
+ }
@@ -0,0 +1 @@
1
+ export declare function stop(): void;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stop = stop;
4
+ const fs_1 = require("fs");
5
+ const reporter_js_1 = require("../reporter.js");
6
+ function isValidPid(value) {
7
+ return Number.isInteger(value) && value > 0;
8
+ }
9
+ function stop() {
10
+ const pidPath = '.ducky.pid';
11
+ if (!(0, fs_1.existsSync)(pidPath)) {
12
+ console.log('no active ducky session');
13
+ return;
14
+ }
15
+ const raw = (0, fs_1.readFileSync)(pidPath, 'utf-8').trim();
16
+ const pid = Number(raw);
17
+ if (!isValidPid(pid)) {
18
+ console.log('invalid PID file, removing...');
19
+ (0, fs_1.unlinkSync)(pidPath);
20
+ return;
21
+ }
22
+ try {
23
+ process.kill(pid, 'SIGTERM');
24
+ }
25
+ catch (err) {
26
+ const nodeErr = err;
27
+ if (nodeErr.code === 'ESRCH') {
28
+ console.log('tracking process already exited');
29
+ }
30
+ else {
31
+ console.log('could not stop tracking process: ' + (nodeErr.message || err));
32
+ return;
33
+ }
34
+ }
35
+ // give daemon time to flush session data
36
+ const deadline = Date.now() + 3000;
37
+ while (Date.now() < deadline) {
38
+ try {
39
+ process.kill(pid, 0);
40
+ }
41
+ catch {
42
+ break; // process exited
43
+ }
44
+ }
45
+ if ((0, fs_1.existsSync)('ducky.session.json')) {
46
+ (0, reporter_js_1.generateReport)();
47
+ }
48
+ (0, fs_1.unlinkSync)(pidPath);
49
+ }
@@ -0,0 +1,2 @@
1
+ export declare function run(): void;
2
+ export declare function shutdown(): void;
package/dist/daemon.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.run = run;
4
+ exports.shutdown = shutdown;
5
+ const processes_js_1 = require("./trackers/processes.js");
6
+ const filesystem_js_1 = require("./trackers/filesystem.js");
7
+ const shell_history_js_1 = require("./trackers/shell-history.js");
8
+ const windows_js_1 = require("./trackers/windows.js");
9
+ const git_log_js_1 = require("./trackers/git-log.js");
10
+ const file_patterns_js_1 = require("./trackers/file-patterns.js");
11
+ const network_js_1 = require("./trackers/network.js");
12
+ const storage_js_1 = require("./storage.js");
13
+ const trackers = [
14
+ new processes_js_1.ProcessWatcher(),
15
+ new filesystem_js_1.FilesystemWatcher(),
16
+ new shell_history_js_1.ShellHistoryWatcher(),
17
+ new windows_js_1.WindowWatcher(),
18
+ new git_log_js_1.GitLogWatcher(),
19
+ new file_patterns_js_1.FilePatternWatcher(),
20
+ new network_js_1.NetworkWatcher(),
21
+ ];
22
+ let saveInterval = null;
23
+ function run() {
24
+ (0, storage_js_1.initSession)(process.cwd());
25
+ for (const tracker of trackers) {
26
+ tracker.start();
27
+ }
28
+ saveInterval = setInterval(() => (0, storage_js_1.writeSession)(trackers), 5000);
29
+ }
30
+ function shutdown() {
31
+ if (saveInterval) {
32
+ clearInterval(saveInterval);
33
+ saveInterval = null;
34
+ }
35
+ (0, storage_js_1.writeSession)(trackers, true);
36
+ for (const tracker of trackers) {
37
+ tracker.stop();
38
+ }
39
+ }
40
+ process.on('SIGTERM', () => {
41
+ shutdown();
42
+ process.exit(0);
43
+ });
44
+ run();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /// <reference types="node" />
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ const start_js_1 = require("./commands/start.js");
6
+ const stop_js_1 = require("./commands/stop.js");
7
+ function printHelp() {
8
+ console.log(`
9
+ Usage: ducky <command>
10
+
11
+ Commands:
12
+ start Begin tracking AI usage in the current directory
13
+ stop Stop tracking and save ducky-report.json
14
+
15
+ Options:
16
+ --help Show this message
17
+ `);
18
+ }
19
+ function main() {
20
+ const command = process.argv[2];
21
+ switch (command) {
22
+ case 'start':
23
+ (0, start_js_1.start)();
24
+ break;
25
+ case 'stop':
26
+ (0, stop_js_1.stop)();
27
+ break;
28
+ case '--help':
29
+ case undefined:
30
+ printHelp();
31
+ break;
32
+ default:
33
+ console.error(`Unknown command: ${command}`);
34
+ printHelp();
35
+ process.exit(1);
36
+ }
37
+ }
38
+ main();
@@ -0,0 +1 @@
1
+ export declare function generateReport(): void;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateReport = generateReport;
4
+ const fs_1 = require("fs");
5
+ function generateReport() {
6
+ let session;
7
+ try {
8
+ const raw = (0, fs_1.readFileSync)('ducky.session.json', 'utf-8');
9
+ session = JSON.parse(raw);
10
+ }
11
+ catch {
12
+ console.error('failed to read session data \u2014 is ducky running?');
13
+ return;
14
+ }
15
+ const startMs = new Date(session.metadata.startTime).getTime();
16
+ const endMs = new Date(session.metadata.endTime).getTime();
17
+ const report = {
18
+ metadata: {
19
+ startTime: session.metadata.startTime,
20
+ endTime: session.metadata.endTime,
21
+ durationsMs: endMs - startMs,
22
+ projectDir: session.metadata.projectDir,
23
+ },
24
+ tracking: session.tracking,
25
+ };
26
+ (0, fs_1.writeFileSync)('ducky-report.json', JSON.stringify(report, null, 2));
27
+ printSummary(report);
28
+ }
29
+ function printSummary(report) {
30
+ const durationSec = Math.round(report.metadata.durationsMs / 1000);
31
+ const trackerCount = Object.keys(report.tracking).length;
32
+ console.log('\nducky session complete');
33
+ console.log('duration: ' + durationSec + 's');
34
+ console.log('trackers: ' + trackerCount);
35
+ console.log('report: ducky-report.json\n');
36
+ }
@@ -0,0 +1,3 @@
1
+ import { TrackerInterface } from './trackers/base.js';
2
+ export declare function initSession(dir: string): void;
3
+ export declare function writeSession(trackers: TrackerInterface[], isFinal?: boolean): void;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initSession = initSession;
4
+ exports.writeSession = writeSession;
5
+ const fs_1 = require("fs");
6
+ let startTime = '';
7
+ let projectDir = '';
8
+ function initSession(dir) {
9
+ startTime = new Date().toISOString();
10
+ projectDir = dir;
11
+ }
12
+ function writeSession(trackers, isFinal = false) {
13
+ const data = {
14
+ metadata: {
15
+ startTime,
16
+ endTime: isFinal ? new Date().toISOString() : '',
17
+ projectDir,
18
+ },
19
+ tracking: Object.fromEntries(trackers.map(t => [t.name, t.getData()])),
20
+ };
21
+ (0, fs_1.writeFileSync)('ducky.session.json', JSON.stringify(data, null, 2));
22
+ }
@@ -0,0 +1,6 @@
1
+ export interface TrackerInterface {
2
+ readonly name: string;
3
+ start(): void;
4
+ stop(): void;
5
+ getData(): Record<string, unknown>;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class FilePatternWatcher implements TrackerInterface {
3
+ readonly name = "file-patterns";
4
+ private cachedData;
5
+ private lastScan;
6
+ private readonly scanInterval;
7
+ start(): void;
8
+ stop(): void;
9
+ getData(): Record<string, unknown>;
10
+ private walkDir;
11
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilePatternWatcher = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const AI_FILE_PATTERNS = ['generated', 'ai-', '.cursor', '.copilot-'];
7
+ class FilePatternWatcher {
8
+ constructor() {
9
+ this.name = 'file-patterns';
10
+ this.cachedData = null;
11
+ this.lastScan = 0;
12
+ this.scanInterval = 60000;
13
+ }
14
+ start() {
15
+ // static analysis — work happens in getData()
16
+ }
17
+ stop() {
18
+ // no-op
19
+ }
20
+ getData() {
21
+ const now = Date.now();
22
+ if (this.cachedData && now - this.lastScan < this.scanInterval) {
23
+ return this.cachedData;
24
+ }
25
+ const files = this.walkDir(process.cwd());
26
+ // files with AI-associated naming patterns
27
+ const aiNamed = files.filter(f => AI_FILE_PATTERNS.some(p => (0, path_1.basename)(f).toLowerCase().includes(p)));
28
+ // burst detection: 3+ files created within 60s
29
+ const withTimes = files
30
+ .map(f => {
31
+ try {
32
+ const s = (0, fs_1.statSync)(f);
33
+ return { file: f, birthtime: s.birthtime.getTime() };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ })
39
+ .filter((f) => f !== null)
40
+ .sort((a, b) => a.birthtime - b.birthtime);
41
+ const bursts = [];
42
+ let i = 0;
43
+ while (i < withTimes.length - 2) {
44
+ const windowEnd = withTimes[i + 2].birthtime;
45
+ if (windowEnd - withTimes[i].birthtime < 60000) {
46
+ let j = i + 3;
47
+ while (j < withTimes.length && withTimes[j].birthtime - withTimes[i].birthtime < 60000)
48
+ j++;
49
+ const burst = withTimes.slice(i, j);
50
+ bursts.push({
51
+ startTime: new Date(burst[0].birthtime).toISOString(),
52
+ count: burst.length,
53
+ files: burst.map(f => f.file),
54
+ });
55
+ i = j;
56
+ }
57
+ else {
58
+ i++;
59
+ }
60
+ }
61
+ this.cachedData = {
62
+ totalFiles: files.length,
63
+ aiNamedFiles: aiNamed.length,
64
+ aiNamedExamples: aiNamed.slice(0, 20),
65
+ burstsDetected: bursts.length,
66
+ burstWindows: bursts,
67
+ };
68
+ this.lastScan = now;
69
+ return this.cachedData;
70
+ }
71
+ walkDir(dir) {
72
+ const result = [];
73
+ try {
74
+ for (const entry of (0, fs_1.readdirSync)(dir, { withFileTypes: true })) {
75
+ const full = (0, path_1.join)(dir, entry.name);
76
+ if (entry.isDirectory()) {
77
+ if (entry.name !== '.git' && entry.name !== 'node_modules') {
78
+ result.push(...this.walkDir(full));
79
+ }
80
+ }
81
+ else {
82
+ result.push(full);
83
+ }
84
+ }
85
+ }
86
+ catch {
87
+ // skip dirs we can't read
88
+ }
89
+ return result;
90
+ }
91
+ }
92
+ exports.FilePatternWatcher = FilePatternWatcher;
@@ -0,0 +1,13 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class FilesystemWatcher implements TrackerInterface {
3
+ readonly name = "file systems";
4
+ private watcher;
5
+ private totalEvents;
6
+ private changesByType;
7
+ private changesByExt;
8
+ private recentEvents;
9
+ private readonly ignorePatterns;
10
+ start(): void;
11
+ stop(): void;
12
+ getData(): Record<string, unknown>;
13
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FilesystemWatcher = void 0;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ class FilesystemWatcher {
7
+ constructor() {
8
+ this.name = "file systems";
9
+ this.watcher = null;
10
+ this.totalEvents = 0;
11
+ this.changesByType = {};
12
+ this.changesByExt = {};
13
+ this.recentEvents = [];
14
+ this.ignorePatterns = /ducky[.-](report|session)\.json|\.ducky\.pid/;
15
+ }
16
+ start() {
17
+ this.watcher = (0, fs_1.watch)(process.cwd(), { recursive: true }, (eventType, filename) => {
18
+ this.watcher.on('error', () => { });
19
+ if (!filename)
20
+ return;
21
+ if (this.ignorePatterns.test(String(filename)))
22
+ return;
23
+ this.totalEvents++;
24
+ this.changesByType[eventType] = (this.changesByType[eventType] || 0) + 1;
25
+ const ext = (0, path_1.extname)(String(filename)).toLowerCase();
26
+ if (ext) {
27
+ this.changesByExt[ext] = (this.changesByExt[ext] || 0) + 1;
28
+ }
29
+ this.recentEvents.push({ file: String(filename), type: eventType, timestamp: new Date().toISOString() });
30
+ if (this.recentEvents.length > 100)
31
+ this.recentEvents.shift();
32
+ });
33
+ }
34
+ stop() {
35
+ try {
36
+ if (this.watcher) {
37
+ this.watcher.close();
38
+ this.watcher = null;
39
+ }
40
+ }
41
+ catch {
42
+ // do nothign watcher doesnt exist
43
+ }
44
+ }
45
+ getData() {
46
+ return {
47
+ totalEvents: this.totalEvents,
48
+ changesByType: this.changesByType,
49
+ changesByExt: this.changesByExt,
50
+ recentEvents: this.recentEvents,
51
+ };
52
+ }
53
+ }
54
+ exports.FilesystemWatcher = FilesystemWatcher;
@@ -0,0 +1,11 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class GitLogWatcher implements TrackerInterface {
3
+ readonly name = "git logs";
4
+ private commits;
5
+ private totalCommits;
6
+ private aiAttributed;
7
+ private burstsDetected;
8
+ start(): void;
9
+ stop(): void;
10
+ getData(): Record<string, unknown>;
11
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitLogWatcher = void 0;
4
+ const child_process_1 = require("child_process");
5
+ class GitLogWatcher {
6
+ constructor() {
7
+ this.name = "git logs";
8
+ this.commits = [];
9
+ this.totalCommits = 0;
10
+ this.aiAttributed = 0;
11
+ this.burstsDetected = 0;
12
+ }
13
+ start() {
14
+ try {
15
+ const projectDir = process.cwd();
16
+ const log = (0, child_process_1.execSync)('git log -30 --format="%H%n%ai%n%s%n%B%x00"', { cwd: projectDir }).toString();
17
+ const commits = [];
18
+ for (const record of log.split('\0').filter(r => r.trim())) {
19
+ const lines = record.split('\n');
20
+ const hash = lines[0] || '';
21
+ const date = lines[1] || '';
22
+ const subject = lines[2] || '';
23
+ const body = lines.slice(3).join('\n');
24
+ const message = subject + '\n' + body;
25
+ const aiAttributed = message.includes('Co-Authored-By');
26
+ commits.push({ hash, message, aiAttributed, date });
27
+ }
28
+ this.commits = commits;
29
+ this.totalCommits = commits.length;
30
+ this.aiAttributed = commits.filter(c => c.aiAttributed).length;
31
+ // burst detection
32
+ let bursts = 0;
33
+ for (let i = 0; i < commits.length - 2; i++) {
34
+ const t1 = new Date(commits[i].date).getTime();
35
+ const t3 = new Date(commits[i + 2].date).getTime();
36
+ if (t1 - t3 < 60000)
37
+ bursts++;
38
+ }
39
+ this.burstsDetected = bursts;
40
+ }
41
+ catch {
42
+ // not a git repo track nothing
43
+ }
44
+ }
45
+ stop() {
46
+ }
47
+ getData() {
48
+ return {
49
+ totalCommits: this.totalCommits,
50
+ aiAttributed: this.aiAttributed,
51
+ burstsDetected: this.burstsDetected,
52
+ commits: this.commits,
53
+ };
54
+ }
55
+ }
56
+ exports.GitLogWatcher = GitLogWatcher;
@@ -0,0 +1,11 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class NetworkWatcher implements TrackerInterface {
3
+ readonly name = "network";
4
+ private intervalId;
5
+ private connections;
6
+ private totalSamples;
7
+ private readonly maxConnections;
8
+ start(): void;
9
+ stop(): void;
10
+ getData(): Record<string, unknown>;
11
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetworkWatcher = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const AI_ENDPOINTS = [
6
+ 'api.anthropic.com',
7
+ 'api.openai.com',
8
+ 'api.github.com',
9
+ 'copilot-proxy.githubusercontent.com',
10
+ 'cursor.sh',
11
+ 'ai.google.dev',
12
+ 'api.perplexity.ai',
13
+ 'api.deepseek.com'
14
+ ];
15
+ class NetworkWatcher {
16
+ constructor() {
17
+ this.name = 'network';
18
+ this.intervalId = null;
19
+ this.connections = [];
20
+ this.totalSamples = 0;
21
+ this.maxConnections = 1000;
22
+ }
23
+ start() {
24
+ const poll = () => {
25
+ this.totalSamples++;
26
+ try {
27
+ const output = (0, child_process_1.execSync)('lsof -i 2>/dev/null', { timeout: 3000 }).toString();
28
+ for (const endpoint of AI_ENDPOINTS) {
29
+ const regex = new RegExp(`(\\S+)\\s+\\d+.*${endpoint.replace(/\./g, '\\.')}`, 'i');
30
+ const match = output.match(regex);
31
+ if (match && match[1]) {
32
+ if (this.connections.length >= this.maxConnections) {
33
+ this.connections.splice(0, this.connections.length - this.maxConnections + 1);
34
+ }
35
+ this.connections.push({
36
+ process: match[1],
37
+ endpoint,
38
+ timestamp: new Date().toISOString(),
39
+ });
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ // lsof not available or failed
45
+ }
46
+ };
47
+ poll();
48
+ this.intervalId = setInterval(poll, 10000);
49
+ }
50
+ stop() {
51
+ if (this.intervalId) {
52
+ clearInterval(this.intervalId);
53
+ this.intervalId = null;
54
+ }
55
+ }
56
+ getData() {
57
+ const uniqueEndpoints = [...new Set(this.connections.map(c => c.endpoint))];
58
+ const processes = [...new Set(this.connections.map(c => c.process))];
59
+ return {
60
+ aiConnections: this.connections.length,
61
+ totalSamples: this.totalSamples,
62
+ endpoints: uniqueEndpoints,
63
+ processes,
64
+ connections: this.connections,
65
+ };
66
+ }
67
+ }
68
+ exports.NetworkWatcher = NetworkWatcher;
@@ -0,0 +1,11 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class ProcessWatcher implements TrackerInterface {
3
+ readonly name = "processes";
4
+ private intervalId;
5
+ private detections;
6
+ private totalSamples;
7
+ private readonly maxDetections;
8
+ start(): void;
9
+ stop(): void;
10
+ getData(): Record<string, unknown>;
11
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessWatcher = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const AI_TOOLS = ['copilot', 'claude', 'cursor', 'codeium', 'tabnine', 'codex'];
6
+ class ProcessWatcher {
7
+ constructor() {
8
+ this.name = 'processes';
9
+ this.intervalId = null;
10
+ this.detections = [];
11
+ this.totalSamples = 0;
12
+ this.maxDetections = 1000;
13
+ }
14
+ start() {
15
+ const poll = () => {
16
+ this.totalSamples++;
17
+ const output = (0, child_process_1.execSync)('ps aux').toString();
18
+ for (const tool of AI_TOOLS) {
19
+ const regex = new RegExp(`\\b${tool}\\b`, 'i');
20
+ if (regex.test(output)) {
21
+ if (this.detections.length >= this.maxDetections) {
22
+ this.detections.splice(0, this.detections.length - this.maxDetections + 1);
23
+ }
24
+ this.detections.push({ tool, timestamp: new Date().toISOString() });
25
+ }
26
+ }
27
+ };
28
+ poll(); // run immediately
29
+ this.intervalId = setInterval(poll, 5000);
30
+ }
31
+ stop() {
32
+ if (this.intervalId) {
33
+ clearInterval(this.intervalId);
34
+ this.intervalId = null;
35
+ }
36
+ }
37
+ getData() {
38
+ const unique = [...new Set(this.detections.map(d => d.tool))];
39
+ return {
40
+ detected: unique,
41
+ totalSamples: this.totalSamples,
42
+ detections: this.detections,
43
+ };
44
+ }
45
+ }
46
+ exports.ProcessWatcher = ProcessWatcher;
@@ -0,0 +1,8 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class ShellHistoryWatcher implements TrackerInterface {
3
+ readonly name = "shell history";
4
+ private detections;
5
+ start(): void;
6
+ stop(): void;
7
+ getData(): Record<string, unknown>;
8
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShellHistoryWatcher = void 0;
4
+ const fs_1 = require("fs");
5
+ const os_1 = require("os");
6
+ const AI_COMMANDS = ['claude', 'cursor', 'copilot', 'chatgpt', 'gpt', 'codeium', 'tabnine'];
7
+ class ShellHistoryWatcher {
8
+ constructor() {
9
+ this.name = "shell history";
10
+ this.detections = [];
11
+ }
12
+ start() {
13
+ try {
14
+ const historyPath = (0, os_1.homedir)() + '/.zsh_history';
15
+ const content = (0, fs_1.readFileSync)(historyPath, 'utf-8');
16
+ for (const line of content.trim().split('\n')) {
17
+ const semiIndex = line.indexOf(';');
18
+ if (semiIndex === -1)
19
+ continue;
20
+ const command = line.slice(semiIndex + 1);
21
+ if (AI_COMMANDS.some(c => command.includes(c))) {
22
+ this.detections.push({ command, timestamp: new Date().toISOString() });
23
+ }
24
+ }
25
+ }
26
+ catch {
27
+ // do nothing
28
+ }
29
+ }
30
+ stop() {
31
+ }
32
+ getData() {
33
+ return {
34
+ detections: this.detections,
35
+ };
36
+ }
37
+ }
38
+ exports.ShellHistoryWatcher = ShellHistoryWatcher;
@@ -0,0 +1,12 @@
1
+ import { TrackerInterface } from './base.js';
2
+ export declare class WindowWatcher implements TrackerInterface {
3
+ readonly name = "open windows";
4
+ private totalSamples;
5
+ private currentApp;
6
+ private currentTitle;
7
+ private detections;
8
+ private intervalId;
9
+ start(): void;
10
+ stop(): void;
11
+ getData(): Record<string, unknown>;
12
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WindowWatcher = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const aiWindows = [
6
+ 'cursor', 'claude', 'chatgpt', 'copilot', 'codeium', 'tabnine',
7
+ 'chat.openai.com', 'anthropic',
8
+ ];
9
+ class WindowWatcher {
10
+ constructor() {
11
+ this.name = "open windows";
12
+ this.totalSamples = 0;
13
+ this.currentApp = null;
14
+ this.currentTitle = null;
15
+ this.detections = [];
16
+ this.intervalId = null;
17
+ }
18
+ start() {
19
+ const poll = () => {
20
+ try {
21
+ const output = (0, child_process_1.execSync)('osascript -e tell application "System Events" to get {name, title} of first application process whose frontmost is true ').toString().trim();
22
+ const commaIndex = output.indexOf(', ');
23
+ const app = output.slice(0, commaIndex);
24
+ const title = output.slice(commaIndex + 2);
25
+ this.totalSamples++;
26
+ this.currentApp = app;
27
+ this.currentTitle = title;
28
+ for (const window of aiWindows) {
29
+ const lowerApp = app.toLowerCase();
30
+ const lowerTitle = title.toLowerCase();
31
+ if (lowerApp.includes(window) || lowerTitle.includes(window)) {
32
+ this.detections.push({ app, title, timestamp: new Date().toISOString() });
33
+ }
34
+ }
35
+ }
36
+ catch {
37
+ // do nothing
38
+ }
39
+ };
40
+ poll();
41
+ this.intervalId = setInterval(poll, 5000);
42
+ }
43
+ stop() {
44
+ if (this.intervalId) {
45
+ clearInterval(this.intervalId);
46
+ this.intervalId = null;
47
+ }
48
+ }
49
+ getData() {
50
+ return {
51
+ totalSamples: this.totalSamples,
52
+ currentApp: this.currentApp,
53
+ currentTitle: this.currentTitle,
54
+ aiDetections: this.detections
55
+ };
56
+ }
57
+ }
58
+ exports.WindowWatcher = WindowWatcher;
@@ -0,0 +1,10 @@
1
+ export interface SessionMetaData {
2
+ startTime: string;
3
+ endTime: string;
4
+ durationsMs: number;
5
+ projectDir: string;
6
+ }
7
+ export interface DuckyReport {
8
+ metadata: SessionMetaData;
9
+ tracking: Record<string, unknown>;
10
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@charan6924/ducky",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool that passively monitors a developer's local environment to capture signals about AI coding assistant usage during a session.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "ducky": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Charan6924/Ducky.git"
18
+ },
19
+ "files": ["dist/"],
20
+ "keywords": ["ai", "tracking", "cli", "developer-tools"],
21
+ "author": "Charan6924",
22
+ "license": "MIT",
23
+ "publishConfig": { "access": "public" },
24
+ "type": "commonjs",
25
+ "bugs": {
26
+ "url": "https://github.com/Charan6924/Ducky/issues"
27
+ },
28
+ "homepage": "https://github.com/Charan6924/Ducky#readme",
29
+ "devDependencies": {
30
+ "@types/node": "^25.7.0",
31
+ "typescript": "^6.0.3",
32
+ "vitest": "^4.1.6"
33
+ }
34
+ }