prreviewbuddy 0.62.0 → 0.64.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.2
4
+
5
+ **Review state says whether you have reviewed a change, in those words.** On the Reviews page the
6
+ filter reads `Review state: All / Queue / Not reviewed / Reviewed`, and a card you have opened but
7
+ not marked says `Not reviewed` where it said `In progress`. The menus and the bulk bar say
8
+ `Mark not reviewed` for the way back. This is a separate question from where a review sits in
9
+ Active, Waiting and Done, so `Not reviewed` and `Done` together is a card you have put away
10
+ without marking it reviewed, not a contradiction.
11
+
12
+ `New` still marks a review you have never opened. The `Not reviewed` filter includes those as well,
13
+ because a review nobody has opened has not been reviewed either. Mark not reviewed from a review's
14
+ own page now always lands on `Not reviewed`, never back on `New`, as it already did from the list.
15
+ Links to the old filter keep working.
16
+
17
+ ## 0.64.1
18
+
19
+ **The author filter names people.** On the Reviews page it reads `Author: Everyone`, like Status
20
+ and PR beside it, and lists each person by their GitHub name with their login beside it:
21
+ `Amy Li · @amy`. Someone with no public name on GitHub is listed by login alone. The list is in
22
+ alphabetical order of what you read, and if someone changes their name on GitHub the filter shows
23
+ the name from their newest review. Author and PR both carry the GitHub mark, because both are
24
+ answered from GitHub. Status is yours, so it has none.
25
+
26
+ ## 0.64.0
27
+
28
+ **Choose the findings you want to raise, and copy them as one message.** Issues and Questions each
29
+ have a Prepare feedback button now. Press it and every card gets a checkbox; tick the ones you want
30
+ to take to the author. Your choices stay as you move between Issues and Questions, and a bar at the
31
+ foot of the review keeps count: `5 chosen · 2 issues · 3 questions`. Each count takes you back to
32
+ its list. The rail shows `2 chosen` under each. Clear empties your choice, and Done leaves it
33
+ altogether.
34
+
35
+ Prepare feedback in that bar opens your choices, large enough to read, issues first, then
36
+ questions, each in the order the review lists them. Preview shows the message as the author will
37
+ read it; Markdown shows the text behind it, to edit. They are one draft: an edit is in the preview
38
+ as soon as you switch back, and Copy Markdown copies what you edited. Each item starts as exactly
39
+ what that card's own Copy gives you: the finding, why it matters, and where it points. Severity and
40
+ status are left out, because they are how PR Review Buddy sorts findings for you, not part of what
41
+ you tell the author. Dismissing or answering a finding you chose leaves your draft exactly as you
42
+ left it. Change what you chose, even to take a tick back, and the draft is made again from the new
43
+ choice. Nothing is rewritten by AI.
44
+
45
+ A choice belongs to the review you made it on. Another review starts with none, and reloading the
46
+ page starts over. Dismiss and Mark answered work as before, while choosing or not.
47
+
48
+ Lists are drawn as GitHub draws them, in the feedback preview and in Buddy AI's answers. A list
49
+ with blank lines between its items is one list, numbered on from its first number. A paragraph
50
+ indented under an item stays in that item, and a list indented under an item is drawn inside it,
51
+ one level deep, with its own paragraphs. What counts is how far a line is indented under its item,
52
+ so a list indented the same throughout is still one list. Before, each item after a blank line was
53
+ numbered 1, and a nested list was drawn as more items of the outer one, so a finding with numbered
54
+ steps of its own previewed them as further findings.
55
+
56
+ KISS's Copy draft comment and Copy recommendations now say Copied in their own words after a
57
+ click, and then go back to their names. Before, the first click left them as a bare icon.
58
+
59
+ ## 0.63.0
60
+
61
+ **Telemetry now records first use.** With telemetry on, PR Review Buddy sends one more event,
62
+ `installation_started`, once per installation: the first time you run `prreviewbuddy` on its own,
63
+ `review`, `open` or `agents`. It marks first use, not when the package was installed, and it adds
64
+ no field. It carries the installation id, version, platform and a timestamp, and no agent. If it
65
+ cannot reach the server it is tried again on your next such command, and never again once it
66
+ arrives. The server keeps one per installation, so a repeat is acknowledged and not stored. Until now only reviews were counted, so somebody who tried PR Review Buddy and stopped
67
+ before a review was invisible. Installations from before this version send it once, on their first
68
+ command after upgrading.
69
+
70
+ `prreviewbuddy` on its own now shows the one-time telemetry notice too, because it can now send.
71
+ Its usage text is unchanged. An installation that already saw the telemetry notice, or turned telemetry
72
+ on, is told once about this one addition; one that turned it off is told nothing. `--help`, `--version` and `config` send nothing and show no notice.
73
+ `prreviewbuddy config set telemetry-upload off` and `PRB_TELEMETRY=off` stop this event exactly as
74
+ they stop the others. The privacy page lists it: https://prreviewbuddy.com/privacy
75
+
3
76
  ## 0.62.0
4
77
 
5
78
  **Buddy AI has its own keys.** On a review, ⌘K B opens a Buddy AI menu. B O opens Buddy AI with
@@ -4278,7 +4278,7 @@ function normalise(ids) {
4278
4278
  * What happens next with a change, as the reviewer says it: still in play, parked until something
4279
4279
  * moves, or finished with.
4280
4280
  *
4281
- * A separate axis from New / In progress / Reviewed, which says what the reviewer has *done*. A
4281
+ * A separate axis from New / Not reviewed / Reviewed, which says what the reviewer has *done*. A
4282
4282
  * change can be reviewed and waiting for another push, or reviewed and done, and one field cannot
4283
4283
  * hold both answers.
4284
4284
  *
@@ -4988,7 +4988,8 @@ function summarise(workspace) {
4988
4988
  prNumber: workspace.changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number,
4989
4989
  commitSha: workspace.session.metadata?.commitSha || (workspace.changeSet.targetType === "commit" ? workspace.changeSet.data.pullRequest.head.sha : "") || void 0,
4990
4990
  author: describeAuthorship(workspace.changeSet, workspace.prContext?.pullRequest)?.label,
4991
- authorHandle: workspace.prContext?.pullRequest.author?.handle || void 0
4991
+ authorHandle: workspace.prContext?.pullRequest.author?.handle || void 0,
4992
+ authorName: workspace.prContext?.pullRequest.author?.name || void 0
4992
4993
  };
4993
4994
  }
4994
4995
  /**
@@ -10260,16 +10261,57 @@ function diagramFigure(code, label) {
10260
10261
  const id = `prb-md-mermaid-source-${++diagramCount}`;
10261
10262
  return `<figure class="prb-md-mermaid" data-md-mermaid data-state="source"><div class="prb-md-mermaid-canvas" data-md-mermaid-canvas role="img" aria-label="${escapeHtml(label)}" hidden></div><p class="prb-md-mermaid-error" data-md-mermaid-error hidden>This diagram couldn\u2019t be displayed.</p><button type="button" class="prb-md-mermaid-toggle" data-md-mermaid-toggle aria-expanded="true" aria-controls="${id}" hidden>Hide source</button>` + codeBlock(code, "Copy source", "Copy source", ` id="${id}" data-md-mermaid-source`) + `</figure>`;
10262
10263
  }
10264
+ /**
10265
+ * The column after `whitespace`, written from column `from`. A tab moves to the next multiple of
10266
+ * four, as CommonMark's tab stops do, so "-\t" and "- " both put text at column 2 and 4 alike.
10267
+ */
10268
+ function columnAfter(whitespace, from = 0) {
10269
+ let column = from;
10270
+ for (const char of whitespace) column = char === " " ? column + 4 - column % 4 : column + 1;
10271
+ return column;
10272
+ }
10273
+ /** How far a line is indented, in columns. */
10274
+ function indentOf(raw) {
10275
+ return columnAfter(raw.match(/^[ \t]*/)[0]);
10276
+ }
10277
+ /**
10278
+ * A list marker line: its kind, an ordered one's number, its text, and its content column, where a
10279
+ * line has to start to belong inside the item. Spaces or tabs may follow the marker, counted to tab
10280
+ * stops. Five or more columns of them make the text an indented code line in CommonMark, so content
10281
+ * then sits one column after the marker.
10282
+ */
10283
+ function markerOf(raw) {
10284
+ const match = raw.match(/^([ \t]*)(?:([-*+])|(\d{1,9})[.)])([ \t]+)(\S.*)$/);
10285
+ if (!match) return null;
10286
+ const [, lead, bullet, number, gap, text] = match;
10287
+ const afterMarker = columnAfter(lead) + (bullet ? 1 : number.length + 1);
10288
+ const afterGap = columnAfter(gap, afterMarker);
10289
+ return {
10290
+ tag: bullet ? "ul" : "ol",
10291
+ start: bullet ? 1 : Number(number),
10292
+ text: text.trim(),
10293
+ content: afterGap - afterMarker > 4 ? afterMarker + 1 : afterGap
10294
+ };
10295
+ }
10263
10296
  function renderBlocks(text, lifted) {
10264
10297
  const out = [];
10265
10298
  let paragraph = [];
10266
10299
  let quote = [];
10267
10300
  let list = null;
10301
+ const startAttr = (l) => l.tag === "ol" && l.start !== 1 ? ` start="${l.start}"` : "";
10268
10302
  let html = null;
10269
10303
  const flush = () => {
10270
10304
  if (paragraph.length) out.push(`<p>${renderInline(paragraph.join(" "), lifted)}</p>`);
10271
10305
  if (quote.length) out.push(`<blockquote>${renderInline(quote.join(" "), lifted)}</blockquote>`);
10272
- if (list) out.push(`<${list.tag}>${list.items.map((item) => `<li>${renderInline(item, lifted)}</li>`).join("")}</${list.tag}>`);
10306
+ if (list) {
10307
+ const paragraphs = (texts) => texts.length === 1 ? renderInline(texts[0], lifted) : texts.map((text) => `<p>${renderInline(text, lifted)}</p>`).join("");
10308
+ const nested = (l) => `<${l.tag}${startAttr(l)}>${l.items.map((item) => `<li>${paragraphs(item.paras)}</li>`).join("")}</${l.tag}>`;
10309
+ const item = ({ blocks }) => {
10310
+ const inline = blocks.filter((block) => typeof block === "string").length === 1;
10311
+ return blocks.map((block) => typeof block !== "string" ? nested(block) : inline ? renderInline(block, lifted) : `<p>${renderInline(block, lifted)}</p>`).join("");
10312
+ };
10313
+ out.push(`<${list.tag}${startAttr(list)}>${list.items.map((entry) => `<li>${item(entry)}</li>`).join("")}</${list.tag}>`);
10314
+ }
10273
10315
  paragraph = [];
10274
10316
  quote = [];
10275
10317
  list = null;
@@ -10285,9 +10327,45 @@ function renderBlocks(text, lifted) {
10285
10327
  html = null;
10286
10328
  }
10287
10329
  if (!line) {
10288
- flush();
10330
+ if (list && !paragraph.length && !quote.length) list.gap = true;
10331
+ else flush();
10289
10332
  continue;
10290
10333
  }
10334
+ if (list?.items.length) {
10335
+ const item = list.items[list.items.length - 1];
10336
+ const tail = item.blocks[item.blocks.length - 1];
10337
+ const inner = typeof tail === "string" ? null : tail;
10338
+ const innerItem = inner?.items[inner.items.length - 1] ?? null;
10339
+ const indent = indentOf(raw);
10340
+ const marker = markerOf(raw);
10341
+ const gap = list.gap;
10342
+ list.gap = false;
10343
+ if (marker && indent >= item.content) {
10344
+ const joins = inner && innerItem && (indent >= innerItem.content || inner.tag === marker.tag);
10345
+ const entry = {
10346
+ paras: [marker.text],
10347
+ content: marker.content
10348
+ };
10349
+ if (joins) inner.items.push(entry);
10350
+ else item.blocks.push({
10351
+ tag: marker.tag,
10352
+ start: marker.start,
10353
+ items: [entry]
10354
+ });
10355
+ continue;
10356
+ }
10357
+ if (!marker && gap) {
10358
+ if (innerItem && indent >= innerItem.content) {
10359
+ innerItem.paras.push(line);
10360
+ continue;
10361
+ }
10362
+ if (indent >= item.content) {
10363
+ item.blocks.push(line);
10364
+ continue;
10365
+ }
10366
+ flush();
10367
+ } else if (marker && gap && marker.tag !== list.tag) flush();
10368
+ }
10291
10369
  if (lifted && startsHtmlBlock(line, lifted)) {
10292
10370
  flush();
10293
10371
  html = [escapeHtml(line)];
@@ -10310,28 +10388,22 @@ function renderBlocks(text, lifted) {
10310
10388
  out.push(`<h${level}>${renderInline(heading[2], lifted)}</h${level}>`);
10311
10389
  continue;
10312
10390
  }
10313
- const bullet = line.match(/^[-*+]\s+(.+)$/);
10314
- if (bullet) {
10315
- if (list?.tag !== "ul") {
10391
+ const marker = markerOf(raw);
10392
+ if (marker) {
10393
+ let open = list;
10394
+ if (open?.tag !== marker.tag) {
10316
10395
  flush();
10317
- list = {
10318
- tag: "ul",
10319
- items: []
10396
+ open = list = {
10397
+ tag: marker.tag,
10398
+ start: marker.start,
10399
+ items: [],
10400
+ gap: false
10320
10401
  };
10321
10402
  }
10322
- list.items.push(bullet[1]);
10323
- continue;
10324
- }
10325
- const ordered = line.match(/^\d{1,9}[.)]\s+(.+)$/);
10326
- if (ordered) {
10327
- if (list?.tag !== "ol") {
10328
- flush();
10329
- list = {
10330
- tag: "ol",
10331
- items: []
10332
- };
10333
- }
10334
- list.items.push(ordered[1]);
10403
+ open.items.push({
10404
+ blocks: [marker.text],
10405
+ content: marker.content
10406
+ });
10335
10407
  continue;
10336
10408
  }
10337
10409
  const quoted = line.match(/^>\s?(.*)$/);
@@ -10341,7 +10413,13 @@ function renderBlocks(text, lifted) {
10341
10413
  continue;
10342
10414
  }
10343
10415
  if (list && list.items.length) {
10344
- list.items[list.items.length - 1] += ` ${line}`;
10416
+ const { blocks } = list.items[list.items.length - 1];
10417
+ const last = blocks[blocks.length - 1];
10418
+ if (typeof last === "string") blocks[blocks.length - 1] = `${last} ${line}`;
10419
+ else {
10420
+ const paras = last.items[last.items.length - 1].paras;
10421
+ paras[paras.length - 1] += ` ${line}`;
10422
+ }
10345
10423
  continue;
10346
10424
  }
10347
10425
  if (quote.length) {
@@ -13586,6 +13664,13 @@ var TIMEOUT_MS = 1500;
13586
13664
  */
13587
13665
  function project(record) {
13588
13666
  const event = record.event;
13667
+ if (event.event === "installation_started") return {
13668
+ event: event.event,
13669
+ installationId: record.anonymousId,
13670
+ cliVersion: record.cliVersion,
13671
+ platform: record.platform,
13672
+ occurredAt: record.at
13673
+ };
13589
13674
  if (event.event !== "review_started" && event.event !== "review_finished") return null;
13590
13675
  const base = {
13591
13676
  event: event.event,
@@ -13614,13 +13699,38 @@ function send(record) {
13614
13699
  if (!telemetryUploadEnabled()) return;
13615
13700
  const payload = project(record);
13616
13701
  if (!payload) return;
13617
- fetch(endpoint(), {
13702
+ post(payload);
13703
+ } catch {}
13704
+ }
13705
+ /**
13706
+ * Send one record and say whether the server took it. Never rejects.
13707
+ *
13708
+ * For the one caller that has to know: `installation_started` is sent until it lands, so it needs
13709
+ * a yes it can trust, which is a 2xx and nothing else. Asks the same question `send` does before
13710
+ * anything goes over the wire.
13711
+ */
13712
+ async function deliver(record) {
13713
+ try {
13714
+ if (!telemetryUploadEnabled()) return false;
13715
+ const payload = project(record);
13716
+ return payload ? await post(payload) : false;
13717
+ } catch {
13718
+ return false;
13719
+ }
13720
+ }
13721
+ async function post(payload) {
13722
+ try {
13723
+ const response = await fetch(endpoint(), {
13618
13724
  method: "POST",
13619
13725
  headers: { "content-type": "application/json" },
13620
13726
  body: JSON.stringify(payload),
13621
13727
  signal: AbortSignal.timeout(TIMEOUT_MS)
13622
- }).catch(() => {});
13623
- } catch {}
13728
+ });
13729
+ await response.body?.cancel().catch(() => {});
13730
+ return response.ok;
13731
+ } catch {
13732
+ return false;
13733
+ }
13624
13734
  }
13625
13735
  /**
13626
13736
  * Where to. Overridable so the endpoint can be pointed at a local server while it is being built,
@@ -13645,29 +13755,44 @@ var LOG_PATH = join(STORE_ROOT, "events.jsonl");
13645
13755
  * The gate is on **transmission only**. Writing a file the developer owns and can read, delete or
13646
13756
  * disable is a different act from sending to a server, and the consent that suits one does not
13647
13757
  * carry to the other, so `events.jsonl` keeps recording the full event union either way and the
13648
- * question asked on first run is about the wire. Nineteen events are written here; two of them,
13758
+ * question asked on first run is about the wire. Twenty events are written here; three of them,
13649
13759
  * carrying eight fields between them, are the most that is ever sent.
13650
13760
  */
13651
13761
  function record(event) {
13652
13762
  try {
13653
- const config = loadConfig();
13654
- if (!config.telemetry) return;
13655
- const line = {
13656
- anonymousId: config.anonymousId,
13657
- cliVersion: BUILD_VERSION,
13658
- platform: platformName(),
13659
- at: (/* @__PURE__ */ new Date()).toISOString(),
13660
- event
13661
- };
13662
- mkdirSync(STORE_ROOT, { recursive: true });
13663
- appendFileSync(LOG_PATH, `${JSON.stringify(line)}\n`, { mode: 384 });
13763
+ const line = stamp(event);
13764
+ if (!line) return;
13765
+ append(line);
13664
13766
  send(line);
13665
13767
  } catch {}
13666
13768
  }
13769
+ /**
13770
+ * The line an event would be logged as, or `null` when the log is switched off.
13771
+ *
13772
+ * Split from `record` for `installation_started`, which is built and sent first and logged only
13773
+ * once the server has it: logging each attempt would leave one line per failed try for an event
13774
+ * that happens once.
13775
+ */
13776
+ function stamp(event) {
13777
+ const config = loadConfig();
13778
+ if (!config.telemetry) return null;
13779
+ return {
13780
+ anonymousId: config.anonymousId,
13781
+ cliVersion: BUILD_VERSION,
13782
+ platform: platformName(),
13783
+ at: (/* @__PURE__ */ new Date()).toISOString(),
13784
+ event
13785
+ };
13786
+ }
13787
+ /** Write one line to the log. Throws on a failed write; callers decide what that costs. */
13788
+ function append(line) {
13789
+ mkdirSync(STORE_ROOT, { recursive: true });
13790
+ appendFileSync(LOG_PATH, `${JSON.stringify(line)}\n`, { mode: 384 });
13791
+ }
13667
13792
  /** This review's own events, oldest first. The only reader, used to derive the rollup. */
13668
13793
  function readEvents(reviewId) {
13669
13794
  try {
13670
- return readFileSync(LOG_PATH, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((entry) => entry.event?.reviewId === reviewId).map((entry) => entry.event);
13795
+ return readFileSync(LOG_PATH, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)).map((entry) => entry.event).filter((event) => event !== void 0 && "reviewId" in event && event.reviewId === reviewId);
13671
13796
  } catch {
13672
13797
  return [];
13673
13798
  }
@@ -16152,4 +16277,4 @@ function lineageIds(id) {
16152
16277
  return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
16153
16278
  }
16154
16279
  //#endregion
16155
- export { REPOSITORY_SCOPE as $, presentedKissRunOf as $n, reviewsUrl as $t, reanalyseReview as A, saveJob as An, recommendationsOf as Ar, renderInlineText as At, RUNTIME_WORDS as B, followedRefName as Bn, clearAgentPreference as Br, removeWorktree as Bt, track as C, followedBranch as Cn, doneVerb as Cr, authorPreview as Ct, endRun as D, fail as Dn, questionsOutstanding as Dr, CODE_BLOCK_CLASS as Dt, completeRun as E, endedAt as En, issuesOutstanding as Er, renderMarkdown as Et, recordTelemetryUploadConsent as F, purposeOf as Fn, displayRef as Fr, MANAGED_ROOT as Ft, partFiles as G, kissDecisionsOf as Gn, AGENT_IDS as Gr, compactQueue as Gt, ARCHITECTURE_DIR as H, indexKeptIds as Hn, writeAgentPreference as Hr, modelHelpLines as Ht, telemetryUploadConsent as I, clearClaim as In, isCommitReview as Ir, createWorktree as It, storeArchitecture as J, lineagePosition as Jn, agentFor as Jr, readIndexToken as Jt, readArchitecture as K, latestKissRun as Kn, DEFAULT_AGENT_ID as Kr, queueForReview as Kt, waitingHistory as L, driverGone as Ln, git as Lr, holdWorktree as Lt, startJob as M, isLiveJob as Mn, WITHHELD_REASON as Mr, extractJsonText as Mt, readEvents as N, phasesFor as Nn, kissOutcome as Nr, objectWithAnyOf as Nt, reportRun as O, isTerminal as On, reviewerDispositions as Or, CODE_COPY_CLASS as Ot, record as P, progressSteps as Pn, resolveTarget as Pr, ReviewBeingDeletedError as Pt, QUOTE_LIMITS as Q, positionInLineage as Qn, readServerRecord as Qt, mapRuntimes as R, isClaimed as Rn, resolveAgent as Rr, isWorktreeHeld as Rt, isDriving as S, checkReview as Sn, readQueue as Sr, UNESTABLISHED_MOTIVATION as St, beginRun as T, allJobs as Tn, isQuestionOutstanding as Tr, GITHUB_MARKDOWN as Tt, checkArchitecture as U, indexReviewGroups as Un, CONFIG_PATH as Ur, relativeTime as Ut, describeRuntimes as V, groupByLineage as Vn, readAgentPreference as Vr, requestJobStop as Vt, loadArchitecture as W, kissDecisionRefusal as Wn, STORE_ROOT as Wr, activateQueued as Wt, trackedFiles as X, matchingWorkspaceIds as Xn, AgentCancelledError as Xr, clearServerRecordOwnedBy as Xt, storedScopes as Y, loadWorkspace as Yn, detectAgents as Yr, bootstrapUrl as Yt, ownersOf as Z, mutateWorkspace as Zn, AgentUnavailableError as Zr, ensureServer as Zt, liveUpdateFor as _, forgeResolver as _n, sectionOf as _r, removeNotification as _t, wasBlocked as a, writeServerRecord as an, touchWorkspace as ar, subjectOf as at, recordKissRun as b, fillFileUrlTemplate as bn, queueRemove as br, withUsageRecorded as bt, RefreshUnavailableError as c, feedbackUrl as cn, LIFECYCLES as cr, createPrUpdate as ct, mergeUpdateInto as d, MIN_WORKSPACE_PORT as dn, isLifecycle as dr, listNotifications as dt, serverEntryExists as en, previousKissResult as er, ancestorsOf as et, updateReview as f, parseWorkspacePort as fn, lifecycleChip as fr, markAllRead as ft, UpdateAlreadyRunningError as g, claimRequestUrl as gn, parseLifecycle as gr, readNotification as gt, askCheckout as h, writePortPreference as hn, offersLifecycle as hr, pruneNotifications as ht, liveJobsFor as i, workspaceUrl as in, summarise as ir, branchOf as it, runJob as j, PHASES as jn, describeAuthorship as jr, asString as jt, stopRun as k, loadJob as kn, processDiscussion as kr, escapeHtml as kt, cancelUpdate as l, DEFAULT_WORKSPACE_PORT as ln, activityAttributes as lr, eventName as lt, isUnchanged as m, statedWorkspacePort as mn, lifecyclePill as mr, markReadForReview as mt, deleteReview as n, stopChild as nn, reviewedCommit as nr, writeJsonAtomically as nt, discardJob as o, BUILD_VERSION as on, workspaceRevision as or, createArchitectureNotification as ot, refreshPrContext as p, resolveWorkspacePort as pn, lifecycleItemLabel as pr, markRead as pt, repositoryKey as q, lineageKeyFor as qn, agentById as qr, settleQueue as qt, lineageIds as r, stopServer as rn, reviewedRepositories as rr, announceJob as rt, FETCH_TIMEOUT_MS as s, PACKAGE_NAME as sn, workspaceStamps as sr, createPrState as st, deleteLineage as t, spawnServer as tn, recentWorkspaces as tr, isArchitectureId as tt, cancellableHere as u, MAX_WORKSPACE_PORT as un, activityLabel as ur, headline as ut, runningJobs as v, forgeFor as vn, queueAdd as vr, toneOf as vt, RUN_KINDS as w, reviewedShaOf as wn, isIssueOutstanding as wr, hasAuthorContent as wt, startKissJob as x, checkCodeFreshness as xn, queueStamp as xr, EXPLAIN_SIMPLY_PROMPT as xt, agentEnvOf as y, remoteHost as yn, queueAddMany as yr, unreadCount as yt, recordRuntimeBestEffort as z, decideKissRecommendation as zn, resolveModel as zr, readMarker as zt };
16280
+ export { storedScopes as $, loadWorkspace as $n, detectAgents as $r, bootstrapUrl as $t, reanalyseReview as A, endedAt as An, issuesOutstanding as Ar, renderMarkdown as At, telemetryUploadEnabled as B, clearClaim as Bn, isCommitReview as Br, createWorktree as Bt, track as C, remoteHost as Cn, queueAddMany as Cr, unreadCount as Ct, endRun as D, followedBranch as Dn, doneVerb as Dr, authorPreview as Dt, completeRun as E, checkReview as En, readQueue as Er, UNESTABLISHED_MOTIVATION as Et, record as F, PHASES as Fn, describeAuthorship as Fr, asString as Ft, describeRuntimes as G, groupByLineage as Gn, readAgentPreference as Gr, requestJobStop as Gt, mapRuntimes as H, isClaimed as Hn, resolveAgent as Hr, isWorktreeHeld as Ht, stamp as I, isLiveJob as In, WITHHELD_REASON as Ir, extractJsonText as It, loadArchitecture as J, kissDecisionRefusal as Jn, STORE_ROOT as Jr, activateQueued as Jt, ARCHITECTURE_DIR as K, indexKeptIds as Kn, writeAgentPreference as Kr, modelHelpLines as Kt, deliver as L, phasesFor as Ln, kissOutcome as Lr, objectWithAnyOf as Lt, startJob as M, isTerminal as Mn, reviewerDispositions as Mr, CODE_COPY_CLASS as Mt, append as N, loadJob as Nn, processDiscussion as Nr, escapeHtml as Nt, reportRun as O, reviewedShaOf as On, isIssueOutstanding as Or, hasAuthorContent as Ot, readEvents as P, saveJob as Pn, recommendationsOf as Pr, renderInlineText as Pt, storeArchitecture as Q, lineagePosition as Qn, agentFor as Qr, readIndexToken as Qt, recordTelemetryUploadConsent as R, progressSteps as Rn, resolveTarget as Rr, ReviewBeingDeletedError as Rt, isDriving as S, forgeFor as Sn, queueAdd as Sr, toneOf as St, beginRun as T, checkCodeFreshness as Tn, queueStamp as Tr, EXPLAIN_SIMPLY_PROMPT as Tt, recordRuntimeBestEffort as U, decideKissRecommendation as Un, resolveModel as Ur, readMarker as Ut, waitingHistory as V, driverGone as Vn, git as Vr, holdWorktree as Vt, RUNTIME_WORDS as W, followedRefName as Wn, clearAgentPreference as Wr, removeWorktree as Wt, readArchitecture as X, latestKissRun as Xn, DEFAULT_AGENT_ID as Xr, queueForReview as Xt, partFiles as Y, kissDecisionsOf as Yn, AGENT_IDS as Yr, compactQueue as Yt, repositoryKey as Z, lineageKeyFor as Zn, agentById as Zr, settleQueue as Zt, liveUpdateFor as _, resolveWorkspacePort as _n, lifecycleItemLabel as _r, markRead as _t, wasBlocked as a, spawnServer as an, recentWorkspaces as ar, isArchitectureId as at, recordKissRun as b, claimRequestUrl as bn, parseLifecycle as br, readNotification as bt, RefreshUnavailableError as c, workspaceUrl as cn, summarise as cr, branchOf as ct, mergeUpdateInto as d, PACKAGE_NAME as dn, workspaceStamps as dr, createPrState as dt, AgentCancelledError as ei, clearServerRecordOwnedBy as en, matchingWorkspaceIds as er, trackedFiles as et, updateReview as f, feedbackUrl as fn, LIFECYCLES as fr, createPrUpdate as ft, UpdateAlreadyRunningError as g, parseWorkspacePort as gn, lifecycleChip as gr, markAllRead as gt, askCheckout as h, MIN_WORKSPACE_PORT as hn, isLifecycle as hr, listNotifications as ht, liveJobsFor as i, serverEntryExists as in, previousKissResult as ir, ancestorsOf as it, runJob as j, fail as jn, questionsOutstanding as jr, CODE_BLOCK_CLASS as jt, stopRun as k, allJobs as kn, isQuestionOutstanding as kr, GITHUB_MARKDOWN as kt, cancelUpdate as l, writeServerRecord as ln, touchWorkspace as lr, subjectOf as lt, isUnchanged as m, MAX_WORKSPACE_PORT as mn, activityLabel as mr, headline as mt, deleteReview as n, readServerRecord as nn, positionInLineage as nr, QUOTE_LIMITS as nt, discardJob as o, stopChild as on, reviewedCommit as or, writeJsonAtomically as ot, refreshPrContext as p, DEFAULT_WORKSPACE_PORT as pn, activityAttributes as pr, eventName as pt, checkArchitecture as q, indexReviewGroups as qn, CONFIG_PATH as qr, relativeTime as qt, lineageIds as r, reviewsUrl as rn, presentedKissRunOf as rr, REPOSITORY_SCOPE as rt, FETCH_TIMEOUT_MS as s, stopServer as sn, reviewedRepositories as sr, announceJob as st, deleteLineage as t, AgentUnavailableError as ti, ensureServer as tn, mutateWorkspace as tr, ownersOf as tt, cancellableHere as u, BUILD_VERSION as un, workspaceRevision as ur, createArchitectureNotification as ut, runningJobs as v, statedWorkspacePort as vn, lifecyclePill as vr, markReadForReview as vt, RUN_KINDS as w, fillFileUrlTemplate as wn, queueRemove as wr, withUsageRecorded as wt, startKissJob as x, forgeResolver as xn, sectionOf as xr, removeNotification as xt, agentEnvOf as y, writePortPreference as yn, offersLifecycle as yr, pruneNotifications as yt, telemetryUploadConsent as z, purposeOf as zn, displayRef as zr, MANAGED_ROOT as zt };
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $t as reviewsUrl, A as reanalyseReview, Bn as followedRefName, Bt as removeWorktree, F as recordTelemetryUploadConsent, Fn as purposeOf, Fr as displayRef, Ft as MANAGED_ROOT, Gr as AGENT_IDS, Hr as writeAgentPreference, Ht as modelHelpLines, I as telemetryUploadConsent, In as clearClaim, Jt as readIndexToken, Kr as DEFAULT_AGENT_ID, Kt as queueForReview, Ln as driverGone, Lr as git, M as startJob, Mr as WITHHELD_REASON, Nr as kissOutcome, On as isTerminal, P as record$1, Pn as progressSteps, Pr as resolveTarget, Qn as positionInLineage, Rn as isClaimed, Rr as resolveAgent, Sn as checkReview, Tn as allJobs, Ur as CONFIG_PATH, Ut as relativeTime, Vn as groupByLineage, Vr as readAgentPreference, Wr as STORE_ROOT, Xn as matchingWorkspaceIds, Yn as loadWorkspace, Yr as detectAgents, Yt as bootstrapUrl, Zn as mutateWorkspace, Zt as ensureServer, _ as liveUpdateFor, _n as forgeResolver, a as wasBlocked, b as recordKissRun, d as mergeUpdateInto, dn as MIN_WORKSPACE_PORT, f as updateReview, fn as parseWorkspacePort, g as UpdateAlreadyRunningError, hn as writePortPreference, i as liveJobsFor, in as workspaceUrl, ir as summarise, j as runJob, jn as PHASES, kn as loadJob, ln as DEFAULT_WORKSPACE_PORT, mn as statedWorkspacePort, n as deleteReview$1, on as BUILD_VERSION, qn as lineageKeyFor, qr as agentById, rn as stopServer, rr as reviewedRepositories, sn as PACKAGE_NAME, tr as recentWorkspaces, un as MAX_WORKSPACE_PORT, v as runningJobs, zr as resolveModel, zt as readMarker } from "./delete_review-LzUgEUzN.js";
2
+ import { $n as loadWorkspace, $r as detectAgents, $t as bootstrapUrl, A as reanalyseReview, B as telemetryUploadEnabled, Bn as clearClaim, En as checkReview, F as record$1, Fn as PHASES, Gn as groupByLineage, Gr as readAgentPreference, Hn as isClaimed, Hr as resolveAgent, I as stamp, Ir as WITHHELD_REASON, Jr as STORE_ROOT, Kr as writeAgentPreference, Kt as modelHelpLines, L as deliver, Lr as kissOutcome, M as startJob, Mn as isTerminal, N as append, Nn as loadJob, Qt as readIndexToken, R as recordTelemetryUploadConsent, Rn as progressSteps, Rr as resolveTarget, Ur as resolveModel, Ut as readMarker, Vn as driverGone, Vr as git, Wn as followedRefName, Wt as removeWorktree, Xr as DEFAULT_AGENT_ID, Xt as queueForReview, Yr as AGENT_IDS, Zn as lineageKeyFor, Zr as agentById, _ as liveUpdateFor, a as wasBlocked, ar as recentWorkspaces, b as recordKissRun, cn as workspaceUrl, cr as summarise, d as mergeUpdateInto, dn as PACKAGE_NAME, er as matchingWorkspaceIds, f as updateReview, g as UpdateAlreadyRunningError, gn as parseWorkspacePort, hn as MIN_WORKSPACE_PORT, i as liveJobsFor, j as runJob, kn as allJobs, mn as MAX_WORKSPACE_PORT, n as deleteReview$1, nr as positionInLineage, pn as DEFAULT_WORKSPACE_PORT, qr as CONFIG_PATH, qt as relativeTime, rn as reviewsUrl, sn as stopServer, sr as reviewedRepositories, tn as ensureServer, tr as mutateWorkspace, un as BUILD_VERSION, v as runningJobs, vn as statedWorkspacePort, xn as forgeResolver, yn as writePortPreference, z as telemetryUploadConsent, zn as purposeOf, zr as displayRef, zt as MANAGED_ROOT } from "./delete_review-l4p_p6oh.js";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -107,15 +107,24 @@ var AUDIENCES = {
107
107
  reviewer: "writes it for somebody reviewing somebody else's work, with whether each one is worth raising"
108
108
  };
109
109
  function parse(argv) {
110
- if (argv.length === 0) return { name: "help" };
110
+ if (argv.length === 0) return {
111
+ name: "help",
112
+ via: "bare"
113
+ };
111
114
  const [head, ...rest] = argv;
112
- if (head === "--help" || head === "-h" || head === "help") return { name: "help" };
115
+ if (head === "--help" || head === "-h" || head === "help") return {
116
+ name: "help",
117
+ via: "asked"
118
+ };
113
119
  if (head === "--version" || head === "-v") return { name: "version" };
114
120
  if (head === void 0 || !(head in VALUED)) return {
115
121
  name: "error",
116
122
  message: unknownCommand(head)
117
123
  };
118
- if (rest.includes("--help") || rest.includes("-h")) return { name: "help" };
124
+ if (rest.includes("--help") || rest.includes("-h")) return {
125
+ name: "help",
126
+ via: "asked"
127
+ };
119
128
  const values = {};
120
129
  const flags = /* @__PURE__ */ new Set();
121
130
  const positional = [];
@@ -1217,12 +1226,26 @@ function unknownSetting(key, known) {
1217
1226
  * little else is attached to it. "Anonymous" is a word with a technical meaning strong enough to be
1218
1227
  * challenged on, and the claim below does not need it to be true.
1219
1228
  */
1229
+ /**
1230
+ * The one addition since the notice an installation already saw, or the yes it already gave. Not the
1231
+ * notice again: repeating what somebody was told reads as something else having changed.
1232
+ */
1233
+ function telemetryFirstUseNotice() {
1234
+ return [
1235
+ "",
1236
+ "Usage telemetry now also records first use: one event, once per installation, with the",
1237
+ "same version, platform and timestamp fields as before. Nothing else changed.",
1238
+ "Turn it off: `prreviewbuddy config set telemetry-upload off`",
1239
+ "Details: https://prreviewbuddy.com/privacy",
1240
+ ""
1241
+ ].join("\n");
1242
+ }
1220
1243
  function telemetryNotice() {
1221
1244
  return [
1222
1245
  "",
1223
- "Non-identifying usage telemetry is on by default: review start and finish, version,",
1224
- "platform, agent, success and duration. Never your code, diffs, repository names or",
1225
- "review content.",
1246
+ "Non-identifying usage telemetry is on by default: first use, review start and finish,",
1247
+ "version, platform, agent, success and duration. Never your code, diffs, repository",
1248
+ "names or review content.",
1226
1249
  "Turn it off: `prreviewbuddy config set telemetry-upload off`",
1227
1250
  "Details: https://prreviewbuddy.com/privacy",
1228
1251
  ""
@@ -3413,6 +3436,7 @@ async function uninstall(command, deps = REAL) {
3413
3436
  */
3414
3437
  var GREETING = "onboarding-shown";
3415
3438
  var TELEMETRY_NOTICE = "telemetry-notice-shown";
3439
+ var FIRST_USE_NOTICE = "telemetry-first-use-notice-shown";
3416
3440
  /** Whether this machine has been greeted. */
3417
3441
  function greeted() {
3418
3442
  return shown(GREETING);
@@ -3433,6 +3457,13 @@ function telemetryNoticed() {
3433
3457
  function recordTelemetryNotice() {
3434
3458
  record(TELEMETRY_NOTICE);
3435
3459
  }
3460
+ /** Whether this machine has been told that 0.63.0 added `installation_started` to what is sent. */
3461
+ function firstUseNoticed() {
3462
+ return shown(FIRST_USE_NOTICE);
3463
+ }
3464
+ function recordFirstUseNotice() {
3465
+ record(FIRST_USE_NOTICE);
3466
+ }
3436
3467
  /**
3437
3468
  * Unreadable reads as already shown.
3438
3469
  *
@@ -3473,16 +3504,108 @@ function record(name) {
3473
3504
  * Both are left alone, which is why the claim everywhere else is "every installation whose
3474
3505
  * behaviour changes is told once" rather than "every machine is told".
3475
3506
  *
3507
+ * **0.63.0 is such a change for everybody who sends.** `installation_started` joined what leaves
3508
+ * the machine, so an installation told the old list, or that said yes to it, is told once about the
3509
+ * addition, under its own marker. A no is still told nothing.
3510
+ *
3476
3511
  * **No terminal check.** The old question could not be asked without one. A disclosure has nobody
3477
3512
  * to wait for, and a headless install that transmits is exactly the install that must not be the
3478
3513
  * one nobody ever told. It costs a CI log three lines on a persisted home, once.
3479
3514
  */
3480
3515
  function noticeTelemetry(write) {
3481
3516
  if (process.env.PRB_TELEMETRY === "off") return;
3482
- if (telemetryUploadConsent() !== null) return;
3483
- if (telemetryNoticed()) return;
3484
- write(telemetryNotice());
3485
- recordTelemetryNotice();
3517
+ const consent = telemetryUploadConsent();
3518
+ if (consent === false) return;
3519
+ if (consent === null && !telemetryNoticed()) {
3520
+ write(telemetryNotice());
3521
+ recordTelemetryNotice();
3522
+ recordFirstUseNotice();
3523
+ return;
3524
+ }
3525
+ if (firstUseNoticed()) return;
3526
+ write(telemetryFirstUseNotice());
3527
+ recordFirstUseNotice();
3528
+ }
3529
+ //#endregion
3530
+ //#region src/product_commands.ts
3531
+ /**
3532
+ * Whether this command is somebody using the product, rather than asking about the binary or
3533
+ * changing a setting.
3534
+ *
3535
+ * One answer for two things, on purpose. These commands print the one-time telemetry notice, and
3536
+ * they are the commands that may send `installation_started`. If the lists ever differed, a
3537
+ * command could transmit before the installation had been told telemetry is on.
3538
+ *
3539
+ * `prreviewbuddy` on its own counts: it is somebody entering the product to see what it does.
3540
+ * `--help` and `--version` do not, nor `config` (where the setting is changed, and turning it off
3541
+ * must not need a disclosure first) or `uninstall`.
3542
+ */
3543
+ function isProductCommand(command) {
3544
+ switch (command.name) {
3545
+ case "review":
3546
+ case "open":
3547
+ case "agents": return true;
3548
+ case "help": return command.via === "bare";
3549
+ default: return false;
3550
+ }
3551
+ }
3552
+ //#endregion
3553
+ //#region ../../packages/review-harness/src/telemetry/installation_started.ts
3554
+ /**
3555
+ * Tell the server, once, that this installation was used.
3556
+ *
3557
+ * The review events only fire from inside a review job, so an installation that was tried and
3558
+ * left before its first review was invisible: nothing told "nobody installs this" apart from
3559
+ * "people install it and stop before reviewing". This is the event that sits in front of them.
3560
+ *
3561
+ * It means first meaningful use that telemetry saw, and the name has to survive that reading:
3562
+ * an installation from before the event existed sends it on its first command after upgrading,
3563
+ * and nothing on this machine knows when the package was actually installed.
3564
+ *
3565
+ * **Sent until it lands.** Every other event is best effort, because losing one review out of a
3566
+ * thousand changes nothing. Losing this one loses the installation from the funnel, so the marker
3567
+ * is written only after a 2xx, and every meaningful command until then tries again. Any failure
3568
+ * retries, a 422 included: a CLI that reached somebody before the API learned the event recovers
3569
+ * on its own once it has. `occurredAt` is the attempt's own time, never a remembered first try,
3570
+ * because the server refuses a timestamp more than two days from when it arrives and a stored
3571
+ * one would be refused forever.
3572
+ *
3573
+ * **Once is kept by the server, not here.** This side cannot know that a request it saw fail was
3574
+ * not stored: an acknowledgement lost after the row committed looks exactly like a failure, and two
3575
+ * commands started together on one home both pass the marker check before either has written it.
3576
+ * Both resend. The API holds one `installation_started` per installation under a unique index and
3577
+ * answers a repeat with a 200 it does not store, which also stops the retrying. The marker only
3578
+ * saves requests.
3579
+ */
3580
+ var MARKER = join(STORE_ROOT, "installation-started-sent");
3581
+ /**
3582
+ * Send `installation_started` if this installation has not had it accepted yet. Never rejects.
3583
+ *
3584
+ * Asks whether it may send before it reads anything, so under `PRB_TELEMETRY=off` it creates no
3585
+ * identity, no marker and no log line: the kill switch is not a decision, and a machine that
3586
+ * unsets it tomorrow has still never been counted.
3587
+ */
3588
+ async function announceInstallation() {
3589
+ try {
3590
+ if (!telemetryUploadEnabled()) return;
3591
+ if (announced()) return;
3592
+ const line = stamp({ event: "installation_started" });
3593
+ if (!line || !await deliver(line)) return;
3594
+ writeMarker();
3595
+ append(line);
3596
+ } catch {}
3597
+ }
3598
+ /** An unreadable home counts as already sent: failing closed on repetition. */
3599
+ function announced() {
3600
+ try {
3601
+ return existsSync(MARKER);
3602
+ } catch {
3603
+ return true;
3604
+ }
3605
+ }
3606
+ function writeMarker() {
3607
+ mkdirSync(STORE_ROOT, { recursive: true });
3608
+ writeFileSync(MARKER, `${(/* @__PURE__ */ new Date()).toISOString()}\n`, { mode: 384 });
3486
3609
  }
3487
3610
  //#endregion
3488
3611
  //#region src/main.ts
@@ -3520,28 +3643,33 @@ function greet(command) {
3520
3643
  /**
3521
3644
  * The one-time telemetry disclosure, printed after the hello and before the work.
3522
3645
  *
3523
- * The greeting's commands minus `config`, which is where the setting is changed: somebody typing
3646
+ * On the commands `isProductCommand` names, which are also the ones that may send
3647
+ * `installation_started`, so nothing is transmitted from a command that could not have disclosed
3648
+ * it. Not `config`, which is where the setting is changed: somebody typing
3524
3649
  * `config set telemetry-upload off` should be able to turn it off without being told first that it
3525
- * is on. That was true when this was a question and it is still true now it is a notice.
3650
+ * is on.
3526
3651
  *
3527
3652
  * On stderr, like every other line here that is not a result, so `prreviewbuddy review | pbcopy`
3528
3653
  * still copies a URL. Unlike the question it replaced it takes no input and checks for no terminal:
3529
3654
  * there is nobody to wait for, and a headless install that transmits is the one that most needs
3530
3655
  * telling.
3531
3656
  */
3532
- var NOTIFIED = [
3533
- "review",
3534
- "open",
3535
- "agents"
3536
- ];
3537
3657
  function notify(command) {
3538
- if (!NOTIFIED.includes(command.name)) return;
3658
+ if (!isProductCommand(command)) return;
3539
3659
  noticeTelemetry((chunk) => void process.stderr.write(chunk));
3540
3660
  }
3541
3661
  async function main(argv) {
3542
3662
  const command = parse(argv);
3543
3663
  greet(command);
3544
3664
  notify(command);
3665
+ const announced = isProductCommand(command) ? announceInstallation() : Promise.resolve();
3666
+ try {
3667
+ return await run(command);
3668
+ } finally {
3669
+ await announced;
3670
+ }
3671
+ }
3672
+ async function run(command) {
3545
3673
  switch (command.name) {
3546
3674
  case "help":
3547
3675
  out(HELP);