diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f8251be..067b8d4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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": [ diff --git a/README.md b/README.md index df2c3da..23c6340 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/claudelog b/bin/claudelog new file mode 100755 index 0000000..2d58504 --- /dev/null +++ b/bin/claudelog @@ -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 [options] + +Commands: + report Generate time reports + backfill Import historical transcripts + +Run claudelog --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); +} diff --git a/package-lock.json b/package-lock.json index 3c81435..4da0e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "claude-code-timelog", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-timelog", - "version": "0.1.0", - "license": "MIT", + "version": "0.2.0", + "license": "Apache-2.0", "devDependencies": { "eslint": "^9" }, diff --git a/package.json b/package.json index 34c2586..6979e07 100644 --- a/package.json +++ b/package.json @@ -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 ." diff --git a/test/bin/claudelog.test.mjs b/test/bin/claudelog.test.mjs new file mode 100644 index 0000000..82ad325 --- /dev/null +++ b/test/bin/claudelog.test.mjs @@ -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); + }); +});