@prohost/cli 0.8.2 → 0.9.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 +37 -0
- package/README.md +46 -1
- package/dist/agent/account_runtime.d.ts +19 -3
- package/dist/agent/account_runtime.js +28 -6
- package/dist/agent/claude.js +21 -5
- package/dist/agent/command.d.ts +9 -0
- package/dist/agent/command.js +64 -2
- package/dist/agent/contract.d.ts +2 -0
- package/dist/agent/contract.js +6 -0
- package/dist/agent/daemon.d.ts +4 -0
- package/dist/agent/daemon.js +4 -0
- package/dist/agent/prompt.d.ts +27 -0
- package/dist/agent/prompt.js +24 -1
- package/dist/agent/run.d.ts +28 -3
- package/dist/agent/run.js +90 -18
- package/dist/agent/scheduler.d.ts +44 -0
- package/dist/agent/scheduler.js +93 -0
- package/dist/agent/worktrees.d.ts +136 -0
- package/dist/agent/worktrees.js +317 -0
- package/dist/index.js +12 -2
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A private checkout of each `--repos` repository, per conversation.
|
|
3
|
+
*
|
|
4
|
+
* A coding agent that opens pull requests does it in a git checkout, and until
|
|
5
|
+
* `--concurrency` there was only ever one run touching it. Two runs sharing one
|
|
6
|
+
* checkout switch each other's branch mid-edit and commit each other's
|
|
7
|
+
* half-finished files — the reason runs were serialized in the first place. So
|
|
8
|
+
* when the operator names the repositories the agent works on, each
|
|
9
|
+
* conversation gets its own `git worktree` of every one of them:
|
|
10
|
+
*
|
|
11
|
+
* $PROHOST_HOME/worktrees/<conversation>/<repo-name>
|
|
12
|
+
*
|
|
13
|
+
* Per *conversation*, not per run, for the same reason sessions are: a
|
|
14
|
+
* follow-up in the thread ("address the review comments") resumes the session
|
|
15
|
+
* that made the branch, and must find that branch where it left it. Runs of one
|
|
16
|
+
* conversation never overlap (see `scheduler.ts`), so one checkout per
|
|
17
|
+
* conversation is exactly as isolated as one per run and far cheaper.
|
|
18
|
+
*
|
|
19
|
+
* The harness makes the worktree; the agent never needs `git worktree add`.
|
|
20
|
+
* The agent command's working directory does not change — it stays the
|
|
21
|
+
* workspace, because that is where its `.claude/` configuration lives and what
|
|
22
|
+
* its sessions are keyed on. The checkouts are named in the prompt and in
|
|
23
|
+
* `$PROHOST_RUN_REPOS`.
|
|
24
|
+
*
|
|
25
|
+
* Worktrees share the source repository's object store, so a branch committed
|
|
26
|
+
* in one survives the worktree being removed. That is what makes pruning safe:
|
|
27
|
+
* `git worktree remove` (never `--force`) refuses a checkout with uncommitted
|
|
28
|
+
* or untracked files, and those are the only things a worktree holds alone.
|
|
29
|
+
*/
|
|
30
|
+
import { execFile } from 'node:child_process';
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import { existsSync, mkdirSync, readdirSync, rmdirSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import { prohostHome } from './credentials.js';
|
|
35
|
+
/** Child-process env var naming the directory that holds this run's checkouts. */
|
|
36
|
+
export const RUN_REPOS_ENV_VAR = 'PROHOST_RUN_REPOS';
|
|
37
|
+
/** Touched on every run of a conversation; its mtime is "last used". */
|
|
38
|
+
const MARKER_FILENAME = '.last-used';
|
|
39
|
+
/** A conversation's checkouts idle this long are removed, if clean. */
|
|
40
|
+
export const WORKTREE_RETENTION_MS = 14 * 24 * 3_600_000;
|
|
41
|
+
/** Pruning walks every conversation directory, so it is not done per run. */
|
|
42
|
+
const PRUNE_INTERVAL_MS = 24 * 3_600_000;
|
|
43
|
+
/** Checking out a large repository takes a while; a wedged git must still end. */
|
|
44
|
+
const GIT_TIMEOUT_MS = 10 * 60_000;
|
|
45
|
+
export class ReposFlagError extends Error {
|
|
46
|
+
}
|
|
47
|
+
export function worktreesRoot(env = process.env) {
|
|
48
|
+
return path.join(prohostHome(env), 'worktrees');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Directory name for a lane (see `sessionKeyFor`).
|
|
52
|
+
*
|
|
53
|
+
* Readable enough to recognise in `ls`, and suffixed with a digest of the whole
|
|
54
|
+
* key because the readable part is lossy: two keys that differ only in
|
|
55
|
+
* punctuation, or in the agent they belong to, must not share a checkout.
|
|
56
|
+
*/
|
|
57
|
+
export function laneDirName(lane) {
|
|
58
|
+
const surface = lane.includes('/') ? lane.slice(lane.indexOf('/') + 1) : lane;
|
|
59
|
+
const readable = surface.replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
|
60
|
+
const digest = createHash('sha256').update(lane).digest('hex').slice(0, 8);
|
|
61
|
+
return readable ? `${readable}-${digest}` : digest;
|
|
62
|
+
}
|
|
63
|
+
function git(args, env) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
execFile('git', args, { env: { ...process.env, ...env }, timeout: GIT_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
66
|
+
resolve({ ok: error === null, stdout: String(stdout), stderr: String(stderr) || (error?.message ?? '') });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function lastLine(text) {
|
|
71
|
+
const lines = text.trim().split('\n');
|
|
72
|
+
return (lines[lines.length - 1] ?? '').trim();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read `--repos a,b` into absolute paths, refusing what can never work.
|
|
76
|
+
*
|
|
77
|
+
* An empty list and two checkouts with one directory name are always errors.
|
|
78
|
+
* A path that is not a git checkout is an error only when `requireCheckouts`
|
|
79
|
+
* is set — which `install-daemon` does and `agent run` does not. At install
|
|
80
|
+
* time a mistyped path should stop the install; at run time the same check
|
|
81
|
+
* would make a daemon whose repository was moved, deleted or is on an
|
|
82
|
+
* unmounted volume exit before connecting, and the service manager restart it
|
|
83
|
+
* forever. A running harness instead reports the checkout it could not make on
|
|
84
|
+
* each run, and keeps answering.
|
|
85
|
+
*
|
|
86
|
+
* @throws ReposFlagError naming the entry that is wrong.
|
|
87
|
+
*/
|
|
88
|
+
export function parseReposFlag(raw, options = {}) {
|
|
89
|
+
const entries = raw
|
|
90
|
+
.split(',')
|
|
91
|
+
.map((entry) => entry.trim())
|
|
92
|
+
.filter((entry) => entry.length > 0);
|
|
93
|
+
if (entries.length === 0)
|
|
94
|
+
throw new ReposFlagError('--repos needs at least one path to a git checkout.');
|
|
95
|
+
const repos = [];
|
|
96
|
+
const names = new Map();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const repo = path.resolve(entry);
|
|
99
|
+
if (options.requireCheckouts && !isCheckout(repo)) {
|
|
100
|
+
throw new ReposFlagError(`--repos: ${repo} is not the top of a git checkout (no .git there).`);
|
|
101
|
+
}
|
|
102
|
+
const name = path.basename(repo);
|
|
103
|
+
const clash = names.get(name);
|
|
104
|
+
if (clash && clash !== repo) {
|
|
105
|
+
throw new ReposFlagError(`--repos: ${clash} and ${repo} are both named "${name}"; their checkouts would land in the same directory.`);
|
|
106
|
+
}
|
|
107
|
+
if (!clash)
|
|
108
|
+
repos.push(repo);
|
|
109
|
+
names.set(name, repo);
|
|
110
|
+
}
|
|
111
|
+
return repos;
|
|
112
|
+
}
|
|
113
|
+
/** Whether `repo` is the top of a git checkout (a clone, or a worktree of one). */
|
|
114
|
+
export function isCheckout(repo) {
|
|
115
|
+
return existsSync(path.join(repo, '.git'));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Makes, reuses and prunes per-conversation worktrees for one harness.
|
|
119
|
+
*
|
|
120
|
+
* Every git mutation goes through one in-process queue. Lanes run side by
|
|
121
|
+
* side, and two `git worktree add` calls against the same repository race for
|
|
122
|
+
* the same administrative name; pruning, meanwhile, must never remove the
|
|
123
|
+
* directory a run arriving this instant is about to use.
|
|
124
|
+
*/
|
|
125
|
+
export class WorktreeManager {
|
|
126
|
+
repos;
|
|
127
|
+
env;
|
|
128
|
+
log;
|
|
129
|
+
now;
|
|
130
|
+
retentionMs;
|
|
131
|
+
chain = Promise.resolve();
|
|
132
|
+
lastPrunedAt = 0;
|
|
133
|
+
constructor(options) {
|
|
134
|
+
this.repos = options.repos;
|
|
135
|
+
this.env = options.env ?? process.env;
|
|
136
|
+
this.log = options.log ?? (() => { });
|
|
137
|
+
this.now = options.now ?? Date.now;
|
|
138
|
+
this.retentionMs = options.retentionMs ?? WORKTREE_RETENTION_MS;
|
|
139
|
+
}
|
|
140
|
+
get enabled() {
|
|
141
|
+
return this.repos.length > 0;
|
|
142
|
+
}
|
|
143
|
+
exclusive(work) {
|
|
144
|
+
const next = this.chain.then(work, work);
|
|
145
|
+
this.chain = next.catch(() => { });
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Ensure this lane has a worktree of every repository, and say where.
|
|
150
|
+
*
|
|
151
|
+
* Never rejects and never fails the run: most runs are questions that touch
|
|
152
|
+
* no repository at all, so a checkout that could not be made is reported in
|
|
153
|
+
* {@link PreparedCheckouts.failures} for the prompt to tell the agent about.
|
|
154
|
+
*/
|
|
155
|
+
prepare(lane) {
|
|
156
|
+
return this.exclusive(async () => {
|
|
157
|
+
const dir = path.join(worktreesRoot(this.env), laneDirName(lane));
|
|
158
|
+
const prepared = { dir, checkouts: [], failures: [] };
|
|
159
|
+
try {
|
|
160
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
161
|
+
this.touch(dir);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
165
|
+
prepared.failures = this.repos.map((source) => ({ source, reason }));
|
|
166
|
+
return prepared;
|
|
167
|
+
}
|
|
168
|
+
for (const source of this.repos) {
|
|
169
|
+
const name = path.basename(source);
|
|
170
|
+
const target = path.join(dir, name);
|
|
171
|
+
let failure;
|
|
172
|
+
try {
|
|
173
|
+
failure = isCheckout(target) ? await this.reuse(source, target) : await this.add(source, target);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
177
|
+
}
|
|
178
|
+
if (failure)
|
|
179
|
+
prepared.failures.push({ source, reason: failure });
|
|
180
|
+
else
|
|
181
|
+
prepared.checkouts.push({ name, path: target, source });
|
|
182
|
+
}
|
|
183
|
+
// Housekeeping for other conversations must not cost this one its run.
|
|
184
|
+
await this.pruneIfDue(dir).catch((err) => {
|
|
185
|
+
this.log(` ! could not prune idle checkouts (${err instanceof Error ? err.message : String(err)})`);
|
|
186
|
+
});
|
|
187
|
+
return prepared;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** The repository a checkout's history lives in, as one comparable path. */
|
|
191
|
+
async commonDir(checkout) {
|
|
192
|
+
const result = await git(['-C', checkout, 'rev-parse', '--path-format=absolute', '--git-common-dir'], this.env);
|
|
193
|
+
const dir = result.stdout.trim();
|
|
194
|
+
return result.ok && dir ? dir : undefined;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Keep an existing checkout — once it is proven to be a worktree of `source`.
|
|
198
|
+
*
|
|
199
|
+
* The directory is named after the repository's basename, so pointing
|
|
200
|
+
* `--repos` from `/old/app` to `/new/app` leaves every conversation with an
|
|
201
|
+
* `app` checkout of the *old* repository. Handing that to the agent as a
|
|
202
|
+
* checkout of the new one would have it commit to the wrong repository.
|
|
203
|
+
*/
|
|
204
|
+
async reuse(source, target) {
|
|
205
|
+
const [mine, theirs] = await Promise.all([this.commonDir(target), this.commonDir(source)]);
|
|
206
|
+
if (theirs === undefined)
|
|
207
|
+
return `${source} is not a git checkout`;
|
|
208
|
+
if (mine === theirs)
|
|
209
|
+
return undefined;
|
|
210
|
+
// Not ours to throw away: without `--force`, git removes it only if it
|
|
211
|
+
// holds nothing uncommitted.
|
|
212
|
+
const removed = mine === undefined
|
|
213
|
+
? { ok: false, stderr: 'it is not a usable git checkout' }
|
|
214
|
+
: await git(['--git-dir', mine, 'worktree', 'remove', target], this.env);
|
|
215
|
+
if (!removed.ok) {
|
|
216
|
+
return (`${target} is a checkout of a different repository and could not be replaced ` +
|
|
217
|
+
`(${lastLine(removed.stderr) || 'git would not remove it'})`);
|
|
218
|
+
}
|
|
219
|
+
this.log(` ⚙ replaced ${target}: it was a checkout of ${mine}, not of ${source}`);
|
|
220
|
+
return this.add(source, target);
|
|
221
|
+
}
|
|
222
|
+
/** Returns why the worktree could not be made, or `undefined` when it was. */
|
|
223
|
+
async add(source, target) {
|
|
224
|
+
// A worktree whose directory was deleted by hand is still registered, and
|
|
225
|
+
// git refuses to add another at that path until the record is pruned.
|
|
226
|
+
await git(['-C', source, 'worktree', 'prune'], this.env);
|
|
227
|
+
// Detached at the remote's default branch: the agent cuts its own branch,
|
|
228
|
+
// and a detached HEAD cannot collide with a branch checked out elsewhere.
|
|
229
|
+
let base = 'HEAD';
|
|
230
|
+
for (const candidate of ['origin/HEAD', 'origin/main', 'origin/master']) {
|
|
231
|
+
const known = await git(['-C', source, 'rev-parse', '--verify', '--quiet', `${candidate}^{commit}`], this.env);
|
|
232
|
+
if (known.ok) {
|
|
233
|
+
base = candidate;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const added = await git(['-C', source, 'worktree', 'add', '--detach', target, base], this.env);
|
|
238
|
+
if (added.ok) {
|
|
239
|
+
this.log(` ⚙ checkout ready: ${target} (worktree of ${source} at ${base})`);
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
// A half-made directory would be mistaken for a checkout on the next run.
|
|
243
|
+
rmSync(target, { recursive: true, force: true });
|
|
244
|
+
return lastLine(added.stderr) || 'git worktree add failed';
|
|
245
|
+
}
|
|
246
|
+
touch(dir) {
|
|
247
|
+
const marker = path.join(dir, MARKER_FILENAME);
|
|
248
|
+
const stamp = new Date(this.now());
|
|
249
|
+
if (existsSync(marker))
|
|
250
|
+
utimesSync(marker, stamp, stamp);
|
|
251
|
+
else {
|
|
252
|
+
writeFileSync(marker, '');
|
|
253
|
+
utimesSync(marker, stamp, stamp);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async pruneIfDue(keep) {
|
|
257
|
+
if (this.now() - this.lastPrunedAt < PRUNE_INTERVAL_MS)
|
|
258
|
+
return;
|
|
259
|
+
this.lastPrunedAt = this.now();
|
|
260
|
+
await this.prune(keep);
|
|
261
|
+
}
|
|
262
|
+
/** Remove idle conversations' checkouts, never the one about to be used. */
|
|
263
|
+
async prune(keep) {
|
|
264
|
+
const root = worktreesRoot(this.env);
|
|
265
|
+
let lanes;
|
|
266
|
+
try {
|
|
267
|
+
lanes = readdirSync(root);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return; // nothing has ever been checked out
|
|
271
|
+
}
|
|
272
|
+
for (const lane of lanes) {
|
|
273
|
+
const dir = path.join(root, lane);
|
|
274
|
+
if (dir === keep)
|
|
275
|
+
continue;
|
|
276
|
+
try {
|
|
277
|
+
if (!statSync(dir).isDirectory())
|
|
278
|
+
continue;
|
|
279
|
+
const marker = path.join(dir, MARKER_FILENAME);
|
|
280
|
+
const usedAt = (existsSync(marker) ? statSync(marker) : statSync(dir)).mtimeMs;
|
|
281
|
+
if (this.now() - usedAt < this.retentionMs)
|
|
282
|
+
continue;
|
|
283
|
+
await this.removeLane(dir);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
this.log(` ! could not prune ${dir} (${err instanceof Error ? err.message : String(err)})`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async removeLane(dir) {
|
|
291
|
+
let kept = 0;
|
|
292
|
+
for (const name of readdirSync(dir)) {
|
|
293
|
+
if (name === MARKER_FILENAME)
|
|
294
|
+
continue;
|
|
295
|
+
const target = path.join(dir, name);
|
|
296
|
+
if (!existsSync(path.join(target, '.git'))) {
|
|
297
|
+
kept += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// Asked of the worktree itself, so pruning still works for a repository
|
|
301
|
+
// the operator has since dropped from `--repos`.
|
|
302
|
+
const common = await git(['-C', target, 'rev-parse', '--path-format=absolute', '--git-common-dir'], this.env);
|
|
303
|
+
const removed = common.ok
|
|
304
|
+
? await git(['--git-dir', common.stdout.trim(), 'worktree', 'remove', target], this.env)
|
|
305
|
+
: common;
|
|
306
|
+
if (!removed.ok) {
|
|
307
|
+
kept += 1;
|
|
308
|
+
this.log(` ! kept idle checkout ${target} — ${lastLine(removed.stderr) || 'git would not remove it'}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (kept === 0) {
|
|
312
|
+
rmSync(path.join(dir, MARKER_FILENAME), { force: true });
|
|
313
|
+
rmdirSync(dir);
|
|
314
|
+
this.log(` ⚙ removed idle checkouts in ${dir}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,8 @@ function printHelp() {
|
|
|
25
25
|
'Usage:',
|
|
26
26
|
' prohost listen --forward-to <local-url> [--api-key <key>] [--url <ws-url>] [--machine <name>] [--subscription-id <id>]',
|
|
27
27
|
' prohost agent pair --code <pairing-code> [--account <label>] [--webhook-url <https-url>] [--base-url <url>]',
|
|
28
|
-
" prohost agent run --exec '<command>' [--
|
|
28
|
+
" prohost agent run --exec '<command>' [--concurrency <n>] [--repos <checkout,…>] [--idle-timeout <seconds>]",
|
|
29
|
+
' [--timeout <seconds>] [--dry-run] [--workdir <dir>]',
|
|
29
30
|
" prohost agent install-daemon --exec '<command>' [--workdir <dir>] [--account <label>] Keep the agent running",
|
|
30
31
|
' prohost agent uninstall-daemon',
|
|
31
32
|
' prohost agent account add <label> --runtime claude|codex [--billing subscription|api_key]',
|
|
@@ -53,6 +54,14 @@ function printHelp() {
|
|
|
53
54
|
' --timeout Seconds one run may take before it is killed, however busy it is.',
|
|
54
55
|
' Defaults to 0 — no wall-clock limit, because a long run is not a stuck',
|
|
55
56
|
' one. Set it if you want a hard ceiling as well as the stall watchdog.',
|
|
57
|
+
' --concurrency How many runs may execute at once, 1 to 16. Defaults to 1. Runs in the same',
|
|
58
|
+
' conversation always take turns, so only different conversations overlap.',
|
|
59
|
+
' Every run spends the same login\'s quota, and they share this machine —',
|
|
60
|
+
' pair it with --repos if the agent edits code.',
|
|
61
|
+
' --repos Comma-separated git checkouts the agent works on. Each conversation gets',
|
|
62
|
+
' its own `git worktree` of every one under $PROHOST_HOME/worktrees, kept for',
|
|
63
|
+
' the life of the thread and removed after 14 idle days if it is clean. The',
|
|
64
|
+
' agent is told to work there and to leave the originals alone.',
|
|
56
65
|
' --dry-run Print the reply that would be posted; write nothing.',
|
|
57
66
|
' --workdir Working directory for the agent command. Defaults to $PROHOST_HOME/workspace.',
|
|
58
67
|
' Avoid pointing it at a code checkout: agents read CLAUDE.md, .claude/settings.json',
|
|
@@ -92,7 +101,8 @@ function printHelp() {
|
|
|
92
101
|
'',
|
|
93
102
|
'agent install-daemon flags: (macOS: launchd; Linux: prints a systemd-user unit)',
|
|
94
103
|
' --exec The same command you would pass to `agent run` (required).',
|
|
95
|
-
' --workdir, --idle-timeout, --timeout, --machine, --url,
|
|
104
|
+
' --workdir, --concurrency, --repos, --idle-timeout, --timeout, --machine, --url,',
|
|
105
|
+
' --allow-unverified, --safe-tools',
|
|
96
106
|
' — passed to `agent run`.',
|
|
97
107
|
'',
|
|
98
108
|
'Global flags:',
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.
|
|
10
|
+
export declare const CLI_VERSION = "0.9.0";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.9.0";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.
|
|
10
|
+
export const CLI_VERSION = '0.9.0';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|