@motir/cli 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +53 -5
  2. package/dist/index.js +583 -63
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -43,9 +43,46 @@ pnpm --filter @motir/cli build # produces dist/index.js (the `motir` binary
43
43
  node packages/cli/dist/index.js --help
44
44
  ```
45
45
 
46
- ## Commands (this subtask)
46
+ ## Authenticate
47
47
 
48
48
  ```sh
49
+ motir login
50
+ ```
51
+
52
+ `motir login` is a **device grant**: it prints a short code and a URL, opens
53
+ Motir in your browser, and waits for you to approve it there. Codes last 15
54
+ minutes and nothing is written to disk until you approve. It is **headless by
55
+ construction** — the code and URL are printed whether or not a browser opens, so
56
+ an SSH session or a container uses the same command; `--no-browser` skips the
57
+ launch attempt outright.
58
+
59
+ The approval mints a CLI-scoped PAT: scopes `read`, `work_items:write`,
60
+ `integration` (fixed — the grant can neither widen nor narrow them), 90-day
61
+ expiry, labelled `CLI · <hostname>`. It lands in `~/.config/motir/config.json`,
62
+ `chmod 600`, keyed by server URL.
63
+
64
+ There are three credential tiers, and `motir login` is the middle one:
65
+
66
+ | Tier | How | For |
67
+ | -------------------------- | ---------------------------------- | ------------------------------- |
68
+ | `MOTIR_TOKEN` | export it — no login step, no file | CI, containers, read-only boxes |
69
+ | `motir login` | browser approval | a person at a terminal |
70
+ | `motir auth login --token` | paste a PAT you already hold | scripts, older servers |
71
+
72
+ `MOTIR_TOKEN` is honoured by **every** command (paired with `MOTIR_SERVER` when
73
+ there is no `.motir.json` to walk up to) and outranks a stored credential.
74
+
75
+ **Disconnecting** is two different things: `motir logout` forgets the local copy
76
+ on this machine, while **revoking the token in Settings → Account → API tokens**
77
+ is the server-side kill switch. Full guide: [`docs/cli.md` §
78
+ Authenticate](../../docs/cli.md#authenticate).
79
+
80
+ ## Setup commands
81
+
82
+ ```sh
83
+ motir login [--server <url>] [--no-browser] # connect this terminal
84
+ motir logout [--server <url>] # forget the local credential
85
+
49
86
  motir auth login [--server <url>] [--token <pat>] # validate + store a PAT
50
87
  motir auth status [--server <url>] # server, token prefix, owning user
51
88
  motir auth logout [--server <url>] # forget the stored token
@@ -67,10 +104,19 @@ motir ready [--kinds <list>] [--assignee <id|me|unassigned>] [--json]
67
104
  motir status [--json] # ready / in-flight + active sprint
68
105
  motir sprints [--state <planned|active|complete>] [--json]
69
106
  motir sprint [ref] [--kinds <list>] [--json] # defaults to the ACTIVE sprint
70
- motir show <key> [--json] # one item, in full
107
+ motir show <key> [--activity|--comments] [--json] # one item, in full
71
108
  motir open <key> [--print] # …in the browser
72
109
  ```
73
110
 
111
+ **`motir show --activity` / `--comments`** add the DISCUSSION the aggregate does
112
+ not carry: `--activity` interleaves comment threads with the change trail,
113
+ `--comments` prints the threads alone. Both are opt-in — the stream is a second
114
+ tool call, so a plain `show` stays one round-trip no matter how loud a card got —
115
+ and comment bodies print **in full**, never excerpted. ONE page is printed and
116
+ never drained: when more remains the footer says how much and points at `motir
117
+ open <key>`; with `--json` the activity page rides along **unaltered** under an
118
+ `activity` key. Passing both flags is refused rather than silently resolved.
119
+
74
120
  `motir sprint`'s **`ref` resolves in order**: omitted → the active sprint; then a
75
121
  sprint **id**; then an **exact name**; then a **name prefix** — each
76
122
  case-insensitive, first rule yielding exactly one sprint wins. An ambiguous
@@ -154,9 +200,11 @@ the item in the session exclude list so the next `motir next` moves past it;
154
200
  > `in_progress` status, so if the store cannot be written the CLI warns once and
155
201
  > the run continues rather than aborting.
156
202
 
157
- **Closing out.** After you merge, `motir done <key>`. The default workflow has
158
- no direct `in_progress → done` edge, so an item dispatched with `--print` (which
159
- never observed an agent finish) needs `motir done --via in_review <key>`; an
203
+ **Closing out.** After you merge, `motir done <key>`. Since MOTIR-1625 the
204
+ default workflow carries a direct `in_progress → done` edge, so this is one legal
205
+ hop whether or not the item ever reached In Review. `motir done --via in_review
206
+ <key>` still walks it through review explicitly — useful on a custom workflow
207
+ that has no direct edge, or when you want the review hop on the record. An
160
208
  illegal flip surfaces the server's own allowed-targets error verbatim. A merged
161
209
  **session** PR closes out in bulk with `motir done --session <branch>`.
162
210
 
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Command as Command2 } from "commander";
5
5
 
6
6
  // src/version.ts
7
- var CLI_VERSION = "0.1.0";
7
+ var CLI_VERSION = "0.1.1";
8
8
 
9
9
  // src/mcpClient.ts
10
10
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -220,6 +220,12 @@ var MotirClient = class {
220
220
  whoami() {
221
221
  return this.callStructured("whoami");
222
222
  }
223
+ /** The token workspace's browsable projects (MOTIR-1879). Takes no arguments:
224
+ * the workspace is the one the PAT is bound to. An empty workspace is an
225
+ * EMPTY LIST, not an error. */
226
+ listProjects() {
227
+ return this.callStructured("list_projects");
228
+ }
223
229
  listReady(args) {
224
230
  return this.callStructured("list_ready", { ...args });
225
231
  }
@@ -229,6 +235,21 @@ var MotirClient = class {
229
235
  getWorkItem(key) {
230
236
  return this.callStructured("get_work_item", { key });
231
237
  }
238
+ /**
239
+ * ONE page of a work item's discussion + change trail (MOTIR-1999).
240
+ *
241
+ * A SECOND call, made only when `motir show` is asked for it: the default read
242
+ * stays one round-trip, so a card with two hundred comments never slows down
243
+ * `show` or the dispatch path that leans on it.
244
+ *
245
+ * `cursor` and `order` are pass-through in both directions — the `all` cursor
246
+ * is an OPAQUE composite carrying both sources' positions, so the CLI must
247
+ * never construct, parse or merge one. Omitting `order` leaves each view on
248
+ * its own shipped default rather than the client inventing one.
249
+ */
250
+ getWorkItemActivity(args) {
251
+ return this.callStructured("get_work_item_activity", { ...args });
252
+ }
232
253
  transitionStatus(args) {
233
254
  return this.callStructured("transition_status", { ...args });
234
255
  }
@@ -351,25 +372,25 @@ function isInteractive() {
351
372
  function promptLine(question, defaultValue) {
352
373
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
353
374
  const rl = createInterface({ input: process.stdin, output: process.stderr });
354
- return new Promise((resolve3) => {
375
+ return new Promise((resolve4) => {
355
376
  rl.question(`${question}${suffix}: `, (answer) => {
356
377
  rl.close();
357
378
  const trimmed = answer.trim();
358
- resolve3(trimmed.length > 0 ? trimmed : defaultValue ?? "");
379
+ resolve4(trimmed.length > 0 ? trimmed : defaultValue ?? "");
359
380
  });
360
381
  });
361
382
  }
362
383
  function promptLineOrNull(question) {
363
384
  const rl = createInterface({ input: process.stdin, output: process.stderr });
364
- return new Promise((resolve3) => {
385
+ return new Promise((resolve4) => {
365
386
  let answered = false;
366
387
  rl.question(`${question}`, (answer) => {
367
388
  answered = true;
368
389
  rl.close();
369
- resolve3(answer);
390
+ resolve4(answer);
370
391
  });
371
392
  rl.on("close", () => {
372
- if (!answered) resolve3(null);
393
+ if (!answered) resolve4(null);
373
394
  });
374
395
  });
375
396
  }
@@ -378,11 +399,11 @@ function promptSecret(question) {
378
399
  rl._writeToOutput = () => {
379
400
  };
380
401
  process.stderr.write(`${question}: `);
381
- return new Promise((resolve3) => {
402
+ return new Promise((resolve4) => {
382
403
  rl.question("", (answer) => {
383
404
  rl.close();
384
405
  process.stderr.write("\n");
385
- resolve3(answer.trim());
406
+ resolve4(answer.trim());
386
407
  });
387
408
  });
388
409
  }
@@ -533,6 +554,307 @@ async function authLogout(opts) {
533
554
  }
534
555
  }
535
556
 
557
+ // src/commands/login.ts
558
+ import { hostname } from "os";
559
+ import { setTimeout as delay } from "timers/promises";
560
+
561
+ // src/browser.ts
562
+ import { spawn } from "child_process";
563
+ function launcher(platform) {
564
+ if (platform === "darwin") return { cmd: "open", args: [] };
565
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
566
+ return { cmd: "xdg-open", args: [] };
567
+ }
568
+ function hasDisplay(env, platform) {
569
+ if (platform === "darwin" || platform === "win32") return true;
570
+ return Boolean(env["DISPLAY"] || env["WAYLAND_DISPLAY"]);
571
+ }
572
+ function openUrl(url, opts = {}) {
573
+ const platform = opts.platform ?? process.platform;
574
+ const env = opts.env ?? process.env;
575
+ if (!hasDisplay(env, platform)) return Promise.resolve(false);
576
+ const { cmd, args } = launcher(platform);
577
+ return new Promise((resolve4) => {
578
+ try {
579
+ const child = spawn(cmd, [...args, url], { stdio: "ignore", detached: true });
580
+ child.on("error", () => resolve4(false));
581
+ child.unref();
582
+ setTimeout(() => resolve4(true), 0);
583
+ } catch {
584
+ resolve4(false);
585
+ }
586
+ });
587
+ }
588
+
589
+ // src/projectLink.ts
590
+ import { homedir as homedir2 } from "os";
591
+ import { resolve as resolve2 } from "path";
592
+ function describeProject(resolved, workspace) {
593
+ const where = resolved.sole ? `the only project in workspace ${workspace}` : `in workspace ${workspace}`;
594
+ return `${resolved.project.key} \u2014 ${resolved.project.name} (${where})`;
595
+ }
596
+ function nextLinkCommand(projectKey) {
597
+ return projectKey ? `motir link --project ${projectKey}` : "motir link";
598
+ }
599
+ async function resolveProject(client, workspace, serverUrl) {
600
+ const { projects } = await client.listProjects();
601
+ if (projects.length === 0) {
602
+ throw new CliError(`The workspace ${workspace} has no projects yet.`, {
603
+ hint: `Create one at ${serverUrl}, then run \`${nextLinkCommand()}\` again.`
604
+ });
605
+ }
606
+ const only = projects[0];
607
+ if (projects.length === 1 && only) return { project: only, sole: true };
608
+ const keys = projects.map((p) => p.key);
609
+ if (!isInteractive()) {
610
+ throw new CliError(`The workspace ${workspace} has ${projects.length} projects.`, {
611
+ hint: `Pass --project <key> \u2014 one of: ${keys.join(", ")}.`
612
+ });
613
+ }
614
+ info(`Projects in ${workspace}:`);
615
+ projects.forEach((p, index) => info(` ${index + 1}) ${p.key} \u2014 ${p.name}`));
616
+ const answer = await promptLine(`Project [1-${projects.length}, or a key]`);
617
+ const picked = pickProject(projects, answer);
618
+ if (!picked) {
619
+ throw new CliError(`"${answer}" is not one of this workspace\u2019s projects.`, {
620
+ hint: `Choose 1-${projects.length}, or pass --project <key> \u2014 one of: ${keys.join(", ")}.`
621
+ });
622
+ }
623
+ return { project: picked, sole: false };
624
+ }
625
+ function pickProject(projects, answer) {
626
+ const trimmed = answer.trim();
627
+ if (/^\d+$/.test(trimmed)) return projects[Number(trimmed) - 1];
628
+ return projects.find((p) => p.key.toLowerCase() === trimmed.toLowerCase());
629
+ }
630
+ async function autoLinkAfterLogin(input) {
631
+ const cwd = resolve2(input.cwd ?? process.cwd());
632
+ const home = resolve2(input.home ?? homedir2());
633
+ let existing;
634
+ try {
635
+ existing = findLink(cwd);
636
+ } catch {
637
+ info(`Found a .motir.json that could not be read \u2014 leaving it alone.`);
638
+ info(`Run \`${nextLinkCommand()}\` to rewrite it once it is fixed.`);
639
+ return null;
640
+ }
641
+ if (existing) {
642
+ info(
643
+ `Already linked: ${existing.dir} \u2192 ${existing.config.workspace}/${existing.config.project}.`
644
+ );
645
+ info(`Run \`${nextLinkCommand(existing.config.project)}\` here to change it.`);
646
+ return null;
647
+ }
648
+ if (cwd === home) {
649
+ info(`Not linking your home directory \u2014 a link there would bind every folder under it.`);
650
+ info(`cd to your workspace root and run \`${nextLinkCommand()}\`.`);
651
+ return null;
652
+ }
653
+ let projects;
654
+ try {
655
+ projects = await input.listProjects();
656
+ } catch {
657
+ info(`Run \`${nextLinkCommand()}\` in your workspace root to bind it to a project.`);
658
+ return null;
659
+ }
660
+ const only = projects[0];
661
+ if (projects.length !== 1 || !only) {
662
+ if (projects.length === 0) {
663
+ info(`No projects in workspace ${input.workspace} yet \u2014 create one at ${input.serverUrl},`);
664
+ info(`then run \`${nextLinkCommand()}\` in your workspace root.`);
665
+ } else {
666
+ const keys = projects.map((p) => p.key).join(", ");
667
+ info(`${projects.length} projects in workspace ${input.workspace}: ${keys}.`);
668
+ info(`Run \`${nextLinkCommand("<key>")}\` in your workspace root to pick one.`);
669
+ }
670
+ return null;
671
+ }
672
+ const config = {
673
+ serverUrl: input.serverUrl,
674
+ workspace: input.workspace,
675
+ project: only.key
676
+ };
677
+ let path;
678
+ try {
679
+ path = writeLink(cwd, config);
680
+ } catch {
681
+ info(`Could not write a link in ${cwd}.`);
682
+ info(`Run \`${nextLinkCommand(only.key)}\` from a writable workspace root.`);
683
+ return null;
684
+ }
685
+ info(`Linked ${cwd} \u2192 ${describeProject({ project: only, sole: true }, input.workspace)}`);
686
+ info(`Wrote ${path} \u2014 move or delete it to bind a different folder.`);
687
+ return path;
688
+ }
689
+
690
+ // src/deviceAuth.ts
691
+ var CLI_CLIENT_ID = "motir-cli";
692
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
693
+ var DEVICE_START_PATH = "/api/cli/device/start";
694
+ var DEVICE_TOKEN_PATH = "/api/cli/device/token";
695
+ async function readJson(res) {
696
+ try {
697
+ return await res.json();
698
+ } catch {
699
+ return void 0;
700
+ }
701
+ }
702
+ async function postJson(url, body, serverUrl) {
703
+ try {
704
+ return await fetch(url, {
705
+ method: "POST",
706
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
707
+ body: JSON.stringify(body)
708
+ });
709
+ } catch {
710
+ throw new CliError(`Could not reach ${serverUrl}.`, {
711
+ hint: "Check the URL (--server <url> or MOTIR_SERVER) and that the server is running."
712
+ });
713
+ }
714
+ }
715
+ function oauthErrorCode(body) {
716
+ const error = body?.error;
717
+ return typeof error === "string" ? error : void 0;
718
+ }
719
+ async function startDeviceGrant(input) {
720
+ const res = await postJson(
721
+ input.serverUrl + DEVICE_START_PATH,
722
+ { hostname: input.hostname },
723
+ input.serverUrl
724
+ );
725
+ const body = await readJson(res);
726
+ if (!res.ok) {
727
+ throw new CliError(`${input.serverUrl} refused to start a login (HTTP ${res.status}).`, {
728
+ hint: "If this is a self-hosted Motir, it may predate `motir login` \u2014 use `motir auth login --token <pat>` instead."
729
+ });
730
+ }
731
+ const grant = body;
732
+ if (typeof grant?.device_code !== "string" || typeof grant.user_code !== "string") {
733
+ throw new CliError(`${input.serverUrl} did not return a device grant.`, {
734
+ hint: "Check that --server points at a Motir server (it should serve /api/cli/device/start)."
735
+ });
736
+ }
737
+ return grant;
738
+ }
739
+ async function pollDeviceGrant(input) {
740
+ const res = await postJson(
741
+ input.serverUrl + DEVICE_TOKEN_PATH,
742
+ {
743
+ grant_type: DEVICE_CODE_GRANT_TYPE,
744
+ device_code: input.deviceCode,
745
+ client_id: CLI_CLIENT_ID
746
+ },
747
+ input.serverUrl
748
+ );
749
+ const body = await readJson(res);
750
+ if (res.ok) {
751
+ const credential = body;
752
+ if (typeof credential?.access_token !== "string") {
753
+ throw new CliError(`${input.serverUrl} approved the login but returned no token.`);
754
+ }
755
+ return { state: "granted", credential };
756
+ }
757
+ switch (oauthErrorCode(body)) {
758
+ case "authorization_pending":
759
+ return { state: "pending" };
760
+ case "slow_down":
761
+ return { state: "slow_down" };
762
+ case "server_error":
763
+ return { state: "retry" };
764
+ case "access_denied":
765
+ throw new CliError("Approval was denied. No credential was written.", {
766
+ hint: "Run `motir login` again if that was not you \u2014 and revoke nothing: nothing was created."
767
+ });
768
+ case "expired_token":
769
+ throw new CliError("The code expired before it was approved.", {
770
+ hint: "Codes last 15 minutes. Run `motir login` again for a fresh one."
771
+ });
772
+ default:
773
+ throw new CliError(`The login could not be completed (HTTP ${res.status}).`, {
774
+ hint: "Run `motir login` again to start a fresh grant."
775
+ });
776
+ }
777
+ }
778
+
779
+ // src/commands/login.ts
780
+ var SLOW_DOWN_STEP_MS = 5e3;
781
+ var MIN_POLL_SECONDS = 1;
782
+ var MIN_BUDGET_SECONDS = 60;
783
+ async function listProjectsWith(input) {
784
+ const client = new MotirClient(input);
785
+ try {
786
+ await client.connect();
787
+ const { projects } = await client.listProjects();
788
+ return projects;
789
+ } finally {
790
+ await client.close();
791
+ }
792
+ }
793
+ function groupUserCode(code) {
794
+ if (code.includes("-")) return code;
795
+ const groups = code.match(/.{1,4}/g);
796
+ return groups ? groups.join("-") : code;
797
+ }
798
+ function printGrant(grant) {
799
+ info("");
800
+ info(` Your code: ${groupUserCode(grant.user_code)}`);
801
+ info(` Open: ${grant.verification_uri}`);
802
+ info("");
803
+ }
804
+ function storeCredential(serverUrl, credential) {
805
+ try {
806
+ setCredential(serverUrl, { token: credential.access_token, user: credential.user });
807
+ } catch {
808
+ throw new CliError(`Could not write the credential to ${configPath()}.`, {
809
+ hint: `Make that directory writable, point MOTIR_CONFIG_HOME at one that is, or set ${TOKEN_ENV_VAR} instead \u2014 the environment tier is never written to disk.`
810
+ });
811
+ }
812
+ }
813
+ async function awaitApproval(input) {
814
+ let intervalMs = Math.max(input.grant.interval, MIN_POLL_SECONDS) * 1e3;
815
+ const budgetMs = Math.max(input.grant.expires_in, MIN_BUDGET_SECONDS) * 1e3;
816
+ let waitedMs = 0;
817
+ for (; ; ) {
818
+ await input.sleep(intervalMs);
819
+ waitedMs += intervalMs;
820
+ const result = await pollDeviceGrant({
821
+ serverUrl: input.serverUrl,
822
+ deviceCode: input.grant.device_code
823
+ });
824
+ if (result.state === "granted") return result.credential;
825
+ if (result.state === "slow_down") intervalMs += SLOW_DOWN_STEP_MS;
826
+ if (waitedMs >= budgetMs) {
827
+ throw new CliError("Timed out waiting for approval.", {
828
+ hint: "Nothing was written. Run `motir login` again when you are ready to approve it."
829
+ });
830
+ }
831
+ }
832
+ }
833
+ async function loginCommand(opts, deps = {}) {
834
+ const sleep = deps.sleep ?? delay;
835
+ const open = deps.openUrl ?? openUrl;
836
+ const serverUrl = resolveServerUrl(opts.server);
837
+ const grant = await startDeviceGrant({ serverUrl, hostname: hostname() });
838
+ printGrant(grant);
839
+ const opened = opts.browser === false ? false : await open(grant.verification_uri_complete);
840
+ if (opened) info("Opened your browser \u2014 approve the request there.");
841
+ else info("Open that URL on any device, sign in, and enter the code.");
842
+ info("Waiting for approval\u2026");
843
+ const credential = await awaitApproval({ serverUrl, grant, sleep });
844
+ storeCredential(serverUrl, credential);
845
+ info(
846
+ `Logged in as ${credential.user.email} on ${serverUrl} (workspace ${credential.workspace.name}).`
847
+ );
848
+ const listProjects = deps.listProjects ?? listProjectsWith;
849
+ await autoLinkAfterLogin({
850
+ serverUrl,
851
+ workspace: credential.workspace.slug,
852
+ listProjects: () => listProjects({ serverUrl, token: credential.access_token }),
853
+ ...deps.cwd === void 0 ? {} : { cwd: deps.cwd },
854
+ ...deps.home === void 0 ? {} : { home: deps.home }
855
+ });
856
+ }
857
+
536
858
  // src/commands/link.ts
537
859
  function tokenFor(serverUrl) {
538
860
  const cred = resolveCredential(serverUrl);
@@ -575,15 +897,17 @@ async function linkCommand(opts) {
575
897
  hint: "Pass --workspace <slug>."
576
898
  });
577
899
  }
578
- let project = opts.project ?? existing?.config.project;
579
- if (!project) {
580
- if (!isInteractive()) {
581
- throw new CliError("No project given.", { hint: "Pass --project <key>." });
582
- }
583
- project = await promptLine("Project key");
900
+ const asserted = opts.project ?? existing?.config.project;
901
+ let project;
902
+ let chosen = null;
903
+ if (asserted) {
904
+ await assertProjectAccessible(client, asserted);
905
+ project = asserted;
906
+ } else {
907
+ const resolved = await resolveProject(client, workspace, serverUrl);
908
+ project = resolved.project.key;
909
+ chosen = describeProject(resolved, workspace);
584
910
  }
585
- if (!project) throw new CliError("A project key is required.");
586
- await assertProjectAccessible(client, project);
587
911
  const config = {
588
912
  serverUrl,
589
913
  workspace,
@@ -594,6 +918,7 @@ async function linkCommand(opts) {
594
918
  const targetDir = existing?.dir ?? cwd;
595
919
  const path = writeLink(targetDir, withRepo);
596
920
  info(`Linked ${targetDir} \u2192 ${workspace}/${project} on ${serverUrl}`);
921
+ if (chosen) info(`Chose ${chosen}.`);
597
922
  info(`Wrote ${path}`);
598
923
  showLink(targetDir, withRepo);
599
924
  } finally {
@@ -980,6 +1305,160 @@ function renderReadinessLine(readiness) {
980
1305
  function renderLineage(detail) {
981
1306
  return [...detail.ancestors.map((a) => a.identifier), detail.item.identifier].join(" \u203A ");
982
1307
  }
1308
+ var NO_VALUE = "\u2014";
1309
+ var NO_COMMENTS_LINE = "No comments yet.";
1310
+ var NO_ACTIVITY_LINE = "No activity yet.";
1311
+ var COMMENT_PREFIX = "[comment] ";
1312
+ var REPLY_PREFIX = " \u21B3 reply ";
1313
+ var CHANGE_PREFIX = "[change] ";
1314
+ var MINUTE = 6e4;
1315
+ var HOUR = 60 * MINUTE;
1316
+ var DAY = 24 * HOUR;
1317
+ var MONTH = 30 * DAY;
1318
+ var YEAR = 365 * DAY;
1319
+ function plural(count, noun) {
1320
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
1321
+ }
1322
+ function relativeTime(iso, now) {
1323
+ const then = Date.parse(iso);
1324
+ if (Number.isNaN(then)) return iso;
1325
+ const delta = now.getTime() - then;
1326
+ if (delta < MINUTE) return "just now";
1327
+ if (delta < HOUR) return `${plural(Math.floor(delta / MINUTE), "minute")} ago`;
1328
+ if (delta < DAY) return `${plural(Math.floor(delta / HOUR), "hour")} ago`;
1329
+ if (delta < MONTH) return `${plural(Math.floor(delta / DAY), "day")} ago`;
1330
+ if (delta < YEAR) return `${plural(Math.floor(delta / MONTH), "month")} ago`;
1331
+ return `${plural(Math.floor(delta / YEAR), "year")} ago`;
1332
+ }
1333
+ function activityValueText(value) {
1334
+ if (value === null || value === void 0) return NO_VALUE;
1335
+ if (typeof value === "string") return value;
1336
+ switch (value.type) {
1337
+ case "none":
1338
+ return NO_VALUE;
1339
+ case "text":
1340
+ return value.text ?? NO_VALUE;
1341
+ case "status":
1342
+ return value.label ?? value.key ?? NO_VALUE;
1343
+ case "user":
1344
+ return value.name ?? value.userId ?? NO_VALUE;
1345
+ case "date":
1346
+ return value.date ?? NO_VALUE;
1347
+ case "sprint":
1348
+ return value.name ?? value.sprintId ?? NO_VALUE;
1349
+ case "issue":
1350
+ return value.identifier ?? value.workItemId ?? NO_VALUE;
1351
+ default:
1352
+ return value.label ?? value.name ?? value.identifier ?? value.text ?? value.key ?? NO_VALUE;
1353
+ }
1354
+ }
1355
+ function activityPartText(part) {
1356
+ switch (part.kind) {
1357
+ case "created":
1358
+ return "created the item";
1359
+ case "archived":
1360
+ return "archived the item";
1361
+ case "unarchived":
1362
+ return "restored the item";
1363
+ case "field":
1364
+ return `changed ${part.field}: ${activityValueText(part.from)} \u2192 ${activityValueText(part.to)}`;
1365
+ // The trail records THAT a body field changed, never its text (the DTO
1366
+ // carries none) — said plainly, so it never reads as a truncation.
1367
+ case "fieldEdited":
1368
+ return `edited ${part.field} (body not shown \u2014 the history trail records no text)`;
1369
+ case "link":
1370
+ return `${part.op} ${part.linkKind} link \u2192 ${activityValueText(part.target)}`;
1371
+ case "collection":
1372
+ return `${part.op} ${part.field}: ${(part.items ?? []).join(", ")}`;
1373
+ case "commentDeleted": {
1374
+ const replies = part.replyCount ?? 0;
1375
+ return `deleted a comment by ${activityValueText(part.author)} (${replies} ${replies === 1 ? "reply" : "replies"})`;
1376
+ }
1377
+ case "generic":
1378
+ return `${part.key}: ${activityValueText(part.from)} \u2192 ${activityValueText(part.to)}`;
1379
+ default:
1380
+ if (part.from === void 0 && part.to === void 0) {
1381
+ return `${part.kind} (unknown change kind \u2014 update the CLI to read it in full)`;
1382
+ }
1383
+ return `${part.kind}: ${activityValueText(part.from)} \u2192 ${activityValueText(part.to)}`;
1384
+ }
1385
+ }
1386
+ function attribution(who, iso, now) {
1387
+ return `${who ?? "former member"} \xB7 ${relativeTime(iso, now)} (${iso})`;
1388
+ }
1389
+ function renderHistoryEntry(entry, now) {
1390
+ const parts = entry.parts.map(activityPartText).join("; ");
1391
+ return `${CHANGE_PREFIX}${attribution(entry.actor.name, entry.changedAt, now)} \u2014 ${parts}`;
1392
+ }
1393
+ function renderComment(comment, prefix, now) {
1394
+ const edited = comment.editedAt === null ? "" : " (edited)";
1395
+ const indent = " ".repeat(prefix.length);
1396
+ const header = `${prefix}${attribution(comment.author.name, comment.createdAt, now)}${edited}`;
1397
+ const body = comment.bodyMd.split("\n").map((line) => line === "" ? "" : `${indent}${line}`).join("\n");
1398
+ return `${header}
1399
+ ${body}`;
1400
+ }
1401
+ function renderCommentThread(thread, now) {
1402
+ return [
1403
+ renderComment(thread, COMMENT_PREFIX, now),
1404
+ ...thread.replies.map((reply) => renderComment(reply, REPLY_PREFIX, now))
1405
+ ].join("\n");
1406
+ }
1407
+ function commentsIn(threads) {
1408
+ return threads.reduce((sum, thread) => sum + 1 + thread.replies.length, 0);
1409
+ }
1410
+ function shownOfTotal(shown, total, noun) {
1411
+ return shown === total ? plural(total, noun) : `${shown} of ${plural(total, noun)}`;
1412
+ }
1413
+ function moreLine(remaining, identifier) {
1414
+ const left = remaining.filter(([count]) => count > 0).map(([count, noun]) => plural(count, noun));
1415
+ const what = left.length > 0 ? `${left.join(" and ")} not on this page` : "this page may be short \u2014 the stream is not exhausted";
1416
+ return `MORE \u2014 ${what}. \`motir show\` prints ONE page and never drains the stream; read the rest in Motir: \`motir open ${identifier}\`.`;
1417
+ }
1418
+ function activitySection(heading, counts, blocks, emptyLine, more) {
1419
+ const body = blocks.length === 0 ? [emptyLine] : blocks;
1420
+ return [`${heading}
1421
+ ${counts}`, body.join("\n\n"), ...more === null ? [] : [more]].join(
1422
+ "\n\n"
1423
+ );
1424
+ }
1425
+ function renderActivityStream(view, page, identifier, now) {
1426
+ if (view === "comments") {
1427
+ const p2 = page;
1428
+ const shown = commentsIn(p2.threads);
1429
+ const direction = p2.order === "asc" ? "oldest first" : "newest first";
1430
+ return activitySection(
1431
+ "COMMENTS",
1432
+ `${shownOfTotal(shown, p2.totalCount, "comment")}, ${direction}`,
1433
+ p2.threads.map((thread) => renderCommentThread(thread, now)),
1434
+ NO_COMMENTS_LINE,
1435
+ p2.nextCursor === null ? null : moreLine([[p2.totalCount - shown, "comment"]], identifier)
1436
+ );
1437
+ }
1438
+ const p = page;
1439
+ const shownComments = commentsIn(
1440
+ p.entries.flatMap((entry) => entry.type === "comment" ? [entry.thread] : [])
1441
+ );
1442
+ const shownChanges = p.entries.filter((entry) => entry.type === "history").length;
1443
+ return activitySection(
1444
+ "ACTIVITY",
1445
+ [
1446
+ shownOfTotal(shownComments, p.totalComments, "comment"),
1447
+ shownOfTotal(shownChanges, p.totalChanges, "change")
1448
+ ].join(" \xB7 "),
1449
+ p.entries.map(
1450
+ (entry) => entry.type === "comment" ? renderCommentThread(entry.thread, now) : renderHistoryEntry(entry.entry, now)
1451
+ ),
1452
+ NO_ACTIVITY_LINE,
1453
+ p.nextCursor === null ? null : moreLine(
1454
+ [
1455
+ [p.totalComments - shownComments, "comment"],
1456
+ [p.totalChanges - shownChanges, "change"]
1457
+ ],
1458
+ identifier
1459
+ )
1460
+ );
1461
+ }
983
1462
  function renderWorkItemDetail(detail, titleWidth = 60) {
984
1463
  const sections = [
985
1464
  renderItemHeader(detail.item),
@@ -1056,34 +1535,6 @@ async function collectSprintItems(client, projectKey, sprintId, filter = {}) {
1056
1535
  return { items, total };
1057
1536
  }
1058
1537
 
1059
- // src/browser.ts
1060
- import { spawn } from "child_process";
1061
- function launcher(platform) {
1062
- if (platform === "darwin") return { cmd: "open", args: [] };
1063
- if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", ""] };
1064
- return { cmd: "xdg-open", args: [] };
1065
- }
1066
- function hasDisplay(env, platform) {
1067
- if (platform === "darwin" || platform === "win32") return true;
1068
- return Boolean(env["DISPLAY"] || env["WAYLAND_DISPLAY"]);
1069
- }
1070
- function openUrl(url, opts = {}) {
1071
- const platform = opts.platform ?? process.platform;
1072
- const env = opts.env ?? process.env;
1073
- if (!hasDisplay(env, platform)) return Promise.resolve(false);
1074
- const { cmd, args } = launcher(platform);
1075
- return new Promise((resolve3) => {
1076
- try {
1077
- const child = spawn(cmd, [...args, url], { stdio: "ignore", detached: true });
1078
- child.on("error", () => resolve3(false));
1079
- child.unref();
1080
- setTimeout(() => resolve3(true), 0);
1081
- } catch {
1082
- resolve3(false);
1083
- }
1084
- });
1085
- }
1086
-
1087
1538
  // src/commands/read.ts
1088
1539
  var WORK_ITEM_KINDS = /* @__PURE__ */ new Set(["epic", "story", "task", "bug", "subtask"]);
1089
1540
  function parseKinds(raw) {
@@ -1194,14 +1645,33 @@ function parseItemKey(raw, command) {
1194
1645
  }
1195
1646
  return key;
1196
1647
  }
1648
+ function activityView(opts) {
1649
+ if (opts.activity && opts.comments) {
1650
+ throw new CliError("`--activity` and `--comments` cannot be combined.", {
1651
+ hint: "`--activity` prints comments AND history; `--comments` prints just the comments. Pick one."
1652
+ });
1653
+ }
1654
+ if (opts.activity) return "all";
1655
+ if (opts.comments) return "comments";
1656
+ return null;
1657
+ }
1197
1658
  async function showCommand(key, opts) {
1198
1659
  const identifier = parseItemKey(key, "show");
1199
- const detail = await withProjectSession(({ client }) => client.getWorkItem(identifier));
1660
+ const view = activityView(opts);
1661
+ const { detail, activity } = await withProjectSession(async ({ client }) => {
1662
+ const detail2 = await client.getWorkItem(identifier);
1663
+ const activity2 = view === null ? null : await client.getWorkItemActivity({ key: identifier, view });
1664
+ return { detail: detail2, activity: activity2 };
1665
+ });
1200
1666
  if (opts.json) {
1201
- json(detailWithWaves(detail));
1667
+ json(view === null ? detailWithWaves(detail) : { ...detailWithWaves(detail), activity });
1202
1668
  return;
1203
1669
  }
1204
1670
  out(renderWorkItemDetail(detail));
1671
+ if (view !== null && activity !== null) {
1672
+ out();
1673
+ out(renderActivityStream(view, activity, identifier, /* @__PURE__ */ new Date()));
1674
+ }
1205
1675
  }
1206
1676
  async function openCommand(key, opts) {
1207
1677
  const trimmed = key.trim();
@@ -1215,9 +1685,9 @@ async function openCommand(key, opts) {
1215
1685
  }
1216
1686
 
1217
1687
  // src/commands/doctor.ts
1218
- import { homedir as homedir2 } from "os";
1688
+ import { homedir as homedir3 } from "os";
1219
1689
  import { accessSync, constants, existsSync as existsSync3, statSync } from "fs";
1220
- import { delimiter, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "path";
1690
+ import { delimiter, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "path";
1221
1691
  import { execFile } from "child_process";
1222
1692
 
1223
1693
  // src/agentProfiles.ts
@@ -1700,7 +2170,7 @@ function countByStatus(checks) {
1700
2170
  for (const check of checks) counts[check.status] += 1;
1701
2171
  return counts;
1702
2172
  }
1703
- function plural(n, one, many) {
2173
+ function plural2(n, one, many) {
1704
2174
  return `${n} ${n === 1 ? one : many}`;
1705
2175
  }
1706
2176
  function renderDoctorReport(report) {
@@ -1714,7 +2184,7 @@ function renderDoctorReport(report) {
1714
2184
  }
1715
2185
  const counts = countByStatus(report.checks);
1716
2186
  const parts = STATUS_ORDER.filter((s) => counts[s] > 0).map(
1717
- (s) => s === "fail" ? plural(counts.fail, "failed", "failed") : s === "warn" ? plural(counts.warn, "warning", "warnings") : plural(counts.pass, "passed", "passed")
2187
+ (s) => s === "fail" ? plural2(counts.fail, "failed", "failed") : s === "warn" ? plural2(counts.warn, "warning", "warnings") : plural2(counts.pass, "passed", "passed")
1718
2188
  );
1719
2189
  lines.push(
1720
2190
  "",
@@ -1777,7 +2247,7 @@ function resolveOnPath(binary, env = process.env, platform = process.platform) {
1777
2247
  return [base, ...exts.map((ext) => base + ext)];
1778
2248
  };
1779
2249
  if (binary.includes("/") || binary.includes("\\")) {
1780
- const direct = isAbsolute2(binary) ? binary : resolve2(binary);
2250
+ const direct = isAbsolute2(binary) ? binary : resolve3(binary);
1781
2251
  return withExtensions(direct).find(isExecutableFile) ?? null;
1782
2252
  }
1783
2253
  for (const dir of (env["PATH"] ?? "").split(delimiter).filter(Boolean)) {
@@ -1840,9 +2310,9 @@ function defaultDoctorProbe() {
1840
2310
  const value = process.env[name];
1841
2311
  return typeof value === "string" && value.length > 0;
1842
2312
  },
1843
- home: () => homedir2(),
1844
- xdgConfigHome: () => process.env["XDG_CONFIG_HOME"] || join4(homedir2(), ".config"),
1845
- xdgDataHome: () => process.env["XDG_DATA_HOME"] || join4(homedir2(), ".local", "share")
2313
+ home: () => homedir3(),
2314
+ xdgConfigHome: () => process.env["XDG_CONFIG_HOME"] || join4(homedir3(), ".config"),
2315
+ xdgDataHome: () => process.env["XDG_DATA_HOME"] || join4(homedir3(), ".local", "share")
1846
2316
  };
1847
2317
  }
1848
2318
  async function doctorCommand(opts, probe = defaultDoctorProbe()) {
@@ -1867,7 +2337,7 @@ async function runAgent(opts) {
1867
2337
  const promptFile = join5(dir, "prompt.md");
1868
2338
  writeFileSync3(promptFile, opts.prompt, { mode: PROMPT_FILE_MODE });
1869
2339
  try {
1870
- return await new Promise((resolve3, reject) => {
2340
+ return await new Promise((resolve4, reject) => {
1871
2341
  const child = spawnFn(opts.command.binary, opts.command.args, {
1872
2342
  cwd: opts.cwd,
1873
2343
  // stdin piped (we write the prompt); stdout/stderr inherited so the
@@ -1889,7 +2359,7 @@ async function runAgent(opts) {
1889
2359
  stdin.end(opts.prompt);
1890
2360
  }
1891
2361
  child.on("close", (code, signal) => {
1892
- resolve3({ exitCode: signal ? 1 : code ?? 1, signal });
2362
+ resolve4({ exitCode: signal ? 1 : code ?? 1, signal });
1893
2363
  });
1894
2364
  });
1895
2365
  } finally {
@@ -2037,6 +2507,17 @@ function cwdReasonLabel(target) {
2037
2507
  function workflowLabel(mode, sessionBranch) {
2038
2508
  return mode === "session_lineage" ? `session lineage on ${sessionBranch ?? "(unnamed branch)"}` : "one pull request of its own";
2039
2509
  }
2510
+ function renderDispatchAdvisories(dispatch) {
2511
+ const advisories = dispatch.advisories ?? [];
2512
+ if (advisories.length === 0) return null;
2513
+ return [
2514
+ `Advisory: ${dispatch.key}'s acceptance criteria name work items it has no blocked_by edge to.`,
2515
+ ...advisories.map((a) => ` - ${a.referenced} (${a.referencedStatus})`),
2516
+ " This is NOT a blocker \u2014 the dispatch proceeds. Before branching,",
2517
+ " check each one is already on origin/main; if it lives only on an open",
2518
+ " PR, add the blocked_by edge and stop instead of rebuilding its half."
2519
+ ].join("\n");
2520
+ }
2040
2521
  function renderDispatchSummary(input) {
2041
2522
  const { key, title, dispatch, target, agent } = input;
2042
2523
  const lines = [
@@ -2117,13 +2598,16 @@ async function deliver(input) {
2117
2598
  target,
2118
2599
  agent: agent ? { command: agent.parsed.command, source: agent.source } : null
2119
2600
  });
2601
+ const advisory = renderDispatchAdvisories(dispatch);
2120
2602
  if (!agent) {
2121
2603
  info(summary);
2604
+ if (advisory) info(advisory);
2122
2605
  info("");
2123
2606
  outVerbatim(dispatch.prompt);
2124
2607
  return;
2125
2608
  }
2126
2609
  info(summary);
2610
+ if (advisory) info(advisory);
2127
2611
  info("");
2128
2612
  const result = await runAgent({
2129
2613
  command: agent.parsed,
@@ -3334,9 +3818,9 @@ function nextStepHint(reviewUrl, anchored) {
3334
3818
  }
3335
3819
 
3336
3820
  // src/commands/plan.ts
3337
- function delay(ms) {
3338
- return new Promise((resolve3) => {
3339
- setTimeout(resolve3, ms);
3821
+ function delay2(ms) {
3822
+ return new Promise((resolve4) => {
3823
+ setTimeout(resolve4, ms);
3340
3824
  });
3341
3825
  }
3342
3826
  async function planCommand(args = [], opts = {}, deps = {}) {
@@ -3428,7 +3912,7 @@ function reportDetached(submitted, reviewUrl, targetKeys) {
3428
3912
  }
3429
3913
  async function watchAndShow(client, planId, reviewUrl, targetKeys, deps) {
3430
3914
  const now = deps.now ?? Date.now;
3431
- const sleep = deps.sleep ?? delay;
3915
+ const sleep = deps.sleep ?? delay2;
3432
3916
  const started = now();
3433
3917
  info(
3434
3918
  `Waiting for the planner (up to ${Math.round(WATCH_TIMEOUT_MS / 6e4)} minutes; --detach skips the wait)\u2026`
@@ -3588,13 +4072,17 @@ function findHelpTopic(name) {
3588
4072
  }
3589
4073
  var EXAMPLES = [
3590
4074
  "EXAMPLES:",
3591
- " $ motir auth login --server https://app.motir.co # store a token",
4075
+ " $ motir login # connect this terminal",
4076
+ " $ motir login --no-browser # \u2026over SSH: print the code",
4077
+ " $ motir auth login --server https://app.motir.co # paste a token you hold",
3592
4078
  " $ motir link --project MOTIR # bind this folder",
3593
4079
  " $ motir doctor # preflight the setup",
3594
4080
  " $ motir ready --kinds subtask --assignee me # what can I pick up?",
3595
4081
  " $ motir status --json # the project pulse",
3596
4082
  " $ motir sprint # what\u2019s in the active sprint",
3597
4083
  " $ motir show MOTIR-7 # the item, its edges, its body",
4084
+ " $ motir show MOTIR-7 --activity # \u2026plus comments + history",
4085
+ " $ motir show MOTIR-7 --comments # \u2026just what people said",
3598
4086
  " $ motir open MOTIR-7 --print # the item\u2019s URL",
3599
4087
  " $ motir auto --agent claude --max 5 # drain 5 items unattended",
3600
4088
  " $ motir batch --agent claude # snapshot now, one PR each",
@@ -3704,11 +4192,36 @@ function buildProgram() {
3704
4192
  "Motir CLI \u2014 terminal dispatch of the work loop (an MCP client of the Motir server)."
3705
4193
  ).version(CLI_VERSION, "-v, --version", "Print the CLI version.");
3706
4194
  applyHelpConfiguration(program);
4195
+ program.command("login").description("Connect this terminal: shows a code, opens Motir, waits for your approval.").helpGroup(HELP_GROUP.setup).option("--server <url>", "Server base URL, e.g. https://app.motir.co").option(
4196
+ "--no-browser",
4197
+ "Do not launch a browser \u2014 just print the code and the URL to open anywhere."
4198
+ ).addHelpText(
4199
+ "after",
4200
+ [
4201
+ "",
4202
+ "No browser on this machine? The code and URL are printed either way, so an",
4203
+ "SSH session or a container uses this same command \u2014 open the URL on any",
4204
+ "device, sign in, and enter the code. For an unattended agent set MOTIR_TOKEN",
4205
+ "instead (`motir help environment`)."
4206
+ ].join("\n")
4207
+ ).action((opts) => loginCommand(opts));
4208
+ program.command("logout").description("Disconnect this terminal: remove the stored token for a server.").helpGroup(HELP_GROUP.setup).option("--server <url>", "Server to log out of (defaults to the linked / single server).").addHelpText(
4209
+ "after",
4210
+ [
4211
+ "",
4212
+ "This removes the credential from THIS machine only. The server-side kill",
4213
+ "switch is revoking the token in Settings \u2192 Account \u2192 API tokens \u2014 a terminal",
4214
+ "connected by `motir login` appears there as `CLI \xB7 <hostname>`."
4215
+ ].join("\n")
4216
+ ).action(authLogout);
3707
4217
  const auth = program.command("auth").description("Authenticate to a Motir server with a PAT.").helpGroup(HELP_GROUP.setup);
3708
4218
  auth.command("login").description("Validate and store a personal access token for a server.").option("--server <url>", "Server base URL, e.g. https://app.motir.co").option("--token <pat>", "Personal access token (or set MOTIR_TOKEN; prompted if omitted).").action(authLogin);
3709
4219
  auth.command("status").description("Show the resolved server, token prefix, and owning user.").option("--server <url>", "Server to report (defaults to the linked / single server).").action(authStatus);
3710
4220
  auth.command("logout").description("Remove the stored token for a server.").option("--server <url>", "Server to log out of (defaults to the linked / single server).").action(authLogout);
3711
- const link = program.command("link").description("Bind this workspace-root folder to a server + workspace + project.").helpGroup(HELP_GROUP.setup).option("--server <url>", "Server base URL (defaults to the existing link / single server).").option("--workspace <slug>", "Workspace slug (defaults to the token\u2019s active workspace).").option("--project <key>", "Project key, e.g. PROD.").option(
4221
+ const link = program.command("link").description("Bind this workspace-root folder to a server + workspace + project.").helpGroup(HELP_GROUP.setup).option("--server <url>", "Server base URL (defaults to the existing link / single server).").option("--workspace <slug>", "Workspace slug (defaults to the token\u2019s active workspace).").option(
4222
+ "--project <key>",
4223
+ "Project key, e.g. PROD. Omit it and the workspace\u2019s only project is used."
4224
+ ).option(
3712
4225
  "--repo <name>",
3713
4226
  'Mark THIS directory as a single repo\u2019s checkout (writes a "." override).'
3714
4227
  ).action(linkCommand);
@@ -3718,7 +4231,14 @@ function buildProgram() {
3718
4231
  program.command("status").description("Show the project pulse: ready / in-flight counts + the active sprint.").helpGroup(HELP_GROUP.read).option("--json", "Emit the pulse as JSON.").action(statusCommand);
3719
4232
  program.command("sprints").description("List the project\u2019s sprints: state, item count, points, window.").helpGroup(HELP_GROUP.read).option("--state <state>", "Only sprints in this state: planned, active, or complete.").option("--json", "Emit the sprint rows as JSON.").action(sprintsCommand);
3720
4233
  program.command("sprint [ref]").description("List ONE sprint\u2019s work items (defaults to the active sprint).").helpGroup(HELP_GROUP.read).option("--kinds <list>", "Comma-separated kinds: epic,story,task,bug,subtask.").option("--json", "Emit the sprint and its items as JSON.").action((ref, opts) => sprintCommand(ref, opts));
3721
- program.command("show <key>").description("Read one work item (e.g. PROD-7): fields, readiness, children, edges, body.").helpGroup(HELP_GROUP.read).option("--json", "Emit the get_work_item payload as JSON.").action(showCommand);
4234
+ program.command("show <key>").description("Read one work item (e.g. PROD-7): fields, readiness, children, edges, body.").helpGroup(HELP_GROUP.read).option("--json", "Emit the get_work_item payload as JSON.").option("--activity", "Also print the activity stream: comments and history, one page.").option("--comments", "Also print the comment threads only, one page.").addHelpText(
4235
+ "after",
4236
+ [
4237
+ "",
4238
+ "ONE page of the stream is printed, never a drain loop: when more remains,",
4239
+ "the output says how much and points at `motir open <key>` for the rest."
4240
+ ].join("\n")
4241
+ ).action(showCommand);
3722
4242
  program.command("doctor").description(
3723
4243
  "Preflight your BYOK setup: auth, project link, agent binary, credential presence."
3724
4244
  ).helpGroup(HELP_GROUP.setup).option("--agent <cmd>", "Check THIS agent command instead of the configured one.").option("--json", "Emit the check results as JSON.").action((opts) => doctorCommand(opts));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motir/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Motir CLI — terminal dispatch of the work loop (an MCP client of the Motir server).",
5
5
  "license": "GPL-3.0-only",
6
6
  "repository": {