@rfany/rf 0.5.0 → 0.7.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/README.md +3 -2
- package/package.json +1 -1
- package/rf.mjs +46 -21
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
People's command line for [rollforward](https://github.com/spoj/rollforward): what its page does, from a shell or a desktop agent, as you.
|
|
4
4
|
|
|
5
5
|
npm i -g @rfany/rf
|
|
6
|
-
rf login https://rollforward.example.com
|
|
6
|
+
rf login https://rollforward.example.com
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
rf opens the page, which asks you to allow it if it shows the same code as rf; or make a token on the page, under Tokens, and
|
|
9
|
+
`rf login URL TOKEN`. `rf` alone lists the commands; `rf help` is the guide for an agent, read before it first uses rf.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rfany/rf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "rollforward's command line for people: what its page does, from a shell or a desktop agent, as you",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/spoj/rollforward.git", "directory": "cli" },
|
package/rf.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// rf for people: what the page does, from a shell or a desktop agent, as you, with a personal token made on the page (Tokens).
|
|
3
3
|
// Published to npm as @rfany/rf, from cli/. PATH is workspace/series/label, as `rf ls` prints it.
|
|
4
4
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import {
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { homedir, hostname } from 'node:os';
|
|
6
7
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
7
8
|
|
|
8
|
-
const USAGE = `rf login URL TOKEN
|
|
9
|
+
const USAGE = `rf login URL [TOKEN] without a TOKEN, through the browser: allow it on the page, where it shows rf's code
|
|
9
10
|
rf ls
|
|
10
11
|
rf pull PATH DIR [--delete] [-n|--dry-run] [--exclude PATTERN]...
|
|
11
12
|
PATH's files into DIR as rsync copies them, whole, if their size or time differ: PATH/ fills DIR,
|
|
@@ -16,7 +17,7 @@ rf upload PATH FILE [--onto ITEM] [--output]
|
|
|
16
17
|
rf wake PATH AT REASON [--label LABEL] AT now, +<n>m|h|d or a date; PATH an open instance, or a series with LABEL
|
|
17
18
|
rf agents [WORKSPACE] [--set FILE|-] the company's AGENTS.md, or a workspace's; setting the company's is for admins
|
|
18
19
|
rf skills
|
|
19
|
-
rf
|
|
20
|
+
rf connections
|
|
20
21
|
rf status [--long MIN] [--json] what goes on and what's stuck: runs going, flagged past MIN minutes (30), and queued, with why;
|
|
21
22
|
what waits on whom since when; open instances whose last run failed
|
|
22
23
|
rf runs [PATH] [--since DATE] [--failed] [--running] [--long MIN] [--json]
|
|
@@ -35,9 +36,12 @@ Admins:
|
|
|
35
36
|
rf people [--set FILE|-] [-n] [--force] everyone as lines of ROLE EMAIL, each with a comment; --set sets the roles of the people
|
|
36
37
|
listed and adds new ones, removing no one
|
|
37
38
|
rf user add EMAIL [admin|publisher] | role EMAIL admin|publisher|user [--force] | disable EMAIL | enable EMAIL | signout EMAIL | remove EMAIL
|
|
38
|
-
rf
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
rf connection set NAME TYPE [--SETTING VALUE]... an outside system runs reach as their runner, by a login each person makes:
|
|
40
|
+
graph, Microsoft 365, with no settings; databricks, --host HOST --warehouse ID, a workspace
|
|
41
|
+
and its SQL warehouse; website, --url URL [--hosts HOST,...] [--title TITLE] [--writeback
|
|
42
|
+
false], where people sign in, the other hosts its pages need, such as *.example.com, and
|
|
43
|
+
whether a run's browser writes its state back
|
|
44
|
+
rf connection remove NAME
|
|
41
45
|
rf brand [--text TEXT] [--logo FILE] [--clear] the deployment's text and logo, a .png, .jpg, .gif, .webp or .svg, which top
|
|
42
46
|
the sidebar and are the page's title and icon; --clear goes back to the plain look, and with
|
|
43
47
|
no option it prints them`;
|
|
@@ -54,15 +58,17 @@ What there is
|
|
|
54
58
|
series, and a reply from whom an ask names; anyone else's is a note, which the agent takes up once the runner does.
|
|
55
59
|
- A comment starts a thread, on the instance or on an item, at a cell such as Base!C25, and others reply in it. An ask is the
|
|
56
60
|
agent's comment that waits on someone, the one it names or else the runner, until they reply or resolve it. rf ls says how many
|
|
57
|
-
asks wait in each instance.
|
|
61
|
+
asks wait in each instance. A sign-off, an ask with signoff true, waits on the runner, who approves or rejects it on the page:
|
|
62
|
+
either commits the instance's folder to its history, .git in it, as closing does.
|
|
58
63
|
|
|
59
64
|
What rf pull copies, the folders as the agent sees them
|
|
60
65
|
AGENTS.md the company's conventions; <workspace>/AGENTS.md the workspace's
|
|
61
66
|
<workspace>/<series>/<label>/
|
|
62
67
|
workspace.json the screen: {rev, status, items: [{id, name, kind, file, from, headline, source}]}
|
|
63
68
|
log.jsonl every event, by seq: by is a person's email, "server" or "agent"; op is upload, comment (thread, item,
|
|
64
|
-
ref, text, ask, to), resolve,
|
|
65
|
-
a run), done (its cost, summary or
|
|
69
|
+
ref, text, ask, to, signoff), resolve, signoff (the runner's answer, with its commit), commit, point and
|
|
70
|
+
item (the screen changing), run, report (the agent's account of a run), done (its cost, summary or
|
|
71
|
+
error), close, and the rest
|
|
66
72
|
upload/ what people uploaded, their comments' attachments in upload/attachments/
|
|
67
73
|
working/ what the agent made, its scripts, and process.md, how it does this series
|
|
68
74
|
session/ the agent's own session: every message and tool call of every run
|
|
@@ -98,18 +104,22 @@ const flags = {}, excludes = [], args
|
|
|
98
104
|
for (let i = 0; i < argv.length; i++) {
|
|
99
105
|
const flag = argv[i] === '-n' ? 'dry-run' : argv[i].match(/^--(.+)$/)?.[1];
|
|
100
106
|
if (!flag) args.push(argv[i]);
|
|
101
|
-
else if (
|
|
102
|
-
else
|
|
107
|
+
else if (['wait', 'output', 'delete', 'dry-run', 'force', 'json', 'failed', 'running', 'clear'].includes(flag)) flags[flag] = true;
|
|
108
|
+
else if (argv[i + 1] === undefined) {
|
|
109
|
+
console.error(`--${flag} takes a value\n\n${USAGE}`);
|
|
110
|
+
process.exit(2);
|
|
111
|
+
} else if (flag === 'exclude') excludes.push(argv[++i]);
|
|
112
|
+
else flags[flag] = argv[++i];
|
|
103
113
|
}
|
|
104
114
|
if (cmd === 'help') {
|
|
105
115
|
console.log(GUIDE);
|
|
106
116
|
process.exit();
|
|
107
117
|
}
|
|
108
118
|
if (cmd !== 'login' && !existsSync(config)) {
|
|
109
|
-
console.error(`Sign in first
|
|
119
|
+
console.error(`Sign in first: rf login URL\n\n${USAGE}`);
|
|
110
120
|
process.exit(2);
|
|
111
121
|
}
|
|
112
|
-
|
|
122
|
+
let { url, token } = cmd === 'login' ? { url: args[0]?.replace(/\/$/, ''), token: args[1] } : JSON.parse(readFileSync(config, 'utf8'));
|
|
113
123
|
|
|
114
124
|
async function api(path , init = {}) {
|
|
115
125
|
const r = await fetch(url + path, { ...init, headers: { authorization: `Bearer ${token}` } });
|
|
@@ -200,7 +210,22 @@ async function pull(path , dir ) {
|
|
|
200
210
|
if (failed) process.exitCode = 1;
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
|
|
213
|
+
// The browser's sign-in: the server makes a request, the person allows it on the page, which shows its code, and rf collects the token.
|
|
214
|
+
async function browserToken() {
|
|
215
|
+
const { id, code } = await (await fetch(`${url}/auth/cli`, { method: 'POST', body: JSON.stringify({ name: `rf on ${hostname()}` }) })).json();
|
|
216
|
+
const link = `${url}/cli?code=${code}`;
|
|
217
|
+
console.log(`Open ${link}\nand allow rf there if the page shows the code ${code}.`);
|
|
218
|
+
spawn(process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open', [link], { stdio: 'ignore', detached: true }).on('error', () => {}).unref();
|
|
219
|
+
for (;;) {
|
|
220
|
+
await new Promise(ok => setTimeout(ok, 2000));
|
|
221
|
+
const r = await fetch(`${url}/auth/cli/poll`, { method: 'POST', body: JSON.stringify({ id }) });
|
|
222
|
+
if (r.status === 200) return (await r.json()).token;
|
|
223
|
+
if (r.status !== 202) throw new Error(await r.text());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (cmd === 'login' && url) {
|
|
228
|
+
token ??= await browserToken();
|
|
204
229
|
const me = await get('/me');
|
|
205
230
|
mkdirSync(dirname(config), { recursive: true });
|
|
206
231
|
writeFileSync(config, JSON.stringify({ url, token }), { mode: 0o600 });
|
|
@@ -255,13 +280,13 @@ else if (cmd === 'comment' && args.length === 2) {
|
|
|
255
280
|
console.log(await (await post(`/skills/${encodeURIComponent(args[1])}/versions`, { from: Number(args[2]) })).text());
|
|
256
281
|
} else if (cmd === 'skills') {
|
|
257
282
|
for (const s of await get('/skills')) console.log(`${s.id} v${s.pushed.version} by ${s.pushed.by} ${s.pushed.at} ${s.description}`);
|
|
258
|
-
} else if (cmd === '
|
|
259
|
-
for (const
|
|
260
|
-
} else if (cmd === '
|
|
261
|
-
await post(`/
|
|
283
|
+
} else if (cmd === 'connections') {
|
|
284
|
+
for (const { name, type, login, updated_by, updated_at, ...settings } of await get('/connections')) console.log(`${name} ${type} ${JSON.stringify(settings)}`);
|
|
285
|
+
} else if (cmd === 'connection' && args[0] === 'set' && args.length === 3) {
|
|
286
|
+
await post(`/connections/${encodeURIComponent(args[1])}`, { type: args[2], ...flags }, 'PUT');
|
|
262
287
|
console.log('saved');
|
|
263
|
-
} else if (cmd === '
|
|
264
|
-
await api(`/
|
|
288
|
+
} else if (cmd === 'connection' && args[0] === 'remove' && args[1]) {
|
|
289
|
+
await api(`/connections/${encodeURIComponent(args[1])}`, { method: 'DELETE' });
|
|
265
290
|
console.log('removed');
|
|
266
291
|
} else if (cmd === 'status' && !args.length) {
|
|
267
292
|
const s = await get('/api/status'), long = Number(flags.long ?? 30);
|
|
@@ -271,7 +296,7 @@ else if (cmd === 'comment' && args.length === 2) {
|
|
|
271
296
|
[`Running`, s.running.map(r => `${r.path} run ${r.run} for ${took((Date.now() - Date.parse(r.at)) / 1000)} ${usd(r.cost)}${Date.now() - Date.parse(r.at) > long * 60e3 ? ` LONG, past ${long}m` : ''}`)],
|
|
272
297
|
[`Queued`, s.queued.map(q => `${q.path} since ${when(q.since)}: ${q.why}`)],
|
|
273
298
|
[`Waiting`, [
|
|
274
|
-
...s.asks.sort((a, b) => a.who.localeCompare(b.who) || a.since.localeCompare(b.since)).map(a => `${a.who} ${a.note ? 'a note' : 'an ask'} in ${a.path} since ${when(a.since)}: ${a.text.split('\n')[0].slice(0, 80)}`),
|
|
299
|
+
...s.asks.sort((a, b) => a.who.localeCompare(b.who) || a.since.localeCompare(b.since)).map(a => `${a.who} ${a.signoff ? 'a sign-off' : a.note ? 'a note' : 'an ask'} in ${a.path} since ${when(a.since)}: ${a.text.split('\n')[0].slice(0, 80)}`),
|
|
275
300
|
...s.expired.map(e => `${e.who} their ${e.login} login expired${e.holding.length ? `, holding ${e.holding.join(', ')}` : ''}`),
|
|
276
301
|
...s.requests.map(r => `${r.path} ${r.from} asks to start ${r.label}, since ${when(r.at)}: ${r.reason}`),
|
|
277
302
|
]],
|