scribeclock 0.1.3 → 0.1.4

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 CHANGED
@@ -50,7 +50,7 @@ Run `scribeclock --help` for the full list and `scribeclock <command> --help` fo
50
50
  | Cloudflare | `cf accounts\|zones\|dns\|dns set\|dns rm\|dns diff\|pages\|deployments\|deploy-status\|workers\|purge\|request` |
51
51
  | Team | `team` · `overlap [files…]` (exit 4 on overlap) · `team report` · `handoff [record]` |
52
52
  | Git | `branch KEY [--create]` · `commit -m "…"` · `pr KEY` · `changelog --since <tag>` |
53
- | Bug reports | `connect [--env] [--vault] [--create]` · `issues [show\|resolve --release X\|ignore\|reopen\|task]` · `bug "title" [-d …] [--log file] [--stack file]` · `ingest keys\|revoke\|test\|snippets` · `health [--days N]` |
53
+ | Bug reports | `connect [--env] [--vault] [--create]` · `issues [show\|resolve --release X\|ignore\|reopen\|task\|urgency]` · `analytics [--days N] [--report X] [--csv]` · `ingest settings [--immediate critical --per-hour 2 --every 120]` · `bug "title" [-d …] [--log file] [--stack file]` · `ingest keys\|revoke\|test\|snippets\|forward [provider]` · `health [--days N]` |
54
54
  | Ops & agents | `doctor` · `watch tasks [--on-change cmd]` · `serve --stdio` · `mcp-config <client> [--install]` · `ask "…"` |
55
55
  | Raw tools | `tools` · `call <tool> [--arg v \| --args '{json}']` · `<tool_name> …` · `refresh` |
56
56
 
@@ -77,6 +77,7 @@ scribeclock issues show 3f2a9c1b # stack trace, breadcrumbs, trace logs, conte
77
77
  scribeclock bug "Checkout button does nothing" -d "Click Pay → nothing" --log logs/app.log
78
78
  scribeclock issues resolve 3f2a9c1b --release 1.4.3 # a later hit in another release reopens it as a regression
79
79
  scribeclock health --days 30
80
+ scribeclock ingest forward sentry # webhook URL + steps to forward Sentry / PostHog / Bugsnag / Rollbar / Crashlytics issues here
80
81
  ```
81
82
 
82
83
  ```bash
@@ -120,6 +120,48 @@ sys.excepthook = lambda t, v, tb: report(v, level="fatal")`,
120
120
  },
121
121
  ];
122
122
  }
123
+ /** Forward webhooks from an existing bug tracker (mirrors supabase/functions/_shared/ingestProviders.ts). */
124
+ export function providerSetups(key) {
125
+ const u = (p) => `${INGEST_URL}/${p}?key=${key}`;
126
+ return [
127
+ { id: "sentry", label: "Sentry", url: u("sentry"), steps: ["Alerts → Create Alert Rule → action “Send a notification via a webhook”, or Settings → Developer Settings → Internal Integrations → Webhook URL (issue + error webhooks on).", "Paste the URL. For an internal integration, store its Client Secret on the key: scribeclock update_ingest_key --key-id <id> --webhook-secret <secret>", "Each Sentry issue becomes one ScribeClock issue; the task links back to Sentry."] },
128
+ { id: "posthog", label: "PostHog", url: u("posthog"), steps: ["Data pipelines → Destinations → Webhook; keep the default body ({ event, person }).", "Filter on the $exception event for error tracking; other events are counted as analytics.", "Optional: header X-ScribeClock-Secret matching the key's webhook secret."] },
129
+ { id: "bugsnag", label: "Bugsnag", url: u("bugsnag"), steps: ["Project Settings → Integrations → Data forwarding → Webhook.", "Paste the URL and pick the triggers to forward (new, reopened, spikes).", "Optional shared secret: add ?secret=… to the URL and set the same value on the key."] },
130
+ { id: "rollbar", label: "Rollbar", url: u("rollbar"), steps: ["Project → Settings → Notifications → Webhook.", "Paste the URL; enable new item / reactivated item / high occurrence rules.", "Optional shared secret: ?secret=… on the URL."] },
131
+ { id: "crashlytics", label: "Firebase Crashlytics", url: u("crashlytics"), steps: ["Crashlytics has no outgoing webhook: add a Cloud Function on the Firebase Alerts trigger (onNewFatalIssuePublished, onRegressionAlertPublished, onNewNonfatalIssuePublished).", "POST { alertType, appId, payload } from the alert to this URL — snippet: https://scribeclock.com/help/bug-reports#forward", "Each Crashlytics issue becomes one ScribeClock issue with a link to the Firebase console."] },
132
+ { id: "webhook", label: "Anything else", url: u("webhook"), steps: ["Point the tool's webhook at this URL with a JSON body.", "Common fields are picked up: message / title, stack, level / severity, url, user, environment, release, id (grouping), url (link back); the whole payload is kept under extra.", "When you control the body, send the native event shape instead (scribeclock ingest snippets)."] },
133
+ ];
134
+ }
135
+ export const ingestForward = cmd({
136
+ path: "ingest forward",
137
+ group: "Bug reports",
138
+ summary: "Webhook URLs and steps to forward Sentry, PostHog, Bugsnag, Rollbar, Crashlytics or any tool's reports into this project.",
139
+ description: "Prints the per-provider URL (the ingest key rides in it, since most tools cannot set headers) and where to paste it. Each issue in the other tool becomes one ScribeClock issue and task that links back. Set a webhook secret on the key (update_ingest_key --webhook-secret) to verify Sentry signatures or a shared secret the tool echoes.",
140
+ tool: ["list_ingest_keys"],
141
+ positionals: [{ name: "provider", description: "sentry, posthog, bugsnag, rollbar, crashlytics or webhook (default: all)." }],
142
+ flags: [{ name: "key", alias: "k", type: "string", description: `Ingest key to embed (default: ${KEY_ENV} from the environment / .env).` }],
143
+ examples: ["scribeclock ingest forward", "scribeclock ingest forward sentry", "scribeclock ingest forward posthog --json | jq -r .providers[0].url"],
144
+ async run(ctx, inv) {
145
+ const key = ingestKeyFor(ctx, str(inv.flags.key)) ?? "sc_ing_<your-key>";
146
+ const want = inv.positionals[0]?.toLowerCase();
147
+ const providers = providerSetups(key).filter((p) => !want || p.id === want);
148
+ if (!providers.length)
149
+ throw usageError(`Unknown provider "${want}".`, "One of: sentry, posthog, bugsnag, rollbar, crashlytics, webhook.");
150
+ if (ctx.flags.json) {
151
+ ctx.out.jsonOut({ ok: true, providers });
152
+ return EXIT.OK;
153
+ }
154
+ for (const p of providers) {
155
+ ctx.out.log(ctx.out.bold(`── ${p.label}`));
156
+ ctx.out.log(` ${p.url}`);
157
+ p.steps.forEach((s, i) => ctx.out.log(` ${i + 1}. ${s}`));
158
+ ctx.out.log("");
159
+ }
160
+ if (key.startsWith("sc_ing_<"))
161
+ ctx.out.info(ctx.out.dim(`No ingest key found: run \`scribeclock connect --env\` first, or pass --key.`));
162
+ return EXIT.OK;
163
+ },
164
+ });
123
165
  // ───────────────────────────── connect ─────────────────────────────
124
166
  export const connectCommand = cmd({
125
167
  path: "connect",
@@ -226,6 +268,18 @@ function levelPaint(ctx, level) {
226
268
  return ctx.out.yellow("warning");
227
269
  return ctx.out.dim(l);
228
270
  }
271
+ function urgencyPaint(ctx, u) {
272
+ const s = String(u ?? "");
273
+ if (s === "critical")
274
+ return ctx.out.red(ctx.out.bold("critical"));
275
+ if (s === "high")
276
+ return ctx.out.red("high");
277
+ if (s === "medium")
278
+ return ctx.out.yellow("medium");
279
+ if (s === "low")
280
+ return ctx.out.dim("low");
281
+ return s;
282
+ }
229
283
  function issueStatusPaint(ctx, status) {
230
284
  const s = String(status ?? "");
231
285
  if (s === "open")
@@ -255,12 +309,13 @@ export const issuesList = cmd({
255
309
  PROJECT_FLAG,
256
310
  REPO_FLAG,
257
311
  { name: "status", alias: "s", type: "string", choices: ["open", "resolved", "ignored", "all"], description: "Default open." },
312
+ { name: "urgency", alias: "u", type: "string", choices: ["critical", "high", "medium", "low"], description: "Only this urgency." },
258
313
  { name: "level", alias: "l", type: "string", choices: ["fatal", "error", "warning", "info"], description: "Only this level." },
259
314
  { name: "kind", alias: "k", type: "string", choices: ["error", "bug_report", "message"], description: "Only this kind." },
260
315
  { name: "environment", alias: "e", type: "string", description: "Only issues seen in this environment." },
261
316
  { name: "release", type: "string", description: "Only issues seen in this release." },
262
317
  { name: "since", type: "string", placeholder: "<date>", description: "Only issues seen since (YYYY-MM-DD or ISO)." },
263
- { name: "order", type: "string", choices: ["last_seen", "first_seen", "count", "users", "level"], description: "Sort (default last_seen)." },
318
+ { name: "order", type: "string", choices: ["urgency", "last_seen", "first_seen", "count", "users", "level"], description: "Sort (default urgency, then last seen)." },
264
319
  { name: "limit", alias: "n", type: "number", description: "Max issues (default 50)." },
265
320
  { name: "ids", type: "boolean", description: "Print short ids only." },
266
321
  ],
@@ -269,6 +324,7 @@ export const issuesList = cmd({
269
324
  const data = await ctx.call("list_issues", {
270
325
  ...issueTargetArgs(ctx, inv),
271
326
  status: str(inv.flags.status),
327
+ urgency: str(inv.flags.urgency),
272
328
  level: str(inv.flags.level),
273
329
  kind: str(inv.flags.kind),
274
330
  environment: str(inv.flags.environment),
@@ -292,8 +348,9 @@ export const issuesList = cmd({
292
348
  ctx.out.info(ctx.out.dim(String(data.hint)));
293
349
  return;
294
350
  }
295
- ctx.out.table(["id", "level", "title", "where", "count", "users", "last seen", "status", "task"], issues.map((i) => [i.short_id, levelPaint(ctx, i.level), i.title, i.culprit ?? "", i.event_count, i.user_count, relTime(String(i.last_seen_at ?? ""), ctx.now()), issueStatusPaint(ctx, i.status), i.task?.key ?? ""]), { maxWidth: 48 });
296
- ctx.out.info(ctx.out.dim(`${String(data.total ?? issues.length)} issue${Number(data.total ?? issues.length) === 1 ? "" : "s"} · ${String(summary.open ?? 0)} open · ${String(summary.new_24h ?? 0)} new in 24 h · ${String(summary.events_24h ?? 0)} events in 24 h${Number(summary.regressed_7d ?? 0) ? ` · ${String(summary.regressed_7d)} regressed this week` : ""}`));
351
+ ctx.out.table(["id", "urgency", "level", "title", "where", "count", "users", "last seen", "status", "task"], issues.map((i) => [i.short_id, urgencyPaint(ctx, i.urgency), levelPaint(ctx, i.level), i.title, i.culprit ?? "", i.event_count, i.user_count, relTime(String(i.last_seen_at ?? ""), ctx.now()), issueStatusPaint(ctx, i.status), i.task?.key ?? ""]), { maxWidth: 44 });
352
+ const u = (summary.by_urgency ?? {});
353
+ ctx.out.info(ctx.out.dim(`${String(data.total ?? issues.length)} issue${Number(data.total ?? issues.length) === 1 ? "" : "s"} · ${String(summary.open ?? 0)} open (${String(u.critical ?? 0)} critical, ${String(u.high ?? 0)} high) · ${String(summary.new_24h ?? 0)} new in 24 h · ${String(summary.events_24h ?? 0)} events in 24 h${Number(summary.regressed_7d ?? 0) ? ` · ${String(summary.regressed_7d)} regressed this week` : ""}`));
297
354
  });
298
355
  },
299
356
  });
@@ -318,8 +375,10 @@ export const issuesShow = cmd({
318
375
  const task = i.task;
319
376
  out.kv([
320
377
  ["status", issueStatusPaint(ctx, i.status)],
378
+ ["urgency", `${urgencyPaint(ctx, i.urgency)}${i.urgency_pinned ? out.dim(" (pinned)") : ""}`],
321
379
  ["level", levelPaint(ctx, i.level)],
322
380
  ["kind", i.kind],
381
+ ["bundled", Number(i.bundled_fingerprints ?? 1) > 1 ? `${String(i.bundled_fingerprints)} similar fingerprints bundled` : ""],
323
382
  ["where", i.culprit],
324
383
  ["project", i.project?.name],
325
384
  ["repo", i.repo],
@@ -768,4 +827,204 @@ export const healthCommand = cmd({
768
827
  });
769
828
  },
770
829
  });
771
- export const INGEST_COMMANDS = [connectCommand, issuesList, issuesShow, issuesResolve, issuesIgnore, issuesReopen, issuesTask, bugCommand, ingestKeys, ingestRevoke, ingestTest, ingestSnippetsCommand, healthCommand];
830
+ export const issuesUrgency = cmd({
831
+ path: "issues urgency",
832
+ group: "Bug reports",
833
+ summary: "Pin an issue's urgency (critical, high, medium, low) or set it back to auto; the task priority follows.",
834
+ tool: "update_issue",
835
+ positionals: [{ name: "issue", required: true, description: "Issue id or task key." }, { name: "urgency", required: true, description: "critical | high | medium | low | auto" }],
836
+ examples: ["scribeclock issues urgency 3f2a9c1b critical", "scribeclock issues urgency '#731' auto"],
837
+ async run(ctx, inv) {
838
+ const ref = requirePositional(inv, 0, "An issue id", "scribeclock issues urgency 3f2a9c1b high");
839
+ const urgency = requirePositional(inv, 1, "An urgency", "scribeclock issues urgency 3f2a9c1b high").toLowerCase();
840
+ if (!["critical", "high", "medium", "low", "auto"].includes(urgency))
841
+ throw usageError(`Unknown urgency "${urgency}".`, "One of: critical, high, medium, low, auto.");
842
+ const data = await ctx.call("update_issue", { issue_id: /^#?\d+$/.test(ref) ? taskKey(ctx, ref) : ref, urgency });
843
+ return emit(ctx, data, () => {
844
+ const i = (data.issue ?? {});
845
+ ctx.out.log(`${ctx.out.green("✓")} ${String(i.short_id ?? ref)} urgency ${urgencyPaint(ctx, i.urgency)}${i.urgency_pinned ? "" : ctx.out.dim(" (auto)")} — ${String(i.title ?? "")}`);
846
+ });
847
+ },
848
+ });
849
+ export const ingestSettings = cmd({
850
+ path: "ingest settings",
851
+ group: "Bug reports",
852
+ summary: "Show or change how bug reports notify people: immediate emails by urgency, hourly cap, digest cadence, extra recipients, similarity.",
853
+ tool: "ingest_settings",
854
+ flags: [
855
+ PROJECT_FLAG,
856
+ { name: "immediate", type: "string", choices: ["critical", "high", "medium", "low", "never"], description: "Email right away at or above this urgency (default high)." },
857
+ { name: "per-hour", type: "number", description: "At most this many immediate emails per hour (default 3); the rest wait for the digest." },
858
+ { name: "digest", type: "string", choices: ["on", "off"], description: "Bundle everything else into one digest email." },
859
+ { name: "every", type: "number", placeholder: "<minutes>", description: "Digest cadence in minutes (15–1440, default 60)." },
860
+ { name: "emails", type: "array", placeholder: "<a@x,b@y>", description: "Extra recipients besides the organization's task managers." },
861
+ { name: "similarity", type: "number", description: "0.3–1.0: how alike two report titles must be to bundle into one issue (default 0.70)." },
862
+ ],
863
+ examples: ["scribeclock ingest settings", "scribeclock ingest settings --immediate critical --per-hour 2 --every 120", "scribeclock ingest settings --digest off --emails oncall@acme.com"],
864
+ async run(ctx, inv) {
865
+ const project_id = projectArg(ctx, inv, { required: true });
866
+ const args = { project_id };
867
+ if (str(inv.flags.immediate))
868
+ args.immediate_min_urgency = inv.flags.immediate;
869
+ if (num(inv.flags["per-hour"]) !== undefined)
870
+ args.immediate_per_hour = inv.flags["per-hour"];
871
+ if (str(inv.flags.digest))
872
+ args.digest_enabled = inv.flags.digest === "on";
873
+ if (num(inv.flags.every) !== undefined)
874
+ args.digest_interval_minutes = inv.flags.every;
875
+ if (list(inv.flags.emails))
876
+ args.notify_emails = list(inv.flags.emails);
877
+ if (num(inv.flags.similarity) !== undefined)
878
+ args.similarity_threshold = inv.flags.similarity;
879
+ const data = await ctx.call("ingest_settings", args);
880
+ return emit(ctx, data, () => {
881
+ const sset = (data.settings ?? {});
882
+ ctx.out.log(ctx.out.bold(`Bug report notifications${data.changed ? ctx.out.green(" (updated)") : ""}`));
883
+ ctx.out.kv([
884
+ ["email immediately", sset.immediate_min_urgency === "never" ? "never" : `${String(sset.immediate_min_urgency)} and above, at most ${String(sset.immediate_per_hour)} per hour`],
885
+ ["digest", sset.digest_enabled ? `every ${String(sset.digest_interval_minutes)} min` : "off"],
886
+ ["extra recipients", sset.notify_emails?.join(", ")],
887
+ ["similarity", String(sset.similarity_threshold)],
888
+ ["pending for digest", data.pending_digest_items],
889
+ ["immediate emails (24 h)", data.immediate_emails_24h],
890
+ ]);
891
+ const recent = (data.recent ?? []);
892
+ if (recent.length) {
893
+ ctx.out.log("");
894
+ ctx.out.log(ctx.out.bold("Recent notifications"));
895
+ ctx.out.table(["when", "kind", "urgency", "mode", "title"], recent.slice(0, 10).map((n) => [relTime(String(n.created_at ?? ""), ctx.now()), n.kind, urgencyPaint(ctx, n.urgency), n.sent_at ? String(n.mode) : `${String(n.mode)} (pending)`, n.title]), { maxWidth: 56, indent: 2 });
896
+ }
897
+ });
898
+ },
899
+ });
900
+ function pct(v) {
901
+ return v === null || v === undefined ? "–" : `${String(v)}%`;
902
+ }
903
+ function delta(cur, prev) {
904
+ const c = Number(cur ?? 0);
905
+ const p = Number(prev ?? 0);
906
+ if (!p)
907
+ return c ? "new" : "–";
908
+ const d = Math.round(((c - p) / p) * 100);
909
+ return `${d >= 0 ? "+" : ""}${d}%`;
910
+ }
911
+ export const analyticsCommand = cmd({
912
+ path: "analytics",
913
+ group: "Bug reports",
914
+ summary: "Usage and stability report: active users, sessions, retention, crash-free rates, top screens and clicks, releases, issues by urgency.",
915
+ description: "Everything behind the project's Analytics page, as tables (or --json for the raw series to chart or feed into a report). Pass --report to print one section only; --csv writes the daily series as CSV.",
916
+ tool: "project_analytics",
917
+ flags: [
918
+ PROJECT_FLAG,
919
+ REPO_FLAG,
920
+ { name: "days", alias: "d", type: "number", description: "Window in days (default 30, max 180)." },
921
+ { name: "environment", alias: "e", type: "string", description: "Only this environment." },
922
+ { name: "report", type: "string", choices: ["overview", "daily", "retention", "screens", "clicks", "events", "breakdowns", "releases", "issues", "hourly"], description: "One section only." },
923
+ { name: "csv", type: "boolean", description: "Print the daily series as CSV." },
924
+ ],
925
+ examples: ["scribeclock analytics", "scribeclock analytics --days 7 --report releases", "scribeclock analytics --days 90 --csv > usage.csv", "scribeclock analytics --json | jq .totals"],
926
+ async run(ctx, inv) {
927
+ const data = await ctx.call("project_analytics", { ...issueTargetArgs(ctx, inv), days: num(inv.flags.days), environment: str(inv.flags.environment) });
928
+ const daily = (data.daily ?? []);
929
+ if (inv.flags.csv) {
930
+ const cols = ["day", "users", "new_users", "sessions", "avg_session_minutes", "pageviews", "clicks", "events", "errors", "crashes", "warnings", "bug_reports", "issues_opened", "issues_resolved", "crash_free_sessions_pct", "crash_free_users_pct"];
931
+ ctx.out.log(cols.join(","));
932
+ for (const d of daily)
933
+ ctx.out.log(cols.map((c) => String(d[c] ?? "")).join(","));
934
+ return EXIT.OK;
935
+ }
936
+ return emit(ctx, data, () => {
937
+ const out = ctx.out;
938
+ const t = (data.totals ?? {});
939
+ const prev = (data.previous ?? {});
940
+ const report = str(inv.flags.report) ?? "all";
941
+ const show = (name) => report === "all" || report === name;
942
+ const p = (data.project ?? {});
943
+ out.log(`${out.bold(String(p.name ?? ""))} · last ${String(data.days)} days${data.environment ? ` · ${String(data.environment)}` : ""}`);
944
+ if (show("overview")) {
945
+ out.log("");
946
+ out.log(out.bold("Overview") + out.dim(" (vs previous window)"));
947
+ out.kv([
948
+ ["active users", `${String(t.users ?? 0)} (${delta(t.users, prev.users)}) · ${String(t.new_users ?? 0)} new · stickiness ${pct(t.stickiness_pct)} · MAU ${String(t.mau ?? 0)}`],
949
+ ["sessions", `${String(t.sessions ?? 0)} (${delta(t.sessions, prev.sessions)}) · ${String(t.avg_session_minutes ?? 0)} min avg · ${String(t.avg_screens_per_session ?? 0)} screens/session`],
950
+ ["crash-free", `${pct(t.crash_free_sessions_pct)} sessions · ${pct(t.crash_free_users_pct)} users`],
951
+ ["page views / clicks", `${String(t.pageviews ?? 0)} / ${String(t.clicks ?? 0)} · ${String(t.events ?? 0)} events (${delta(t.events, prev.events)})`],
952
+ ["errors / crashes", `${String(t.errors ?? 0)} (${delta(t.errors, prev.errors)}) / ${String(t.crashes ?? 0)} (${delta(t.crashes, prev.crashes)}) · ${String(t.users_affected ?? 0)} users affected`],
953
+ ["bug reports", `${String(t.bug_reports ?? 0)} (${delta(t.bug_reports, prev.bug_reports)})`],
954
+ ["issues", `${String(t.issues_opened ?? 0)} opened · ${String(t.issues_resolved ?? 0)} resolved · ${String(t.issues_regressed ?? 0)} regressed · ${String(t.open_issues ?? 0)} open · MTTR ${String(t.mttr_hours ?? 0)} h`],
955
+ ["emails", `${String(t.emails_immediate ?? 0)} immediate · ${String(t.emails_digest ?? 0)} digests`],
956
+ ]);
957
+ }
958
+ if (show("daily") && daily.some((d) => Number(d.users) || Number(d.events) || Number(d.errors) || Number(d.crashes))) {
959
+ out.log("");
960
+ out.log(out.bold("Per day"));
961
+ out.table(["day", "users", "new", "sessions", "views", "clicks", "errors", "crashes", "bugs", "crash-free"], daily.map((d) => [d.day, d.users, d.new_users, d.sessions, d.pageviews, d.clicks, d.errors, d.crashes, d.bug_reports, pct(d.crash_free_sessions_pct)]), { indent: 2 });
962
+ }
963
+ const ret = (data.retention ?? []);
964
+ if (show("retention") && ret.length) {
965
+ out.log("");
966
+ out.log(out.bold("Retention") + out.dim(" weekly cohorts: active 1–4 weeks after first seen"));
967
+ out.table(["cohort week", "users", "week 1", "week 2", "week 3", "week 4"], ret.map((r) => [r.cohort_week, r.users, pct(r.week1_pct), pct(r.week2_pct), pct(r.week3_pct), pct(r.week4_pct)]), { indent: 2 });
968
+ }
969
+ const screens = (data.top_screens ?? []);
970
+ if (show("screens") && screens.length) {
971
+ out.log("");
972
+ out.log(out.bold("Top screens"));
973
+ out.table(["screen", "views", "users", "clicks", "errors"], screens.map((x) => [x.screen, x.views, x.users, x.clicks, x.errors]), { indent: 2, maxWidth: 50 });
974
+ }
975
+ const clicks = (data.top_clicks ?? []);
976
+ if (show("clicks") && clicks.length) {
977
+ out.log("");
978
+ out.log(out.bold("Top clicks"));
979
+ out.table(["screen", "element", "clicks"], clicks.map((x) => [x.screen, x.element, x.clicks]), { indent: 2, maxWidth: 50 });
980
+ }
981
+ const events = (data.top_events ?? []);
982
+ if (show("events") && events.length) {
983
+ out.log("");
984
+ out.log(out.bold("Top events"));
985
+ out.table(["event", "count", "users", "environments"], events.map((x) => [x.name, x.count, x.users, x.environments?.join(", ") ?? ""]), { indent: 2 });
986
+ }
987
+ const bd = (data.breakdowns ?? {});
988
+ if (show("breakdowns")) {
989
+ for (const key of ["platform", "country", "browser", "os", "environment"]) {
990
+ const rows = (bd[key] ?? []);
991
+ if (!rows.length || (rows.length === 1 && rows[0].value === "unknown"))
992
+ continue;
993
+ out.log("");
994
+ out.log(out.bold(`By ${key}`));
995
+ out.table([key, "sessions", "users"], rows.map((x) => [x.value, x.sessions, x.users]), { indent: 2 });
996
+ }
997
+ }
998
+ const releases = (data.releases ?? []);
999
+ if (show("releases") && releases.length) {
1000
+ out.log("");
1001
+ out.log(out.bold("Releases"));
1002
+ out.table(["release", "sessions", "users", "crash-free", "errors", "crashes", "issues", "adoption", "first seen"], releases.map((r) => [r.release, r.sessions, r.users, pct(r.crash_free_sessions_pct), r.errors, r.crashes, r.issues, pct(r.adoption_pct), relTime(String(r.first_seen_at ?? ""), ctx.now())]), { indent: 2 });
1003
+ }
1004
+ const iss = (data.issues ?? {});
1005
+ if (show("issues")) {
1006
+ const u = (iss.open_by_urgency ?? {});
1007
+ out.log("");
1008
+ out.log(out.bold("Issues") + out.dim(` open: ${String(u.critical ?? 0)} critical · ${String(u.high ?? 0)} high · ${String(u.medium ?? 0)} medium · ${String(u.low ?? 0)} low`));
1009
+ const top = (iss.top_by_users ?? []);
1010
+ if (top.length)
1011
+ out.table(["id", "urgency", "title", "users", "count", "task"], top.map((i) => [i.short_id, urgencyPaint(ctx, i.urgency), i.title, i.user_count, i.event_count, i.task?.key ?? ""]), { indent: 2, maxWidth: 50 });
1012
+ const byScreen = (iss.errors_by_screen ?? []);
1013
+ if (byScreen.length) {
1014
+ out.log(out.dim(` errors by screen: ${byScreen.map((x) => `${String(x.screen)} ${String(x.errors)}`).join(" · ")}`));
1015
+ }
1016
+ }
1017
+ const hourly = (data.hourly ?? []);
1018
+ if (show("hourly") && hourly.some((h) => Number(h.events) || Number(h.errors))) {
1019
+ out.log("");
1020
+ out.log(out.bold("Hour of day (UTC)") + out.dim(" events / errors"));
1021
+ const max = Math.max(1, ...hourly.map((h) => Number(h.events)));
1022
+ out.log(` ${hourly.map((h) => String(h.hour).padStart(2, "0")).join(" ")}`);
1023
+ out.log(` ${hourly.map((h) => { const n = Number(h.events); const lvl = n === 0 ? " " : n < max / 4 ? "▁" : n < max / 2 ? "▃" : n < (3 * max) / 4 ? "▅" : "█"; return ` ${lvl}`; }).join(" ")}`);
1024
+ }
1025
+ if (data.hint && !Number(t.events) && !Number(t.sessions))
1026
+ out.info(out.dim(String(data.hint)));
1027
+ });
1028
+ },
1029
+ });
1030
+ export const INGEST_COMMANDS = [connectCommand, issuesList, issuesShow, issuesResolve, issuesIgnore, issuesReopen, issuesTask, bugCommand, ingestKeys, ingestRevoke, ingestTest, ingestSnippetsCommand, ingestForward, ingestSettings, issuesUrgency, analyticsCommand, healthCommand];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scribeclock",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "ScribeClock from the terminal: tasks, time clock, projects, PRDs, meetings, files, vault and Cloudflare — a thin client over the ScribeClock MCP server.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Haydar Al-Karar LLC, dba Karar Tech",