cawdev-cli 0.9.0 → 1.0.0-beta

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/README.md CHANGED
@@ -1,175 +1,8 @@
1
1
  # tools
2
-
3
- Plain Node, **zero dependencies**, by design (see `CLAUDE.md`): stdio JSON-RPC
2
+ # Cawdev CLI Runner
3
+ Plain Node, **zero dependencies**, by design : stdio JSON-RPC
4
4
  and spawning processes are Node's home turf, and a file with no dependencies is
5
5
  one you can read before running it against your repositories.
6
+ visit https://cawdev.com to learn more
6
7
 
7
- Everything here reads `CAWDEV_URL`, `CAWDEV_TOKEN` and optionally
8
- `CAWDEV_PROJECT` from the environment or a `.env` at the repository root —
9
- **read on every call**, so editing `.env` needs no restart.
10
-
11
- Mint a token in the console under **Agent tokens**. It needs `roadmap:write`
12
- and `changelog:write` on the project you are exporting.
13
-
14
- ## The generated files
15
-
16
- `ROADMAP.md` and `CHANGELOG.md` at the repository root are **exports**. Editing
17
- them by hand is lost at the next export. Change the entry in the console or
18
- through MCP, then regenerate:
19
-
20
- ```sh
21
- node tools/roadmap/export.mjs cawdev
22
- node tools/changelog/export.mjs cawdev
23
- ```
24
-
25
- Both are byte-stable: the same data produces the same bytes, so a regenerated
26
- file shows a diff only when the roadmap actually changed. CI proves
27
- export → import → export is a fixed point.
28
-
29
- ### Where a section goes in the file
30
-
31
- An entry's `section` is a planning decision and lives in the database. The
32
- **order** the sections appear in is prose layout, and lives beside the exporter
33
- as `SECTION_ORDER` in `tools/roadmap/export.mjs`. `Considering` and `Declined`
34
- are not in that list: they are derived from status by `sectionOf`, and always
35
- come last.
36
-
37
- A section that is not in `SECTION_ORDER` is **appended, and named in the note
38
- the exporter prints** under its per-status counts, along with the number of
39
- entries it holds. It is a note rather than a failure — a new phase is
40
- legitimate, and an exporter that refused one would block the release that
41
- introduced it — but it should not arrive unannounced. Silence is how the list
42
- fell three sections behind and `"Roadmap"`, the fallback for an entry with no
43
- section of its own, became a junk drawer holding eight built features (R45).
44
-
45
- When you see the note: add the section to `SECTION_ORDER` in the position it
46
- should read in, or give those entries a section that is already there. Sections
47
- are renamed, merged and unfiled from the console's **Sections** dialog on the
48
- roadmap board (R264), and that dialog cannot reach this file: after a rename,
49
- update `SECTION_ORDER` too, or the note will list the new name.
50
-
51
- ## `tools/roadmap/import.mjs`
52
-
53
- The one-time move from a hand-written `ROADMAP.md` into the platform, and the
54
- other half of that fixed point.
55
-
56
- ```sh
57
- node tools/roadmap/import.mjs cawdev --file ROADMAP.md
58
- ```
59
-
60
- It is **idempotent enough to re-run after a partial failure**: entries that
61
- already exist are updated rather than duplicated. It refuses to start when
62
- preserving ids is impossible — ids are permanent, and silently renumbering them
63
- would break every commit message that points at one.
64
-
65
- ## `roadmap.mjs` (repository root)
66
-
67
- ```sh
68
- node roadmap.mjs # shape: ids unique, statuses legal, required fields present
69
- node roadmap.mjs --live # also: SHIPPED versions are real tags, CODING and
70
- # REVIEW branches exist, and the file matches the database
71
- node roadmap.mjs --live --project cawdev # when CAWDEV_PROJECT is not set
72
- ```
73
-
74
- Both forms run in CI. The shape checks catch what a review forgets — an entry
75
- that says `CODING` without naming a branch, an entry that says `MERGED` without
76
- naming the merge, a `Related:` pointing at nothing. `--live` catches the claims a
77
- file cannot check about itself: that a version someone wrote down was in fact
78
- tagged, and that the file is still the export it says it is. A `REVIEW` entry's
79
- branch is checked like a `CODING` one — the status claims there is something to
80
- read. A `MERGED` entry is checked against neither tags nor branches, because its
81
- branch is meant to have been deleted.
82
-
83
- That last one re-runs `tools/roadmap/export.mjs` into a temporary file and
84
- compares the bytes. It goes through the exporter rather than rendering a second
85
- copy of it, so the two cannot drift apart. A difference fails and names the
86
- entries — missing here, missing there, or changed — with the remedy. If every
87
- entry agrees and the bytes still differ, the preamble or the section layout was
88
- edited by hand.
89
-
90
- It needs a `CAWDEV_TOKEN` and a project. Without them it prints a note saying it
91
- was skipped and the git checks still run; a check that cannot answer says so
92
- rather than passing (R41). If the token is set and the platform rejects it or is
93
- down, that is not "cannot answer" — it fails, with the API's own message.
94
-
95
- ## Tests
96
-
97
- ```sh
98
- node --test "tools/**/*.test.mjs"
99
- ```
100
-
101
- `tools/lib/roadmap-format.test.mjs` covers the parse/render round trip. If those
102
- two stop agreeing, the generated file churns on every export and its diff stops
103
- meaning "the roadmap changed".
104
-
105
- `tools/lib/usage.test.mjs` covers what a turn reports. One of its tests asserts
106
- that **no dollar figure reaches a transcript**: the CLI quotes list prices, and
107
- a subscription does not work that way.
108
-
109
- `tools/lib/tool-rules.test.mjs` covers the matcher for what a run may do (R51).
110
- Every assertion of `false` in it is asserting that somebody gets asked a
111
- question they may have answered before — the cheap failure. The expensive one is
112
- a `true` nobody intended.
113
-
114
- `tools/mcp/approve.test.mjs` drives the real server over stdio against a faked
115
- platform, and pins the shape Claude Code reads back: a permission decision is
116
- JSON in the result text, and getting it wrong does not fail loudly — it stops a
117
- session, which is the failure R51 exists to end.
118
-
119
- `tools/runner/control.test.mjs` covers the socket a daemon offers (R52) on a
120
- real socket, including the backlog somebody attaching to an hour-old session
121
- needs, and that an unknown command is ignored rather than obeyed — that socket
122
- is a read-only view on purpose.
123
-
124
- `tools/runner/attach.test.mjs` covers the rendering arithmetic, which is where a
125
- terminal UI goes wrong silently: transcripts carry the agent's ANSI colour, and
126
- measuring it as plain text wraps the pane into nonsense.
127
-
128
- `tools/runner/workspaces.test.mjs` is R47 against the real daemon: two coding
129
- runs on one project, at the same time, in different checkouts, each reporting
130
- which. Getting that gate wrong is expensive in both directions — too tight and a
131
- machine with three checkouts still runs one session, too loose and two agents
132
- share a working copy.
133
-
134
- `tools/runner/test-platform.mjs` is the cawdev those two tests register with.
135
- Not a test itself, and not production code: it is a fake platform with a queue,
136
- shared because two fakes drift.
137
-
138
- `tools/runner/daemon-boot.test.mjs` starts the **real daemon** against a faked
139
- platform and a fake agent command. It is the only test of the first ten seconds
140
- — registering, the R51 permission-flag probe, opening the socket, and removing
141
- it again on the way out — a path that otherwise needs a `runner:operate` token
142
- to reach at all.
143
-
144
- ## `tools/mcp/`
145
-
146
- The MCP server an agent talks to — zero dependencies, stdio JSON-RPC.
147
- `tools/mcp/README.md` covers setting it up in a repository, and is also written
148
- for the agent that reads it: it teaches the working method (branch first,
149
- `CODING` names the branch before the first commit, `roadmap_where` when in
150
- doubt).
151
-
152
- `tools/mcp/smoke.mjs` drives the server the way a client does — spawn, write
153
- lines to stdin, read lines from stdout — because the parts most likely to break
154
- are the transport and the framing. CI runs it against the compose stack.
155
-
156
- ## `tools/runner/`
157
-
158
- The daemon that claims queued runs and spawns a real agent CLI in a working
159
- copy. `tools/runner/README.md` covers running one.
160
-
161
- ## `tools/console/`
162
-
163
- `console-smoke.mjs` is the end-to-end test for the console's half of the loop
164
- (R12): start a run on an entry, watch the messages, answer the question from the
165
- inbox, see the finish report carrying the branch — all over the endpoints the
166
- browser calls, on a real session cookie. Point it at a **scratch project**: it
167
- creates an entry, cancels any live run, and declines the entry afterwards.
168
-
169
- ```sh
170
- node tools/console/console-smoke.mjs scratch
171
- ```
172
-
173
- Unlike everything else here it signs in rather than using `CAWDEV_TOKEN`, because
174
- starting a run and answering a question are both a *person's* acts — an agent
175
- cannot do either, so the test cannot bootstrap itself from a token.
8
+ https://github.com/4dali/cawdev-cli
@@ -92,6 +92,46 @@ export function agentFile(agent) {
92
92
  return `${head.join('\n')}\n${agent.body ?? ''}\n`;
93
93
  }
94
94
 
95
+ /**
96
+ * Antigravity's own name for the same idea — R279. `agy plugin install
97
+ * <directory>` reads almost the same shape Claude Code's plugin does
98
+ * (`agents/*.md`, same frontmatter, same `agentFile()` below), confirmed by
99
+ * hand against the real CLI (v1.2.6): the one difference is where the
100
+ * manifest lives, `plugin.json` at the plugin's own root rather than under
101
+ * `.claude-plugin/`. No `skills/` here — R278 already inlines those into the
102
+ * prompt for this agent, since it has no lazy `Skill` tool to fetch one
103
+ * through a plugin the way Claude Code does.
104
+ *
105
+ * <p>A fixed name, like `registerAntigravityMcp`'s `cawdev` server: `agy
106
+ * plugin install` is machine-wide state, one project's experts at a time, so
107
+ * the runner removes whatever this name currently holds and reinstalls
108
+ * fresh before every spawn rather than trying to reconcile two projects'
109
+ * agents under one name.
110
+ */
111
+ export const ANTIGRAVITY_EXPERTS_PLUGIN = 'cawdev-experts';
112
+
113
+ /**
114
+ * Writes the run's experts as an Antigravity plugin directory — R279's half
115
+ * of `writeRunPlugin`. Returns the directory, or null when nothing was
116
+ * turned on, same contract.
117
+ */
118
+ export async function writeAntigravityExpertsPlugin(directory, expertAgents) {
119
+ if (!expertAgents.length) {
120
+ return null;
121
+ }
122
+ const root = join(directory, ANTIGRAVITY_EXPERTS_PLUGIN);
123
+ await mkdir(join(root, 'agents'), { recursive: true });
124
+ await writeFile(join(root, 'plugin.json'), JSON.stringify({
125
+ name: ANTIGRAVITY_EXPERTS_PLUGIN,
126
+ description: 'What this project turned on in cawdev.',
127
+ version: '1.0.0',
128
+ }, null, 2));
129
+ for (const agent of expertAgents) {
130
+ await writeFile(join(root, 'agents', `${agent.key}.md`), agentFile(agent));
131
+ }
132
+ return root;
133
+ }
134
+
95
135
  export function skillFile(skill) {
96
136
  const head = [
97
137
  '---',
@@ -112,6 +112,33 @@ export function parseUsage(text, now = new Date()) {
112
112
  if (!text) {
113
113
  return [];
114
114
  }
115
+
116
+ // Handle Agy output format
117
+ if (String(text).includes('Limit Remaining')) {
118
+ const windows = [];
119
+ for (const raw of String(text).split('\n')) {
120
+ const line = raw.trim();
121
+ if (!line || line.startsWith('Quota:')) continue;
122
+
123
+ const match = /^(.*?)\s+(Weekly|Five Hour)\s+Limit Remaining\s+(\d{1,3})%\s+(.*Z)$/i.exec(line);
124
+ if (!match) continue;
125
+
126
+ const model = match[1].trim();
127
+ const kind = match[2].toLowerCase() === 'weekly' ? 'WEEKLY' : 'FIVE_HOUR';
128
+ const remainingPercent = Number(match[3]);
129
+ const resetsAt = new Date(match[4].trim());
130
+
131
+ windows.push({
132
+ kind,
133
+ model: model,
134
+ percent: 100 - remainingPercent,
135
+ resetsAt: isNaN(resetsAt.getTime()) ? null : resetsAt,
136
+ label: line,
137
+ });
138
+ }
139
+ return windows;
140
+ }
141
+
115
142
  const windows = [];
116
143
  for (const raw of String(text).split('\n')) {
117
144
  const line = raw.trim();
@@ -0,0 +1,48 @@
1
+ // R283 — the CLI's own version, and whether it is old enough to matter.
2
+ //
3
+ // Zero dependencies, like everything in tools/: no semver package, just the
4
+ // three numbers this project actually uses (`tools/package.json`'s own
5
+ // version is always `major.minor.patch`, and so is what the platform sends
6
+ // back on `/api/health`).
7
+
8
+ import { readFile } from 'node:fs/promises';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+
14
+ /** This CLI's own version, read from `tools/package.json`. */
15
+ export async function cliVersion() {
16
+ const path = join(HERE, '..', 'package.json');
17
+ const parsed = JSON.parse(await readFile(path, 'utf8'));
18
+ return parsed.version;
19
+ }
20
+
21
+ /**
22
+ * `major.minor.patch` as three numbers, or null for anything that is not
23
+ * shaped that way — a pre-release suffix, a build nobody tagged, garbage from
24
+ * a setting typed by hand. Null is "cannot compare", never "oldest possible":
25
+ * refusing a version we cannot parse would turn a typo in a setting into
26
+ * every runner on the platform refusing to boot.
27
+ */
28
+ function parts(version) {
29
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version ?? '').trim());
30
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
31
+ }
32
+
33
+ /**
34
+ * Whether `version` is older than `minimum`. False when either cannot be
35
+ * parsed, or when `minimum` is blank — "cannot tell" and "no requirement" are
36
+ * both the safe reading here, matching `PlatformSettingService`'s own
37
+ * treatment of a blank `minimum_cli_version`.
38
+ */
39
+ export function isBelow(version, minimum) {
40
+ if (!minimum || !String(minimum).trim()) return false;
41
+ const have = parts(version);
42
+ const need = parts(minimum);
43
+ if (!have || !need) return false;
44
+ for (let i = 0; i < 3; i += 1) {
45
+ if (have[i] !== need[i]) return have[i] < need[i];
46
+ }
47
+ return false;
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cawdev-cli",
3
- "version": "0.9.0",
3
+ "version": "1.0.0-beta",
4
4
  "description": "cawdev's terminal client and runner daemon — zero dependencies, on purpose",
5
5
  "type": "module",
6
6
  "bin": {
package/runner/README.md CHANGED
@@ -76,6 +76,7 @@ cawdev --config <path> the runner config to start a daemon from
76
76
  cawdev --no-start attach only; never launch a daemon
77
77
  cawdev --watch-only do not sign in; watch without being able to act
78
78
  cawdev --leave-running leave the daemon running when you quit
79
+ cawdev --version this CLI's version, and the platform's
79
80
  ```
80
81
 
81
82
  **Quitting stops the daemon** — R123. One word starts the machine and the
@@ -94,6 +95,59 @@ process and one terminal**, which is what you want when the daemon should die
94
95
  with the window — there `q` stops it, and asks first when sessions are live.
95
96
  `node runner.mjs attach` joins a daemon started elsewhere.
96
97
 
98
+ ### `c`, `/config`, `cawdev config` — one change, not the whole walk
99
+
100
+ `cawdev --setup` (above) asks everything a fresh machine needs at once. Once a
101
+ machine is configured, most changes are smaller than that. **Inside the
102
+ terminal, press `c`** (or type `/config`): a picker over what this machine does
103
+ — add a project, add a workspace, each agent with its current state ("Disable
104
+ claude", "Enable agy"), the console-rules switch, and the config itself with the
105
+ token cut short. Every row is one of the shell commands below, driven through
106
+ the terminal's own line editor and picker; `esc` anywhere in a step changes
107
+ nothing. The screen edits the file the daemon says it booted from (the hello
108
+ carries `configPath` since R288), so an older daemon is told to restart rather
109
+ than guessed at, and adding a project asks for a `/login` first because it
110
+ widens the machine's token.
111
+
112
+ **Choosing where a checkout goes** (R289) is a picker before it is a line: the
113
+ folder beside the checkouts this machine already serves, the default
114
+ `~/cawdev/<slug>`, for a second workspace `<checkout>-2` — each row saying what
115
+ is there now ("new folder — will clone here", "already a checkout of cawdev",
116
+ "a checkout of …/else, not cawdev"). The last row opens a line **pre-filled**
117
+ with the first suggestion; `tab` completes directories from disk the way a
118
+ shell does, `~` expands, and a preview under the line says what the path is as
119
+ you type. Before anything is cloned the URL and the path are confirmed once; a
120
+ checkout of the same repository already at the path is taken as it is, with no
121
+ question; anything else that exists is refused in words. `paths.mjs` is that
122
+ logic, shared with the shell form below so the two doors agree. The shell form
123
+ is for scripts and for a machine with no daemon to attach to:
124
+
125
+ ```
126
+ cawdev config a menu, the same Select widget as everywhere else
127
+ cawdev config add-project serve one more project you can write to
128
+ cawdev config add-workspace another checkout for a project already served
129
+ cawdev config agent enable|disable <claude|agy>
130
+ cawdev config rules on|off accept permission rules from the console — R126
131
+ cawdev config show this machine's config, token redacted
132
+ ```
133
+
134
+ Every one of these writes `runner.config.json` and nothing else. Adding a
135
+ project widens the machine's existing `runner:operate` token in place
136
+ (`PUT /api/tokens/{id}/grants`) rather than minting a second one — the id
137
+ travels beside the secret since this card, and a machine set up before it
138
+ falls back to a lookup by label the first time this runs, after which it
139
+ remembers.
140
+
141
+ Adding a project or a workspace needs a restart to take effect, same as any
142
+ other change to `"projects"` — a live daemon says so. Enabling or disabling an
143
+ agent and turning `rules` on or off do **not**: `cawdev config` — and the `c`
144
+ screen, through the pid the hello names — sends the running daemon a `SIGHUP`,
145
+ which re-reads just those three settings from the file and updates what the
146
+ hello advertises, so the next `c` shows the new state. That is a unix convention asked of a process this operator already
147
+ owns, not a command sent over the control socket in `attach.mjs` — that socket
148
+ is deliberately read-only (see its own header), and a config change is not an
149
+ exception carved into it.
150
+
97
151
  ### What it shows you
98
152
 
99
153
  The console shows you a session. This shows you **the machine** — and the
@@ -130,6 +184,7 @@ long it has been going, and the key that stops it. It is absent when nothing is.
130
184
  | `Y` | allow always, here — writes a project rule |
131
185
  | `x`, twice | cancel the session |
132
186
  | `g` | print the daemon's own log instead of the transcript |
187
+ | `c` | configure this machine — the `cawdev config` menu, inside the terminal (R288) |
133
188
  | `q` | stop the runner and leave (`--leave-running` keeps it up) |
134
189
  | `esc` | close whatever is open, without ending the session |
135
190
  | `ctrl+c`, twice | the same, and then leave |
@@ -144,6 +199,7 @@ move through the command list while one is open.
144
199
  | `/login` | sign in through the browser |
145
200
  | `/logout` | forget the stored session on this machine |
146
201
  | `/runs` | the run list — the same as `L` |
202
+ | `/config` | this machine's config — the same as `c` |
147
203
  | `/cancel` | cancel the session you are watching |
148
204
  | `/log` | the daemon's own log, on or off |
149
205
  | `/quit` | leave |