@opennous/cli 0.13.0 → 0.14.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 +8 -0
- package/package.json +1 -1
- package/src/backfill.js +9 -4
- package/src/index.js +54 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @opennous/cli changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.1
|
|
4
|
+
|
|
5
|
+
### Fixed: carrying on with `backfill --workspace`
|
|
6
|
+
|
|
7
|
+
The carry-on line now repeats `--workspace`, so a rerun stays in the same workspace. A workspace you
|
|
8
|
+
signed in to for a backfill keeps its key, so carrying on doesn't ask you to sign in again. `logout`
|
|
9
|
+
removes these keys too (GET-1087).
|
|
10
|
+
|
|
3
11
|
## 0.13.0
|
|
4
12
|
|
|
5
13
|
### Added: `backfill`, in one command
|
package/package.json
CHANGED
package/src/backfill.js
CHANGED
|
@@ -13,6 +13,8 @@ import { join } from "node:path";
|
|
|
13
13
|
|
|
14
14
|
/** What to type to carry on. `npx` because most people never installed `nous` (GET-1087). */
|
|
15
15
|
export const RERUN = "npx @opennous/cli@latest backfill";
|
|
16
|
+
/** The same, for the workspace this run read: without it a rerun goes to the saved key's. Pure. */
|
|
17
|
+
export const rerunCommand = (workspaceId) => (workspaceId ? `${RERUN} --workspace ${workspaceId}` : RERUN);
|
|
16
18
|
export const NO_AGENT = "Sign in to Claude Code (`claude`) or Codex (`codex login`) first: this runs on your own plan, so it needs one of them. Then run this again.";
|
|
17
19
|
|
|
18
20
|
export const POLL_MS = 2000;
|
|
@@ -97,10 +99,13 @@ function runCommand({ cmd, args, input }, cwd) {
|
|
|
97
99
|
});
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
/** Nothing to read in this window. Pure. */
|
|
103
|
+
export const emptyEstimate = (counts) => !Object.values(counts ?? {}).some((n) => n > 0);
|
|
104
|
+
|
|
100
105
|
/** One line on how far the job is, or null when there is nothing to say yet. Pure. */
|
|
101
|
-
export function progressLine(reading, answered) {
|
|
106
|
+
export function progressLine(reading, answered, rerun = RERUN) {
|
|
102
107
|
if (reading?.finished) return "Wrapping up: last summaries for your accounts.";
|
|
103
|
-
const stop = `Stop any time with Ctrl-C; run \`${
|
|
108
|
+
const stop = `Stop any time with Ctrl-C; run \`${rerun}\` again to carry on.`;
|
|
104
109
|
if (reading?.total) return `Read ${reading.done} of ${reading.total} ${reading.kind === "meetings" ? "meetings" : "email threads"}. ${stop}`;
|
|
105
110
|
if (answered) return `${answered} reads done on your plan. ${stop}`;
|
|
106
111
|
return `Getting ready to read. This can take a few minutes. ${stop}`;
|
|
@@ -111,7 +116,7 @@ export function progressLine(reading, answered) {
|
|
|
111
116
|
* `run` (one command → stdout), `sleep`, `log`. Stopping loses nothing: every read that finished
|
|
112
117
|
* is saved, and unanswered calls come back to the next run.
|
|
113
118
|
*/
|
|
114
|
-
export async function runBackfill({ api, agent, jobId, run, sleep, log, now = Date.now, workers = WORKERS, limitWaitMs = LIMIT_WAIT_MS, pollMs = POLL_MS, progressMs = PROGRESS_MS }) {
|
|
119
|
+
export async function runBackfill({ api, agent, jobId, run, sleep, log, now = Date.now, workers = WORKERS, limitWaitMs = LIMIT_WAIT_MS, pollMs = POLL_MS, progressMs = PROGRESS_MS, rerun = RERUN }) {
|
|
115
120
|
let waitUntil = 0, answered = 0, lastLine = null, lastAt = -Infinity;
|
|
116
121
|
const say = (line) => {
|
|
117
122
|
if (!line || (line === lastLine && now() - lastAt < progressMs) || now() - lastAt < Math.min(progressMs, 1000)) return;
|
|
@@ -141,7 +146,7 @@ export async function runBackfill({ api, agent, jobId, run, sleep, log, now = Da
|
|
|
141
146
|
if (page.status === "failed") throw new Error(page.error || "The backfill failed on our side.");
|
|
142
147
|
// The server gave up waiting for us (a long sleep, say). We're back, so pick it up.
|
|
143
148
|
if (page.status === "paused_own_model") { await api.resume?.(); await sleep(pollMs); continue; }
|
|
144
|
-
say(progressLine(page.reading, answered));
|
|
149
|
+
say(progressLine(page.reading, answered, rerun));
|
|
145
150
|
if (!page.calls.length) { await sleep(pollMs); continue; }
|
|
146
151
|
await Promise.all(page.calls.map((c) => answerOne(c, page.models)));
|
|
147
152
|
}
|
package/src/index.js
CHANGED
|
@@ -110,9 +110,9 @@ function writeConfig(cfg) {
|
|
|
110
110
|
/** A key is there to use: from the environment, or saved by a sign-in. */
|
|
111
111
|
const hasKey = () => !!(process.env.NOUS_API_KEY || readConfig().apiKey);
|
|
112
112
|
|
|
113
|
-
function apiClient() {
|
|
113
|
+
function apiClient(key) {
|
|
114
114
|
const cfg = readConfig();
|
|
115
|
-
const apiKey = process.env.NOUS_API_KEY || cfg.apiKey;
|
|
115
|
+
const apiKey = key || process.env.NOUS_API_KEY || cfg.apiKey;
|
|
116
116
|
const apiUrl = process.env.NOUS_API_URL || cfg.apiUrl || "https://api.opennous.cloud";
|
|
117
117
|
|
|
118
118
|
if (!apiKey) {
|
|
@@ -192,8 +192,9 @@ program
|
|
|
192
192
|
.description("Sign out — remove the saved API key")
|
|
193
193
|
.action(() => {
|
|
194
194
|
const cfg = readConfig();
|
|
195
|
-
if (!cfg.apiKey) { console.log("Not signed in."); return; }
|
|
195
|
+
if (!cfg.apiKey && !cfg.workspaceKeys) { console.log("Not signed in."); return; }
|
|
196
196
|
delete cfg.apiKey;
|
|
197
|
+
delete cfg.workspaceKeys;
|
|
197
198
|
writeConfig(cfg);
|
|
198
199
|
console.log("✓ Signed out — removed the saved API key from ~/.nous/config.json");
|
|
199
200
|
})
|
|
@@ -288,14 +289,18 @@ async function browserLogin(url, opts = {}) {
|
|
|
288
289
|
} catch { continue; }
|
|
289
290
|
|
|
290
291
|
if (j.status === "approved" && j.api_key) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
// `save: false` is one command's key: the sign-in on this machine stays what it was.
|
|
293
|
+
if (opts.save !== false) {
|
|
294
|
+
const cfg = readConfig();
|
|
295
|
+
cfg.apiKey = j.api_key;
|
|
296
|
+
cfg.apiUrl = apiUrl;
|
|
297
|
+
writeConfig(cfg);
|
|
298
|
+
}
|
|
295
299
|
const who = await fetchWhoami(apiUrl, j.api_key);
|
|
296
300
|
console.log(`\n\n ${green("✓")} ${bold("Signed in")}${who ? ` as ${identityLine(who)}` : ""}`);
|
|
297
|
-
console.log(` ${dim("Key saved to ~/.nous/config.json")}
|
|
298
|
-
|
|
301
|
+
if (opts.save !== false) console.log(` ${dim("Key saved to ~/.nous/config.json")}`);
|
|
302
|
+
console.log("");
|
|
303
|
+
return j.api_key;
|
|
299
304
|
}
|
|
300
305
|
if (j.status === "denied") { console.log("\nAuthorization was denied."); process.exit(1); }
|
|
301
306
|
if (j.status === "expired") { console.log("\nThis sign-in expired. Run it again."); process.exit(1); }
|
|
@@ -546,10 +551,11 @@ program
|
|
|
546
551
|
.description("Read your email and meeting history on your own Claude or ChatGPT plan")
|
|
547
552
|
.option("--agent <agent>", "claude or codex (default: whichever you're signed in to)")
|
|
548
553
|
.option("--days <days>", "how many days back to read, 1 to 730", "180")
|
|
554
|
+
.option("--workspace <id>", "the workspace to read; the app's command fills this in")
|
|
549
555
|
.option("--yes", "start without asking")
|
|
550
556
|
.option("--url <url>", "API base URL (default: https://api.opennous.cloud)")
|
|
551
557
|
.action(async (opts) => {
|
|
552
|
-
const { detectAgents, runBackfill, liveRun,
|
|
558
|
+
const { detectAgents, runBackfill, liveRun, emptyEstimate, rerunCommand, NO_AGENT } = await import("./backfill.js");
|
|
553
559
|
const days = Number(opts.days);
|
|
554
560
|
if (!Number.isInteger(days) || days < 1 || days > 730) {
|
|
555
561
|
console.error("--days must be a whole number from 1 to 730.");
|
|
@@ -563,9 +569,39 @@ program
|
|
|
563
569
|
}
|
|
564
570
|
// Asked after the check above, so nobody signs in to be told they can't run this.
|
|
565
571
|
if (!hasKey()) await browserLogin(opts.url);
|
|
566
|
-
|
|
572
|
+
// A key saved here may be for another workspace, so say which one this is, and never read the
|
|
573
|
+
// wrong one: the app's command names its workspace, and a key for any other is not used.
|
|
574
|
+
const cfg = readConfig();
|
|
575
|
+
const whoamiUrl = opts.url || process.env.NOUS_API_URL || cfg.apiUrl || "https://api.opennous.cloud";
|
|
576
|
+
let key = process.env.NOUS_API_KEY || cfg.apiKey;
|
|
577
|
+
let who = await fetchWhoami(whoamiUrl, key);
|
|
578
|
+
// A workspace signed in to here before keeps its key, so carrying on doesn't mean signing in again.
|
|
579
|
+
const kept = opts.workspace && who?.workspace?.id !== opts.workspace ? cfg.workspaceKeys?.[opts.workspace] : null;
|
|
580
|
+
if (kept) {
|
|
581
|
+
const keptWho = await fetchWhoami(whoamiUrl, kept);
|
|
582
|
+
if (keptWho?.workspace?.id === opts.workspace) { key = kept; who = keptWho; }
|
|
583
|
+
}
|
|
584
|
+
if (opts.workspace && who?.workspace?.id !== opts.workspace) {
|
|
585
|
+
console.log(`This machine is signed in to ${who?.workspace?.name ?? "another workspace"}. Sign in to the workspace this backfill is for.`);
|
|
586
|
+
key = await browserLogin(opts.url, { save: false });
|
|
587
|
+
who = await fetchWhoami(whoamiUrl, key);
|
|
588
|
+
if (who?.workspace?.id !== opts.workspace) {
|
|
589
|
+
console.error(`That sign-in is for ${who?.workspace?.name ?? "another workspace"}. Open the right workspace in the app, then run this again.`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
const saved = readConfig();
|
|
593
|
+
writeConfig({ ...saved, workspaceKeys: { ...saved.workspaceKeys, [opts.workspace]: key } });
|
|
594
|
+
}
|
|
595
|
+
const workspace = who?.workspace?.name ?? "your workspace";
|
|
596
|
+
const rerun = rerunCommand(opts.workspace);
|
|
597
|
+
const api = apiClient(key);
|
|
567
598
|
const est = await api.post("/v2/backfill/own/estimate", { days });
|
|
568
|
-
|
|
599
|
+
if (emptyEstimate(est.counts)) {
|
|
600
|
+
console.log(`\nNothing to read in ${workspace} for the last ${days} days. If your sync is still running, try again when it's done.`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
console.log(`\nWorkspace: ${workspace}`);
|
|
604
|
+
console.log(`Last ${days} days: about ${est.counts.email_thread} email threads, ${est.counts.meeting} meetings, ${est.counts.call} calls.`);
|
|
569
605
|
console.log(`They're filtered first, then read on your ${agent} plan. None of it uses your OpenNous credits.`);
|
|
570
606
|
console.log("You can stop at any time and run this again: it carries on where it stopped.\n");
|
|
571
607
|
if (!opts.yes) {
|
|
@@ -576,13 +612,13 @@ program
|
|
|
576
612
|
if (!ok) return;
|
|
577
613
|
}
|
|
578
614
|
const job = await api.post("/v2/backfill/own", { days });
|
|
579
|
-
console.log(job.resumed ? "Picking up where you left off. Stop any time with Ctrl-C." : `Started. Stop any time with Ctrl-C; run \`${
|
|
615
|
+
console.log(job.resumed ? "Picking up where you left off. Stop any time with Ctrl-C." : `Started. Stop any time with Ctrl-C; run \`${rerun}\` again to carry on.`);
|
|
580
616
|
process.once("SIGINT", () => {
|
|
581
|
-
console.log(`\nStopped. Everything read so far is saved. Run \`${
|
|
617
|
+
console.log(`\nStopped. Everything read so far is saved. Run \`${rerun}\` to carry on.`);
|
|
582
618
|
process.exit(130);
|
|
583
619
|
});
|
|
584
620
|
const result = await runBackfill({
|
|
585
|
-
agent, jobId: job.job_id, run: liveRun(), log: console.log,
|
|
621
|
+
agent, jobId: job.job_id, run: liveRun(), log: console.log, rerun,
|
|
586
622
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
587
623
|
api: {
|
|
588
624
|
calls: (id, limit) => api.get(`/v2/backfill/own/${id}/calls`, { limit }),
|
|
@@ -591,7 +627,7 @@ program
|
|
|
591
627
|
},
|
|
592
628
|
});
|
|
593
629
|
console.log(`Done. This run: ${result.read} conversations read, ${result.answered} model calls on your ${agent} plan.`);
|
|
594
|
-
if (result.failed) console.log(`${result.failed} conversations couldn't be read. Run \`${
|
|
630
|
+
if (result.failed) console.log(`${result.failed} conversations couldn't be read. Run \`${rerun}\` again to retry them.`);
|
|
595
631
|
});
|
|
596
632
|
|
|
597
633
|
program
|
|
@@ -599,8 +635,9 @@ program
|
|
|
599
635
|
.description("Sign out — remove the saved API key")
|
|
600
636
|
.action(() => {
|
|
601
637
|
const cfg = readConfig();
|
|
602
|
-
if (!cfg.apiKey) { console.log("Not signed in."); return; }
|
|
638
|
+
if (!cfg.apiKey && !cfg.workspaceKeys) { console.log("Not signed in."); return; }
|
|
603
639
|
delete cfg.apiKey;
|
|
640
|
+
delete cfg.workspaceKeys;
|
|
604
641
|
writeConfig(cfg);
|
|
605
642
|
console.log("✓ Signed out — removed the saved API key from ~/.nous/config.json");
|
|
606
643
|
});
|