@ucsandman/legcli 0.13.1 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 (2026-09-18)
4
+
5
+ The 14-day trial is back.
6
+
7
+ - **Free for 14 days, no key, no card.** The first `leg <agent>` on a machine
8
+ starts a 14-day trial with every gate open, Team's `leg share` included, and
9
+ records it in `$LEG_HOME/trial.json`. `leg license status` shows the days
10
+ left without starting the clock. After the trial, or with a refused key once
11
+ it is over, a session exits 4 with the price and the 30-day guarantee, which
12
+ stays as the second net after buying. Deleting the file resets the clock,
13
+ which is the known limit of an offline trial and is accepted. The license
14
+ agreement gains the trial clause; the site, README, support page and
15
+ `llms.txt` say "free for 14 days" where they said "no trial".
16
+ - **The README stops calling the repository private.** It has been public at
17
+ github.com/ucsandman/legcli; two sentences still said otherwise.
18
+
3
19
  ## 0.13.1 (2026-09-18)
4
20
 
5
21
  A one-line fix in the terminal and the marketing site brought up to what 0.13.0 does.
package/LICENSE CHANGED
@@ -9,6 +9,8 @@ install or use Leg.
9
9
 
10
10
  1. License. When you buy a license we grant you a non-exclusive,
11
11
  non-transferable license to install and use Leg as follows.
12
+ Trial: before buying, one person may install and use Leg to evaluate it
13
+ for 14 days from its first use on a machine, on the Team terms below.
12
14
  Personal: one named person, on any number of machines that person uses.
13
15
  Team: one named person per seat purchased. A Team license also covers use
14
16
  of the shared board (leg share) by the people holding seats. A Personal
package/README.md CHANGED
@@ -23,7 +23,7 @@ files and regions). `leg claude --model opus` is
23
23
  1. **A board.** Opened once in your browser, reused after that. Every Leg
24
24
  session in every terminal is a row on it, and the row's register reads
25
25
  status, repo on branch, uncommitted and unpushed counts, agent and model,
26
- and how long it has been quiet: `waiting on you baton on main dirty 3
26
+ and how long it has been quiet: `waiting on you leg on main dirty 3
27
27
  ahead 2 claude/fable`. Under that come the prompt, the one thing worth
28
28
  knowing, the files it is touching and the usage bucket that will stop it.
29
29
  Capacity is one strip at the top rather than a region, with the login
@@ -138,8 +138,8 @@ agent name passes straight through (`leg codex -m gpt-5.3-codex-spark`,
138
138
  through unchanged, and your settings file is never edited: Leg's hooks ride
139
139
  in a separate per-session `--settings` file.
140
140
 
141
- The source repository is private. Leg is commercial, source-available
142
- software: every `.mjs` file that runs is in the package you just installed, at
141
+ Leg is commercial, source-available software, and the source is on
142
+ [GitHub](https://github.com/ucsandman/legcli): every `.mjs` file that runs is in the package you just installed, at
143
143
  `$(npm root -g)/legcli/src`, and the license lets you read it and modify
144
144
  your own copy. There is nothing compiled, minified or bundled to see through.
145
145
 
@@ -697,8 +697,8 @@ drift. CI waits for the Ubuntu and Windows test matrix, validates both
697
697
  when that version is missing and newer than the stable `latest`. Existing
698
698
  versions skip cleanly; older, prerelease, lockstep, and registry-error cases
699
699
  fail the job. The npm trusted publisher is bound to `ucsandman/legcli` and
700
- `.github/workflows/ci.yml` (bind `leg-agents` the same way). The repository
701
- stays private, so publication uses `--provenance=false`.
700
+ `.github/workflows/ci.yml` (bind `leg-agents` the same way). Publication
701
+ uses `--provenance=false`.
702
702
 
703
703
  ## Privacy and attribution
704
704
 
@@ -714,17 +714,19 @@ Leg is commercial software under the [Leg License Agreement](LICENSE).
714
714
  It ships as readable JavaScript so you can see what it does on your machine,
715
715
  and you may modify it for your own use, but not redistribute it or work
716
716
  around the license check. Versions 0.2.0 and 0.3.0 were published under MIT
717
- and remain available. The version in this source tree is 0.13.1; see
717
+ and remain available. The version in this source tree is 0.14.0; see
718
718
  [npm](https://www.npmjs.com/package/legcli) for published versions and
719
719
  [CHANGELOG.md](CHANGELOG.md) for release notes.
720
720
 
721
- Using it needs a license: **Personal, $79 once**, one human on any number of
721
+ The first 14 days are a **free trial**: install it, type `leg claude`, and
722
+ everything in both plans is on, with no key and no card. The clock starts on
723
+ your first session and `leg license status` shows what is left. After that
724
+ it needs a license: **Personal, $79 once**, one human on any number of
722
725
  machines, every release for 12 months and the version you have keeps working
723
726
  after that; **Team, $12 per seat per month**, Personal plus `leg share` for
724
727
  more than one human on the board. Buy at the site, then
725
- `leg license activate <key>`. There is no trial; there is a **30-day
726
- money-back guarantee**, so the way to evaluate Leg is to use it on real work
727
- and ask for a refund if it does not earn its place. A key is a signed token
728
+ `leg license activate <key>`. There is also a **30-day money-back
729
+ guarantee** after buying, no reason required. A key is a signed token
728
730
  checked offline with the public key in `src/license.mjs`; only a Team key
729
731
  renewal talks to the site. The bare agent CLIs are never affected by any of
730
732
  this; only what Leg adds is licensed.
package/bin/leg.mjs CHANGED
@@ -493,10 +493,11 @@ async function main() {
493
493
  // The paid gate. Keys verify offline against the public key in
494
494
  // src/license.mjs; nothing here talks to the network except refresh.
495
495
  if (!cmd || cmd === 'status') {
496
- const ent = entitlement()
496
+ // looking does not start the trial clock; the first session does
497
+ const ent = entitlement({ startTrial: false })
497
498
  out(describeLicense(ent))
498
499
  if (ent.source === 'license') out(`stored at ${licensePath()}`)
499
- if (!ent.ok) out(`Buy: ${BUY_URL} then: leg license activate <key>`)
500
+ if (!ent.ok || ent.source === 'trial') out(`Buy: ${BUY_URL} then: leg license activate <key>`)
500
501
  return
501
502
  }
502
503
  if (cmd === 'activate') {
@@ -508,7 +509,7 @@ async function main() {
508
509
  } catch (err) { die(2, err.message) }
509
510
  return
510
511
  }
511
- if (cmd === 'deactivate') return out(deactivateLicense() ? `removed ${licensePath()}; Leg needs a key again before it will run` : 'no license was stored')
512
+ if (cmd === 'deactivate') return out(deactivateLicense() ? `removed ${licensePath()}; Leg is back on the trial if it has days left, otherwise it needs a key` : 'no license was stored')
512
513
  if (cmd === 'refresh') {
513
514
  try { const p = await refreshLicense(); out(`renewed ${p.plan} license ${p.id}, valid through ${p.expires}`) } catch (err) { die(2, err.message) }
514
515
  return
package/docs/DECISIONS.md CHANGED
@@ -100,7 +100,7 @@ The repository stays private (see the 2026-09-11 license decision). That left ev
100
100
  ## 2026-09-11: Leg is a commercial product; the site stays on the free Vercel address
101
101
 
102
102
  - **License.** Wes: "if we're trying to sell this thing it shouldn't be open source and MIT." The repo stays private and the package ships under the Leg License Agreement (LICENSE): commercial, source readable in the package for inspection and own-use modification, no redistribution, no working around the license check. The FSL option from the pricing research was dropped for the same reason. Versions 0.2.0 and 0.3.0 remain available under MIT.
103
- - **Pricing.** Personal $79 once with 12 months of releases (Sublime shape); Team $12 per seat per month (adds `leg share`); no trial, and a 30-day money-back guarantee as the risk reversal instead (2026-09-15: a trial suits daily-habit products, and Leg's value is bursty, it pays off in the moment a limit lands, which a fortnight of evaluation does not reliably contain; a buyer already paying for two or three agent subscriptions is not price-sensitive at $79, they are trust-sensitive, and a trial does not answer trust). Keys are Ed25519 tokens signed with a private key that lives only in the seller's `.env` and the site's Vercel env; the public key is in `src/license.mjs`. A Personal key is a window over `RELEASE_DATE`, so every release bumps that constant.
103
+ - **Pricing.** Personal $79 once with 12 months of releases (Sublime shape); Team $12 per seat per month (adds `leg share`); ~~no trial, and a 30-day money-back guarantee as the risk reversal instead~~ superseded 2026-09-18 (2026-09-15: a trial suits daily-habit products, and Leg's value is bursty, it pays off in the moment a limit lands, which a fortnight of evaluation does not reliably contain; a buyer already paying for two or three agent subscriptions is not price-sensitive at $79, they are trust-sensitive, and a trial does not answer trust). 2026-09-18, Wes, ahead of the Hacker News post: the 14-day trial from 0.4.0 is back, started by the first session and recorded in `$LEG_HOME/trial.json` with every gate open, and the 30-day guarantee stays as the second net after buying. A launch audience installs to look, and "buy first" on a repo they found ten minutes ago is a wall the guarantee does not lower. Keys are Ed25519 tokens signed with a private key that lives only in the seller's `.env` and the site's Vercel env; the public key is in `src/license.mjs`. A Personal key is a window over `RELEASE_DATE`, so every release bumps that constant.
104
104
  - **Checkout.** Stripe payment links (live) with automatic tax, `site/api/key` and `site/api/webhook` on Vercel functions, Resend from `legcli@practicalsystems.io`. `scripts/stripe-setup.mjs` is idempotent per site origin; a test-mode purchase was run end to end on 2026-09-11 (checkout, thanks page, key activated in the CLI, webhook 200 twice).
105
105
  - **Domain.** A `legcli.com` purchase ($11.25) was started and cancelled at Wes's "just deploy it to a free vercel site"; nothing was bought. The site is https://legcli.com/.
106
106
  - **Not a lawyer.** The license text was drafted in-session; a review before the first sale outside the US is Wes's call.
@@ -116,6 +116,7 @@ The repository stays private (see the 2026-09-11 license decision). That left ev
116
116
  - **Root Directory had to change first.** It was `.`, which is correct when deploying from inside `site/` but would have published the repo root on a git build: no `index.html`, and `api/key` and `api/webhook` gone, so a purchase in flight would not have received its key.
117
117
  - ~~**Not every push.** `site/vercel.json` carries `ignoreCommand: git diff --quiet HEAD^ HEAD .`, which Vercel maps to the Ignored Build Step. A commit that touches nothing under `site/` cancels the build. The command failing (a shallow clone with no `HEAD^`) exits non-zero, which builds, so the failure mode is a redundant deploy rather than a missed one.~~ **Superseded 2026-09-15: the failure mode was a MISSED deploy.** `HEAD^ HEAD` compares one commit, not the push. Fast-forwarding five commits to main where only the first touched `site/` left the tip with no `site/` change, so Vercel cancelled the build and the redesigned site never went out while the repo said it had. The `ignoreCommand` is removed: every push to `main` deploys the site. Vercel's own "Skip deployments when there are no changes to the root directory" toggle is the safe version of this idea, because it compares against the last deployment rather than against `HEAD^`.
118
118
  - **Agent sessions cannot do this part.** `vercel --prod` and `vercel git connect` are both denied by the harness classifier as production deploys, and the CLI auth token cannot be read. Wes ran both.
119
+ - **2026-09-18: the git deploy did not fire, and the manual fallback has two traps.** The push of 0.13.1 (`19667ba`) produced no `vercel[bot]` deployment on GitHub (commit status stayed `pending`, no statuses), while the pushes an hour earlier had; the account had an overdue invoice at the time, which is the likeliest cause, and the git integration should be re-checked in the Vercel dashboard after a rename (the GitHub repository was renamed since the connection was made; `git remote -v` has the current name and the project still says `baton`). The fallback that works, run from the **repo root**, not `site/`: a root `.vercel/project.json` copied from `site/.vercel/` (gitignored), a root `.vercelignore` of `*`, `!site`, `!site/**`, `site/.vercel`, `site/.env*`, then `vercel --prod --yes --archive=tgz`. Trap one: `vercel --prod --yes` from inside `site/` uploads `site/` as the root, Vercel applies Root Directory `site` on top of it, and the deployment errors with an empty message. Trap two: that same `--yes` auto-linked the repo root to a **new** Vercel project named `leg` (empty, no domain); it is junk and `vercel project rm leg` removes it. Trap three: with no `.vercelignore` the CLI uploads the untracked probe debris too and refuses at 15,000 files.
119
120
 
120
121
 
121
122
  ## 2026-09-15: the board is a dark product surface, and DESIGN.md was the defect
package/docs/ERRORS.md CHANGED
@@ -3,6 +3,28 @@
3
3
  What broke, why, and what fixed it. One entry per failure, newest first. A first
4
4
  occurrence has to be written down or a repeat is never countable.
5
5
 
6
+ ## 2026-09-18: 0.14.0 pushed, CI green everywhere, and npm still served 0.13.1
7
+
8
+ **Fixed by putting the `repository` block back into the lockfile root
9
+ (`packages[""]`) and pushing again.** `scripts/npm-publish-gate.mjs` requires
10
+ `package-lock.json`'s root `repository.url` to equal the trusted-publisher
11
+ binding, and npm 10.9 (the local install) does not write that block; npm 11
12
+ does, which is where the 0.13.1 lockfile got it. `npm version 0.14.0` on npm
13
+ 10 rewrote the lockfile without it, the test matrix and the site deploy passed,
14
+ and only the `publish-npm` job failed, on the gate, before publishing anything.
15
+ The lesson: a lockfile touched by a different npm major is a release change,
16
+ and "CI green" after a push means the run, not the publish; `npm view
17
+ @ucsandman/legcli version` is the check.
18
+
19
+ ## 2026-09-18: `vercel --prod --yes` from `site/` failed the deploy and created a stray Vercel project
20
+
21
+ **Fixed by deploying from the repo root with a root `.vercelignore` and
22
+ `--archive=tgz`; the recipe is in DECISIONS.md under "the site deploys itself
23
+ from git".** The `--yes` also auto-created an empty Vercel project named `leg`
24
+ that still needs `vercel project rm leg`. The lesson: on a project with a Root
25
+ Directory set, the CLI must run from the repo root, and `--yes` is a consent to
26
+ create projects, not only to skip a confirmation.
27
+
6
28
  ## 2026-09-18: the terminal's opening `next:` line named the same agent twice and no model
7
29
 
8
30
  **Fixed in `src/attach.mjs`: the line maps the chain through `rungLabel`, so it
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ucsandman/legcli",
3
- "version": "0.13.1",
4
- "description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. $79 once, 30-day money-back guarantee.",
3
+ "version": "0.14.0",
4
+ "description": "Usage-limit monitor and automatic handoff for Claude Code, Codex, agy and Grok. Type leg claude|codex|agy|grok and get the same interactive agent with a board alongside, auto-approve on by default, usage tracking per agent and account, a live context handoff bundle, and at the limit the next agent continuing in the same terminal. 14-day free trial, then $79 once, 30-day money-back guarantee.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://legcli.com",
@@ -68,7 +68,7 @@
68
68
  // release lands on disk under a board that was started before it: the page
69
69
  // then draws controls the process has no routes for (an empty agent select,
70
70
  // no buckets). test/files-version.test.mjs pins this to package.json.
71
- const FILES_VERSION = '0.13.1'
71
+ const FILES_VERSION = '0.14.0'
72
72
  const TIMELINE_CAP = 12
73
73
  // mirrors LOOPBACK in src/auth.mjs; state.bind is "<host>:<port>" and an IPv6
74
74
  // host arrives bracketed
@@ -50,7 +50,7 @@
50
50
  // name. test/board-updates.test.mjs pins `state.stopped` in this file by
51
51
  // source text, which is why the module object keeps the name.
52
52
  // pinned to package.json by test/files-version.test.mjs; see board.js FILES_VERSION
53
- const FILES_VERSION = '0.13.1'
53
+ const FILES_VERSION = '0.14.0'
54
54
  const state = { es: null, retryMs: 1000, timers: [], stopped: false, sseRequest: 0, floorRequest: 0, trunkRequest: 0, headRequest: 0, cardsRequest: 0, lastReadingAt: null, bind: (typeof location !== 'undefined' && location.host) || '127.0.0.1:4747', pendingFloor: null, pendingCards: null, cards: new Map(), blockers: new Map(), doneOpen: false, ringId: null, scheduler: {}, trunkOff: false }
55
55
 
56
56
  // What the entry row reads on every render: the terminals (for the repo it
package/src/license.mjs CHANGED
@@ -8,10 +8,11 @@
8
8
  // team per-seat subscription; the key carries an expiry a few days past
9
9
  // the billing period, `leg license refresh` fetches a renewed one
10
10
  //
11
- // There is no trial. Leg pays off in the moment a limit lands mid-flow, which
12
- // is not a thing a fortnight of evaluation reliably contains; the risk reversal
13
- // is a 30-day money-back guarantee instead, which costs no code and no expiry
14
- // machinery. Key shape: LEG-<base64url payload>.<base64url signature> (legacy
11
+ // Before any key there is a 14-day trial, started the first time a session
12
+ // asks and recorded under $LEG_HOME/trial.json with every gate open. Deleting
13
+ // that file resets the clock; that is a known and accepted limit of an offline
14
+ // trial. Past the trial, or with a refused key, Leg needs a license; the risk
15
+ // reversal after buying is a 30-day money-back guarantee. Key shape: LEG-<base64url payload>.<base64url signature> (legacy
15
16
  // BATON- prefix still accepted) where the signature is Ed25519 over the payload
16
17
  // bytes exactly as encoded.
17
18
  import { createPublicKey, createPrivateKey, verify as cryptoVerify, sign as cryptoSign, createHash } from 'node:crypto'
@@ -29,11 +30,13 @@ export const PUBLIC_KEY_B64 = 'MCowBQYDK2VwAyEAIpVQymHHJAkIrZHv0u4o0bgfFmtW3Crm7
29
30
  const ACTIVE_PUBLIC_KEY = process.env.LEG_PUBLIC_KEY_B64 || process.env.BATON_PUBLIC_KEY_B64 || PUBLIC_KEY_B64
30
31
  // The date this release was cut. A personal key activates when this is on or
31
32
  // before its updates_until. Bumped with every published version.
32
- export const RELEASE_DATE = '2026-09-16'
33
+ export const RELEASE_DATE = '2026-09-18'
34
+ export const TRIAL_DAYS = 14
33
35
  export const GUARANTEE_DAYS = 30
34
36
  export const SITE = process.env.LEG_SITE || process.env.BATON_SITE || 'https://legcli.com'
35
37
  export const BUY_URL = `${SITE}/#pricing`
36
38
  export const PLANS = {
39
+ trial: { label: 'Trial', gates: ['run', 'share'] },
37
40
  personal: { label: 'Personal', gates: ['run'] },
38
41
  team: { label: 'Team', gates: ['run', 'share'] },
39
42
  }
@@ -86,6 +89,7 @@ export function verifyLicense(key, { publicKeyB64 = ACTIVE_PUBLIC_KEY, today = i
86
89
  }
87
90
 
88
91
  export function licensePath() { return join(home(), 'license.json') }
92
+ export function trialPath() { return join(home(), 'trial.json') }
89
93
 
90
94
  function readJson(f) { try { return JSON.parse(readFileSync(f, 'utf8')) } catch { return null } }
91
95
  function writeJson(f, obj) {
@@ -114,14 +118,30 @@ export function deactivate() {
114
118
  return had
115
119
  }
116
120
 
117
- // What this machine may do right now: a valid key, or nothing, with the reason
118
- // the stored key (if any) was refused.
119
- export function entitlement({ today = isoToday(), releaseDate = RELEASE_DATE } = {}) {
121
+ // The trial starts the first time something asks with start on (a session;
122
+ // `leg license status` asks with it off, so looking does not start the clock).
123
+ export function trial({ today = isoToday(), start = true } = {}) {
124
+ let t = readJson(trialPath())
125
+ if (!t?.started && start) { t = { started: today }; writeJson(trialPath(), t) }
126
+ if (!t?.started) return { started: null, daysLeft: TRIAL_DAYS, expired: false }
127
+ const used = Math.floor((Date.parse(today) - Date.parse(t.started)) / 86400000)
128
+ const daysLeft = Math.max(0, TRIAL_DAYS - used)
129
+ return { started: t.started, daysLeft, expired: used >= TRIAL_DAYS || used < 0 }
130
+ }
131
+
132
+ // What this machine may do right now. A valid key wins; otherwise the trial;
133
+ // otherwise nothing, with the reason the stored key (if any) was refused.
134
+ export function entitlement({ today = isoToday(), releaseDate = RELEASE_DATE, startTrial = true } = {}) {
120
135
  const lic = readLicense()
121
- if (!lic) return { ok: false, plan: 'none', reason: 'no-license', refused: null }
122
- const v = verifyLicense(lic.key, { today, releaseDate })
123
- if (v.ok) return { ok: true, plan: v.payload.plan, seats: v.payload.seats ?? 1, payload: v.payload, source: 'license' }
124
- return { ok: false, plan: 'none', reason: v.reason, refused: v.reason }
136
+ let refused = null
137
+ if (lic) {
138
+ const v = verifyLicense(lic.key, { today, releaseDate })
139
+ if (v.ok) return { ok: true, plan: v.payload.plan, seats: v.payload.seats ?? 1, payload: v.payload, source: 'license' }
140
+ refused = v.reason
141
+ }
142
+ const t = trial({ today, start: startTrial })
143
+ if (!t.expired) return { ok: true, plan: 'trial', started: t.started, daysLeft: t.daysLeft, source: 'trial', refused }
144
+ return { ok: false, plan: 'none', reason: refused ?? 'trial-expired', refused }
125
145
  }
126
146
 
127
147
  export function allows(ent, gate) {
@@ -136,13 +156,18 @@ export function explain(reason, payload) {
136
156
  case 'unknown-plan': return `the key names a plan this version does not know (${payload?.plan})`
137
157
  case 'personal-updates-ended': return `this Personal key covers releases up to ${payload?.updates_until}; this release is dated ${RELEASE_DATE}. Keep the version you have, or renew at ${BUY_URL}`
138
158
  case 'team-expired': return `this Team key expired on ${payload?.expires}; run "leg license refresh" (the subscription renews it) or see ${BUY_URL}`
139
- case 'no-license': return `Leg needs a license key. Buy one at ${BUY_URL} (${GUARANTEE_DAYS}-day money-back guarantee), then: leg license activate <key>`
159
+ case 'trial-expired': return `the ${TRIAL_DAYS}-day trial has ended; Leg needs a license key. Buy one at ${BUY_URL} (${GUARANTEE_DAYS}-day money-back guarantee), then: leg license activate <key>`
140
160
  default: return String(reason)
141
161
  }
142
162
  }
143
163
 
144
164
  export function describe(ent) {
145
165
  if (!ent.ok) return `no license: ${explain(ent.reason)}`
166
+ if (ent.plan === 'trial') {
167
+ const note = ent.refused ? ` (stored key refused: ${ent.refused})` : ''
168
+ if (!ent.started) return `${TRIAL_DAYS}-day trial, not started; it starts with your first leg <agent>${note}`
169
+ return `${TRIAL_DAYS}-day trial, ${ent.daysLeft} day${ent.daysLeft === 1 ? '' : 's'} left; buy at ${BUY_URL}${note}`
170
+ }
146
171
  const p = ent.payload
147
172
  const until = p.plan === 'personal' ? `updates through ${p.updates_until}` : `renews; valid through ${p.expires}`
148
173
  return `${PLANS[p.plan].label} license ${p.id}${p.seats > 1 ? `, ${p.seats} seats` : ''}, ${until}`