@profullstack/timer 0.1.0 → 0.2.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 +7 -0
- package/README.md +21 -2
- package/package.json +1 -1
- package/src/cli.mjs +33 -6
- package/src/entries.mjs +6 -0
package/AGENTS.md
CHANGED
|
@@ -42,6 +42,8 @@ Everything `--json` returns is built from this shape:
|
|
|
42
42
|
"billable": true,
|
|
43
43
|
"rate": null,
|
|
44
44
|
"agent": "claude-opus-5",
|
|
45
|
+
"agents": 1,
|
|
46
|
+
"agentHours": 1.5,
|
|
45
47
|
"notes": "",
|
|
46
48
|
"meta": {}
|
|
47
49
|
}
|
|
@@ -59,6 +61,11 @@ ID=$(timer start acme --task "refactor auth" --json | jq -r .started.id)
|
|
|
59
61
|
timer stop --id "$ID" --json
|
|
60
62
|
```
|
|
61
63
|
|
|
64
|
+
`--agents N` records how many engines were working during the entry, which is
|
|
65
|
+
what an agent-priced rate multiplies by. It takes a whole number: `auto` is
|
|
66
|
+
rejected rather than silently treated as 1, because this package has no herd to
|
|
67
|
+
count and under-billing quietly is worse than an error.
|
|
68
|
+
|
|
62
69
|
Several clocks may run at once, which is the point: parallel agents each track
|
|
63
70
|
their own work and do not stop each other. Use `--switch` only if you mean to
|
|
64
71
|
close everyone else's clock.
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ you start a clock.
|
|
|
36
36
|
|
|
37
37
|
| Command | What it does |
|
|
38
38
|
| --- | --- |
|
|
39
|
-
| `start <project> [task…]` | Start a clock. `--at 09:15`, `--at -20m`, `--tag`, `--note`, `--rate`, `--switch` |
|
|
39
|
+
| `start <project> [task…]` | Start a clock. `--at 09:15`, `--at -20m`, `--tag`, `--note`, `--rate`, `--agents N`, `--switch` |
|
|
40
40
|
| `stop [id]` | Stop the newest clock, an id, `--project <p>`, or `--all` |
|
|
41
41
|
| `status` | Running clocks and today's total |
|
|
42
42
|
| `log [project]` | List entries in a window |
|
|
@@ -65,6 +65,23 @@ A window compares against the entry's **start**, and `--until` is exclusive. An
|
|
|
65
65
|
entry that runs past midnight therefore belongs to the day it began on — which
|
|
66
66
|
is what keeps a total from being counted twice.
|
|
67
67
|
|
|
68
|
+
### Counting agents
|
|
69
|
+
|
|
70
|
+
An hour of agentic work is an hour times however many engines ran in it, so an
|
|
71
|
+
entry carries an agent count:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
timer start acme refactor auth --agents 4
|
|
75
|
+
timer add acme code review --duration 45m --agents 2
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`@profullstack/billing` multiplies by it when the rate says to
|
|
79
|
+
(`$100/hour/agent/upto:4`), and ignores it when the rate is flat. It defaults to
|
|
80
|
+
1, so you can ignore the whole idea until you need it.
|
|
81
|
+
|
|
82
|
+
`start` is also spelled `on` and `stop` is also spelled `off`, so muscle memory
|
|
83
|
+
from other timers works.
|
|
84
|
+
|
|
68
85
|
### Billable and not
|
|
69
86
|
|
|
70
87
|
Every entry is billable unless you say otherwise with `--no-billable`. Reports
|
|
@@ -97,7 +114,9 @@ timer log --today --json
|
|
|
97
114
|
```
|
|
98
115
|
|
|
99
116
|
`--meta '{"pr":42}'` hangs your own identifiers off an entry, and they survive
|
|
100
|
-
round-trip unchanged.
|
|
117
|
+
round-trip unchanged. `--agents N` records how many engines were working, which
|
|
118
|
+
is what an agent-priced rate multiplies by; it takes a number, not `auto`, since
|
|
119
|
+
this package has no herd to count.
|
|
101
120
|
|
|
102
121
|
There is more detail, including the entry schema, in [AGENTS.md](AGENTS.md).
|
|
103
122
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/timer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"keywords": ["timer", "time-tracking", "timesheet", "billable", "hours", "cli", "agent", "freelance"],
|
package/src/cli.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./entries.mjs";
|
|
25
25
|
import { formatDuration, hours, parseMoment, resolveWindow, shortStamp } from "./time.mjs";
|
|
26
26
|
|
|
27
|
-
export const VERSION = "0.
|
|
27
|
+
export const VERSION = "0.2.0";
|
|
28
28
|
|
|
29
29
|
/** A bad command line — worth a different exit code than a failed operation. */
|
|
30
30
|
export class UsageError extends Error {
|
|
@@ -73,6 +73,8 @@ function serialize(entry, now = new Date()) {
|
|
|
73
73
|
billable: entry.billable,
|
|
74
74
|
rate: entry.rate,
|
|
75
75
|
agent: entry.agent,
|
|
76
|
+
agents: entry.agents ?? 1,
|
|
77
|
+
agentHours: hours(secs * (entry.agents ?? 1)),
|
|
76
78
|
notes: entry.notes,
|
|
77
79
|
meta: entry.meta,
|
|
78
80
|
};
|
|
@@ -84,6 +86,7 @@ const ENTRY_COLUMNS = [
|
|
|
84
86
|
{ header: "PROJECT", get: (e) => e.project },
|
|
85
87
|
{ header: "TASK", get: (e) => e.task || "-" },
|
|
86
88
|
{ header: "TIME", get: (e) => formatDuration(e.seconds), align: "right" },
|
|
89
|
+
{ header: "AGENTS", get: (e) => (e.agents > 1 ? e.agents : "") , align: "right" },
|
|
87
90
|
{ header: "TAGS", get: (e) => (e.tags.length ? e.tags.join(",") : "-") },
|
|
88
91
|
{ header: "", get: (e) => (e.running ? "running" : (e.billable ? "" : "unbillable")) },
|
|
89
92
|
];
|
|
@@ -107,6 +110,26 @@ function sumOf(rows) {
|
|
|
107
110
|
return { entries: rows.length, seconds: secs, hours: hours(secs), billableSeconds: billable, billableHours: hours(billable) };
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
/**
|
|
114
|
+
* How many agents were working, from --agents.
|
|
115
|
+
*
|
|
116
|
+
* A number only. moshcode's `--agents auto` reads the count off its own herd
|
|
117
|
+
* and passes the result here: this package has no herd to ask, and silently
|
|
118
|
+
* treating "auto" as 1 would under-bill every entry it appeared on.
|
|
119
|
+
*/
|
|
120
|
+
function agentCount(flags) {
|
|
121
|
+
if (flags.agents == null) return 1;
|
|
122
|
+
const n = Number(flags.agents);
|
|
123
|
+
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
|
|
124
|
+
throw new UsageError(
|
|
125
|
+
`--agents: "${flags.agents}" is not a whole number of agents`
|
|
126
|
+
+ (String(flags.agents).toLowerCase() === "auto"
|
|
127
|
+
? " (this package has no herd to count; pass the number)" : ""),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return n;
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
function requireProject(positional, flags) {
|
|
111
134
|
const project = flags.project || positional[0];
|
|
112
135
|
if (!project) throw new UsageError("which project? e.g. timer start acme");
|
|
@@ -118,11 +141,11 @@ function requireProject(positional, flags) {
|
|
|
118
141
|
const COMMANDS = [
|
|
119
142
|
{
|
|
120
143
|
name: "start",
|
|
121
|
-
aliases: ["begin", "in"],
|
|
144
|
+
aliases: ["begin", "in", "on"],
|
|
122
145
|
args: "<project> [task words…]",
|
|
123
146
|
summary: "start the clock on a project",
|
|
124
147
|
booleans: ["billable", "switch"],
|
|
125
|
-
values: ["task", "note", "agent", "rate", "at", "project", "meta"],
|
|
148
|
+
values: ["task", "note", "agent", "agents", "rate", "at", "project", "meta"],
|
|
126
149
|
multi: ["tag"],
|
|
127
150
|
detail: [
|
|
128
151
|
"Everything after the project name is taken as the task, so you can type",
|
|
@@ -154,6 +177,7 @@ const COMMANDS = [
|
|
|
154
177
|
start: at,
|
|
155
178
|
notes: flags.note || "",
|
|
156
179
|
agent: flags.agent || process.env.TIMER_AGENT || null,
|
|
180
|
+
agents: agentCount(flags),
|
|
157
181
|
rate: flags.rate,
|
|
158
182
|
billable: flags.billable !== false,
|
|
159
183
|
meta,
|
|
@@ -175,7 +199,7 @@ const COMMANDS = [
|
|
|
175
199
|
},
|
|
176
200
|
{
|
|
177
201
|
name: "stop",
|
|
178
|
-
aliases: ["out"],
|
|
202
|
+
aliases: ["out", "off"],
|
|
179
203
|
args: "[id]",
|
|
180
204
|
summary: "stop a running clock",
|
|
181
205
|
booleans: ["all"],
|
|
@@ -299,7 +323,7 @@ const COMMANDS = [
|
|
|
299
323
|
args: "<project> [task words…]",
|
|
300
324
|
summary: "record time you did not clock",
|
|
301
325
|
booleans: ["billable"],
|
|
302
|
-
values: ["from", "to", "duration", "task", "note", "agent", "rate", "project", "meta"],
|
|
326
|
+
values: ["from", "to", "duration", "task", "note", "agent", "agents", "rate", "project", "meta"],
|
|
303
327
|
multi: ["tag"],
|
|
304
328
|
detail: [
|
|
305
329
|
"Give any two of --from, --to and --duration; with only --duration the entry",
|
|
@@ -338,6 +362,7 @@ const COMMANDS = [
|
|
|
338
362
|
end: bounds.end,
|
|
339
363
|
notes: flags.note || "",
|
|
340
364
|
agent: flags.agent || process.env.TIMER_AGENT || null,
|
|
365
|
+
agents: agentCount(flags),
|
|
341
366
|
rate: flags.rate,
|
|
342
367
|
billable: flags.billable !== false,
|
|
343
368
|
meta,
|
|
@@ -358,7 +383,7 @@ const COMMANDS = [
|
|
|
358
383
|
args: "<id>",
|
|
359
384
|
summary: "change an entry",
|
|
360
385
|
booleans: ["billable"],
|
|
361
|
-
values: ["project", "task", "note", "agent", "rate", "from", "to", "duration", "meta"],
|
|
386
|
+
values: ["project", "task", "note", "agent", "agents", "rate", "from", "to", "duration", "meta"],
|
|
362
387
|
multi: ["tag"],
|
|
363
388
|
detail: ["Only the fields you name change. --tag replaces the whole tag list."],
|
|
364
389
|
run({ positional, flags, file }) {
|
|
@@ -372,6 +397,7 @@ const COMMANDS = [
|
|
|
372
397
|
if (flags.task != null) e.task = flags.task;
|
|
373
398
|
if (flags.note != null) e.notes = flags.note;
|
|
374
399
|
if (flags.agent != null) e.agent = flags.agent || null;
|
|
400
|
+
if (flags.agents != null) e.agents = agentCount(flags);
|
|
375
401
|
if (flags.rate != null) e.rate = Number(flags.rate);
|
|
376
402
|
if ("billable" in flags) e.billable = Boolean(flags.billable);
|
|
377
403
|
if (flags.tag) e.tags = [...new Set(flags.tag)];
|
|
@@ -459,6 +485,7 @@ const COMMANDS = [
|
|
|
459
485
|
start: at,
|
|
460
486
|
notes: "",
|
|
461
487
|
agent: source.agent,
|
|
488
|
+
agents: source.agents ?? 1,
|
|
462
489
|
rate: source.rate,
|
|
463
490
|
billable: source.billable,
|
|
464
491
|
meta: source.meta,
|
package/src/entries.mjs
CHANGED
|
@@ -24,6 +24,7 @@ export function makeEntry({
|
|
|
24
24
|
end = null,
|
|
25
25
|
notes = "",
|
|
26
26
|
agent = null,
|
|
27
|
+
agents = 1,
|
|
27
28
|
rate = null,
|
|
28
29
|
billable = true,
|
|
29
30
|
meta = {},
|
|
@@ -38,6 +39,11 @@ export function makeEntry({
|
|
|
38
39
|
end,
|
|
39
40
|
notes: String(notes || ""),
|
|
40
41
|
agent: agent ? String(agent) : null,
|
|
42
|
+
// How many engines were working during this entry. An hour of agentic work
|
|
43
|
+
// is an hour times however many agents ran in it, and a rate priced per
|
|
44
|
+
// agent needs that number per entry - averaging it across a day bills a
|
|
45
|
+
// two-agent afternoon at the four-agent rate.
|
|
46
|
+
agents: Math.max(1, Math.round(Number(agents) || 1)),
|
|
41
47
|
rate: rate == null ? null : Number(rate),
|
|
42
48
|
billable: Boolean(billable),
|
|
43
49
|
meta: meta && typeof meta === "object" ? meta : {},
|