tracksiy 0.1.1 → 0.3.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 CHANGED
@@ -128,10 +128,15 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
128
128
  | `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
129
129
  | `issues list` | Recent issues of a project, newest activity first, with the frames that matter inlined. | read | `issues_list` |
130
130
  | `issues view <id>` | One issue in enough depth to act on: latest event, stack frames, tags, recent volume. | read | `issues_view` |
131
- | `issues resolve <ids...>` | Mark issues resolved. A new event on a resolved issue reopens it as a regression. | write | `issues_resolve` |
132
- | `issues ignore <ids...>` | Mark issues ignored: they stop alerting until unignored. | write | `issues_ignore` |
131
+ | `issues resolve <ids...>` | Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why. | write | `issues_resolve` |
132
+ | `issues ignore <ids...>` | Mark issues ignored: they stop alerting until unignored. Records why. | write | `issues_ignore` |
133
133
  | `issues unresolve <ids...>` | Reopen issues. | write | `issues_unresolve` |
134
134
  | `issues bulk-status` | Change the status of every issue matching a filter. Dry run unless --apply. | write | `issues_bulk_status` |
135
+ | `issues comment <id>` | Leave a comment on an issue for the next agent or human: what you found, what you tried. | write | `issues_comment` |
136
+ | `issues activity <id>` | Who did what to an issue and why: resolutions (and in which release), regressions, comments, claims. | read | `issues_activity` |
137
+ | `issues verify-fixed <id>` | Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed. | read | `issues_verify_fixed` |
138
+ | `issues claim <id>` | Take a lease on an issue so no other agent works on it meanwhile. Lapses on its own. | write | `issues_claim` |
139
+ | `issues unclaim <id>` | Give back your lease on an issue. | write | `issues_unclaim` |
135
140
  | `projects create <name>` | Everything a new app needs in one idempotent call: the project, an ingest key per surface, an upload token, and the next steps. | write | `projects_create` |
136
141
  | `projects verify` | Did the integration actually work? Checks events, releases, environments, source maps, privacy and alerting. Exit 10 if not. | read | `projects_verify` |
137
142
  | `onboarding` | What to know before integrating an app. No token needed. | read | `onboarding` |
package/dist/tracksiy.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync as readFileSync2 } from "node:fs";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = true ? "0.1.1" : "0.0.0-dev";
7
+ var VERSION = true ? "0.3.0" : "0.0.0-dev";
8
8
  var USER_AGENT = `tracksiy-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
9
9
 
10
10
  // src/core/errors.ts
@@ -115,6 +115,7 @@ var Api = class {
115
115
  const headers = { "User-Agent": USER_AGENT, Accept: "application/json" };
116
116
  const useAuth = opts.auth !== false;
117
117
  if (useAuth && this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
118
+ if (this.opts.agent) headers["X-Tracksiy-Agent"] = this.opts.agent;
118
119
  let body;
119
120
  if (opts.form) body = opts.form;
120
121
  else if (opts.body !== void 0) {
@@ -375,6 +376,8 @@ function resolveConfig(globals, env = process.env, cwd = process.cwd()) {
375
376
  verbose: Boolean(globals.verbose),
376
377
  quiet: Boolean(globals.quiet)
377
378
  };
379
+ const agent = (globals.as ?? env.TRACKSIY_AGENT ?? "").trim();
380
+ if (agent) out.agent = agent.slice(0, 100);
378
381
  if (pf) out.projectFile = pf.path;
379
382
  if (globals.token) [out.token, out.tokenSource] = [globals.token, "--token"];
380
383
  else if (env.TRACKSIY_TOKEN) [out.token, out.tokenSource] = [env.TRACKSIY_TOKEN, "TRACKSIY_TOKEN"];
@@ -641,6 +644,7 @@ function missingToken(url) {
641
644
  var auth_default = [login, status, logout];
642
645
 
643
646
  // src/commands/_util.ts
647
+ import { readFileSync as readFileSync3 } from "node:fs";
644
648
  async function projectPath(ctx, suffix = "") {
645
649
  const { org, project: project2 } = await ctx.project();
646
650
  return `projects/${seg(org)}/${seg(project2)}${suffix ? `/${suffix}` : ""}`;
@@ -679,6 +683,26 @@ function rate(value, flag) {
679
683
  function compact(obj) {
680
684
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
681
685
  }
686
+ function textArg(value, flag) {
687
+ if (value === void 0 || value === null) return void 0;
688
+ const raw = String(value);
689
+ if (raw === "-") return readFileSync3(0, "utf8");
690
+ if (raw.startsWith("@")) {
691
+ try {
692
+ return readFileSync3(raw.slice(1), "utf8");
693
+ } catch (err) {
694
+ throw usageError("bad_value", `--${flag}: cannot read ${raw.slice(1)}: ${err.message}`);
695
+ }
696
+ }
697
+ return raw;
698
+ }
699
+ function duration(value, flag) {
700
+ if (value === void 0 || value === null || value === "") return void 0;
701
+ const s = String(value).trim();
702
+ const m = /^(\d+)\s*([smhdw])?$/.exec(s);
703
+ if (!m) throw usageError("bad_value", `--${flag} expects a duration like 30m / 2h / 1d, got ${JSON.stringify(s)}`);
704
+ return Number(m[1]) * (m[2] ? UNITS[m[2]] : 1);
705
+ }
682
706
 
683
707
  // src/commands/fleet.ts
684
708
  var orgs = {
@@ -920,6 +944,26 @@ var FILTERS = {
920
944
  "first-seen-since": { type: "string", value: TIME, description: "First seen at or after this: new issues only." },
921
945
  "first-seen-until": { type: "string", value: TIME, description: "First seen before this." }
922
946
  };
947
+ var WHY = {
948
+ reason: { type: "string", enum: ["fixed", "duplicate", "third_party", "expected", "wont_fix"], description: "Why, recorded on the issue's activity log." },
949
+ note: { type: "string", value: "<md|@file|->", description: "A note for the next agent (markdown), kept on the activity log." },
950
+ ref: { type: "string", value: "<sha|url>", description: "The evidence: a commit, PR or URL." }
951
+ };
952
+ var FIXED_IN = {
953
+ "in-release": { type: "string", value: "<version>", description: "Fixed in this release: events from older releases count as stale, not as a regression. Must exist (releases declare)." },
954
+ "in-commit": { type: "string", value: "<sha>", description: "Fixed in the release that carries this commit (its ref, or a version ending @<sha>); bound when that release appears." },
955
+ "in-next-release": { type: "boolean", description: "Fixed in whatever is released next: only newer releases can reopen it." }
956
+ };
957
+ function whyBody(input) {
958
+ return compact({
959
+ inRelease: input.inRelease,
960
+ inCommit: input.inCommit,
961
+ inNextRelease: input.inNextRelease || void 0,
962
+ reason: input.reason,
963
+ note: textArg(input.note, "note"),
964
+ ref: input.ref
965
+ });
966
+ }
923
967
  function filterQuery(input) {
924
968
  return compact({
925
969
  status: input.status,
@@ -952,14 +996,17 @@ var list = {
952
996
  ...FILTERS,
953
997
  sort: { type: "string", enum: ["lastSeen", "firstSeen", "count"], description: "Order. Default lastSeen." },
954
998
  limit: { type: "integer", value: "<n>", description: "How many issues (server default 25)." },
955
- frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." }
999
+ frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." },
1000
+ unclaimed: { type: "boolean", description: "Only issues nobody holds a live claim on: what you can pick up." },
1001
+ "claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." }
956
1002
  },
957
1003
  examples: [
958
1004
  { command: "tracksiy issues list --since 24h", description: "Unresolved issues seen in the last day." },
1005
+ { command: "tracksiy issues list --unclaimed --limit 5", description: "Five issues no other agent is working on." },
959
1006
  { command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
960
1007
  { command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
961
1008
  ],
962
- output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, permalink, frames?}]}",
1009
+ output: "{project, issueCount, total, hasMore, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, frames?}]}",
963
1010
  text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
964
1011
  ${issueRows(d.issues)}`,
965
1012
  async run(ctx) {
@@ -967,7 +1014,9 @@ ${issueRows(d.issues)}`,
967
1014
  ...filterQuery(ctx.input),
968
1015
  sort: ctx.input.sort,
969
1016
  limit: ctx.input.limit,
970
- frames: ctx.input.frames
1017
+ frames: ctx.input.frames,
1018
+ unclaimed: ctx.input.unclaimed || void 0,
1019
+ claimedBy: ctx.input.claimedBy
971
1020
  });
972
1021
  }
973
1022
  };
@@ -980,7 +1029,7 @@ var view = {
980
1029
  args: [{ name: "id", required: true, description: "Issue id (numeric, from `issues list`)." }],
981
1030
  flags: { frames: { type: "integer", value: "<n>", description: "How many frames to include." } },
982
1031
  examples: [{ command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." }],
983
- output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, latestEvent{release, environment, exceptionType, exceptionValue, contexts}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, permalink}",
1032
+ output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, resolution?, staleEvents, claimedBy?, latestEvent{release, environment, exceptionType, exceptionValue, contexts}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, activity[], recentComments[], permalink}",
984
1033
  text: (d) => {
985
1034
  const head = kv(d, ["id", "shortId", "title", "culprit", "level", "status", "count", "eventsLast24h", "eventsLast7d", "firstSeen", "lastSeen", "regressedAt", "permalink"]);
986
1035
  const ev = d.latestEvent ? `
@@ -988,15 +1037,22 @@ latest event:
988
1037
  ${kv(d.latestEvent, ["release", "environment", "exceptionType", "exceptionValue", "transaction", "platform"]).replace(/^/gm, " ")}` : "";
989
1038
  const frames = (d.frames ?? []).map((f) => ` ${f.inApp ? "*" : " "} ${f.filename ?? f.module ?? "?"}:${f.lineno ?? "?"} in ${f.function ?? "?"}${f.contextLine ? `
990
1039
  ${String(f.contextLine).trim()}` : ""}`).join("\n");
991
- return `${head}${ev}${frames ? `
1040
+ const res = d.resolution ? `
1041
+ resolved ${d.resolution.type.replace(/_/g, " ")}: ${d.resolution.release ?? d.resolution.commit ?? "-"}${d.resolution.pending ? " (pending)" : ""}; ${d.staleEvents ?? 0} stale event(s)` : "";
1042
+ const claim2 = d.claimedBy ? `
1043
+ claimed by ${d.claimedBy} until ${d.claimedUntil}` : "";
1044
+ const act = (d.activity ?? []).slice(0, 5).map((a) => ` ${ago(a.at)} ${a.action}${a.reason ? ` (${a.reason})` : ""} by ${a.actor ?? a.user ?? a.via}${a.note ? `: ${String(a.note).split("\n")[0].slice(0, 100)}` : ""}`).join("\n");
1045
+ return `${head}${res}${claim2}${ev}${frames ? `
992
1046
  frames (* = in app):
993
- ${frames}` : ""}`;
1047
+ ${frames}` : ""}${act ? `
1048
+ activity:
1049
+ ${act}` : ""}`;
994
1050
  },
995
1051
  async run(ctx) {
996
1052
  return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames });
997
1053
  }
998
1054
  };
999
- function statusCommand(name, status2, summary) {
1055
+ function statusCommand(name, status2, summary, examples) {
1000
1056
  return {
1001
1057
  path: ["issues", name],
1002
1058
  summary,
@@ -1005,19 +1061,139 @@ function statusCommand(name, status2, summary) {
1005
1061
  idempotent: true,
1006
1062
  endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/status" },
1007
1063
  args: [{ name: "ids", required: true, variadic: true, description: "One or more issue ids." }],
1008
- examples: [{ command: `tracksiy issues ${name} 4812 4813`, description: `Mark two issues ${status2}.` }],
1009
- output: "One id: the issue's new state. Several: {matched, changed, ids, dryRun:false}.",
1064
+ flags: name === "resolve" ? { ...FIXED_IN, ...WHY } : { ...WHY },
1065
+ examples,
1066
+ output: "One id: the issue's new state with changed, requested, applied. Several: {matched, updated, alreadyInStatus, resolution, dryRun:false}.",
1010
1067
  async run(ctx) {
1011
1068
  const ids = ctx.input.ids;
1012
1069
  for (const id of ids) if (!/^\d+$/.test(id)) throw usageError("bad_value", `Issue ids are numeric; got ${JSON.stringify(id)}`);
1013
- if (ids.length === 1) return ctx.api.send("POST", await projectPath(ctx, `issues/${ids[0]}/status`), { status: status2 });
1014
- return ctx.api.send("POST", await projectPath(ctx, "issues/status"), { status: status2, ids, dryRun: false });
1070
+ const why = whyBody(ctx.input);
1071
+ if (ids.length === 1) return ctx.api.send("POST", await projectPath(ctx, `issues/${ids[0]}/status`), { status: status2, ...why });
1072
+ return ctx.api.send("POST", await projectPath(ctx, "issues/status"), { status: status2, ids, dryRun: false, ...why });
1015
1073
  }
1016
1074
  };
1017
1075
  }
1018
- var resolve2 = statusCommand("resolve", "resolved", "Mark issues resolved. A new event on a resolved issue reopens it as a regression.");
1019
- var ignore = statusCommand("ignore", "ignored", "Mark issues ignored: they stop alerting until unignored.");
1020
- var unresolve = statusCommand("unresolve", "unresolved", "Reopen issues.");
1076
+ var resolve2 = statusCommand(
1077
+ "resolve",
1078
+ "resolved",
1079
+ "Mark issues resolved, optionally fixed in a release so old clients do not reopen them. Records why.",
1080
+ [
1081
+ { command: "tracksiy issues resolve 4812 --in-release web@$(git rev-parse HEAD) --reason fixed --ref $(git rev-parse HEAD)", description: "Fixed in this deploy; old bundles count as stale." },
1082
+ { command: "tracksiy issues resolve 4812 --in-next-release --note @why.md", description: "Fixed in whatever ships next, with the reasoning." },
1083
+ { command: "tracksiy issues resolve 5562 --reason third_party --note 'WebView injects addEL_hook'", description: "Not our code: closed with the reason on record." }
1084
+ ]
1085
+ );
1086
+ var ignore = statusCommand("ignore", "ignored", "Mark issues ignored: they stop alerting until unignored. Records why.", [
1087
+ { command: "tracksiy issues ignore 4812 --reason expected --note 'Bots hitting a removed URL'", description: "Ignore, and say why for the next agent." }
1088
+ ]);
1089
+ var unresolve = statusCommand("unresolve", "unresolved", "Reopen issues.", [{ command: "tracksiy issues unresolve 4812 --note 'Came back on Android'", description: "Reopen with a note." }]);
1090
+ var comment = {
1091
+ path: ["issues", "comment"],
1092
+ summary: "Leave a comment on an issue for the next agent or human: what you found, what you tried.",
1093
+ context: "project",
1094
+ effect: "write",
1095
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/comments" },
1096
+ args: [{ name: "id", required: true, description: "Issue id." }],
1097
+ flags: { text: { type: "string", required: true, value: "<md|@file|->", description: "The comment (markdown). @file reads a file, - reads stdin." } },
1098
+ examples: [{ command: "tracksiy issues comment 4812 --text @findings.md", description: "Attach what you found." }],
1099
+ output: "{id, created, user, text}",
1100
+ async run(ctx) {
1101
+ return ctx.api.send("POST", await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/comments`), { text: textArg(ctx.input.text, "text") });
1102
+ }
1103
+ };
1104
+ var activity = {
1105
+ path: ["issues", "activity"],
1106
+ summary: "Who did what to an issue and why: resolutions (and in which release), regressions, comments, claims.",
1107
+ context: "project",
1108
+ effect: "read",
1109
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/activity" },
1110
+ args: [{ name: "id", required: true, description: "Issue id." }],
1111
+ flags: { limit: { type: "integer", value: "<n>", description: "How many entries (1..500, default 100)." } },
1112
+ examples: [{ command: "tracksiy issues activity 4812", description: "The issue's history, newest first." }],
1113
+ output: "{issue, activity:[{at, action, via, actor, user, reason, note, ref, data}], comments:[{id, created, user, text}]}",
1114
+ text: (d) => table(d.activity ?? [], [
1115
+ ["when", (r) => ago(r.at)],
1116
+ "action",
1117
+ ["by", (r) => r.actor ?? r.user ?? r.via],
1118
+ ["reason", (r) => r.reason ?? ""],
1119
+ ["note", (r) => String(r.note ?? (r.data?.release ? `release ${r.data.release}` : "")).split("\n")[0].slice(0, 80)]
1120
+ ]),
1121
+ async run(ctx) {
1122
+ return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/activity`), { limit: ctx.input.limit });
1123
+ }
1124
+ };
1125
+ var verifyFixed = {
1126
+ path: ["issues", "verify-fixed"],
1127
+ summary: "Did the fix hold? Judged by the fix release's own traffic: fixed, regressed or undetermined. Exit 10 unless fixed.",
1128
+ description: "regressed: the issue has an event from the fix release or a newer one. undetermined: the fix release has had too little traffic (--min-traffic), or the issue's old rate predicts fewer than 3 occurrences in that traffic. fixed otherwise. The fix release defaults to the one the issue was resolved in. --wait polls until the verdict is not undetermined.",
1129
+ context: "project",
1130
+ effect: "read",
1131
+ endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/verify" },
1132
+ args: [{ name: "id", required: true, description: "Issue id." }],
1133
+ flags: {
1134
+ release: { type: "string", value: "<version>", description: "The fix release. Default: the one it was resolved in." },
1135
+ "min-traffic": { type: "integer", value: "<n>", description: "Transactions + errors + sessions from the fix release needed before silence counts (default 100)." },
1136
+ wait: { type: "integer", value: "<seconds>", description: "Poll every 30 s until fixed or regressed, up to this long." }
1137
+ },
1138
+ examples: [
1139
+ { command: "tracksiy issues verify-fixed 4812", description: "Verdict now." },
1140
+ { command: "tracksiy issues verify-fixed 4812 --wait 1800", description: "In a deploy script: wait up to 30 min for enough traffic." }
1141
+ ],
1142
+ output: "{verdict, reason, release, since, eventsSince, staleEvents, traffic{transactions, errors, sessions, total}, baselinePer1000, expectedEvents, latestEvent?}",
1143
+ text: (d) => kv(d, ["verdict", "reason", "release", "since", "eventsSince", "staleEvents", "expectedEvents"]) + `
1144
+ traffic: ${d.traffic?.total ?? 0} (${d.traffic?.transactions ?? 0} transactions, ${d.traffic?.errors ?? 0} errors, ${d.traffic?.sessions ?? 0} sessions)`,
1145
+ async run(ctx) {
1146
+ const path = await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/verify`);
1147
+ const query = { release: ctx.input.release, minTraffic: ctx.input.minTraffic };
1148
+ const deadline = Date.now() + (ctx.input.wait ?? 0) * 1e3;
1149
+ let data = await ctx.api.get(path, query);
1150
+ while (data?.verdict === "undetermined" && Date.now() + 3e4 <= deadline) {
1151
+ ctx.log(`undetermined: ${data.reason} -- checking again in 30 s`);
1152
+ await new Promise((r) => setTimeout(r, 3e4));
1153
+ data = await ctx.api.get(path, query);
1154
+ }
1155
+ if (data?.verdict !== "fixed") throw new CheckFailed(`${data?.verdict ?? "unknown"}: ${data?.reason ?? ""}`, data, data?.verdict === "regressed" ? "Reopened by its own release: look at latestEvent." : "Wait for traffic (--wait), or lower --min-traffic.");
1156
+ return data;
1157
+ }
1158
+ };
1159
+ var claim = {
1160
+ path: ["issues", "claim"],
1161
+ summary: "Take a lease on an issue so no other agent works on it meanwhile. Lapses on its own.",
1162
+ description: "The holder is your handle: --as, TRACKSIY_AGENT, else the token's label. Claiming what you hold extends it. Someone else's live claim is an error naming them, unless --force. Resolving or ignoring releases it.",
1163
+ context: "project",
1164
+ effect: "write",
1165
+ endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/claim" },
1166
+ args: [{ name: "id", required: true, description: "Issue id." }],
1167
+ flags: {
1168
+ for: { type: "string", value: "<30m|2h|1d>", description: "Lease length (60 s to 7 days). Default 2h." },
1169
+ force: { type: "boolean", description: "Take it from another live holder." }
1170
+ },
1171
+ examples: [{ command: "TRACKSIY_AGENT=logistivo-app tracksiy issues claim 4812 --for 2h", description: "Work on 4812 for two hours." }],
1172
+ output: "The issue with claimedBy, claimedUntil, changed, previousClaimedBy.",
1173
+ async run(ctx) {
1174
+ return ctx.api.send(
1175
+ "POST",
1176
+ await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/claim`),
1177
+ compact({ seconds: duration(ctx.input.for, "for"), force: ctx.input.force || void 0 })
1178
+ );
1179
+ }
1180
+ };
1181
+ var unclaim = {
1182
+ path: ["issues", "unclaim"],
1183
+ summary: "Give back your lease on an issue.",
1184
+ context: "project",
1185
+ effect: "write",
1186
+ idempotent: true,
1187
+ endpoint: { method: "DELETE", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}/claim" },
1188
+ args: [{ name: "id", required: true, description: "Issue id." }],
1189
+ flags: { force: { type: "boolean", description: "Release someone else's claim." } },
1190
+ examples: [{ command: "tracksiy issues unclaim 4812", description: "Done, or giving up: let others take it." }],
1191
+ output: "The issue with changed and previousClaimedBy.",
1192
+ async run(ctx) {
1193
+ const path = await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}/claim`);
1194
+ return (await ctx.api.request("DELETE", path, { query: ctx.input.force ? { force: true } : void 0 })).data;
1195
+ }
1196
+ };
1021
1197
  var bulk = {
1022
1198
  path: ["issues", "bulk-status"],
1023
1199
  summary: "Change the status of every issue matching a filter. Dry run unless --apply.",
@@ -1030,7 +1206,9 @@ var bulk = {
1030
1206
  ids: { type: "list", value: "<id,...>", description: "Exactly these issues." },
1031
1207
  ...FILTERS,
1032
1208
  all: { type: "boolean", description: "Every issue of the project (required when no other selector is given)." },
1033
- apply: { type: "boolean", description: "Actually change them. Default is a dry run." }
1209
+ apply: { type: "boolean", description: "Actually change them. Default is a dry run." },
1210
+ ...FIXED_IN,
1211
+ ...WHY
1034
1212
  },
1035
1213
  examples: [
1036
1214
  { command: "tracksiy issues bulk-status --to resolved --until 30d", description: "Preview resolving everything not seen for 30 days." },
@@ -1045,12 +1223,13 @@ var bulk = {
1045
1223
  ids: ctx.input.ids?.map(String),
1046
1224
  filter: hasFilter ? filter : void 0,
1047
1225
  all: ctx.input.all || void 0,
1048
- dryRun: !ctx.input.apply
1226
+ dryRun: !ctx.input.apply,
1227
+ ...whyBody(ctx.input)
1049
1228
  });
1050
1229
  return ctx.api.send("POST", await projectPath(ctx, "issues/status"), body);
1051
1230
  }
1052
1231
  };
1053
- var issues_default = [list, view, resolve2, ignore, unresolve, bulk];
1232
+ var issues_default = [list, view, resolve2, ignore, unresolve, bulk, comment, activity, verifyFixed, claim, unclaim];
1054
1233
 
1055
1234
  // src/commands/projects.ts
1056
1235
  var create = {
@@ -1149,12 +1328,12 @@ var onboarding = {
1149
1328
  var projects_default = [create, verify, onboarding];
1150
1329
 
1151
1330
  // src/commands/alerts.ts
1152
- var RECIPIENT_TYPES = ["email", "webhook", "discord", "googlechat", "telegram"];
1331
+ var RECIPIENT_TYPES = ["email", "webhook", "discord", "googlechat", "telegram", "peerpacket"];
1153
1332
  var DECLARE = {
1154
1333
  recipient: {
1155
1334
  type: "list",
1156
1335
  value: "email|<type>:<url>",
1157
- description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id).`
1336
+ description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
1158
1337
  },
1159
1338
  delivery: { type: "string", enum: ["immediate", "digest"], description: "For --recipient: send each alert now, or batch into the digest hours. Default immediate." },
1160
1339
  "new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
@@ -1231,7 +1410,8 @@ var set = {
1231
1410
  flags: DECLARE,
1232
1411
  examples: [
1233
1412
  { command: "tracksiy alerts set --recipient email --threshold 50/10", description: "Email the project's team on new issues, on bursts, and on downtime." },
1234
- { command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." }
1413
+ { command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." },
1414
+ { command: "tracksiy alerts set --recipient 'peerpacket:https://peerpacket.com/channels/shop-ops?to=shop-agent&as=tracksiy-alerts&key=pp_live_\u2026'", description: "Alerts land as PeerPacket tickets for the shop's agent." }
1235
1415
  ],
1236
1416
  output: "The stored alerting state, same shape as `alerts get`.",
1237
1417
  text: rulesText,
@@ -1466,7 +1646,7 @@ var releases_default = [list3, health2, declare];
1466
1646
 
1467
1647
  // src/commands/artifacts.ts
1468
1648
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1469
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1649
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
1470
1650
 
1471
1651
  // src/lib/sourcemaps.ts
1472
1652
  import { createHash } from "node:crypto";
@@ -1841,7 +2021,7 @@ skipped, no source map: ${d.skipped.join(", ")}` : ""}`,
1841
2021
  };
1842
2022
  function uuidFromDebugMeta(path) {
1843
2023
  if (!existsSync3(path)) throw usageError("bad_path", `No such file: ${path}`);
1844
- const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync3(path, "utf8"));
2024
+ const m = /ProguardUuids\s*[=:]\s*([0-9a-fA-F-]{36})/.exec(readFileSync4(path, "utf8"));
1845
2025
  if (!m) throw usageError("bad_value", `No ProguardUuids entry in ${path}`);
1846
2026
  return m[1].toLowerCase();
1847
2027
  }
@@ -2053,16 +2233,16 @@ var support = {
2053
2233
  var support_default = [support];
2054
2234
 
2055
2235
  // src/commands/init.ts
2056
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
2236
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
2057
2237
  import { appendFile, writeFile as writeFile3 } from "node:fs/promises";
2058
2238
  import { join as join4, resolve as resolve4 } from "node:path";
2059
2239
 
2060
2240
  // src/lib/detect.ts
2061
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync } from "node:fs";
2241
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync } from "node:fs";
2062
2242
  import { basename, join as join3 } from "node:path";
2063
2243
  function readJson(path) {
2064
2244
  try {
2065
- return JSON.parse(readFileSync4(path, "utf8"));
2245
+ return JSON.parse(readFileSync5(path, "utf8"));
2066
2246
  } catch {
2067
2247
  return void 0;
2068
2248
  }
@@ -2134,7 +2314,7 @@ function detect(dir, guideBase) {
2134
2314
  }
2135
2315
  const py = has(dir, "pyproject.toml", "requirements.txt", "Pipfile", "manage.py");
2136
2316
  if (py) {
2137
- const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync4(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
2317
+ const text = ["pyproject.toml", "requirements.txt", "Pipfile"].map((f) => existsSync4(join3(dir, f)) ? readFileSync5(join3(dir, f), "utf8") : "").join("\n").toLowerCase();
2138
2318
  const fw = existsSync4(join3(dir, "manage.py")) || text.includes("django") ? "django" : text.includes("fastapi") ? "fastapi" : text.includes("flask") ? "flask" : "python";
2139
2319
  evidence.push(`${py} (${fw})`);
2140
2320
  return { platform: fw === "python" ? "python" : `python-${fw}`, framework: fw, surfaces: ["backend"], env: { backend: "SENTRY_DSN" }, guide: guide("python-sentry-sdk"), name, evidence };
@@ -2230,7 +2410,7 @@ ${d.steps.map((s, i) => `${i + 1}. ${s.detail}${s.run ? `
2230
2410
  if (!existsSync5(envPath)) {
2231
2411
  throw usageError("no_env_file", `${envPath} does not exist; nothing was written to it.`, "Create it (and make sure it is gitignored), or set the variables yourself from `keys` in the output.");
2232
2412
  }
2233
- const current = readFileSync5(envPath, "utf8");
2413
+ const current = readFileSync6(envPath, "utf8");
2234
2414
  const added = [];
2235
2415
  const kept = [];
2236
2416
  let block = "";
@@ -2285,12 +2465,13 @@ function camel(name) {
2285
2465
  }
2286
2466
 
2287
2467
  // src/core/args.ts
2288
- import { readFileSync as readFileSync6 } from "node:fs";
2468
+ import { readFileSync as readFileSync7 } from "node:fs";
2289
2469
  var GLOBAL_FLAGS = {
2290
2470
  org: { type: "string", value: "<slug>", description: "Organization slug. Env: TRACKSIY_ORG. Default: .tracksiy.json, saved login, or your only organization." },
2291
2471
  project: { type: "string", value: "<slug>", description: "Project slug, or org/project. Env: TRACKSIY_PROJECT. Default: .tracksiy.json." },
2292
2472
  url: { type: "string", value: "<url>", description: "Server root. Env: TRACKSIY_URL. Default: https://tracksiy.com." },
2293
2473
  token: { type: "string", value: "<token>", description: "API token. Env: TRACKSIY_TOKEN (preferred: flags show up in process lists)." },
2474
+ as: { type: "string", value: "<handle>", description: "Your agent handle, recorded on issue activity and claims (X-Tracksiy-Agent). Env: TRACKSIY_AGENT. Default: the token's label." },
2294
2475
  json: { type: "boolean", description: "Print JSON. This is the default whenever stdout is not a terminal." },
2295
2476
  format: { type: "string", enum: ["json", "text"], description: "Output format. Env: TRACKSIY_FORMAT." },
2296
2477
  select: { type: "list", value: "<fields>", description: "Keep only these fields of `data`, comma-separated; dot paths reach into objects and lists (e.g. issues.id,issues.title)." },
@@ -2301,7 +2482,7 @@ var GLOBAL_FLAGS = {
2301
2482
  };
2302
2483
  var stdinCache;
2303
2484
  function readStdin() {
2304
- stdinCache ??= readFileSync6(0, "utf8");
2485
+ stdinCache ??= readFileSync7(0, "utf8");
2305
2486
  return stdinCache;
2306
2487
  }
2307
2488
  function parseJsonValue(name, raw) {
@@ -2309,7 +2490,7 @@ function parseJsonValue(name, raw) {
2309
2490
  if (raw === "-") text = readStdin();
2310
2491
  else if (raw.startsWith("@")) {
2311
2492
  try {
2312
- text = readFileSync6(raw.slice(1), "utf8");
2493
+ text = readFileSync7(raw.slice(1), "utf8");
2313
2494
  } catch (err) {
2314
2495
  throw usageError("bad_value", `--${name}: cannot read ${raw.slice(1)}: ${err.message}`);
2315
2496
  }
@@ -2649,7 +2830,7 @@ function makeContext(spec, input, globals, surface, io) {
2649
2830
  if (!quiet || surface === "mcp" && config.verbose) io.stderr(`${message}
2650
2831
  `);
2651
2832
  };
2652
- const api2 = new Api({ url: config.url, token: config.token, timeoutMs: config.timeoutMs, verbose: config.verbose, log, fetch: io.fetch });
2833
+ const api2 = new Api({ url: config.url, token: config.token, agent: config.agent, timeoutMs: config.timeoutMs, verbose: config.verbose, log, fetch: io.fetch });
2653
2834
  let orgs2;
2654
2835
  const visible = () => {
2655
2836
  orgs2 ??= api2.get("organizations").catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracksiy",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Agent-first CLI and MCP server for Tracksiy: errors, uptime, alerts, releases and source maps, from one token.",
5
5
  "type": "module",
6
6
  "bin": {