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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "timelog",
"version": "0.1.0",
"version": "0.2.0",
"description": "Automatic time tracking for Claude Code sessions. Logs project, ticket, and prompt data as JSONL for timesheet reconstruction.",
"license": "Apache-2.0",
"keywords": [
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,47 @@ immediately with sensible defaults:

No configuration needed for basic use.

## CLI usage

The `claudelog` command lets you run reports
and backfills from any terminal — no active
Claude Code session needed.

```bash
claudelog report --week --by-project
claudelog report --month --timesheet --json
claudelog backfill
claudelog --help
```

### Adding to PATH

The plugin installs to a versioned cache
directory. Pick one of:

**Option A: Symlink (recommended)**

```bash
ln -sf ~/.claude/plugins/cache/\
remotecto-plugins/timelog/*/bin/claudelog \
~/.local/bin/claudelog
```

**Option B: PATH in shell profile**

```bash
# Add to .zshrc or .bashrc
TIMELOG_BIN="$(ls -d \
~/.claude/plugins/cache/\
remotecto-plugins/timelog/*/bin \
2>/dev/null | tail -1)"
[ -n "$TIMELOG_BIN" ] && \
export PATH="$TIMELOG_BIN:$PATH"
```

Either way, `claudelog report --week` then
works from anywhere.

## Getting the best results

### How project detection works
Expand Down
53 changes: 53 additions & 0 deletions bin/claudelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env node

import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const ROOT = dirname(
dirname(fileURLToPath(import.meta.url))
);

const COMMANDS = new Set(['report', 'backfill']);

const USAGE = `Usage: claudelog <command> [options]

Commands:
report Generate time reports
backfill Import historical transcripts

Run claudelog <command> --help for details.

Examples:
claudelog report --week --by-project
claudelog report --month --timesheet
claudelog backfill`;

const cmd = process.argv[2];

if (!cmd || cmd === '--help' || cmd === 'help') {
const out = cmd ? process.stdout : process.stderr;
out.write(USAGE + '\n');
process.exit(cmd ? 0 : 1);
}

if (!COMMANDS.has(cmd)) {
process.stderr.write(
`Unknown command: ${cmd}\n\n${USAGE}\n`
);
process.exit(1);
}

const script = join(
ROOT, 'scripts', `${cmd}.mjs`
);

try {
execFileSync(
process.execPath,
[script, ...process.argv.slice(3)],
{ stdio: 'inherit' }
);
} catch (err) {
process.exit(err.status ?? 1);
}
6 changes: 3 additions & 3 deletions package-lock.json

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

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "claude-code-timelog",
"version": "0.1.0",
"version": "0.2.0",
"type": "module",
"bin": {
"claudelog": "bin/claudelog"
},
"scripts": {
"test": "node --test test/**/*.test.mjs",
"lint": "eslint ."
Expand Down
78 changes: 78 additions & 0 deletions test/bin/claudelog.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const BIN = join(
fileURLToPath(import.meta.url),
'..', '..', '..', 'bin', 'claudelog'
);

const ENV = {
...process.env,
CLAUDE_TIMELOG_DIR: '/tmp/claudelog-test',
};

function run(...args) {
return spawnSync('node', [BIN, ...args], {
encoding: 'utf8',
env: ENV,
timeout: 10000,
});
}

describe('bin/claudelog', () => {
it('prints usage with no arguments', () => {
const r = run();
assert.equal(r.status, 1);
assert.match(r.stderr, /usage:/i);
assert.match(r.stderr, /claudelog/);
assert.match(r.stderr, /report/);
assert.match(r.stderr, /backfill/);
});

it('prints usage with --help', () => {
const r = run('--help');
assert.equal(r.status, 0);
assert.match(r.stdout, /usage:/i);
assert.match(r.stdout, /report/);
assert.match(r.stdout, /backfill/);
});

it('prints usage with help subcommand', () => {
const r = run('help');
assert.equal(r.status, 0);
assert.match(r.stdout, /usage:/i);
});

it('rejects unknown subcommands', () => {
const r = run('nonsense');
assert.equal(r.status, 1);
assert.match(r.stderr, /unknown command/i);
});

it('runs report --help without error', () => {
const r = run('report', '--help');
assert.equal(r.status, 0);
assert.match(r.stdout, /usage:/i);
});

it('forwards exit code from subcommand', () => {
// Far-future range guarantees no data
const r = run(
'report',
'--from', '2099-01-01',
'--to', '2099-01-07'
);
assert.notEqual(r.status, 0);
assert.match(
r.stderr, /no timelog data/i
);
});

it('runs backfill without error', () => {
const r = run('backfill');
assert.equal(r.status, 0);
});
});