@profullstack/timer 0.1.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/AGENTS.md ADDED
@@ -0,0 +1,103 @@
1
+ # timer, for agents
2
+
3
+ `timer` is a time tracker whose second audience is a program. This file is the
4
+ contract.
5
+
6
+ ## The rules
7
+
8
+ 1. **`--json` works on every command.** It prints one JSON document on stdout
9
+ and nothing else.
10
+ 2. **Advisories go to stderr.** `timer start` may warn that another clock is
11
+ running; that never appears on stdout.
12
+ 3. **A failed run prints nothing on stdout.** The error goes to stderr as
13
+ `{"error": "...", "kind": "..."}`. Parsing stdout can never give you a
14
+ success shape for a command that failed.
15
+ 4. **Exit codes mean something.**
16
+
17
+ | Code | Meaning |
18
+ | --- | --- |
19
+ | 0 | success |
20
+ | 1 | runtime failure (unreadable timesheet, locked file) |
21
+ | 2 | bad command line (unknown flag, missing argument, impossible range) |
22
+ | 3 | you named an entry that does not exist |
23
+
24
+ 5. **An unknown flag is an error**, never silently ignored. If `timer log
25
+ --bogus` exits 0 you are on a different tool.
26
+
27
+ ## The entry schema
28
+
29
+ Everything `--json` returns is built from this shape:
30
+
31
+ ```json
32
+ {
33
+ "id": "4f2ap8qk",
34
+ "project": "acme",
35
+ "task": "fix the login redirect",
36
+ "tags": ["dev", "api"],
37
+ "start": "2026-08-29T09:00:00.000Z",
38
+ "end": "2026-08-29T10:30:00.000Z",
39
+ "running": false,
40
+ "seconds": 5400,
41
+ "hours": 1.5,
42
+ "billable": true,
43
+ "rate": null,
44
+ "agent": "claude-opus-5",
45
+ "notes": "",
46
+ "meta": {}
47
+ }
48
+ ```
49
+
50
+ `end` is `null` while the clock runs, and `seconds` counts up to now.
51
+ `hours` is rounded to two decimals — it is the number that goes on an invoice.
52
+
53
+ ## Clocking your own work
54
+
55
+ ```sh
56
+ export TIMER_AGENT="claude-opus-5"
57
+ ID=$(timer start acme --task "refactor auth" --json | jq -r .started.id)
58
+ # ... do the work ...
59
+ timer stop --id "$ID" --json
60
+ ```
61
+
62
+ Several clocks may run at once, which is the point: parallel agents each track
63
+ their own work and do not stop each other. Use `--switch` only if you mean to
64
+ close everyone else's clock.
65
+
66
+ If you did the work before you thought to time it, record it after the fact:
67
+
68
+ ```sh
69
+ timer add acme --task "refactor auth" --duration 45m --json
70
+ ```
71
+
72
+ ## Attaching your own identifiers
73
+
74
+ `--meta` takes a JSON object and stores it unchanged:
75
+
76
+ ```sh
77
+ timer add acme --duration 20m --meta '{"repo":"acme/api","pr":42}' --json
78
+ ```
79
+
80
+ Filter later with `timer log --agent claude-opus-5 --json`.
81
+
82
+ ## Reading the timesheet directly
83
+
84
+ If you would rather not shell out, the file is plain JSON at
85
+ `~/.profullstack/timer/timesheet.json` (or `$TIMER_DATA`):
86
+
87
+ ```json
88
+ { "version": 1, "entries": [ /* raw entries */ ] }
89
+ ```
90
+
91
+ Two cautions. Raw entries have no `seconds`/`hours`/`running` — those are
92
+ computed. And writes are locked: if you write the file yourself while a `timer`
93
+ process is running you can lose an entry. Prefer the CLI for writes and the
94
+ file for reads.
95
+
96
+ ## Billing the hours
97
+
98
+ `@profullstack/billing` reads the same file and will not bill the same entry
99
+ twice:
100
+
101
+ ```sh
102
+ billing invoice new --client acme --from-timer --month --json
103
+ ```
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Profullstack, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # timer
2
+
3
+ Track time against projects, from the terminal, on Linux, macOS and Windows.
4
+
5
+ It is a stopwatch with a memory: start a clock on a project, stop it, and get
6
+ billable hours back. Every command also speaks `--json`, so a coding agent can
7
+ clock its own work the same way you do — and
8
+ [`@profullstack/billing`](https://github.com/profullstack/billing) turns those
9
+ hours into invoices.
10
+
11
+ ```sh
12
+ npm install -g @profullstack/timer
13
+ ```
14
+
15
+ Node 20.11 or newer. No runtime dependencies.
16
+
17
+ ## Use it
18
+
19
+ ```sh
20
+ timer start acme fix the login redirect # everything after the project is the task
21
+ timer status # what is running, and today's total
22
+ timer stop
23
+
24
+ timer add acme code review --duration 45m # time you forgot to clock
25
+ timer add acme --from 09:00 --to 11:30
26
+
27
+ timer log --week # what happened
28
+ timer report --month --group project # what it adds up to
29
+ timer projects # everything you have ever tracked
30
+ ```
31
+
32
+ Nothing is configured before first use. The timesheet appears the first time
33
+ you start a clock.
34
+
35
+ ## Commands
36
+
37
+ | Command | What it does |
38
+ | --- | --- |
39
+ | `start <project> [task…]` | Start a clock. `--at 09:15`, `--at -20m`, `--tag`, `--note`, `--rate`, `--switch` |
40
+ | `stop [id]` | Stop the newest clock, an id, `--project <p>`, or `--all` |
41
+ | `status` | Running clocks and today's total |
42
+ | `log [project]` | List entries in a window |
43
+ | `add <project> [task…]` | Record untimed work from two of `--from` / `--to` / `--duration` |
44
+ | `edit <id>` | Change any field of an entry |
45
+ | `rm <id…>` | Delete entries (`--force` for a running one) |
46
+ | `resume [id]` | Start a fresh clock like the last one |
47
+ | `note <text…>` | Append a note to the running clock |
48
+ | `report` | Totals, `--group project\|task\|tag\|day\|agent\|none` |
49
+ | `projects` | Projects seen, with totals and last activity |
50
+ | `export` | `--format json\|ndjson\|csv`, `--out <file>` |
51
+ | `config` | Where the timesheet lives |
52
+
53
+ `timer help <command>` prints the flags and examples for one command.
54
+
55
+ ### Windows of time
56
+
57
+ `log`, `report`, `projects` and `export` all take the same window flags:
58
+ `--today`, `--yesterday`, `--week` (from Monday), `--month`, `--year`, or an
59
+ explicit `--since` / `--until`.
60
+
61
+ Dates are loose on purpose: `09:15`, `2026-08-01`, `-2h`, `yesterday`, or a
62
+ full ISO instant. A bare date means local midnight, not UTC midnight.
63
+
64
+ A window compares against the entry's **start**, and `--until` is exclusive. An
65
+ entry that runs past midnight therefore belongs to the day it began on — which
66
+ is what keeps a total from being counted twice.
67
+
68
+ ### Billable and not
69
+
70
+ Every entry is billable unless you say otherwise with `--no-billable`. Reports
71
+ carry both numbers, because "how long did this take" and "what can I charge for
72
+ it" are different questions:
73
+
74
+ ```sh
75
+ timer add internal standup --duration 15m --no-billable
76
+ timer report --month
77
+ ```
78
+
79
+ ## For agents
80
+
81
+ Two things make this usable by an agentic CLI without a wrapper.
82
+
83
+ **Every command answers `--json`** with a single JSON document on stdout and
84
+ nothing else. Advisory lines go to stderr. A command that fails prints its
85
+ error as JSON on **stderr** and leaves stdout empty, so parsing stdout can
86
+ never yield a success shape for a failed run.
87
+
88
+ **Exit codes are distinct**: `0` success, `1` a runtime failure, `2` a bad
89
+ command line, `3` you named something that is not there. `timer stop` with no
90
+ clock running is a `0` — that is an answer, not a failure.
91
+
92
+ ```sh
93
+ export TIMER_AGENT="claude-opus-5" # stamps every entry it creates
94
+ timer start acme --task "refactor auth" --json
95
+ timer stop --json
96
+ timer log --today --json
97
+ ```
98
+
99
+ `--meta '{"pr":42}'` hangs your own identifiers off an entry, and they survive
100
+ round-trip unchanged.
101
+
102
+ There is more detail, including the entry schema, in [AGENTS.md](AGENTS.md).
103
+
104
+ ## Where the data lives
105
+
106
+ One JSON file, in one place on every platform:
107
+
108
+ ```
109
+ ~/.profullstack/timer/timesheet.json
110
+ ```
111
+
112
+ | Variable | Overrides |
113
+ | --- | --- |
114
+ | `TIMER_DATA` | the timesheet file itself |
115
+ | `TIMER_HOME` | the directory it sits in |
116
+ | `PROFULLSTACK_HOME` | the parent shared with other Profullstack CLIs |
117
+ | `TIMER_AGENT` | the agent name stamped on new entries |
118
+ | `NO_COLOR` | turns off colour |
119
+
120
+ `timer config` prints all of it. The file is plain JSON you can read and edit;
121
+ writes are atomic and locked, so several agents can clock in at once without
122
+ losing an entry.
123
+
124
+ ## With billing
125
+
126
+ `@profullstack/billing` reads this file directly — it does not need `timer` on
127
+ `PATH` — and turns unbilled hours into invoice line items:
128
+
129
+ ```sh
130
+ billing invoice new --client acme --from-timer --month
131
+ ```
132
+
133
+ ## In moshcode
134
+
135
+ [moshcode](https://github.com/moshcoder/moshcode) installs and fronts it:
136
+
137
+ ```
138
+ moshcode install timer
139
+ /timer start acme fix the login redirect
140
+ ```
141
+
142
+ ## Licence
143
+
144
+ MIT © Profullstack, LLC
package/bin/timer.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.mjs";
3
+
4
+ process.exitCode = main();
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@profullstack/timer",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A time tracker for the terminal and for agents — start a clock against a project, stop it, and get billable hours back as text or JSON.",
6
+ "keywords": ["timer", "time-tracking", "timesheet", "billable", "hours", "cli", "agent", "freelance"],
7
+ "repository": { "type": "git", "url": "git+https://github.com/profullstack/timer.git" },
8
+ "homepage": "https://github.com/profullstack/timer#readme",
9
+ "bugs": { "url": "https://github.com/profullstack/timer/issues" },
10
+ "license": "MIT",
11
+ "author": "Profullstack, LLC",
12
+ "bin": { "timer": "bin/timer.mjs" },
13
+ "files": ["bin", "src", "README.md", "AGENTS.md", "LICENSE"],
14
+ "engines": { "node": ">=20.11" },
15
+ "scripts": {
16
+ "test": "node --test",
17
+ "start": "node bin/timer.mjs"
18
+ }
19
+ }
package/src/args.mjs ADDED
@@ -0,0 +1,83 @@
1
+ // A small argv parser, hand-rolled so the package stays dependency-free.
2
+ //
3
+ // The one rule worth stating: a flag only consumes the next token if it is
4
+ // declared as taking a value. Guessing from the shape of the next token is
5
+ // what makes `timer log --json acme` mean two different things on two
6
+ // different days, and this tool is meant to be scripted by agents.
7
+ export function parseArgs(argv, { booleans = [], values = [], multi = [], aliases = {} } = {}) {
8
+ const isBool = new Set(booleans);
9
+ const takesValue = new Set([...values, ...multi]);
10
+ const isMulti = new Set(multi);
11
+ const flags = Object.create(null);
12
+ const positional = [];
13
+ const rest = [];
14
+ const unknown = [];
15
+
16
+ const resolve = (name) => aliases[name] || name;
17
+ const set = (name, value) => {
18
+ if (isMulti.has(name)) (flags[name] ||= []).push(value);
19
+ else flags[name] = value;
20
+ };
21
+
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const token = argv[i];
24
+ if (token === "--") {
25
+ rest.push(...argv.slice(i + 1));
26
+ break;
27
+ }
28
+ if (token.startsWith("--")) {
29
+ let body = token.slice(2);
30
+ let inline = null;
31
+ const eq = body.indexOf("=");
32
+ if (eq !== -1) {
33
+ inline = body.slice(eq + 1);
34
+ body = body.slice(0, eq);
35
+ }
36
+ if (body.startsWith("no-")) {
37
+ const name = resolve(body.slice(3));
38
+ flags[name] = false;
39
+ continue;
40
+ }
41
+ const name = resolve(body);
42
+ if (isBool.has(name)) {
43
+ // `--json=false` is the one way to turn a boolean off inline; agents
44
+ // building command lines from templates rely on it.
45
+ flags[name] = inline == null ? true : !/^(0|false|no)$/i.test(inline);
46
+ continue;
47
+ }
48
+ if (takesValue.has(name)) {
49
+ const value = inline != null ? inline : argv[++i];
50
+ if (value === undefined) throw new Error(`--${body} needs a value`);
51
+ set(name, value);
52
+ continue;
53
+ }
54
+ unknown.push(`--${body}`);
55
+ continue;
56
+ }
57
+ if (token.length > 1 && token.startsWith("-") && !/^-\d/.test(token)) {
58
+ // Short flags, including bundles (-qj). A bundled flag that takes a
59
+ // value must be last, the way tar and grep do it.
60
+ const letters = token.slice(1).split("");
61
+ for (let j = 0; j < letters.length; j += 1) {
62
+ const name = resolve(letters[j]);
63
+ if (isBool.has(name)) { flags[name] = true; continue; }
64
+ if (takesValue.has(name)) {
65
+ const inline = letters.slice(j + 1).join("");
66
+ const value = inline || argv[++i];
67
+ if (value === undefined) throw new Error(`-${letters[j]} needs a value`);
68
+ set(name, value);
69
+ break;
70
+ }
71
+ unknown.push(`-${letters[j]}`);
72
+ }
73
+ continue;
74
+ }
75
+ positional.push(token);
76
+ }
77
+ return { flags, positional, rest, unknown };
78
+ }
79
+
80
+ /** Flags every command answers to, so they are declared once. */
81
+ export const GLOBAL_BOOLEANS = ["json", "help", "version", "quiet"];
82
+ export const GLOBAL_VALUES = ["data"];
83
+ export const GLOBAL_ALIASES = { h: "help", v: "version", q: "quiet", j: "json" };