@opennous/cli 0.17.1 → 0.18.1
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 +16 -0
- package/package.json +1 -1
- package/src/index.js +19 -4
- package/src/runner.js +43 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @opennous/cli changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.1
|
|
4
|
+
|
|
5
|
+
### Fixed: one machine is one runner
|
|
6
|
+
|
|
7
|
+
The runner named your machine by its network hostname, which on a Mac changes with the network, so
|
|
8
|
+
one Mac could show up as two runners. It now uses the Mac's own name, saved once (GET-1308).
|
|
9
|
+
|
|
10
|
+
## 0.18.0
|
|
11
|
+
|
|
12
|
+
### Changed: one command sets everything up, and the runner is quiet when idle
|
|
13
|
+
|
|
14
|
+
`runner install` is the only command you need: with no history read yet, the runner starts that read
|
|
15
|
+
itself (six months, on your plan), then keeps everything new current. Idle, it checks in about once a
|
|
16
|
+
minute and still picks up new work within seconds. `runner uninstall` tells Ownable this machine
|
|
17
|
+
stopped, so with no runner left your reading goes back to credits (GET-1307).
|
|
18
|
+
|
|
3
19
|
## 0.17.1
|
|
4
20
|
|
|
5
21
|
### Fixed: a sign-out or an outage no longer fails your reads
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -154,6 +154,7 @@ function apiClient(key) {
|
|
|
154
154
|
get: (path, query) => request("GET", path, { query }),
|
|
155
155
|
post: (path, body) => request("POST", path, { body }),
|
|
156
156
|
softPost: (path, body) => request("POST", path, { body, soft: true }),
|
|
157
|
+
softDelete: (path, body) => request("DELETE", path, { body, soft: true }),
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -662,17 +663,19 @@ const runnerCmd = program
|
|
|
662
663
|
const { detectAgents, liveRun, NO_AGENT } = await import("./backfill.js");
|
|
663
664
|
const { runRunner } = await import("./runner.js");
|
|
664
665
|
const agent = await pickAgent(opts.agent, detectAgents, NO_AGENT);
|
|
666
|
+
const host = await runnerHost();
|
|
665
667
|
const key = await runnerKey(opts);
|
|
666
668
|
const api = apiClient(key);
|
|
667
669
|
const service = process.env.NOUS_RUNNER_SERVICE === "1";
|
|
668
670
|
const stamp = (line) => (service ? `${new Date().toISOString()} ${line}` : line);
|
|
669
671
|
console.log(stamp(`Reading on your ${agent} plan. ${service ? "" : "Stop with Ctrl-C; `nous runner install` keeps it going on its own."}`.trim()));
|
|
670
672
|
await runRunner({
|
|
671
|
-
agent, version, run: liveRun(), log: (l) => console.log(stamp(l)),
|
|
673
|
+
agent, host, version, run: liveRun(), log: (l) => console.log(stamp(l)),
|
|
672
674
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
673
675
|
api: {
|
|
674
676
|
poll: (body) => api.softPost("/v2/runner/poll", body),
|
|
675
677
|
answer: (id, body) => api.softPost(`/v2/runner/calls/${id}`, body),
|
|
678
|
+
startBackfill: (body) => api.softPost("/v2/backfill/own", body),
|
|
676
679
|
},
|
|
677
680
|
});
|
|
678
681
|
});
|
|
@@ -690,14 +693,14 @@ runnerCmd
|
|
|
690
693
|
const key = await runnerKey(opts);
|
|
691
694
|
// One poll before installing, so a bad key or plan fails here and not silently in the background.
|
|
692
695
|
try {
|
|
693
|
-
await apiClient(key).softPost("/v2/runner/poll", { agent, host:
|
|
696
|
+
await apiClient(key).softPost("/v2/runner/poll", { agent, host: await runnerHost(), version, limit: 0 });
|
|
694
697
|
} catch (e) {
|
|
695
698
|
console.error(`Couldn't connect: ${e.message}`);
|
|
696
699
|
process.exit(1);
|
|
697
700
|
}
|
|
698
701
|
try {
|
|
699
702
|
const where = installService({ agent, workspace: opts.workspace ?? null });
|
|
700
|
-
console.log(`Connected.
|
|
703
|
+
console.log(`Connected. Your calls and email are read on your ${agent} plan: your history first, then everything new. It starts every time you log in.`);
|
|
701
704
|
if (where.log) console.log(`Log: ${where.log}`);
|
|
702
705
|
console.log("Check on it with `npx @opennous/cli@latest runner status`. Stop it with `npx @opennous/cli@latest runner uninstall`.");
|
|
703
706
|
} catch (e) {
|
|
@@ -711,7 +714,10 @@ runnerCmd
|
|
|
711
714
|
.description("Stop the runner and stop it starting at login")
|
|
712
715
|
.action(async () => {
|
|
713
716
|
const { uninstallService } = await import("./runner.js");
|
|
714
|
-
|
|
717
|
+
const removed = uninstallService();
|
|
718
|
+
// The server stops waiting on this machine (GET-1307): with no runner left, your reading goes back to credits.
|
|
719
|
+
if (hasKey()) await apiClient().softDelete("/v2/runner", { host: await runnerHost() }).catch(() => {});
|
|
720
|
+
console.log(removed ? "Stopped. It won't start at login any more, and your reading is back on credits unless another machine runs it." : "No runner is installed on this machine.");
|
|
715
721
|
});
|
|
716
722
|
|
|
717
723
|
runnerCmd
|
|
@@ -725,6 +731,15 @@ runnerCmd
|
|
|
725
731
|
if (s.log) console.log(`Log: ${s.log}`);
|
|
726
732
|
});
|
|
727
733
|
|
|
734
|
+
/** This machine's name for its runner, kept in the config so it is the same every run (GET-1308). */
|
|
735
|
+
async function runnerHost() {
|
|
736
|
+
const { machineName } = await import("./runner.js");
|
|
737
|
+
const cfg = readConfig();
|
|
738
|
+
const name = machineName({ saved: cfg.runnerHost ?? null });
|
|
739
|
+
if (name !== cfg.runnerHost) writeConfig({ ...cfg, runnerHost: name });
|
|
740
|
+
return name;
|
|
741
|
+
}
|
|
742
|
+
|
|
728
743
|
/** The agent to read with: the one asked for, or whichever is signed in. Exits with what to do. */
|
|
729
744
|
async function pickAgent(asked, detectAgents, noAgent) {
|
|
730
745
|
const agents = detectAgents();
|
package/src/runner.js
CHANGED
|
@@ -11,10 +11,17 @@ import { dirname, join } from "node:path";
|
|
|
11
11
|
import { commandFor, parseAnswer, troubleOf, resetAt } from "./backfill.js";
|
|
12
12
|
|
|
13
13
|
export const WORKERS = 3;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* How long the server may hold a poll open waiting for a call (GET-1307). Idle, the runner checks in
|
|
16
|
+
* about once a minute and still picks up a new call within seconds.
|
|
17
|
+
*/
|
|
18
|
+
export const WAIT_MS = 45_000;
|
|
19
|
+
/** A server that answers at once (one without long polls): ask again after this. */
|
|
15
20
|
export const BUSY_MS = 2000;
|
|
16
|
-
/** Idle: nothing going. A heartbeat still has to land well inside the server's two minutes. */
|
|
17
21
|
export const IDLE_MS = 20_000;
|
|
22
|
+
/** A history read the runner started that the server could not start yet: try again after this. */
|
|
23
|
+
export const BACKFILL_RETRY_MS = 30 * 60_000;
|
|
24
|
+
export const BACKFILL_DAYS = 180;
|
|
18
25
|
/** At the plan's limit: take nothing, keep the heartbeat. */
|
|
19
26
|
export const LIMITED_POLL_MS = 60_000;
|
|
20
27
|
/** A limit whose message gives no reset time is tried again after this. */
|
|
@@ -27,6 +34,21 @@ const ERROR_BACKOFF_MS = 30_000;
|
|
|
27
34
|
export const LABEL = "cloud.opennous.runner";
|
|
28
35
|
export const RUN_COMMAND = "npx -y @opennous/cli@latest runner";
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
* This machine's name for Ownable (GET-1308), the same every run. `os.hostname()` on a Mac follows
|
|
39
|
+
* the network ("Mac.lan" on one, "Bennets-MacBook-Air.local" on another), which made one machine
|
|
40
|
+
* two runners. The Mac's own name (System Settings → General → About) is asked once and kept.
|
|
41
|
+
*/
|
|
42
|
+
export function machineName({ saved = null, os = platform(), run = spawnSync, host = hostname } = {}) {
|
|
43
|
+
if (saved) return saved;
|
|
44
|
+
if (os === "darwin") {
|
|
45
|
+
const r = run("scutil", ["--get", "ComputerName"], { encoding: "utf8" });
|
|
46
|
+
const name = r.status === 0 ? String(r.stdout).trim() : "";
|
|
47
|
+
if (name) return name;
|
|
48
|
+
}
|
|
49
|
+
return String(host()).replace(/\.(local|lan|home|localdomain)$/i, "");
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
/** What the status line says about the read in progress, or null. Pure. */
|
|
31
53
|
export function readingLine(reading) {
|
|
32
54
|
if (!reading) return null;
|
|
@@ -53,7 +75,7 @@ export function pauseLine(agent, reason, until, now = Date.now()) {
|
|
|
53
75
|
*/
|
|
54
76
|
export async function runRunner({ api, agent, host = hostname(), version = null, run, sleep, log, now = Date.now,
|
|
55
77
|
workers = WORKERS, limitWaitMs = LIMIT_WAIT_MS, stop = () => false }) {
|
|
56
|
-
let limitedUntil = 0, limitReason = null, answered = 0, lastLine = null, lastError = null,
|
|
78
|
+
let limitedUntil = 0, limitReason = null, answered = 0, lastLine = null, lastError = null, backfillAt = -Infinity;
|
|
57
79
|
const answerOne = async (call, models) => {
|
|
58
80
|
for (let attempt = 0; ; attempt++) {
|
|
59
81
|
try {
|
|
@@ -80,8 +102,9 @@ export async function runRunner({ api, agent, host = hostname(), version = null,
|
|
|
80
102
|
};
|
|
81
103
|
while (!stop()) {
|
|
82
104
|
let page;
|
|
105
|
+
const asked = now();
|
|
83
106
|
try {
|
|
84
|
-
page = await api.poll({ agent, host, version, answered, limit: workers,
|
|
107
|
+
page = await api.poll({ agent, host, version, answered, limit: workers, wait_ms: WAIT_MS,
|
|
85
108
|
limited_until: limitedUntil > now() ? new Date(limitedUntil).toISOString() : null,
|
|
86
109
|
limit_reason: limitedUntil > now() ? limitReason : null });
|
|
87
110
|
if (lastError) { log("Back online."); lastError = null; }
|
|
@@ -91,15 +114,27 @@ export async function runRunner({ api, agent, host = hostname(), version = null,
|
|
|
91
114
|
await sleep(ERROR_BACKOFF_MS);
|
|
92
115
|
continue;
|
|
93
116
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
// One command sets everything up (GET-1307): with no history read yet, the runner starts that
|
|
118
|
+
// read itself, on this plan, and answers its calls like any other.
|
|
119
|
+
if (page.needs_backfill && api.startBackfill && now() - backfillAt >= BACKFILL_RETRY_MS) {
|
|
120
|
+
backfillAt = now();
|
|
121
|
+
try {
|
|
122
|
+
await api.startBackfill({ days: BACKFILL_DAYS, confirm: "yes" });
|
|
123
|
+
log(`Reading your last ${BACKFILL_DAYS / 30} months of calls and email on your plan. This can take a while; everything new is read after.`);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
log(`Your history read can't start yet (${err.message}). Trying again in ${BACKFILL_RETRY_MS / 60000} minutes.`);
|
|
126
|
+
}
|
|
97
127
|
}
|
|
98
128
|
const line = readingLine(page.reading);
|
|
99
129
|
if (line && line !== lastLine) { log(line); }
|
|
100
130
|
lastLine = line;
|
|
101
131
|
const calls = page.calls ?? [];
|
|
102
|
-
if (!calls.length) {
|
|
132
|
+
if (!calls.length) {
|
|
133
|
+
if (limitedUntil > now()) { await sleep(LIMITED_POLL_MS); continue; }
|
|
134
|
+
// The server held the poll open: ask again at once. One that answered at once: wait a little.
|
|
135
|
+
if (now() - asked < 5000) await sleep(page.reading ? BUSY_MS : IDLE_MS);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
103
138
|
await Promise.all(calls.map((c) => answerOne(c, page.models)));
|
|
104
139
|
}
|
|
105
140
|
return { answered };
|