@tidegap/cli 0.0.0-stage → 0.18.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/LICENSE +201 -0
- package/README.md +125 -2
- package/dist/commands/env-file.d.ts +53 -0
- package/dist/commands/env-file.d.ts.map +1 -0
- package/dist/commands/env-file.js +127 -0
- package/dist/commands/env-file.js.map +1 -0
- package/dist/commands/install.d.ts +84 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +299 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/queue.d.ts +139 -0
- package/dist/commands/queue.d.ts.map +1 -0
- package/dist/commands/queue.js +491 -0
- package/dist/commands/queue.js.map +1 -0
- package/dist/commands/receipts.d.ts +15 -0
- package/dist/commands/receipts.d.ts.map +1 -0
- package/dist/commands/receipts.js +70 -0
- package/dist/commands/receipts.js.map +1 -0
- package/dist/commands/register-wake.d.ts +21 -0
- package/dist/commands/register-wake.d.ts.map +1 -0
- package/dist/commands/register-wake.js +76 -0
- package/dist/commands/register-wake.js.map +1 -0
- package/dist/commands/stats.d.ts +31 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +165 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/tick.d.ts +73 -0
- package/dist/commands/tick.d.ts.map +1 -0
- package/dist/commands/tick.js +294 -0
- package/dist/commands/tick.js.map +1 -0
- package/dist/commands/ui.d.ts +25 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +83 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/commands/verify.d.ts +32 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +135 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/env.d.ts +55 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +90 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +30 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +66 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/linux.d.ts +81 -0
- package/dist/platform/linux.d.ts.map +1 -0
- package/dist/platform/linux.js +158 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +69 -0
- package/dist/platform/macos.d.ts.map +1 -0
- package/dist/platform/macos.js +151 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/windows.d.ts +31 -0
- package/dist/platform/windows.d.ts.map +1 -0
- package/dist/platform/windows.js +58 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/ui/api.d.ts +59 -0
- package/dist/ui/api.d.ts.map +1 -0
- package/dist/ui/api.js +459 -0
- package/dist/ui/api.js.map +1 -0
- package/dist/ui/dispatcher.d.ts +84 -0
- package/dist/ui/dispatcher.d.ts.map +1 -0
- package/dist/ui/dispatcher.js +150 -0
- package/dist/ui/dispatcher.js.map +1 -0
- package/dist/ui/events.d.ts +27 -0
- package/dist/ui/events.d.ts.map +1 -0
- package/dist/ui/events.js +85 -0
- package/dist/ui/events.js.map +1 -0
- package/dist/ui/server.d.ts +69 -0
- package/dist/ui/server.d.ts.map +1 -0
- package/dist/ui/server.js +258 -0
- package/dist/ui/server.js.map +1 -0
- package/package.json +54 -3
- package/ui/agenda.js +256 -0
- package/ui/api.js +111 -0
- package/ui/app.css +707 -0
- package/ui/app.js +597 -0
- package/ui/detail.js +431 -0
- package/ui/dialog.js +279 -0
- package/ui/dnd.js +131 -0
- package/ui/dom.js +255 -0
- package/ui/index.html +37 -0
- package/ui/journal.js +207 -0
- package/ui/keys.js +92 -0
- package/ui/row.js +153 -0
- package/ui/state.js +412 -0
- package/ui/timeline.js +202 -0
- package/ui/toast.js +69 -0
- package/ui/tokens.css +106 -0
- package/ui/views.js +31 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap register-wake <task-id>` — schedule an RTC/pmset wake event 30s
|
|
3
|
+
* before the task's `scheduledFor`. macOS uses `pmset schedule wake`;
|
|
4
|
+
* Linux uses `rtcwake -m no -t <epoch>`.
|
|
5
|
+
*/
|
|
6
|
+
import { TaskStore } from "@tidegap/core";
|
|
7
|
+
import { currentPlatform, } from "../platform/index.js";
|
|
8
|
+
import { pmsetCommand, pmsetScheduleWake } from "../platform/macos.js";
|
|
9
|
+
import { rtcwakeCommand, rtcwakeScheduleWake, } from "../platform/linux.js";
|
|
10
|
+
import { defaultDbPath } from "./tick.js";
|
|
11
|
+
export async function runRegisterWake(opts) {
|
|
12
|
+
const platform = opts.platform ?? currentPlatform();
|
|
13
|
+
if (platform === "windows") {
|
|
14
|
+
return {
|
|
15
|
+
command: "",
|
|
16
|
+
ok: false,
|
|
17
|
+
stderr: "non-posix",
|
|
18
|
+
message: `[tidegap/cli] register-wake is not supported on Windows — there is no ` +
|
|
19
|
+
`pmset/rtcwake equivalent wired. Use Task Scheduler's "Wake the ` +
|
|
20
|
+
`computer to run this task" option on your tidegap-tick job instead.`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const dbPath = opts.db ?? defaultDbPath();
|
|
24
|
+
const store = new TaskStore({ dbPath });
|
|
25
|
+
let scheduledFor;
|
|
26
|
+
try {
|
|
27
|
+
const rec = store.get(opts.taskId);
|
|
28
|
+
if (!rec) {
|
|
29
|
+
throw new Error(`register-wake: task ${opts.taskId} not found in ${dbPath}`);
|
|
30
|
+
}
|
|
31
|
+
if (!rec.scheduledFor) {
|
|
32
|
+
throw new Error(`register-wake: task ${opts.taskId} has no scheduled_for; nothing to register against`);
|
|
33
|
+
}
|
|
34
|
+
scheduledFor = rec.scheduledFor;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
store.close();
|
|
38
|
+
}
|
|
39
|
+
const at = new Date(new Date(scheduledFor).getTime() - 30 * 1000);
|
|
40
|
+
const command = platform === "macos" ? pmsetCommand(at) : rtcwakeCommand(at);
|
|
41
|
+
if (opts.dryRun) {
|
|
42
|
+
return {
|
|
43
|
+
command,
|
|
44
|
+
ok: true,
|
|
45
|
+
stderr: "",
|
|
46
|
+
message: `(dry-run) ${command}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const res = platform === "macos"
|
|
50
|
+
? await pmsetScheduleWake(at)
|
|
51
|
+
: await rtcwakeScheduleWake(at);
|
|
52
|
+
return {
|
|
53
|
+
command: res.command,
|
|
54
|
+
ok: res.ok,
|
|
55
|
+
stderr: res.stderr,
|
|
56
|
+
message: res.ok
|
|
57
|
+
? `registered: ${res.command}`
|
|
58
|
+
: `failed (${res.stderr}).\n` +
|
|
59
|
+
`Run as root, or pre-authorize password-less wake scheduling — add this\n` +
|
|
60
|
+
`line via \`sudo visudo\` (replace ${whoami()} with your login):\n` +
|
|
61
|
+
` ${sudoersLine(platform)}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Best-effort current username for the sudoers hint. */
|
|
65
|
+
function whoami() {
|
|
66
|
+
return (process.env.SUDO_USER ??
|
|
67
|
+
process.env.USER ??
|
|
68
|
+
process.env.LOGNAME ??
|
|
69
|
+
"youruser");
|
|
70
|
+
}
|
|
71
|
+
/** The exact NOPASSWD sudoers line the user should paste. */
|
|
72
|
+
function sudoersLine(platform) {
|
|
73
|
+
const bin = platform === "macos" ? "/usr/bin/pmset" : "/usr/sbin/rtcwake";
|
|
74
|
+
return `${whoami()} ALL=(root) NOPASSWD: ${bin}`;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=register-wake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-wake.js","sourceRoot":"","sources":["../../src/commands/register-wake.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EACL,eAAe,GAEhB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EACL,cAAc,EACd,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAiB1C,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAyB;IAEzB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,eAAe,EAAE,CAAC;IACpD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO;YACL,OAAO,EAAE,EAAE;YACX,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,WAAW;YACnB,OAAO,EACL,wEAAwE;gBACxE,iEAAiE;gBACjE,qEAAqE;SACxE,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,IAAI,aAAa,EAAE,CAAC;IAC1C,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,IAAI,YAAgC,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,CAAC,MAAM,iBAAiB,MAAM,EAAE,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,uBAAuB,IAAI,CAAC,MAAM,oDAAoD,CACvF,CAAC;QACJ,CAAC;QACD,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;IAClC,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAED,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAClE,MAAM,OAAO,GACX,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IAC/D,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO;YACL,OAAO;YACP,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,EAAE;YACV,OAAO,EAAE,aAAa,OAAO,EAAE;SAChC,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GACP,QAAQ,KAAK,OAAO;QAClB,CAAC,CAAC,MAAM,iBAAiB,CAAC,EAAE,CAAC;QAC7B,CAAC,CAAC,MAAM,mBAAmB,CAAC,EAAE,CAAC,CAAC;IACpC,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO,EAAE,GAAG,CAAC,EAAE;YACb,CAAC,CAAC,eAAe,GAAG,CAAC,OAAO,EAAE;YAC9B,CAAC,CAAC,WAAW,GAAG,CAAC,MAAM,MAAM;gBAC3B,0EAA0E;gBAC1E,qCAAqC,MAAM,EAAE,sBAAsB;gBACnE,OAAO,WAAW,CAAC,QAAQ,CAAC,EAAE;KACnC,CAAC;AACJ,CAAC;AAED,yDAAyD;AACzD,SAAS,MAAM;IACb,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,SAAS;QACrB,OAAO,CAAC,GAAG,CAAC,IAAI;QAChB,OAAO,CAAC,GAAG,CAAC,OAAO;QACnB,UAAU,CACX,CAAC;AACJ,CAAC;AAED,6DAA6D;AAC7D,SAAS,WAAW,CAAC,QAAsB;IACzC,MAAM,GAAG,GACP,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAC;IAChE,OAAO,GAAG,MAAM,EAAE,yBAAyB,GAAG,EAAE,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap stats` — local-only personal-impact aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Reads the SQLite ledger at ~/.tidegap/queue.db (or --db) and renders the
|
|
5
|
+
* cumulative carbon stats + per-region breakdown + achievement-badge list.
|
|
6
|
+
* No telemetry, no network, no third-party reporting — everything stays
|
|
7
|
+
* on the user's machine. The dashboard `/stats` route reads the same
|
|
8
|
+
* shape over the (planned v0.9) scheduler HTTP API.
|
|
9
|
+
*
|
|
10
|
+
* v0.8 surface: read-only. Future versions may add `--since` window
|
|
11
|
+
* filters and `--per-day` time-bucket histograms.
|
|
12
|
+
*/
|
|
13
|
+
import { type CarbonStats, type RegionStats, type BandHistogram, type Achievement, type CarbonBudgetStatus } from "@tidegap/core";
|
|
14
|
+
export interface StatsCommandOptions {
|
|
15
|
+
db?: string;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
/** Override the carbon-budget config path (tests). Defaults to ~/.tidegap/config. */
|
|
18
|
+
budgetConfig?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface StatsResult {
|
|
21
|
+
rendered: string;
|
|
22
|
+
stats: CarbonStats;
|
|
23
|
+
perRegion: RegionStats[];
|
|
24
|
+
bands: BandHistogram;
|
|
25
|
+
badges: Achievement[];
|
|
26
|
+
/** Aggregate carbon-budget status when a budget is configured (ROADMAP item 4). */
|
|
27
|
+
budget?: CarbonBudgetStatus;
|
|
28
|
+
}
|
|
29
|
+
export declare function renderStats(r: StatsResult): string;
|
|
30
|
+
export declare function runStats(opts?: StatsCommandOptions): Promise<StatsResult>;
|
|
31
|
+
//# sourceMappingURL=stats.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/commands/stats.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,mBAAmB;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,mFAAmF;IACnF,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAuBD,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAuElD;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,WAAW,CAAC,CAgEnF"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap stats` — local-only personal-impact aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Reads the SQLite ledger at ~/.tidegap/queue.db (or --db) and renders the
|
|
5
|
+
* cumulative carbon stats + per-region breakdown + achievement-badge list.
|
|
6
|
+
* No telemetry, no network, no third-party reporting — everything stays
|
|
7
|
+
* on the user's machine. The dashboard `/stats` route reads the same
|
|
8
|
+
* shape over the (planned v0.9) scheduler HTTP API.
|
|
9
|
+
*
|
|
10
|
+
* v0.8 surface: read-only. Future versions may add `--since` window
|
|
11
|
+
* filters and `--per-day` time-bucket histograms.
|
|
12
|
+
*/
|
|
13
|
+
import { mkdirSync } from "node:fs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import { TaskStore, aggregateStats, aggregateByRegion, bandHistogram, achievements, carbonBudgetStatus, loadCarbonBudgetConfig, } from "@tidegap/core";
|
|
16
|
+
import { readEnvCredentials } from "../env.js";
|
|
17
|
+
import { defaultDbPath } from "./tick.js";
|
|
18
|
+
function fmtNumber(n) {
|
|
19
|
+
if (n === 0)
|
|
20
|
+
return "0";
|
|
21
|
+
if (Math.abs(n) < 1)
|
|
22
|
+
return n.toFixed(3);
|
|
23
|
+
if (Math.abs(n) < 10)
|
|
24
|
+
return n.toFixed(2);
|
|
25
|
+
if (Math.abs(n) < 100)
|
|
26
|
+
return n.toFixed(1);
|
|
27
|
+
return Math.round(n).toLocaleString("en-US");
|
|
28
|
+
}
|
|
29
|
+
function fmtPercent(num, den) {
|
|
30
|
+
if (den === 0)
|
|
31
|
+
return "—";
|
|
32
|
+
return `${Math.round((num / den) * 100)}%`;
|
|
33
|
+
}
|
|
34
|
+
function relativeTime(iso) {
|
|
35
|
+
if (!iso)
|
|
36
|
+
return "—";
|
|
37
|
+
const then = new Date(iso).getTime();
|
|
38
|
+
if (Number.isNaN(then))
|
|
39
|
+
return iso;
|
|
40
|
+
const days = Math.max(1, Math.round((Date.now() - then) / (24 * 60 * 60 * 1000)));
|
|
41
|
+
return `${iso.slice(0, 10)} (${days}d ago)`;
|
|
42
|
+
}
|
|
43
|
+
export function renderStats(r) {
|
|
44
|
+
const { stats, perRegion, bands, badges, budget } = r;
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push("Tidegap · personal impact");
|
|
47
|
+
lines.push("=".repeat(48));
|
|
48
|
+
lines.push("");
|
|
49
|
+
if (budget) {
|
|
50
|
+
const bar = "#".repeat(Math.min(32, Math.round((budget.pct / 100) * 32)));
|
|
51
|
+
const flag = budget.exceeded ? " ⚠ OVER BUDGET" : "";
|
|
52
|
+
lines.push(`carbon budget (${budget.windowKind})`);
|
|
53
|
+
lines.push("-".repeat(48));
|
|
54
|
+
lines.push(` used ${fmtNumber(budget.usedG)} / ${fmtNumber(budget.thresholdG)} g ` +
|
|
55
|
+
`(${budget.pct}%)${flag}`);
|
|
56
|
+
lines.push(` ${bar}`);
|
|
57
|
+
lines.push(` window started ${budget.windowStart.slice(0, 10)} · ${budget.taskCount} tasks`);
|
|
58
|
+
if (budget.alerted)
|
|
59
|
+
lines.push(` alert fired for this window`);
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
lines.push(`Tasks dispatched ${fmtNumber(stats.taskCount)}`);
|
|
63
|
+
lines.push(`Estimated CO2e accounted ${fmtNumber(stats.totalEstimatedCarbonGCo2)} g`);
|
|
64
|
+
lines.push(`Cleanest-window hits ${stats.scoredHits} (${fmtPercent(stats.scoredHits, stats.taskCount)})`);
|
|
65
|
+
lines.push(`Run-now dispatches ${stats.currentDispatches}`);
|
|
66
|
+
lines.push(`Expedited (skip window) ${stats.expeditedDispatches}`);
|
|
67
|
+
lines.push(`First task ${relativeTime(stats.firstRanAt)}`);
|
|
68
|
+
lines.push(`Most recent ${relativeTime(stats.lastRanAt)}`);
|
|
69
|
+
lines.push("");
|
|
70
|
+
if (perRegion.length > 0) {
|
|
71
|
+
lines.push("by region");
|
|
72
|
+
lines.push("-".repeat(48));
|
|
73
|
+
for (const r of perRegion.slice(0, 20)) {
|
|
74
|
+
const tail = `${String(r.taskCount).padStart(5)} ${fmtNumber(r.totalEstimatedCarbonGCo2).padStart(8)} g (${fmtNumber(r.avgEstimatedCarbonGCo2)}/task)`;
|
|
75
|
+
lines.push(` ${r.region.padEnd(20)}${tail}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push("");
|
|
78
|
+
}
|
|
79
|
+
const totalBands = bands.veryClean + bands.clean + bands.average + bands.dirty + bands.veryDirty;
|
|
80
|
+
if (totalBands > 0) {
|
|
81
|
+
lines.push("dispatch grid-band distribution");
|
|
82
|
+
lines.push("-".repeat(48));
|
|
83
|
+
for (const [k, v] of Object.entries(bands)) {
|
|
84
|
+
const pct = fmtPercent(v, totalBands).padStart(4);
|
|
85
|
+
const bar = "#".repeat(Math.round((v / totalBands) * 32));
|
|
86
|
+
lines.push(` ${k.padEnd(12)} ${String(v).padStart(5)} ${pct} ${bar}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push("");
|
|
89
|
+
}
|
|
90
|
+
const unlocked = badges.filter((b) => b.unlocked);
|
|
91
|
+
const locked = badges.filter((b) => !b.unlocked);
|
|
92
|
+
lines.push(`achievements (${unlocked.length}/${badges.length} unlocked)`);
|
|
93
|
+
lines.push("-".repeat(48));
|
|
94
|
+
for (const b of badges) {
|
|
95
|
+
const mark = b.unlocked ? "✓" : "·";
|
|
96
|
+
lines.push(` ${mark} ${b.label.padEnd(22)} ${b.description}`);
|
|
97
|
+
}
|
|
98
|
+
lines.push("");
|
|
99
|
+
if (stats.taskCount === 0) {
|
|
100
|
+
lines.push("Queue is empty. Run `/tidegap:defer …` from a Claude Code session,");
|
|
101
|
+
lines.push("or `npx -y @tidegap/mcp` from your MCP host, to start.");
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
export async function runStats(opts = {}) {
|
|
106
|
+
const dbPath = opts.db ?? defaultDbPath();
|
|
107
|
+
// Ensure the parent directory exists before better-sqlite3 opens the
|
|
108
|
+
// file. Matches the MCP server's behaviour, so a first-time `tidegap
|
|
109
|
+
// stats` invocation before the MCP server has ever written to the
|
|
110
|
+
// ledger returns an empty result rather than throwing ENOENT.
|
|
111
|
+
if (dbPath !== ":memory:") {
|
|
112
|
+
try {
|
|
113
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Best-effort; the open below will surface any real error.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const store = new TaskStore({ dbPath });
|
|
120
|
+
try {
|
|
121
|
+
const completed = store.list({ status: "completed" });
|
|
122
|
+
const stats = aggregateStats(completed);
|
|
123
|
+
const perRegion = aggregateByRegion(completed);
|
|
124
|
+
const bands = bandHistogram(completed);
|
|
125
|
+
const badges = achievements(stats, perRegion);
|
|
126
|
+
// Aggregate carbon-budget status (ROADMAP item 4): only when configured.
|
|
127
|
+
// Core is environment-pure, so the CLI (the host) reads the two
|
|
128
|
+
// TIDEGAP_CARBON_BUDGET_* variables and passes them in — same precedence as
|
|
129
|
+
// before: an exported variable beats the ~/.tidegap/config file.
|
|
130
|
+
const budgetConfig = loadCarbonBudgetConfig({
|
|
131
|
+
path: opts.budgetConfig,
|
|
132
|
+
env: readEnvCredentials().carbonBudget,
|
|
133
|
+
});
|
|
134
|
+
let budget;
|
|
135
|
+
if (budgetConfig) {
|
|
136
|
+
const now = new Date();
|
|
137
|
+
const status = carbonBudgetStatus(completed, budgetConfig, now, false);
|
|
138
|
+
let alerted = false;
|
|
139
|
+
try {
|
|
140
|
+
alerted = store.hasBudgetAlert(budgetConfig.windowKind, status.windowStart, budgetConfig.thresholdG);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// best-effort — leave alerted false
|
|
144
|
+
}
|
|
145
|
+
budget = { ...status, alerted };
|
|
146
|
+
}
|
|
147
|
+
const result = {
|
|
148
|
+
rendered: "",
|
|
149
|
+
stats,
|
|
150
|
+
perRegion,
|
|
151
|
+
bands,
|
|
152
|
+
badges,
|
|
153
|
+
...(budget ? { budget } : {}),
|
|
154
|
+
};
|
|
155
|
+
result.rendered = renderStats(result);
|
|
156
|
+
if (opts.json) {
|
|
157
|
+
result.rendered = JSON.stringify({ stats, perRegion, bands, badges, ...(budget ? { budget } : {}) }, null, 2);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
store.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/commands/stats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap tick` — drain due provider-call tasks.
|
|
3
|
+
*
|
|
4
|
+
* One-shot by default; `--daemon` loops on the supplied interval until
|
|
5
|
+
* SIGINT. Reads API keys from the environment and constructs the
|
|
6
|
+
* matching adapters lazily. If neither key is set we still exit 0 with
|
|
7
|
+
* a warning — useful for "this cron only kicks in once a key is added".
|
|
8
|
+
*/
|
|
9
|
+
import { type TickAdapters } from "@tidegap/core";
|
|
10
|
+
import { type SpawnOptions } from "node:child_process";
|
|
11
|
+
import { type ProviderCredentials } from "../env.js";
|
|
12
|
+
export interface TickCommandOptions {
|
|
13
|
+
db?: string;
|
|
14
|
+
daemon?: boolean;
|
|
15
|
+
once?: boolean;
|
|
16
|
+
interval?: number;
|
|
17
|
+
region?: string;
|
|
18
|
+
/** Override the secrets file path (tests). Defaults to ~/.config/tidegap/env. */
|
|
19
|
+
envFile?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Override the ~/.tidegap/config path (tests): carbon budget and quota
|
|
22
|
+
* reset schedules are both read from it.
|
|
23
|
+
*/
|
|
24
|
+
budgetConfig?: string;
|
|
25
|
+
/** `false` (`--no-notify`) posts no OS notification for finished tasks. */
|
|
26
|
+
notify?: boolean;
|
|
27
|
+
/** Spawner for the notification command (tests). Defaults to node's spawn. */
|
|
28
|
+
spawnNotify?: NotifySpawner;
|
|
29
|
+
/** Platform for the notification command (tests). Defaults to os.platform(). */
|
|
30
|
+
platform?: NodeJS.Platform;
|
|
31
|
+
}
|
|
32
|
+
/** Starts a notification process; the tick never waits on it. */
|
|
33
|
+
export type NotifySpawner = (cmd: string, args: string[], opts: SpawnOptions) => void;
|
|
34
|
+
/** `TIDEGAP_NOTIFY=0` (or false/off/no; 0.17: `EBB_NOTIFY`) in ~/.tidegap/config turns notifications off. */
|
|
35
|
+
export declare function notifyDisabledInConfig(path?: string): boolean;
|
|
36
|
+
export interface TickRunResult {
|
|
37
|
+
exitCode: number;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Default queue path. Matches the MCP server's persistence default
|
|
42
|
+
* (`~/.tidegap/queue.db`, established in v0.7.1) so `tidegap tick`,
|
|
43
|
+
* `tidegap queue list`, `tidegap receipts list`, and `tidegap stats` all read
|
|
44
|
+
* the same SQLite ledger the MCP server writes to. Prior versions
|
|
45
|
+
* defaulted to `~/.ebb/queue.sqlite`, which silently diverged from
|
|
46
|
+
* the MCP server's actual path — fixed in v0.8.x.
|
|
47
|
+
*
|
|
48
|
+
* If the legacy `~/.ebb/queue.sqlite` exists and the new path
|
|
49
|
+
* doesn't, the legacy path is returned so users who pre-date v0.7.1
|
|
50
|
+
* keep seeing their historical data. Once they begin writing to
|
|
51
|
+
* the new path (via the MCP server), pass `--db` explicitly to
|
|
52
|
+
* read the old one.
|
|
53
|
+
*/
|
|
54
|
+
export declare function defaultDbPath(): string;
|
|
55
|
+
/**
|
|
56
|
+
* Build the tick adapters from host-supplied credentials.
|
|
57
|
+
*
|
|
58
|
+
* `@tidegap/core` no longer reads the environment, so the CLI (the host)
|
|
59
|
+
* reads it once via `readEnvCredentials()` and injects the values here. The
|
|
60
|
+
* registration rules are unchanged: an adapter appears exactly when its
|
|
61
|
+
* credential is present, Gemini prefers GEMINI_API_KEY over GOOGLE_API_KEY,
|
|
62
|
+
* and Ollama is an explicit OLLAMA_HOST opt-in.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildAdapters(creds?: ProviderCredentials): TickAdapters;
|
|
65
|
+
/**
|
|
66
|
+
* Inspect the ledger for pending provider-call tasks whose provider key
|
|
67
|
+
* is missing from the environment. Returns the distinct missing keys.
|
|
68
|
+
* Best-effort — never throws (a broken/absent DB just yields []).
|
|
69
|
+
*/
|
|
70
|
+
export declare function missingProviderKeysForPending(dbPath: string): string[];
|
|
71
|
+
export declare function runTickOnce(opts?: TickCommandOptions): Promise<TickRunResult>;
|
|
72
|
+
export declare function runTick(opts?: TickCommandOptions): Promise<number>;
|
|
73
|
+
//# sourceMappingURL=tick.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick.d.ts","sourceRoot":"","sources":["../../src/commands/tick.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAmBL,KAAK,YAAY,EAElB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAI9D,OAAO,EAAsB,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGzE,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,aAAa,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B;AAED,iEAAiE;AACjE,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;AAqBtF,6GAA6G;AAC7G,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,MAAiC,GAAG,OAAO,CAQvF;AA0CD,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,KAAK,GAAE,mBAA0C,GAChD,YAAY,CAmBd;AAaD;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BtE;AAED,wBAAsB,WAAW,CAC/B,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAuFxB;AAED,wBAAsB,OAAO,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB5E"}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tidegap tick` — drain due provider-call tasks.
|
|
3
|
+
*
|
|
4
|
+
* One-shot by default; `--daemon` loops on the supplied interval until
|
|
5
|
+
* SIGINT. Reads API keys from the environment and constructs the
|
|
6
|
+
* matching adapters lazily. If neither key is set we still exit 0 with
|
|
7
|
+
* a warning — useful for "this cron only kicks in once a key is added".
|
|
8
|
+
*/
|
|
9
|
+
import { AnthropicAdapter, buildDefaultGridFeed, carbonBudgetConfigPath, GeminiAdapter, loadCarbonBudgetConfig, loadQuotaResetConfig, OllamaAdapter, OpenAIAdapter, osNotifyCommand, parseKeyValues, resolveTidegapHome, withTidegapKeys, prepareTick, resolveRegion, Scheduler, taskNotificationContent, TaskStore, } from "@tidegap/core";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
+
import { homedir, platform as osPlatform } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { readEnvCredentials } from "../env.js";
|
|
15
|
+
import { loadEnvFileIntoProcess } from "./env-file.js";
|
|
16
|
+
const NOTIFY_TIMEOUT_MS = 5_000;
|
|
17
|
+
/**
|
|
18
|
+
* True under a test runner. A test that reaches the default spawner without
|
|
19
|
+
* injecting `spawnNotify` must not post a real notification on the
|
|
20
|
+
* developer's desktop.
|
|
21
|
+
*/
|
|
22
|
+
function underTest() {
|
|
23
|
+
return process.env.VITEST !== undefined || process.env.NODE_ENV === "test";
|
|
24
|
+
}
|
|
25
|
+
const defaultSpawnNotify = (cmd, args, opts) => {
|
|
26
|
+
if (underTest())
|
|
27
|
+
return;
|
|
28
|
+
const child = spawn(cmd, args, opts);
|
|
29
|
+
// A missing binary (no notify-send) or a failed toast is not a tick error.
|
|
30
|
+
child.on("error", () => { });
|
|
31
|
+
child.unref();
|
|
32
|
+
};
|
|
33
|
+
/** `TIDEGAP_NOTIFY=0` (or false/off/no; 0.17: `EBB_NOTIFY`) in ~/.tidegap/config turns notifications off. */
|
|
34
|
+
export function notifyDisabledInConfig(path = carbonBudgetConfigPath()) {
|
|
35
|
+
try {
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
return false;
|
|
38
|
+
const v = withTidegapKeys(parseKeyValues(readFileSync(path, "utf8"))).TIDEGAP_NOTIFY;
|
|
39
|
+
return v !== undefined && /^(0|false|off|no)$/i.test(v.trim());
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Post one OS notification per task that reached `completed` or `failed`
|
|
47
|
+
* in this tick. Best-effort: an unknown platform, a missing record, or a
|
|
48
|
+
* spawner that throws is skipped silently.
|
|
49
|
+
*/
|
|
50
|
+
function notifyFinished(dbPath, entries, opts) {
|
|
51
|
+
const finished = entries.filter((e) => e.status === "completed" || e.status === "failed");
|
|
52
|
+
if (finished.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
const platform = opts.platform ?? osPlatform();
|
|
55
|
+
const spawnNotify = opts.spawnNotify ?? defaultSpawnNotify;
|
|
56
|
+
let store;
|
|
57
|
+
try {
|
|
58
|
+
store = new TaskStore({ dbPath });
|
|
59
|
+
for (const entry of finished) {
|
|
60
|
+
const record = store.get(entry.taskId);
|
|
61
|
+
if (!record)
|
|
62
|
+
continue;
|
|
63
|
+
const { title, body } = taskNotificationContent(record);
|
|
64
|
+
const command = osNotifyCommand(platform, title, body);
|
|
65
|
+
if (!command)
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
spawnNotify(command.cmd, command.args, {
|
|
69
|
+
stdio: "ignore",
|
|
70
|
+
timeout: NOTIFY_TIMEOUT_MS,
|
|
71
|
+
...(command.env ? { env: { ...process.env, ...command.env } } : {}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// ignore: a notification never changes the tick outcome
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// unreadable ledger: skip notifications
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
store?.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Default queue path. Matches the MCP server's persistence default
|
|
88
|
+
* (`~/.tidegap/queue.db`, established in v0.7.1) so `tidegap tick`,
|
|
89
|
+
* `tidegap queue list`, `tidegap receipts list`, and `tidegap stats` all read
|
|
90
|
+
* the same SQLite ledger the MCP server writes to. Prior versions
|
|
91
|
+
* defaulted to `~/.ebb/queue.sqlite`, which silently diverged from
|
|
92
|
+
* the MCP server's actual path — fixed in v0.8.x.
|
|
93
|
+
*
|
|
94
|
+
* If the legacy `~/.ebb/queue.sqlite` exists and the new path
|
|
95
|
+
* doesn't, the legacy path is returned so users who pre-date v0.7.1
|
|
96
|
+
* keep seeing their historical data. Once they begin writing to
|
|
97
|
+
* the new path (via the MCP server), pass `--db` explicitly to
|
|
98
|
+
* read the old one.
|
|
99
|
+
*/
|
|
100
|
+
export function defaultDbPath() {
|
|
101
|
+
const newPath = join(resolveTidegapHome(), "queue.db");
|
|
102
|
+
const legacyPath = join(homedir(), ".ebb", "queue.sqlite");
|
|
103
|
+
if (!existsSync(newPath) && existsSync(legacyPath))
|
|
104
|
+
return legacyPath;
|
|
105
|
+
return newPath;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build the tick adapters from host-supplied credentials.
|
|
109
|
+
*
|
|
110
|
+
* `@tidegap/core` no longer reads the environment, so the CLI (the host)
|
|
111
|
+
* reads it once via `readEnvCredentials()` and injects the values here. The
|
|
112
|
+
* registration rules are unchanged: an adapter appears exactly when its
|
|
113
|
+
* credential is present, Gemini prefers GEMINI_API_KEY over GOOGLE_API_KEY,
|
|
114
|
+
* and Ollama is an explicit OLLAMA_HOST opt-in.
|
|
115
|
+
*/
|
|
116
|
+
export function buildAdapters(creds = readEnvCredentials()) {
|
|
117
|
+
const out = {};
|
|
118
|
+
if (creds.anthropicApiKey) {
|
|
119
|
+
out.anthropic = new AnthropicAdapter({ apiKey: creds.anthropicApiKey });
|
|
120
|
+
}
|
|
121
|
+
if (creds.openaiApiKey) {
|
|
122
|
+
out.openai = new OpenAIAdapter({ apiKey: creds.openaiApiKey });
|
|
123
|
+
}
|
|
124
|
+
// Gemini reads GEMINI_API_KEY, falling back to GOOGLE_API_KEY (resolved in
|
|
125
|
+
// readEnvCredentials, so `geminiApiKey` here is already the winner).
|
|
126
|
+
if (creds.geminiApiKey) {
|
|
127
|
+
out.gemini = new GeminiAdapter({ apiKey: creds.geminiApiKey });
|
|
128
|
+
}
|
|
129
|
+
// Ollama is local + keyless: register it only when OLLAMA_HOST is set
|
|
130
|
+
// (explicit opt-in). The adapter defaults to http://localhost:11434.
|
|
131
|
+
if (creds.ollamaHost) {
|
|
132
|
+
out.ollama = new OllamaAdapter({ host: creds.ollamaHost });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Provider key expected in the env for a given provider name. Ollama has no
|
|
138
|
+
* key (it is local + keyless), so it is intentionally absent — a pending
|
|
139
|
+
* Ollama task is never reported as "missing a key".
|
|
140
|
+
*/
|
|
141
|
+
const PROVIDER_KEY = {
|
|
142
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
143
|
+
openai: "OPENAI_API_KEY",
|
|
144
|
+
gemini: "GEMINI_API_KEY",
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Inspect the ledger for pending provider-call tasks whose provider key
|
|
148
|
+
* is missing from the environment. Returns the distinct missing keys.
|
|
149
|
+
* Best-effort — never throws (a broken/absent DB just yields []).
|
|
150
|
+
*/
|
|
151
|
+
export function missingProviderKeysForPending(dbPath) {
|
|
152
|
+
const missing = new Set();
|
|
153
|
+
let store;
|
|
154
|
+
try {
|
|
155
|
+
store = new TaskStore({ dbPath });
|
|
156
|
+
const pending = [
|
|
157
|
+
...store.list({ status: "queued" }),
|
|
158
|
+
...store.list({ status: "scheduled" }),
|
|
159
|
+
...store.list({ status: "submitted" }),
|
|
160
|
+
];
|
|
161
|
+
for (const rec of pending) {
|
|
162
|
+
if (!rec.bodyJson)
|
|
163
|
+
continue;
|
|
164
|
+
let provider;
|
|
165
|
+
try {
|
|
166
|
+
provider = JSON.parse(rec.bodyJson)
|
|
167
|
+
.provider;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const key = provider ? PROVIDER_KEY[provider] : undefined;
|
|
173
|
+
if (key && !process.env[key])
|
|
174
|
+
missing.add(key);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// absent / unreadable DB — nothing to warn about.
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
store?.close();
|
|
182
|
+
}
|
|
183
|
+
return [...missing];
|
|
184
|
+
}
|
|
185
|
+
export async function runTickOnce(opts = {}) {
|
|
186
|
+
// Load the standardized secrets file into the environment for any key
|
|
187
|
+
// not already set — covers launchd (bare env), systemd, and manual
|
|
188
|
+
// cron uniformly. Explicit env always wins.
|
|
189
|
+
loadEnvFileIntoProcess(opts.envFile);
|
|
190
|
+
// The CLI is the HOST: it reads its environment here, once (after the
|
|
191
|
+
// secrets file has been folded in), and injects the values into the
|
|
192
|
+
// environment-pure core library.
|
|
193
|
+
const env = readEnvCredentials();
|
|
194
|
+
const dbPath = opts.db ?? defaultDbPath();
|
|
195
|
+
const adapters = buildAdapters(env);
|
|
196
|
+
if (Object.keys(adapters).length === 0) {
|
|
197
|
+
// Loud warning when pending provider tasks exist but no adapter is
|
|
198
|
+
// configured (no ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY /
|
|
199
|
+
// OLLAMA_HOST).
|
|
200
|
+
const missing = missingProviderKeysForPending(dbPath);
|
|
201
|
+
const detail = missing.length > 0
|
|
202
|
+
? `; pending tasks need ${missing.join(" / ")} — set them in ~/.config/tidegap/env`
|
|
203
|
+
: "";
|
|
204
|
+
return {
|
|
205
|
+
exitCode: 0,
|
|
206
|
+
message: `tick: no adapters configured (set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY, or OLLAMA_HOST for local); nothing to dispatch${detail}`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Even with some keys present, warn about pending tasks needing a key
|
|
210
|
+
// we do not have (e.g. OpenAI tasks queued but only ANTHROPIC set).
|
|
211
|
+
const missingForPending = missingProviderKeysForPending(dbPath);
|
|
212
|
+
// Aggregate carbon-budget alerts (ROADMAP item 4): read the local budget
|
|
213
|
+
// config (env wins over ~/.tidegap/config) and log any crossing prominently.
|
|
214
|
+
// Core no longer reads the environment, so the two TIDEGAP_CARBON_BUDGET_*
|
|
215
|
+
// variables are read here and passed in — same precedence as before.
|
|
216
|
+
const carbonBudget = loadCarbonBudgetConfig({
|
|
217
|
+
path: opts.budgetConfig,
|
|
218
|
+
env: env.carbonBudget,
|
|
219
|
+
});
|
|
220
|
+
const firedAlerts = [];
|
|
221
|
+
const quotaConfig = loadQuotaResetConfig(opts.budgetConfig !== undefined ? { path: opts.budgetConfig } : {});
|
|
222
|
+
const scheduler = new Scheduler({
|
|
223
|
+
dbPath,
|
|
224
|
+
// A quota_reset series rolls to the label's next reset (ui v2).
|
|
225
|
+
quotaConfig: () => quotaConfig,
|
|
226
|
+
defaultRegion: resolveRegion(undefined, opts.region).region,
|
|
227
|
+
// Same feed the MCP server uses, so the receipt's dispatch-time
|
|
228
|
+
// intensity comes from the region's live feed (mock only as fallback).
|
|
229
|
+
feed: buildDefaultGridFeed(env.grid),
|
|
230
|
+
...(carbonBudget
|
|
231
|
+
? {
|
|
232
|
+
carbonBudget,
|
|
233
|
+
onCarbonAlert: (a) => {
|
|
234
|
+
firedAlerts.push(a);
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
: {}),
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
// Shared pre-tick step (same as the OpenClaw loop): record last_tick_at
|
|
241
|
+
// and re-resolve quota_reset tasks whose weekly reset already passed, so
|
|
242
|
+
// they are rescheduled through the window picker, not dispatched late.
|
|
243
|
+
const preTick = await prepareTick(scheduler, { quotaConfig });
|
|
244
|
+
const result = await scheduler.tick(adapters);
|
|
245
|
+
if (opts.notify !== false && !env.noNotify && !notifyDisabledInConfig(opts.budgetConfig)) {
|
|
246
|
+
notifyFinished(dbPath, result.results, opts);
|
|
247
|
+
}
|
|
248
|
+
let msg = `tick: ${result.inspected} inspected, ${result.dispatched} dispatched, ${result.failed} failed`;
|
|
249
|
+
for (const line of preTick)
|
|
250
|
+
msg += `\n${line}`;
|
|
251
|
+
for (const a of firedAlerts) {
|
|
252
|
+
msg +=
|
|
253
|
+
`\n!! CARBON BUDGET ALERT: ${a.windowKind} carbon budget crossed — ` +
|
|
254
|
+
`${a.actualG} gCO2e used this window vs ${a.thresholdG} g threshold ` +
|
|
255
|
+
`(crossed by task ${a.taskIdThatCrossed}). ` +
|
|
256
|
+
`Window started ${a.windowStart}.`;
|
|
257
|
+
}
|
|
258
|
+
if (missingForPending.length > 0) {
|
|
259
|
+
msg +=
|
|
260
|
+
`\n!! WARNING: pending tasks need ${missingForPending.join(" / ")} ` +
|
|
261
|
+
`but that key is not set — those tasks cannot dispatch. ` +
|
|
262
|
+
`Add it to ~/.config/tidegap/env.`;
|
|
263
|
+
}
|
|
264
|
+
return { exitCode: result.failed > 0 ? 1 : 0, message: msg };
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
scheduler.shutdown();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export async function runTick(opts = {}) {
|
|
271
|
+
if (opts.daemon) {
|
|
272
|
+
const interval = Math.max(1, opts.interval ?? 60);
|
|
273
|
+
// eslint-disable-next-line no-console
|
|
274
|
+
console.log(`[tidegap] tick daemon: every ${interval}s, db=${opts.db ?? defaultDbPath()}`);
|
|
275
|
+
let stopped = false;
|
|
276
|
+
const onSignal = () => {
|
|
277
|
+
stopped = true;
|
|
278
|
+
};
|
|
279
|
+
process.on("SIGINT", onSignal);
|
|
280
|
+
process.on("SIGTERM", onSignal);
|
|
281
|
+
while (!stopped) {
|
|
282
|
+
const result = await runTickOnce(opts);
|
|
283
|
+
// eslint-disable-next-line no-console
|
|
284
|
+
console.log(result.message);
|
|
285
|
+
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
286
|
+
}
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
const result = await runTickOnce(opts);
|
|
290
|
+
// eslint-disable-next-line no-console
|
|
291
|
+
console.log(result.message);
|
|
292
|
+
return result.exitCode;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=tick.js.map
|