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
24 changes: 24 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@
},
"homepage": "https://github.com/moshcoder/moshcode#crypto",
"keywords": ["crypto", "bitcoin", "markets", "prices", "advis0r"]
},
{
"name": "timer",
"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.",
"source": "./plugins/timer",
"category": "productivity",
"author": {
"name": "moshcoder",
"url": "https://moshcode.sh"
},
"homepage": "https://github.com/profullstack/timer#readme",
"keywords": ["timer", "time-tracking", "billable", "hours", "profullstack"]
},
{
"name": "billing",
"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.",
"source": "./plugins/billing",
"category": "productivity",
"author": {
"name": "moshcoder",
"url": "https://moshcode.sh"
},
"homepage": "https://github.com/profullstack/billing#readme",
"keywords": ["billing", "invoice", "clients", "rates", "profullstack"]
}
]
}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,26 @@ Every agentic CLI helps you do the work. This one also bills for it. Six words,
each useful on its own — the timer needs no client, the rate needs no gateway
(PRD [0012](prd/0012-billing-baked-into-the-agent-cli.md)).

> **`/timer` and `/billing` now prefer their own CLIs.** Tracking time and
> sending an invoice are not moshcode ideas — they are useful under any agentic
> CLI, and on Windows, where moshcode does not go. So they also ship standalone:
> [`@profullstack/timer`](https://github.com/profullstack/timer) and
> [`@profullstack/billing`](https://github.com/profullstack/billing).
>
> ```sh
> moshcode install timer billing # or: npm install -g @profullstack/timer @profullstack/billing
> ```
>
> With them installed, `/timer` and `/billing` hand the command straight to the
> CLI, the way `/gh` conducts `gh`. Without them, the in-process implementation
> below runs exactly as it always has, so upgrading changes nothing until you
> choose to install. `MOSHCODE_BUILTIN_BILLING=1` pins the built-in either way.
>
> The standalone billing carries the same rate model (`$100/hour/agent/upto:4`)
> and bills **agent-hours**, and `billing import` brings across a ledger that
> started in `~/.moshcode/`. `/client`, `/rate`, `/payments` and `/team` stay
> here: the rails and the permission model are moshcode integration.

```sh
moshcode timer on acme --task "batch payments" --agents auto # auto counts the herd
moshcode timer off # → 1h 12m, $480.00
Expand Down
12 changes: 12 additions & 0 deletions bin/moshcode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,15 @@ async function main() {
// the startup path of `moshcode claude`, which is what this binary is mostly
// asked to do.
if (cmd === "timer") {
// @profullstack/timer when it is installed, the built-in otherwise. See
// src/business-delegate.mjs for why the external one wins.
const { delegate, installHint } = await import("../src/business-delegate.mjs");
const handed = await delegate("timer", rest, {});
if (handed.delegated) { process.exitCode = handed.code; return; }
const { timerCommand } = await import("../src/timer.mjs");
process.exitCode = (await timerCommand(rest)) || 0;
const hint = installHint("timer");
if (hint) process.stderr.write(`${hint}\n`);
return;
}
if (cmd === "client" || cmd === "business" || cmd === "merchant" || cmd === "customer") {
Expand All @@ -443,8 +450,13 @@ async function main() {
return;
}
if (cmd === "billing" || cmd === "invoice") {
const { delegate, installHint } = await import("../src/business-delegate.mjs");
const handed = await delegate(cmd, rest, {});
if (handed.delegated) { process.exitCode = handed.code; return; }
const { billingCommand } = await import("../src/billing.mjs");
process.exitCode = billingCommand(rest) || 0;
const hint = installHint(cmd);
if (hint) process.stderr.write(`${hint}\n`);
return;
}
if (cmd === "payments") {
Expand Down
13 changes: 13 additions & 0 deletions plugins/billing/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
"name": "billing",
"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.",
"version": "0.1.0",
"author": {
"name": "moshcoder",
"url": "https://moshcode.sh"
},
"homepage": "https://github.com/profullstack/billing#readme",
"license": "MIT",
"keywords": ["billing", "invoice", "clients", "rates", "profullstack"]
}
42 changes: 42 additions & 0 deletions plugins/billing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# billing

Slash commands for
[`@profullstack/billing`](https://github.com/profullstack/billing) — clients,
rates and invoices, built on the hours `@profullstack/timer` tracked.

```
/billing:rate set acme '$100/hour/agent/upto:4'
/billing:hours --client acme --month
/billing:invoice --client acme --from-timer --month
/billing:report
```

## Install the CLI

```sh
npm install -g @profullstack/billing
```

or, inside moshcode:

```
moshcode install billing
```

## What it is for

A rate is the sentence from the contract, parsed. `$100/hour/agent/upto:4` means
four agents cost four hundred an hour and so do six, and the invoice bills
**agent-hours** so the client can check the line by hand: `quantity × rate`
always equals `amount`.

Two rules the shape enforces. The same hour never reaches two invoices — each
invoice records the timer entry ids it covers, so voiding one releases them.
And creating an invoice is a proposal: `--dry-run` validates the whole thing and
writes nothing, a new invoice is a draft, and nothing is ever emailed.

## Coming from moshcode

moshcode used to keep this layer internally. `billing import` brings it across
from `~/.moshcode/business.json` and `timers.json`, shows the plan first, and
never modifies the originals.
37 changes: 37 additions & 0 deletions plugins/billing/commands/hours.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
description: Tracked hours not yet on an invoice, priced at the client's rate.
argument-hint: --client <name> [--month|--week]
allowed-tools: Bash(billing hours:*), Bash(billing client:*), Bash(billing rate:*)
---

## Task

```bash
billing hours $ARGUMENTS --json
```

This is the preview of what `billing invoice new --from-timer` would bill. Same
filters, same grouping, same arithmetic — it writes nothing.

## Reading the response

- `items[]` — the line items, each with `description`, `quantity` (in the rate's
own billing unit), `unitPriceMajor`, `amount`, `hours` and `timerIds`.
- `unit` — what `quantity` is measured in: `hours`, or `agent-hours` when the
rate is priced per agent.
- `hours` / `units` / `subtotal` — the totals.
- `skipped` — `{ running, unbillable, alreadyBilled }`.

## Rules

- **Always read `skipped.running` back to the user.** Those hours are not
missing, they are on a clock that is still ticking and become billable the
moment it stops. "Nothing to bill" is misleading when the real answer is "stop
the clock first".
- Exit 3 means no unbilled hours matched, or the client does not exist. Read the
message; it distinguishes them.
- When `unit` is `agent-hours`, explain the multiplier if the user seems
surprised: 3 hours with 2 agents is 6 agent-hours. `quantity * unitPrice`
always equals `amount`.
- If there is no rate, the error says so and names the command that sets one.
Do not invent a rate to get past it.
39 changes: 39 additions & 0 deletions plugins/billing/commands/invoice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
description: Draft an invoice from tracked time or fixed line items, for a person to approve.
argument-hint: --client <name> --from-timer --month
allowed-tools: Bash(billing invoice:*), Bash(billing hours:*), Bash(billing client:*), Bash(billing rate:*)
---

## Task

Propose an invoice for `$ARGUMENTS`. **Always dry-run first:**

```bash
billing invoice new $ARGUMENTS --dry-run --json
```

Show the user what it would create. Only run it again without `--dry-run` when
they have said to.

Line items come from `--from-timer` (tracked hours), from `--item
"Description|quantity|price"`, or both on the same invoice.

## Rules

- **Creating an invoice is a business action. Do not write one unprompted.**
`--dry-run` builds and validates the entire invoice — the rate lookup and the
double-billing check included — so a dry run that succeeds means the real one
will. There is no reason to skip it.
- A new invoice is a **draft**. `billing invoice mark <n> sent` is a separate,
deliberate step and this tool never emails anything. Do not mark an invoice
sent or paid unless the user asked.
- Read `amounts` (decimal) when talking to a person; the bare `total` is in
minor units.
- Exit 3 on `--from-timer` means there were no unbilled hours. Check
`billing hours` and report `skipped.running` rather than concluding there is
nothing to bill.
- The same hour cannot reach two invoices: each invoice records the timer entry
ids it covers. If the user wants to re-bill something, voiding the old invoice
releases those hours.
- Prefer `billing invoice render <n> --format html --out <file>` when they want
something to send: it is one self-contained file that prints to PDF.
40 changes: 40 additions & 0 deletions plugins/billing/commands/rate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
description: Read or set what your time costs, written the way the contract says it.
argument-hint: "[set <client|default> '$100/hour/agent/upto:4']"
allowed-tools: Bash(billing rate:*), Bash(billing client:*)
---

## Task

```bash
billing rate $ARGUMENTS --json
```

With no arguments this lists every rate. `set`, `show` and `rm` take a target,
which is either a client handle or `default`.

## The spec

A price, then any of these in any order:

- a period: `hour`, `day` (8h), `week` (40h), `month` (160h), `project`, `task`
- a unit that gets multiplied: `agent`, `seat`, `person`, `team`
- `upto:N` to cap the multiplier, `min:N` for a minimum billed period

`$100/hour/agent/upto:4` means four agents cost four hundred an hour, and so do
six. `0.5 SOL/day`, `250 USDC/task` and `$5000/project` all parse too.

## Rules

- **Do not set or change a rate unless the user asked.** This is the number in
somebody's contract.
- Read `describes` back to them when confirming — it is the rate as a sentence,
and it is how you catch a spec that parsed differently from how it was meant.
- Settlement (`--prefer SOL --accept fiat`) is deliberately separate from the
price. The number in the contract does not change because the rail did, so do
not "convert" a rate to a preferred ticker.
- A price given in a ticker invoices in that ticker. Do not turn `0.5 SOL` into
a dollar figure: nobody computed that number.
- If a spec is rejected, the error names the words that are allowed. Fix the
spec rather than falling back to a bare number, which would silently mean
"per hour, flat".
28 changes: 28 additions & 0 deletions plugins/billing/commands/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
description: What has been billed, collected, and what is still owed.
argument-hint: "[--client <name>] [--year|--month]"
allowed-tools: Bash(billing report:*), Bash(billing invoice:*)
---

## Task

```bash
billing report $ARGUMENTS --json
```

## Reading the response

- `totals` — `billed`, `collected`, `outstanding`, `overdue`, `draft`, all as
decimal numbers in `currency`.
- `byClient[]` — the same figures per client, biggest outstanding first.

## Rules

- **`draft` is not money anybody owes you.** A draft invoice has not been
issued, so keep it out of any "you are owed X" sentence and name it
separately.
- `overdue` is derived from the due date at read time, never stored. It is a
subset of `outstanding`, not an addition to it — do not sum them.
- Windows apply to the **issue date** here, not to when the work was done.
- For the invoices behind a figure, use `billing invoice list --overdue --json`
or `--status sent`. Do not guess at which invoices make up a total.
13 changes: 13 additions & 0 deletions plugins/timer/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
"name": "timer",
"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.",
"version": "0.1.0",
"author": {
"name": "moshcoder",
"url": "https://moshcode.sh"
},
"homepage": "https://github.com/profullstack/timer#readme",
"license": "MIT",
"keywords": ["timer", "time-tracking", "billable", "hours", "profullstack"]
}
36 changes: 36 additions & 0 deletions plugins/timer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# timer

Slash commands for [`@profullstack/timer`](https://github.com/profullstack/timer)
— a time tracker that runs on Linux, macOS and Windows, and answers `--json` on
every command so an agent can clock its own work.

```
/timer:start acme fix the login redirect
/timer:status
/timer:stop
/timer:report --week --group day
```

## Install the CLI

```sh
npm install -g @profullstack/timer
```

or, inside moshcode:

```
moshcode install timer
```

## What it is for

An hour of agentic work is an hour times however many engines ran in it, so an
entry carries an agent count (`--agents 4`). `@profullstack/billing` multiplies
by it when the rate says to and ignores it when the rate is flat.

Several clocks may run at once. That is deliberate: parallel agents each track
their own work and do not stop each other.

The timesheet is one JSON file at `~/.profullstack/timer/timesheet.json`, and
billing reads it directly.
32 changes: 32 additions & 0 deletions plugins/timer/commands/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
description: Totals for a period, grouped by project, task, day, tag or agent.
argument-hint: "[--week|--month] [--group project|task|day|tag|agent]"
allowed-tools: Bash(timer report:*), Bash(timer log:*), Bash(timer projects:*)
---

## Task

```bash
timer report $ARGUMENTS --json
```

Windows: `--today`, `--yesterday`, `--week` (from Monday), `--month`, `--year`,
or explicit `--since` / `--until`. Groups: `project` (default), `task`, `day`,
`tag`, `agent`, `none`.

## Reading the response

`rows[]` each carry `key`, `entries`, `hours` and `billableHours`. `totals` has
the same figures for the whole window.

## Rules

- **A window compares against the entry's start, and `--until` is exclusive.**
An entry that ran past midnight belongs to the day it began on. Say so if the
user questions a boundary rather than guessing at a bug.
- Report `hours` and `billableHours` separately whenever they differ.
- `timer log` is the command for the entries behind a number. Reach for it when
the user asks why a total looks the way it does.
- Do not convert hours into money here. The rate lives in
`@profullstack/billing`, which knows about agent multipliers and caps; a
hours-times-rate figure invented here will disagree with the invoice.
Loading
Loading