meksus 0.3.0 → 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/auth.mjs +5 -6
- package/src/cards.mjs +132 -0
- package/src/hooks.mjs +23 -2
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/auth.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// typed, pasted or shown:
|
|
5
5
|
// 1. ask the ingest Worker where Supabase lives (/v1/cli/config: public values only)
|
|
6
6
|
// 2. listen on http://127.0.0.1:<random port> for one callback
|
|
7
|
-
// 3. open <supabase>/auth/v1/authorize
|
|
8
|
-
//
|
|
7
|
+
// 3. open https://<app>/cli-callback?challenge=…, which forwards to <supabase>/auth/v1/authorize (GitHub,
|
|
8
|
+
// PKCE). The browser comes back to the same static page, which forwards ?code= to the local listener. The code is
|
|
9
9
|
// useless without this process's verifier, which never leaves it.
|
|
10
10
|
// 4. exchange code + verifier for a session; keep it in config.json (0600)
|
|
11
11
|
// Access tokens last an hour; getAccessToken() refreshes them (rotating refresh token) when needed.
|
|
@@ -55,10 +55,9 @@ export async function login({ noBrowser = false } = {}) {
|
|
|
55
55
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
56
56
|
const port = server.address().port;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})}`;
|
|
58
|
+
// The browser starts at the app's own /cli-callback page, which forwards to GitHub sign-in (through
|
|
59
|
+
// Supabase Auth) and later hands the code back here. Only public values travel: port, state, challenge.
|
|
60
|
+
const authorize = `${cfg.app_url.replace(/\/$/, "")}/cli-callback?${new URLSearchParams({ port: String(port), state, challenge })}`;
|
|
62
61
|
|
|
63
62
|
console.log("\n Sign in to M.E.K.S.U.S. with GitHub\n");
|
|
64
63
|
console.log(` Open: ${authorize}\n`);
|
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
|
+
}
|
package/src/hooks.mjs
CHANGED
|
@@ -132,9 +132,29 @@ function settingsPath(cwd) {
|
|
|
132
132
|
return join(d, ".claude", "settings.local.json");
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// Reads the settings file, or explains (and changes nothing) when it isn't valid JSON — often two
|
|
136
|
+
// { … } blocks pasted one after the other, which Claude Code can't read either.
|
|
137
|
+
function readSettings(file) {
|
|
138
|
+
if (!existsSync(file)) return {};
|
|
139
|
+
const text = readFileSync(file, "utf8");
|
|
140
|
+
try {
|
|
141
|
+
const value = JSON.parse(text);
|
|
142
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const at = /line (\d+)/.exec(error.message)?.[1];
|
|
145
|
+
console.error(`✖ ${file} isn't valid JSON${at ? ` (line ${at})` : ""}, so nothing was changed.`);
|
|
146
|
+
if (/after JSON/.test(error.message)) console.error(" It holds more than one { … } block. Merge them into one object, then run this again.");
|
|
147
|
+
console.error(" Claude Code can't read this file either until it's fixed.");
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
console.error(`✖ ${file} should hold one JSON object, so nothing was changed.`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
export function install(cwd = process.cwd()) {
|
|
136
155
|
const file = settingsPath(cwd);
|
|
137
|
-
const settings =
|
|
156
|
+
const settings = readSettings(file);
|
|
157
|
+
if (!settings) return 1;
|
|
138
158
|
settings.hooks ??= {};
|
|
139
159
|
const command = `node "${CLI}" hook`;
|
|
140
160
|
for (const event of EVENTS) {
|
|
@@ -155,7 +175,8 @@ export function install(cwd = process.cwd()) {
|
|
|
155
175
|
export function uninstall(cwd = process.cwd()) {
|
|
156
176
|
const file = settingsPath(cwd);
|
|
157
177
|
if (!existsSync(file)) { console.log("No hooks file here."); return 0; }
|
|
158
|
-
const settings =
|
|
178
|
+
const settings = readSettings(file);
|
|
179
|
+
if (!settings) return 1;
|
|
159
180
|
for (const event of Object.keys(settings.hooks ?? {})) {
|
|
160
181
|
settings.hooks[event] = settings.hooks[event]
|
|
161
182
|
.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !String(h.command).includes(MARK)) }))
|