moshcode 0.71.0 → 0.72.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/.claude-plugin/marketplace.json +24 -0
- package/README.md +32 -0
- package/bin/moshcode.mjs +12 -0
- package/package.json +1 -1
- package/plugins/billing/.claude-plugin/plugin.json +13 -0
- package/plugins/billing/README.md +42 -0
- package/plugins/billing/commands/hours.md +37 -0
- package/plugins/billing/commands/invoice.md +39 -0
- package/plugins/billing/commands/rate.md +40 -0
- package/plugins/billing/commands/report.md +28 -0
- package/plugins/timer/.claude-plugin/plugin.json +13 -0
- package/plugins/timer/README.md +36 -0
- package/plugins/timer/commands/report.md +32 -0
- package/plugins/timer/commands/start.md +33 -0
- package/plugins/timer/commands/status.md +27 -0
- package/plugins/timer/commands/stop.md +30 -0
- package/src/business-delegate.mjs +105 -0
- package/src/plugins.mjs +39 -7
- package/src/tools.mjs +18 -0
- package/src/tui.mjs +25 -2
|
@@ -30,6 +30,30 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/moshcoder/moshcode#crypto",
|
|
32
32
|
"keywords": ["crypto", "bitcoin", "markets", "prices", "advis0r"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "timer",
|
|
36
|
+
"description": "Track time against projects from inside your engine, backed by @profullstack/timer: start and stop a clock, log time you forgot, and report billable hours with the agent count that priced them.",
|
|
37
|
+
"source": "./plugins/timer",
|
|
38
|
+
"category": "productivity",
|
|
39
|
+
"author": {
|
|
40
|
+
"name": "moshcoder",
|
|
41
|
+
"url": "https://moshcode.sh"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/profullstack/timer#readme",
|
|
44
|
+
"keywords": ["timer", "time-tracking", "billable", "hours", "profullstack"]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "billing",
|
|
48
|
+
"description": "Turn tracked hours into an invoice, backed by @profullstack/billing: clients, rate cards written as contract sentences, agent-hour arithmetic, and invoices rendered to HTML you can print to PDF.",
|
|
49
|
+
"source": "./plugins/billing",
|
|
50
|
+
"category": "productivity",
|
|
51
|
+
"author": {
|
|
52
|
+
"name": "moshcoder",
|
|
53
|
+
"url": "https://moshcode.sh"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/profullstack/billing#readme",
|
|
56
|
+
"keywords": ["billing", "invoice", "clients", "rates", "profullstack"]
|
|
33
57
|
}
|
|
34
58
|
]
|
|
35
59
|
}
|
package/README.md
CHANGED
|
@@ -949,6 +949,38 @@ Every agentic CLI helps you do the work. This one also bills for it. Six words,
|
|
|
949
949
|
each useful on its own — the timer needs no client, the rate needs no gateway
|
|
950
950
|
(PRD [0012](prd/0012-billing-baked-into-the-agent-cli.md)).
|
|
951
951
|
|
|
952
|
+
> **`/timer` and `/billing` now prefer their own CLIs.** Tracking time and
|
|
953
|
+
> sending an invoice are not moshcode ideas — they are useful under any agentic
|
|
954
|
+
> CLI, and on Windows, where moshcode does not go. So they also ship standalone:
|
|
955
|
+
> [`@profullstack/timer`](https://github.com/profullstack/timer) and
|
|
956
|
+
> [`@profullstack/billing`](https://github.com/profullstack/billing).
|
|
957
|
+
>
|
|
958
|
+
> ```sh
|
|
959
|
+
> moshcode install timer billing # or: npm install -g @profullstack/timer @profullstack/billing
|
|
960
|
+
> ```
|
|
961
|
+
>
|
|
962
|
+
> Installing them changes nothing on its own. Switch the hand-over on when you
|
|
963
|
+
> are ready to move:
|
|
964
|
+
>
|
|
965
|
+
> ```sh
|
|
966
|
+
> billing import # look at what would come across
|
|
967
|
+
> billing import --apply # move it
|
|
968
|
+
> export MOSHCODE_EXTERNAL_BILLING=1 # /timer and /billing now run the CLIs
|
|
969
|
+
> ```
|
|
970
|
+
>
|
|
971
|
+
> **It is opt-in for a reason.** Only half this layer has an outside home:
|
|
972
|
+
> `/client`, `/rate`, `/payments` and `/team` stay here, because the rails and
|
|
973
|
+
> the permission model are moshcode's and `/client`'s freeform dotted fields
|
|
974
|
+
> have no shape in the package's typed client model. Handing over the other
|
|
975
|
+
> half automatically would split your records across two stores — `/client` and
|
|
976
|
+
> `/rate` writing `~/.moshcode/business.json` while `/billing` reads the
|
|
977
|
+
> package's own ledger, so the invoice for a client you had just created would
|
|
978
|
+
> not exist. `billing import` is what closes that gap, which is why it comes
|
|
979
|
+
> first.
|
|
980
|
+
>
|
|
981
|
+
> The standalone billing carries the same rate model (`$100/hour/agent/upto:4`)
|
|
982
|
+
> and bills **agent-hours**.
|
|
983
|
+
|
|
952
984
|
```sh
|
|
953
985
|
moshcode timer on acme --task "batch payments" --agents auto # auto counts the herd
|
|
954
986
|
moshcode timer off # → 1h 12m, $480.00
|
package/bin/moshcode.mjs
CHANGED
|
@@ -423,8 +423,15 @@ async function main() {
|
|
|
423
423
|
// the startup path of `moshcode claude`, which is what this binary is mostly
|
|
424
424
|
// asked to do.
|
|
425
425
|
if (cmd === "timer") {
|
|
426
|
+
// @profullstack/timer when it is installed, the built-in otherwise. See
|
|
427
|
+
// src/business-delegate.mjs for why the external one wins.
|
|
428
|
+
const { delegate, installHint } = await import("../src/business-delegate.mjs");
|
|
429
|
+
const handed = await delegate("timer", rest, {});
|
|
430
|
+
if (handed.delegated) { process.exitCode = handed.code; return; }
|
|
426
431
|
const { timerCommand } = await import("../src/timer.mjs");
|
|
427
432
|
process.exitCode = (await timerCommand(rest)) || 0;
|
|
433
|
+
const hint = installHint("timer");
|
|
434
|
+
if (hint) process.stderr.write(`${hint}\n`);
|
|
428
435
|
return;
|
|
429
436
|
}
|
|
430
437
|
if (cmd === "client" || cmd === "business" || cmd === "merchant" || cmd === "customer") {
|
|
@@ -443,8 +450,13 @@ async function main() {
|
|
|
443
450
|
return;
|
|
444
451
|
}
|
|
445
452
|
if (cmd === "billing" || cmd === "invoice") {
|
|
453
|
+
const { delegate, installHint } = await import("../src/business-delegate.mjs");
|
|
454
|
+
const handed = await delegate(cmd, rest, {});
|
|
455
|
+
if (handed.delegated) { process.exitCode = handed.code; return; }
|
|
446
456
|
const { billingCommand } = await import("../src/billing.mjs");
|
|
447
457
|
process.exitCode = billingCommand(rest) || 0;
|
|
458
|
+
const hint = installHint(cmd);
|
|
459
|
+
if (hint) process.stderr.write(`${hint}\n`);
|
|
448
460
|
return;
|
|
449
461
|
}
|
|
450
462
|
if (cmd === "payments") {
|
package/package.json
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
|
+
"name": "billing",
|
|
4
|
+
"description": "Turn tracked hours into an invoice, backed by @profullstack/billing: clients, rate cards written as contract sentences, agent-hour arithmetic, and invoices rendered to HTML you can print to PDF.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "moshcoder",
|
|
8
|
+
"url": "https://moshcode.sh"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/profullstack/billing#readme",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["billing", "invoice", "clients", "rates", "profullstack"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# billing
|
|
2
|
+
|
|
3
|
+
Slash commands for
|
|
4
|
+
[`@profullstack/billing`](https://github.com/profullstack/billing) — clients,
|
|
5
|
+
rates and invoices, built on the hours `@profullstack/timer` tracked.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/billing:rate set acme '$100/hour/agent/upto:4'
|
|
9
|
+
/billing:hours --client acme --month
|
|
10
|
+
/billing:invoice --client acme --from-timer --month
|
|
11
|
+
/billing:report
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install the CLI
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install -g @profullstack/billing
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or, inside moshcode:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
moshcode install billing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What it is for
|
|
27
|
+
|
|
28
|
+
A rate is the sentence from the contract, parsed. `$100/hour/agent/upto:4` means
|
|
29
|
+
four agents cost four hundred an hour and so do six, and the invoice bills
|
|
30
|
+
**agent-hours** so the client can check the line by hand: `quantity × rate`
|
|
31
|
+
always equals `amount`.
|
|
32
|
+
|
|
33
|
+
Two rules the shape enforces. The same hour never reaches two invoices — each
|
|
34
|
+
invoice records the timer entry ids it covers, so voiding one releases them.
|
|
35
|
+
And creating an invoice is a proposal: `--dry-run` validates the whole thing and
|
|
36
|
+
writes nothing, a new invoice is a draft, and nothing is ever emailed.
|
|
37
|
+
|
|
38
|
+
## Coming from moshcode
|
|
39
|
+
|
|
40
|
+
moshcode used to keep this layer internally. `billing import` brings it across
|
|
41
|
+
from `~/.moshcode/business.json` and `timers.json`, shows the plan first, and
|
|
42
|
+
never modifies the originals.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Tracked hours not yet on an invoice, priced at the client's rate.
|
|
3
|
+
argument-hint: --client <name> [--month|--week]
|
|
4
|
+
allowed-tools: Bash(billing hours:*), Bash(billing client:*), Bash(billing rate:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
billing hours $ARGUMENTS --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This is the preview of what `billing invoice new --from-timer` would bill. Same
|
|
14
|
+
filters, same grouping, same arithmetic — it writes nothing.
|
|
15
|
+
|
|
16
|
+
## Reading the response
|
|
17
|
+
|
|
18
|
+
- `items[]` — the line items, each with `description`, `quantity` (in the rate's
|
|
19
|
+
own billing unit), `unitPriceMajor`, `amount`, `hours` and `timerIds`.
|
|
20
|
+
- `unit` — what `quantity` is measured in: `hours`, or `agent-hours` when the
|
|
21
|
+
rate is priced per agent.
|
|
22
|
+
- `hours` / `units` / `subtotal` — the totals.
|
|
23
|
+
- `skipped` — `{ running, unbillable, alreadyBilled }`.
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
- **Always read `skipped.running` back to the user.** Those hours are not
|
|
28
|
+
missing, they are on a clock that is still ticking and become billable the
|
|
29
|
+
moment it stops. "Nothing to bill" is misleading when the real answer is "stop
|
|
30
|
+
the clock first".
|
|
31
|
+
- Exit 3 means no unbilled hours matched, or the client does not exist. Read the
|
|
32
|
+
message; it distinguishes them.
|
|
33
|
+
- When `unit` is `agent-hours`, explain the multiplier if the user seems
|
|
34
|
+
surprised: 3 hours with 2 agents is 6 agent-hours. `quantity * unitPrice`
|
|
35
|
+
always equals `amount`.
|
|
36
|
+
- If there is no rate, the error says so and names the command that sets one.
|
|
37
|
+
Do not invent a rate to get past it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Draft an invoice from tracked time or fixed line items, for a person to approve.
|
|
3
|
+
argument-hint: --client <name> --from-timer --month
|
|
4
|
+
allowed-tools: Bash(billing invoice:*), Bash(billing hours:*), Bash(billing client:*), Bash(billing rate:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
Propose an invoice for `$ARGUMENTS`. **Always dry-run first:**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
billing invoice new $ARGUMENTS --dry-run --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Show the user what it would create. Only run it again without `--dry-run` when
|
|
16
|
+
they have said to.
|
|
17
|
+
|
|
18
|
+
Line items come from `--from-timer` (tracked hours), from `--item
|
|
19
|
+
"Description|quantity|price"`, or both on the same invoice.
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
- **Creating an invoice is a business action. Do not write one unprompted.**
|
|
24
|
+
`--dry-run` builds and validates the entire invoice — the rate lookup and the
|
|
25
|
+
double-billing check included — so a dry run that succeeds means the real one
|
|
26
|
+
will. There is no reason to skip it.
|
|
27
|
+
- A new invoice is a **draft**. `billing invoice mark <n> sent` is a separate,
|
|
28
|
+
deliberate step and this tool never emails anything. Do not mark an invoice
|
|
29
|
+
sent or paid unless the user asked.
|
|
30
|
+
- Read `amounts` (decimal) when talking to a person; the bare `total` is in
|
|
31
|
+
minor units.
|
|
32
|
+
- Exit 3 on `--from-timer` means there were no unbilled hours. Check
|
|
33
|
+
`billing hours` and report `skipped.running` rather than concluding there is
|
|
34
|
+
nothing to bill.
|
|
35
|
+
- The same hour cannot reach two invoices: each invoice records the timer entry
|
|
36
|
+
ids it covers. If the user wants to re-bill something, voiding the old invoice
|
|
37
|
+
releases those hours.
|
|
38
|
+
- Prefer `billing invoice render <n> --format html --out <file>` when they want
|
|
39
|
+
something to send: it is one self-contained file that prints to PDF.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Read or set what your time costs, written the way the contract says it.
|
|
3
|
+
argument-hint: "[set <client|default> '$100/hour/agent/upto:4']"
|
|
4
|
+
allowed-tools: Bash(billing rate:*), Bash(billing client:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
billing rate $ARGUMENTS --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
With no arguments this lists every rate. `set`, `show` and `rm` take a target,
|
|
14
|
+
which is either a client handle or `default`.
|
|
15
|
+
|
|
16
|
+
## The spec
|
|
17
|
+
|
|
18
|
+
A price, then any of these in any order:
|
|
19
|
+
|
|
20
|
+
- a period: `hour`, `day` (8h), `week` (40h), `month` (160h), `project`, `task`
|
|
21
|
+
- a unit that gets multiplied: `agent`, `seat`, `person`, `team`
|
|
22
|
+
- `upto:N` to cap the multiplier, `min:N` for a minimum billed period
|
|
23
|
+
|
|
24
|
+
`$100/hour/agent/upto:4` means four agents cost four hundred an hour, and so do
|
|
25
|
+
six. `0.5 SOL/day`, `250 USDC/task` and `$5000/project` all parse too.
|
|
26
|
+
|
|
27
|
+
## Rules
|
|
28
|
+
|
|
29
|
+
- **Do not set or change a rate unless the user asked.** This is the number in
|
|
30
|
+
somebody's contract.
|
|
31
|
+
- Read `describes` back to them when confirming — it is the rate as a sentence,
|
|
32
|
+
and it is how you catch a spec that parsed differently from how it was meant.
|
|
33
|
+
- Settlement (`--prefer SOL --accept fiat`) is deliberately separate from the
|
|
34
|
+
price. The number in the contract does not change because the rail did, so do
|
|
35
|
+
not "convert" a rate to a preferred ticker.
|
|
36
|
+
- A price given in a ticker invoices in that ticker. Do not turn `0.5 SOL` into
|
|
37
|
+
a dollar figure: nobody computed that number.
|
|
38
|
+
- If a spec is rejected, the error names the words that are allowed. Fix the
|
|
39
|
+
spec rather than falling back to a bare number, which would silently mean
|
|
40
|
+
"per hour, flat".
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: What has been billed, collected, and what is still owed.
|
|
3
|
+
argument-hint: "[--client <name>] [--year|--month]"
|
|
4
|
+
allowed-tools: Bash(billing report:*), Bash(billing invoice:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
billing report $ARGUMENTS --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Reading the response
|
|
14
|
+
|
|
15
|
+
- `totals` — `billed`, `collected`, `outstanding`, `overdue`, `draft`, all as
|
|
16
|
+
decimal numbers in `currency`.
|
|
17
|
+
- `byClient[]` — the same figures per client, biggest outstanding first.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- **`draft` is not money anybody owes you.** A draft invoice has not been
|
|
22
|
+
issued, so keep it out of any "you are owed X" sentence and name it
|
|
23
|
+
separately.
|
|
24
|
+
- `overdue` is derived from the due date at read time, never stored. It is a
|
|
25
|
+
subset of `outstanding`, not an addition to it — do not sum them.
|
|
26
|
+
- Windows apply to the **issue date** here, not to when the work was done.
|
|
27
|
+
- For the invoices behind a figure, use `billing invoice list --overdue --json`
|
|
28
|
+
or `--status sent`. Do not guess at which invoices make up a total.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
|
+
"name": "timer",
|
|
4
|
+
"description": "Track time against projects from inside your engine, backed by @profullstack/timer: start and stop a clock, log time you forgot, and report billable hours with the agent count that priced them.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "moshcoder",
|
|
8
|
+
"url": "https://moshcode.sh"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/profullstack/timer#readme",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["timer", "time-tracking", "billable", "hours", "profullstack"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# timer
|
|
2
|
+
|
|
3
|
+
Slash commands for [`@profullstack/timer`](https://github.com/profullstack/timer)
|
|
4
|
+
— a time tracker that runs on Linux, macOS and Windows, and answers `--json` on
|
|
5
|
+
every command so an agent can clock its own work.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/timer:start acme fix the login redirect
|
|
9
|
+
/timer:status
|
|
10
|
+
/timer:stop
|
|
11
|
+
/timer:report --week --group day
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Install the CLI
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install -g @profullstack/timer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
or, inside moshcode:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
moshcode install timer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What it is for
|
|
27
|
+
|
|
28
|
+
An hour of agentic work is an hour times however many engines ran in it, so an
|
|
29
|
+
entry carries an agent count (`--agents 4`). `@profullstack/billing` multiplies
|
|
30
|
+
by it when the rate says to and ignores it when the rate is flat.
|
|
31
|
+
|
|
32
|
+
Several clocks may run at once. That is deliberate: parallel agents each track
|
|
33
|
+
their own work and do not stop each other.
|
|
34
|
+
|
|
35
|
+
The timesheet is one JSON file at `~/.profullstack/timer/timesheet.json`, and
|
|
36
|
+
billing reads it directly.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Totals for a period, grouped by project, task, day, tag or agent.
|
|
3
|
+
argument-hint: "[--week|--month] [--group project|task|day|tag|agent]"
|
|
4
|
+
allowed-tools: Bash(timer report:*), Bash(timer log:*), Bash(timer projects:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
timer report $ARGUMENTS --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Windows: `--today`, `--yesterday`, `--week` (from Monday), `--month`, `--year`,
|
|
14
|
+
or explicit `--since` / `--until`. Groups: `project` (default), `task`, `day`,
|
|
15
|
+
`tag`, `agent`, `none`.
|
|
16
|
+
|
|
17
|
+
## Reading the response
|
|
18
|
+
|
|
19
|
+
`rows[]` each carry `key`, `entries`, `hours` and `billableHours`. `totals` has
|
|
20
|
+
the same figures for the whole window.
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
|
|
24
|
+
- **A window compares against the entry's start, and `--until` is exclusive.**
|
|
25
|
+
An entry that ran past midnight belongs to the day it began on. Say so if the
|
|
26
|
+
user questions a boundary rather than guessing at a bug.
|
|
27
|
+
- Report `hours` and `billableHours` separately whenever they differ.
|
|
28
|
+
- `timer log` is the command for the entries behind a number. Reach for it when
|
|
29
|
+
the user asks why a total looks the way it does.
|
|
30
|
+
- Do not convert hours into money here. The rate lives in
|
|
31
|
+
`@profullstack/billing`, which knows about agent multipliers and caps; a
|
|
32
|
+
hours-times-rate figure invented here will disagree with the invoice.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Start the clock on a project, optionally recording how many agents are working.
|
|
3
|
+
argument-hint: <project> [task words…]
|
|
4
|
+
allowed-tools: Bash(timer start:*), Bash(timer status:*), Bash(timer on:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
Start a clock for `$ARGUMENTS`.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
timer start $ARGUMENTS --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The first word is the project; everything after it is the task, so no quoting is
|
|
16
|
+
needed. Useful flags:
|
|
17
|
+
|
|
18
|
+
- `--agents N` — how many engines are working. An agent-priced rate multiplies
|
|
19
|
+
by this, so it is the difference between a $400 afternoon and a $1,600 one.
|
|
20
|
+
- `--at 09:15` or `--at -20m` — the clock you meant to start earlier.
|
|
21
|
+
- `--tag`, `--note`, `--no-billable`.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- **Do not pass `--switch` unless the user asked to stop their other clocks.**
|
|
26
|
+
Several clocks running at once is normal here: parallel agents each track
|
|
27
|
+
their own work, and stopping someone else's is not recoverable from the log.
|
|
28
|
+
- If `started.alsoRunning` comes back non-empty, mention how many other clocks
|
|
29
|
+
are running. Do not stop them.
|
|
30
|
+
- Report the entry id — `timer stop --id <id>` needs it, and so does the user if
|
|
31
|
+
they want to correct the entry later.
|
|
32
|
+
- If the command exits 2, the command line was wrong; read the error and fix it
|
|
33
|
+
rather than retrying the same thing.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: What is running right now, and what today adds up to.
|
|
3
|
+
allowed-tools: Bash(timer status:*), Bash(timer log:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Task
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
timer status --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Reading the response
|
|
13
|
+
|
|
14
|
+
- `running[]` — every live clock, each with `seconds` counting up to now.
|
|
15
|
+
- `today` — totals for entries that *started* today: `hours` and `billableHours`.
|
|
16
|
+
- `dataFile` — where the timesheet lives, worth quoting if the user is surprised
|
|
17
|
+
by what is or is not in it.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
- An empty `running` array is a normal answer. Exit status is 0 either way, so
|
|
22
|
+
never report "nothing running" as a failure.
|
|
23
|
+
- `hours` and `billableHours` differ when something is marked `--no-billable`.
|
|
24
|
+
If they differ, give both — "how long did this take" and "what can I charge
|
|
25
|
+
for it" are different questions.
|
|
26
|
+
- A running clock's duration is still moving. Do not present it as a final
|
|
27
|
+
figure, and note that billing will not invoice it until it is stopped.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Stop a running clock and report what it came to.
|
|
3
|
+
argument-hint: "[id]"
|
|
4
|
+
allowed-tools: Bash(timer stop:*), Bash(timer off:*), Bash(timer status:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Task
|
|
8
|
+
|
|
9
|
+
Stop the clock for `$ARGUMENTS` (empty means the newest one).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
timer stop $ARGUMENTS --json
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Targets: nothing (the most recently started clock), an id or an unambiguous
|
|
16
|
+
prefix of one, `--project <p>`, or `--all`.
|
|
17
|
+
|
|
18
|
+
## Reading the response
|
|
19
|
+
|
|
20
|
+
`stopped[]` carries `seconds`, `hours` and `agents` for each entry closed.
|
|
21
|
+
`hours` is the figure an invoice uses.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- **`stopped: []` with exit 0 means nothing was running.** That is an answer,
|
|
26
|
+
not a failure — say so plainly and do not retry or treat it as an error.
|
|
27
|
+
- Do not pass `--all` unless the user asked to stop everything. Other clocks may
|
|
28
|
+
belong to other agents.
|
|
29
|
+
- `--at` can backdate the stop, but a stop earlier than the start is refused
|
|
30
|
+
rather than clamped to zero. If that happens, the entry is untouched.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Where /timer and /billing actually run.
|
|
2
|
+
//
|
|
3
|
+
// Both started life inside moshcode (PRD 0012) and now also exist as their own
|
|
4
|
+
// cross-platform CLIs — @profullstack/timer and @profullstack/billing — because
|
|
5
|
+
// neither is a moshcode idea: tracking time and sending an invoice are things
|
|
6
|
+
// you want under any agentic CLI, and on Windows, where moshcode does not go.
|
|
7
|
+
//
|
|
8
|
+
// So the rule here is: when asked, moshcode conducts them the way /gh conducts
|
|
9
|
+
// gh. The two are NOT kept in sync, and that is the point of preferring the
|
|
10
|
+
// external one where it is wanted: a second copy of a billing model is a copy
|
|
11
|
+
// that drifts, and the published package is the one that gets the fixes.
|
|
12
|
+
//
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Why this is opt-in rather than "delegate whenever the CLI is on PATH", which
|
|
15
|
+
// is what it did when it first landed:
|
|
16
|
+
//
|
|
17
|
+
// Only half the business layer has somewhere to go. /timer and /billing have
|
|
18
|
+
// standalone equivalents; /client, /rate, /payments and /team do not — the
|
|
19
|
+
// rails and the permission model are moshcode's, and /client's freeform dotted
|
|
20
|
+
// fields have no shape in the package's typed client model, so moving it would
|
|
21
|
+
// lose data rather than relocate it.
|
|
22
|
+
//
|
|
23
|
+
// Delegating that half by default splits one person's records across two
|
|
24
|
+
// stores. /client and /rate keep writing ~/.moshcode/business.json while
|
|
25
|
+
// /billing reads ~/.profullstack/billing/ledger.json, so:
|
|
26
|
+
//
|
|
27
|
+
// /client create "Acme Inc" → written to moshcode
|
|
28
|
+
// /rate set acme-inc $100/hour → written to moshcode
|
|
29
|
+
// /billing acme-inc → "no client acme-inc"
|
|
30
|
+
//
|
|
31
|
+
// That is not a missing feature, it is somebody's invoice failing to exist. And
|
|
32
|
+
// it only happens on a machine that installed the CLIs, so CI — which has not —
|
|
33
|
+
// stays green while every developer box that took the install goes red.
|
|
34
|
+
//
|
|
35
|
+
// Hence: opt in, knowing that `billing import` is how the existing ledger comes
|
|
36
|
+
// across. When the whole layer has an outside home, this becomes the default.
|
|
37
|
+
import { isInstalled } from "./engines.mjs";
|
|
38
|
+
import { TOOLS, openTool } from "./tools.mjs";
|
|
39
|
+
|
|
40
|
+
/** Commands that have an external CLI, and the TOOLS key that owns it. */
|
|
41
|
+
export const DELEGATED = { timer: "timer", billing: "billing", invoice: "billing" };
|
|
42
|
+
|
|
43
|
+
/** Whether this machine has asked for the standalone CLIs to be used. */
|
|
44
|
+
export function externalEnabled() {
|
|
45
|
+
return /^(1|true|yes)$/i.test(String(process.env.MOSHCODE_EXTERNAL_BILLING || ""));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The external CLI for a command, if it is enabled and this machine has it. */
|
|
49
|
+
export function externalFor(cmd) {
|
|
50
|
+
if (!externalEnabled()) return null;
|
|
51
|
+
const key = DELEGATED[String(cmd || "").toLowerCase()];
|
|
52
|
+
if (!key) return null;
|
|
53
|
+
const tool = TOOLS[key];
|
|
54
|
+
if (!tool || !isInstalled(tool.bin, tool.binDirs)) return null;
|
|
55
|
+
return { key, tool };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Hand a command to its CLI, or report that there is nothing to hand it to.
|
|
60
|
+
*
|
|
61
|
+
* Returns `{ delegated: false }` when the CLI is absent so the caller can fall
|
|
62
|
+
* through to the built-in rather than failing — a missing optional tool is not
|
|
63
|
+
* an error, it is just the older path.
|
|
64
|
+
*/
|
|
65
|
+
export async function delegate(cmd, argv = [], opts = {}) {
|
|
66
|
+
const found = externalFor(cmd);
|
|
67
|
+
if (!found) return { delegated: false, code: 0 };
|
|
68
|
+
// openPassthrough resolves { ok, code, signal }, not a number. Assigning the
|
|
69
|
+
// object straight to process.exitCode throws ERR_INVALID_ARG_TYPE *after* the
|
|
70
|
+
// child has already printed its output, which reads as the tool crashing when
|
|
71
|
+
// in fact it succeeded.
|
|
72
|
+
const result = await openTool(found.tool, argv, opts);
|
|
73
|
+
return { delegated: true, code: exitCodeOf(result) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One number out of a passthrough result.
|
|
78
|
+
*
|
|
79
|
+
* A child killed by a signal reports `code: null`, and passing that on as 0
|
|
80
|
+
* would call an interrupted invoice run a success.
|
|
81
|
+
*/
|
|
82
|
+
export function exitCodeOf(result) {
|
|
83
|
+
if (typeof result === "number") return result;
|
|
84
|
+
if (!result || typeof result !== "object") return 0;
|
|
85
|
+
if (Number.isInteger(result.code)) return result.code;
|
|
86
|
+
if (result.signal) return 1;
|
|
87
|
+
return result.ok === false ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The one-line nudge shown after the built-in runs.
|
|
92
|
+
*
|
|
93
|
+
* Written to stderr, and only when the CLI is actually installed and simply not
|
|
94
|
+
* switched on, so it never lands in the middle of `--json` output and never
|
|
95
|
+
* advertises a tool that is not there. Someone who has installed the package is
|
|
96
|
+
* the only person for whom the variable is worth mentioning.
|
|
97
|
+
*/
|
|
98
|
+
export function installHint(cmd) {
|
|
99
|
+
const key = DELEGATED[String(cmd || "").toLowerCase()];
|
|
100
|
+
if (!key || externalEnabled()) return null;
|
|
101
|
+
const tool = TOOLS[key];
|
|
102
|
+
if (!tool || !isInstalled(tool.bin, tool.binDirs)) return null;
|
|
103
|
+
return `tip: @profullstack/${key} is installed — set MOSHCODE_EXTERNAL_BILLING=1 to use it`
|
|
104
|
+
+ " (move your records first with: billing import)";
|
|
105
|
+
}
|
package/src/plugins.mjs
CHANGED
|
@@ -53,6 +53,7 @@ export function marketplaceSource(env = process.env) {
|
|
|
53
53
|
export const PLUGINS = [
|
|
54
54
|
{
|
|
55
55
|
name: "stocks",
|
|
56
|
+
family: "markets",
|
|
56
57
|
version: "0.4.0",
|
|
57
58
|
description: "equity research slash commands backed by advis0r.com",
|
|
58
59
|
// `list` rather than `reports`: one letter from `report` is a coin-flip at
|
|
@@ -67,6 +68,7 @@ export const PLUGINS = [
|
|
|
67
68
|
},
|
|
68
69
|
{
|
|
69
70
|
name: "crypto",
|
|
71
|
+
family: "markets",
|
|
70
72
|
version: "0.4.0",
|
|
71
73
|
description: "crypto market data slash commands backed by advis0r.com",
|
|
72
74
|
commands: [
|
|
@@ -75,18 +77,48 @@ export const PLUGINS = [
|
|
|
75
77
|
],
|
|
76
78
|
example: "/crypto:report BTC",
|
|
77
79
|
},
|
|
80
|
+
// The two business plugins front CLIs that live outside this repo
|
|
81
|
+
// (@profullstack/timer, @profullstack/billing), which is why their commands
|
|
82
|
+
// are thin: the rules that matter — never stop somebody else's clock, never
|
|
83
|
+
// issue an invoice unprompted — belong in the command files, and the
|
|
84
|
+
// arithmetic belongs in the package.
|
|
85
|
+
{
|
|
86
|
+
name: "timer",
|
|
87
|
+
family: "business",
|
|
88
|
+
version: "0.1.0",
|
|
89
|
+
description: "track time against projects, backed by @profullstack/timer",
|
|
90
|
+
commands: ["/timer:start", "/timer:stop", "/timer:status", "/timer:report"],
|
|
91
|
+
example: "/timer:start acme fix the login redirect",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "billing",
|
|
95
|
+
family: "business",
|
|
96
|
+
version: "0.1.0",
|
|
97
|
+
description: "clients, rates and invoices, backed by @profullstack/billing",
|
|
98
|
+
commands: ["/billing:hours", "/billing:invoice", "/billing:rate", "/billing:report"],
|
|
99
|
+
example: "/billing:hours --client acme --month",
|
|
100
|
+
},
|
|
78
101
|
];
|
|
79
102
|
|
|
80
103
|
/**
|
|
81
|
-
* Command names
|
|
104
|
+
* Command names the plugins in a family are expected to share.
|
|
105
|
+
*
|
|
106
|
+
* Two plugins in a family can never ship the same *set* — a crypto pair has no
|
|
107
|
+
* earnings transcript and an equity has no order book, and a timer has no
|
|
108
|
+
* clients. What they can share is vocabulary: the same question is spelled the
|
|
109
|
+
* same way on both sides, so knowing one plugin means knowing half the other.
|
|
110
|
+
* A test holds this, because the natural drift is for one side to grow a
|
|
111
|
+
* synonym (`coin` for `lookup`, `stocks` for `report`). Both market plugins
|
|
112
|
+
* did, for two releases.
|
|
82
113
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* both sides, so knowing one plugin means knowing half the other. A test holds
|
|
87
|
-
* this, because the natural drift is for one side to grow a synonym.
|
|
114
|
+
* Keyed by family rather than one flat list, because the list only ever made
|
|
115
|
+
* sense for the pair it was written for: `quote` and `lookup` are market words,
|
|
116
|
+
* and requiring them of a timer would force two commands nobody would run.
|
|
88
117
|
*/
|
|
89
|
-
export const SHARED_COMMANDS =
|
|
118
|
+
export const SHARED_COMMANDS = {
|
|
119
|
+
markets: ["help", "report", "quote", "lookup"],
|
|
120
|
+
business: ["report"],
|
|
121
|
+
};
|
|
90
122
|
|
|
91
123
|
/** How Claude Code namespaces a plugin's command. */
|
|
92
124
|
export function pluginCommandName(plugin, file) {
|
package/src/tools.mjs
CHANGED
|
@@ -82,6 +82,24 @@ export const TOOLS = {
|
|
|
82
82
|
// where those words mean something else entirely.
|
|
83
83
|
aliases: { cmd: "cli-tools", args: ["aliases", "--json"] },
|
|
84
84
|
},
|
|
85
|
+
timer: {
|
|
86
|
+
desc: "Profullstack timer - track time against projects, for people and for agents",
|
|
87
|
+
bin: "timer",
|
|
88
|
+
// The standalone half of what /timer used to do entirely in-process. It
|
|
89
|
+
// lives outside moshcode because tracking time is not a moshcode idea: it
|
|
90
|
+
// works under any agentic CLI, on Linux, macOS and Windows, and
|
|
91
|
+
// @profullstack/billing reads its timesheet directly. `npm install -g` is
|
|
92
|
+
// idempotent, so it doubles as the upgrade path.
|
|
93
|
+
install: { cmd: "npm", args: ["install", "-g", "@profullstack/timer"] },
|
|
94
|
+
},
|
|
95
|
+
billing: {
|
|
96
|
+
desc: "Profullstack billing - clients, rates and invoices from tracked hours",
|
|
97
|
+
bin: "billing",
|
|
98
|
+
// The other half. It carries the rate model /rate parses ($100/hour/agent/
|
|
99
|
+
// upto:4) and bills agent-hours from the timer's entries. `billing import`
|
|
100
|
+
// brings across a ledger that started in ~/.moshcode/business.json.
|
|
101
|
+
install: { cmd: "npm", args: ["install", "-g", "@profullstack/billing"] },
|
|
102
|
+
},
|
|
85
103
|
bo: {
|
|
86
104
|
desc: "BufferOverride — capture a failing command, redact it, and find the answer that already exists",
|
|
87
105
|
// The product is BufferOverride and the binary is `bo`, the same split
|
package/src/tui.mjs
CHANGED
|
@@ -1112,8 +1112,21 @@ export async function tui() {
|
|
|
1112
1112
|
// and none of these close the readline interface: they print and return,
|
|
1113
1113
|
// like /ps and /cost, so the prompt never moves.
|
|
1114
1114
|
if (cmd === "timer") {
|
|
1115
|
+
// @profullstack/timer when it is installed, the built-in otherwise. The
|
|
1116
|
+
// readline interface is closed around the external one the way /secrets
|
|
1117
|
+
// and /payments do it: the CLI prints its own tables and has to own
|
|
1118
|
+
// stdout while it runs.
|
|
1119
|
+
const { delegate, externalFor, installHint } = await import("./business-delegate.mjs");
|
|
1120
|
+
if (externalFor("timer")) {
|
|
1121
|
+
rl.close();
|
|
1122
|
+
await delegate("timer", rest, {});
|
|
1123
|
+
rl = mkrl();
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1115
1126
|
const { timerCommand } = await import("./timer.mjs");
|
|
1116
1127
|
await timerCommand(rest, { write: (l) => console.log(l) });
|
|
1128
|
+
const hint = installHint("timer");
|
|
1129
|
+
if (hint) console.log(hint);
|
|
1117
1130
|
continue;
|
|
1118
1131
|
}
|
|
1119
1132
|
if (cmd === "client" || cmd === "business" || cmd === "merchant" || cmd === "customer") {
|
|
@@ -1132,12 +1145,22 @@ export async function tui() {
|
|
|
1132
1145
|
continue;
|
|
1133
1146
|
}
|
|
1134
1147
|
if (cmd === "billing" || cmd === "invoice") {
|
|
1148
|
+
// Closed and reopened around the call either way: the external CLI owns
|
|
1149
|
+
// stdout while it runs, and the built-in's `--send --yes` hands the
|
|
1150
|
+
// terminal to the gateway's own CLI, which may prompt.
|
|
1151
|
+
const { delegate, externalFor, installHint } = await import("./business-delegate.mjs");
|
|
1152
|
+
if (externalFor(cmd)) {
|
|
1153
|
+
rl.close();
|
|
1154
|
+
await delegate(cmd, rest, {});
|
|
1155
|
+
rl = mkrl();
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1135
1158
|
const { billingCommand } = await import("./billing.mjs");
|
|
1136
|
-
// Closed and reopened around the call: `--send --yes` hands the terminal
|
|
1137
|
-
// to the gateway's own CLI, which may prompt.
|
|
1138
1159
|
rl.close();
|
|
1139
1160
|
billingCommand(rest, { write: (l) => console.log(l) });
|
|
1140
1161
|
rl = mkrl();
|
|
1162
|
+
const hint = installHint(cmd);
|
|
1163
|
+
if (hint) console.log(hint);
|
|
1141
1164
|
continue;
|
|
1142
1165
|
}
|
|
1143
1166
|
if (cmd === "payments") {
|