meksus 0.3.1 → 0.4.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 +5 -2
- package/bin/meksus.mjs +7 -2
- package/package.json +1 -1
- package/src/cards.mjs +132 -0
package/README.md
CHANGED
|
@@ -38,7 +38,9 @@ Start a new Claude Code session after installing the hooks. It then appears on t
|
|
|
38
38
|
## Check
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
meksus status # who you're signed in as,
|
|
41
|
+
meksus status # who you're signed in as, this repo's project, then three cards:
|
|
42
|
+
# AI assistants, services & spending, problems to fix (--all: every project)
|
|
43
|
+
meksus ack 3f2a # acknowledge a problem (the id's first characters are enough); resolve works the same
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
## What is sent
|
|
@@ -56,7 +58,8 @@ The server scrubs anything that looks like a secret.
|
|
|
56
58
|
| Command | What it does |
|
|
57
59
|
|---|---|
|
|
58
60
|
| `meksus login` / `logout` | Sign in with GitHub, or sign out |
|
|
59
|
-
| `meksus status` | Account
|
|
61
|
+
| `meksus status [--all] [--json]` | Account and this repo's project, then the dashboard's three cards: AI assistants, services & spending, problems to fix |
|
|
62
|
+
| `meksus ack <id>` · `meksus resolve <id>` | Acknowledge or resolve a problem from the terminal; the Discord card updates for everyone |
|
|
60
63
|
| `meksus project list` | Your organisations, Personal, and their projects |
|
|
61
64
|
| `meksus project create <name> [--in <parent>] [--use]` | Create a project (owners and admins) |
|
|
62
65
|
| `meksus project use <owner/project>` | Write this repo's `.meksus` |
|
package/bin/meksus.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { login, logout } from "../src/auth.mjs";
|
|
|
5
5
|
import { install, runHook, uninstall } from "../src/hooks.mjs";
|
|
6
6
|
import { apiBase, dir, readConfig } from "../src/config.mjs";
|
|
7
7
|
import { projectCommand, readProjectFile } from "../src/project.mjs";
|
|
8
|
+
import { incidentCommand, printCards } from "../src/cards.mjs";
|
|
8
9
|
import { flush } from "../src/spool.mjs";
|
|
9
10
|
import { taskLinesCommand } from "../src/tasks.mjs";
|
|
10
11
|
import { VERSION } from "../src/version.mjs";
|
|
@@ -14,7 +15,8 @@ const HELP = `M.E.K.S.U.S. CLI ${VERSION}
|
|
|
14
15
|
|
|
15
16
|
meksus login [--no-browser] Sign in with GitHub (your account, not this machine)
|
|
16
17
|
meksus logout End this sign-in
|
|
17
|
-
meksus status
|
|
18
|
+
meksus status [--all] [--json] Who you are, then AI assistants, services & spend, and problems
|
|
19
|
+
meksus ack <id> | meksus resolve <id> Acknowledge or resolve a problem (id from status; a prefix is enough)
|
|
18
20
|
meksus project list Your organisations, Personal, and their projects
|
|
19
21
|
meksus project create <name> [--in <parent>] [--slug <slug>] [--use]
|
|
20
22
|
meksus project use <owner/project> Write this repo's .meksus file (commit it)
|
|
@@ -36,6 +38,8 @@ switch (command) {
|
|
|
36
38
|
case "login": code = await login({ noBrowser: flag("--no-browser") }); break;
|
|
37
39
|
case "logout": code = await logout(); break;
|
|
38
40
|
case "status": code = await status(); break;
|
|
41
|
+
case "ack": case "acknowledge": code = await incidentCommand("acknowledge", rest[0]); break;
|
|
42
|
+
case "resolve": code = await incidentCommand("resolve", rest[0]); break;
|
|
39
43
|
case "project": case "projects": code = (await requireAccess()) ? await projectCommand(rest) : 1; break;
|
|
40
44
|
case "hooks":
|
|
41
45
|
if (rest[0] === "install") code = (await requireAccess()) ? install() : 1;
|
|
@@ -64,5 +68,6 @@ async function status() {
|
|
|
64
68
|
}
|
|
65
69
|
console.log(project ? `This repo reports to ${project.name ?? project.id} (${project.file})` : "This repo has no .meksus file: nothing here is reported. `meksus project use <owner/project>` adds one.");
|
|
66
70
|
console.log(`API ${apiBase(c)}`);
|
|
67
|
-
|
|
71
|
+
if (!c?.session) return 1;
|
|
72
|
+
return printCards({ json: flag("--json"), all: flag("--all") });
|
|
68
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meksus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "M.E.K.S.U.S. CLI: see what your AI coding agents (Claude Code) and builds are doing, live, and get Discord alerts when they fail or need you. Status only: code, prompts and output never leave your machine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"meksus",
|
package/src/cards.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// `meksus status` (the dashboard's three cards in the terminal) and `meksus ack|resolve <id>` (CLI-3/4).
|
|
2
|
+
// Reads go straight to the database as the signed-in user, so its access rules decide what's shown,
|
|
3
|
+
// exactly as on the dashboard. Actions use the same incident_action as the dashboard (recorded with
|
|
4
|
+
// source "cli"), then ask the ingest Worker to redraw the Discord card for everyone.
|
|
5
|
+
import { getAccessToken } from "./auth.mjs";
|
|
6
|
+
import { apiBase, readConfig } from "./config.mjs";
|
|
7
|
+
import { readProjectFile } from "./project.mjs";
|
|
8
|
+
import { VERSION } from "./version.mjs";
|
|
9
|
+
|
|
10
|
+
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
11
|
+
const paint = (code) => (s) => (color ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
12
|
+
const dim = paint("2"), bold = paint("1"), red = paint("31"), orange = paint("33"), green = paint("32"), cyan = paint("36");
|
|
13
|
+
const SEV = { critical: (s) => red(bold(s)), high: red, medium: orange, low: dim };
|
|
14
|
+
|
|
15
|
+
export const shortId = (id) => id.slice(0, 8);
|
|
16
|
+
|
|
17
|
+
async function db(config, token, path, init = {}) {
|
|
18
|
+
const res = await fetch(`${config.supabase_url.replace(/\/$/, "")}/rest/v1/${path}`, {
|
|
19
|
+
...init,
|
|
20
|
+
headers: { apikey: config.supabase_publishable_key, Authorization: `Bearer ${token}`, "Content-Type": "application/json",
|
|
21
|
+
"User-Agent": `meksus-cli/${VERSION}`, "X-Meksus-Source": "cli", ...(init.headers ?? {}) },
|
|
22
|
+
signal: AbortSignal.timeout(10_000),
|
|
23
|
+
});
|
|
24
|
+
const body = await res.json().catch(() => null);
|
|
25
|
+
return { status: res.status, body };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ago = (iso) => {
|
|
29
|
+
const s = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
|
|
30
|
+
return s < 60 ? `${s}s ago` : s < 3600 ? `${Math.round(s / 60)}m ago` : s < 86400 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`;
|
|
31
|
+
};
|
|
32
|
+
const clip = (s, n) => (s && s.length > n ? `${s.slice(0, n - 1)}…` : s ?? "");
|
|
33
|
+
const usd = (v) => (typeof v === "number" ? `$${v.toFixed(2)}` : "—");
|
|
34
|
+
|
|
35
|
+
/** The three cards' data for one project (the repo's .meksus) or every project the user can see. */
|
|
36
|
+
export async function loadOverview(config, token, projectId) {
|
|
37
|
+
const only = projectId ? `&project_id=eq.${projectId}` : "";
|
|
38
|
+
const [projects, incidents, agents, connectors] = await Promise.all([
|
|
39
|
+
db(config, token, `projects?select=id,name${projectId ? `&id=eq.${projectId}` : ""}&order=name`),
|
|
40
|
+
db(config, token, `incidents?select=id,title,severity,status,occurrences,last_seen_at,project_id,muted&status=neq.resolved${only}&order=last_seen_at.desc&limit=100`),
|
|
41
|
+
db(config, token, `agent_sessions?select=id,label,agent,state,task,last_event_at,project_id&ended_at=is.null${only}&order=last_event_at.desc&limit=20`),
|
|
42
|
+
db(config, token, `connectors?select=provider,status,snapshot,project_id${only}`),
|
|
43
|
+
]);
|
|
44
|
+
for (const r of [projects, incidents, agents, connectors]) if (r.status === 401) return { error: "signed_out" };
|
|
45
|
+
if ([projects, incidents, agents, connectors].some((r) => !Array.isArray(r.body))) return { error: "unavailable" };
|
|
46
|
+
const name = Object.fromEntries(projects.body.map((p) => [p.id, p.name]));
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const live = agents.body.filter((a) => now - Date.parse(a.last_event_at) < 30 * 60_000);
|
|
49
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
50
|
+
const problems = incidents.body.filter((i) => !i.muted)
|
|
51
|
+
.sort((a, b) => (order[a.severity] - order[b.severity]) || Date.parse(b.last_seen_at) - Date.parse(a.last_seen_at));
|
|
52
|
+
return {
|
|
53
|
+
scope: projectId ? name[projectId] ?? "this project" : `${projects.body.length} project${projects.body.length === 1 ? "" : "s"}`,
|
|
54
|
+
agents: live.map((a) => ({ ...a, project: name[a.project_id], quiet: a.state !== "waiting" && now - Date.parse(a.last_event_at) > 5 * 60_000 })),
|
|
55
|
+
spend: connectors.body.map((c) => ({ provider: c.provider, status: c.status, today_usd: c.snapshot?.usage_daily_usd ?? null, remaining_usd: c.snapshot?.remaining_usd ?? null, project: name[c.project_id] })),
|
|
56
|
+
problems: problems.map((i) => ({ ...i, project: name[i.project_id] })),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Plain-text cards (no colour when piped or NO_COLOR is set). */
|
|
61
|
+
export function renderCards(o, { width = 78 } = {}) {
|
|
62
|
+
const out = [];
|
|
63
|
+
const head = (title, note) => { out.push(""); out.push(`${bold(title)}${note ? ` ${dim(note)}` : ""}`); out.push(dim("─".repeat(Math.min(width, 78)))); };
|
|
64
|
+
const waiting = o.agents.filter((a) => a.state === "waiting").length;
|
|
65
|
+
head("AI assistants", o.agents.length ? `${o.agents.length} live · ${waiting} waiting for you` : "none running");
|
|
66
|
+
if (!o.agents.length) out.push(dim(" Claude Code and anything under `meksus watch` show up here while they work."));
|
|
67
|
+
for (const a of o.agents.slice(0, 5)) {
|
|
68
|
+
const state = a.state === "waiting" ? orange("waiting for you") : a.quiet ? orange("gone quiet") : a.state === "working" ? green("working") : a.state;
|
|
69
|
+
out.push(` ${clip(a.label ?? a.agent, 32).padEnd(32)} ${state} ${dim(ago(a.last_event_at))}${a.task ? `\n ${dim("→")} ${clip(a.task, width - 6)}` : ""}`);
|
|
70
|
+
}
|
|
71
|
+
head("Service health & spending", o.spend.length ? `${o.spend.length} connected` : null);
|
|
72
|
+
if (!o.spend.length) out.push(dim(" No services connected yet. Connect one on the dashboard (Integrations / Spend)."));
|
|
73
|
+
for (const s of o.spend) {
|
|
74
|
+
const st = s.status === "ok" ? green("ok") : red(s.status);
|
|
75
|
+
out.push(` ${(s.provider === "openrouter" ? "OpenRouter" : s.provider).padEnd(14)} ${st} ${usd(s.today_usd)} today · ${usd(s.remaining_usd)} left${s.project ? dim(` ${s.project}`) : ""}`);
|
|
76
|
+
}
|
|
77
|
+
const needs = o.problems.filter((p) => p.status === "open").length;
|
|
78
|
+
head("Problems to fix", o.problems.length ? `${o.problems.length} open · ${needs} need a person` : null);
|
|
79
|
+
if (!o.problems.length) out.push(` ${green("All steady.")} ${dim("Nothing needs you.")}`);
|
|
80
|
+
for (const p of o.problems.slice(0, 10)) {
|
|
81
|
+
const sev = (SEV[p.severity] ?? dim)(p.severity.padEnd(8));
|
|
82
|
+
out.push(` ${cyan(shortId(p.id))} ${sev} ${clip(p.title, width - 36)}${p.occurrences > 1 ? dim(` ×${p.occurrences}`) : ""}${p.status === "acknowledged" ? dim(" (ack)") : ""}`);
|
|
83
|
+
out.push(` ${dim(`${p.project ?? ""} · last ${ago(p.last_seen_at)}`)}`);
|
|
84
|
+
}
|
|
85
|
+
if (o.problems.length > 10) out.push(dim(` … and ${o.problems.length - 10} more on the dashboard`));
|
|
86
|
+
if (o.problems.length) out.push(dim("\n meksus ack <id> · meksus resolve <id> (the first characters of the id are enough)"));
|
|
87
|
+
return out.join("\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function session() {
|
|
91
|
+
const config = readConfig();
|
|
92
|
+
if (!config?.session) return { error: "Not signed in. Run `meksus login`." };
|
|
93
|
+
const token = await getAccessToken(config);
|
|
94
|
+
if (!token) return { error: "Your sign-in expired. Run `meksus login`." };
|
|
95
|
+
return { config, token };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Appended to `meksus status` (after who you are and which project this repo reports to). */
|
|
99
|
+
export async function printCards({ json = false, all = false } = {}) {
|
|
100
|
+
const s = await session();
|
|
101
|
+
if (s.error) return 1;
|
|
102
|
+
const project = all ? null : readProjectFile();
|
|
103
|
+
const o = await loadOverview(s.config, s.token, project?.id ?? null);
|
|
104
|
+
if (o.error) { console.error(o.error === "signed_out" ? "Your sign-in expired. Run `meksus login`." : "Couldn't load the cards right now. Try again."); return 1; }
|
|
105
|
+
if (json) { console.log(JSON.stringify(o, null, 2)); return 0; }
|
|
106
|
+
console.log(dim(`\nShowing ${o.scope}${project && !all ? " (this repo's .meksus; --all for every project)" : ""}`));
|
|
107
|
+
console.log(renderCards(o, { width: Math.max(60, Math.min(process.stdout.columns ?? 80, 100)) }));
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `meksus ack <id>` / `meksus resolve <id>`: the id prefix is matched among open problems you can see. */
|
|
112
|
+
export async function incidentCommand(action, ref) {
|
|
113
|
+
if (!ref || !/^[0-9a-f-]{4,36}$/i.test(ref)) { console.error(`Usage: meksus ${action === "acknowledge" ? "ack" : "resolve"} <id> (see \`meksus status\`)`); return 2; }
|
|
114
|
+
const s = await session();
|
|
115
|
+
if (s.error) { console.error(s.error); return 1; }
|
|
116
|
+
const list = await db(s.config, s.token, "incidents?select=id,title,status,project_id&status=neq.resolved&order=last_seen_at.desc&limit=200");
|
|
117
|
+
if (!Array.isArray(list.body)) { console.error("Couldn't load your problems right now. Try again."); return 1; }
|
|
118
|
+
const matches = list.body.filter((i) => i.id.startsWith(ref.toLowerCase()));
|
|
119
|
+
if (!matches.length) { console.error(`No open problem starts with ${ref}. \`meksus status --all\` lists them.`); return 1; }
|
|
120
|
+
if (matches.length > 1) { console.error(`${ref} matches ${matches.length} problems; type more of the id:\n${matches.map((m) => ` ${m.id} ${m.title}`).join("\n")}`); return 1; }
|
|
121
|
+
const i = matches[0];
|
|
122
|
+
const r = await db(s.config, s.token, "rpc/incident_action", { method: "POST", body: JSON.stringify({ p_incident: i.id, p_action: action }) });
|
|
123
|
+
if (r.status >= 300 || !r.body?.ok) { console.error(`Not done: ${r.body?.error ?? r.body?.message ?? `HTTP ${r.status}`}.`); return 1; }
|
|
124
|
+
// Redraw the Discord card(s) for everyone, like the dashboard does. Best effort.
|
|
125
|
+
let synced = false;
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(`${apiBase(s.config)}/v1/incidents/${i.id}/discord-sync`, { method: "POST", headers: { Authorization: `Bearer ${s.token}`, "User-Agent": `meksus-cli/${VERSION}` }, signal: AbortSignal.timeout(10_000) });
|
|
128
|
+
synced = res.ok && (await res.json().catch(() => null))?.synced !== false;
|
|
129
|
+
} catch { /* the next action redraws it */ }
|
|
130
|
+
console.log(`${green("✔")} ${action === "acknowledge" ? "Acknowledged" : "Resolved"}: ${i.title}${synced ? dim(" (Discord card updated)") : ""}`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|