@brunyee-studio/onus-cli 2.28.0 → 2.30.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
@@ -200,8 +200,8 @@ onus team get <id> [--json]
200
200
  ```bash
201
201
  onus project list [--json]
202
202
  onus project get <id> [--json]
203
- onus project create --name <name> --team <teamId> [--status <status>] [--priority <priority>] [--lead <leadId>] [--target-date <date>] [--json]
204
- onus project update <id> [--name <name>] [--status <status>] [--priority <priority>] [--lead <leadId>] [--target-date <date>] [--json]
203
+ onus project create --name <name> --team <teamId> [--status <status>] [--priority <priority>] [--lead <leadId>] [--target-date <date>] [--description <text>] [--description-file <path>] [--agent-context <text>] [--agent-context-file <path>] [--json]
204
+ onus project update <id> [--name <name>] [--status <status>] [--priority <priority>] [--lead <leadId>] [--target-date <date>] [--description <text>] [--description-file <path>] [--agent-context <text>] [--agent-context-file <path>] [--json]
205
205
  ```
206
206
 
207
207
  ### `onus issue`
@@ -211,8 +211,8 @@ Issues are addressed by human reference (`ENG-42`); UUIDs are also accepted.
211
211
  ```bash
212
212
  onus issue list [--team <id>] [--project <id>] [--assignee <id>] [--parent <id>] [--status <status>] [--q <query>] [--limit <n>] [--cursor <cursor>] [--json]
213
213
  onus issue get <ref> [--json]
214
- onus issue create --title <title> --team <teamId> [-d <description>] [--status <status>] [--priority <priority>] [--assignee <id>] [--project <id>] [--parent <id>] [--label <labelId>] [--json]
215
- onus issue update <ref> [--title <title>] [-d <description>] [--status <status>] [--priority <priority>] [--assignee <id>] [--project <id>] [--parent <id>] [--add-label <labelId>] [--remove-label <labelId>] [--json]
214
+ onus issue create --title <title> --team <teamId> [-d <description>] [--description-file <path>] [--status <status>] [--priority <priority>] [--assignee <id>] [--project <id>] [--parent <id>] [--label <labelId>] [--json]
215
+ onus issue update <ref> [--title <title>] [-d <description>] [--description-file <path>] [--status <status>] [--priority <priority>] [--assignee <id>] [--project <id>] [--parent <id>] [--add-label <labelId>] [--remove-label <labelId>] [--json]
216
216
  onus issue delete <ref> [--yes] [--json]
217
217
  ```
218
218
 
@@ -224,9 +224,9 @@ aliased as `edit`.
224
224
  #### Comment subcommands
225
225
 
226
226
  ```bash
227
- onus issue comment create <ref> --body <body> [--json]
227
+ onus issue comment create <ref> [--body <body> | --body-file <path>] [--json]
228
228
  onus issue comment list <ref> [--json]
229
- onus issue comment edit <ref> --comment <commentId> --body <body> [--json]
229
+ onus issue comment edit <ref> --comment <commentId> [--body <body> | --body-file <path>] [--json]
230
230
  onus issue comment delete <ref> --comment <commentId> [--json]
231
231
  ```
232
232
 
@@ -270,7 +270,7 @@ onus issue unlink-pr <ref> --pr-link <linkId> [--json]
270
270
 
271
271
  ```bash
272
272
  onus comment list <issueId> [--json]
273
- onus comment create <issueId> --body <body> [--json]
273
+ onus comment create <issueId> [--body <body> | --body-file <path>] [--json]
274
274
  ```
275
275
 
276
276
  ### `onus label`
@@ -352,6 +352,35 @@ export ONUS_TOKEN="<your-onus-token>"
352
352
  onus me --json
353
353
  ```
354
354
 
355
+ ### 6. Long text via per-field file input
356
+
357
+ Long, multi-line values (issue and project descriptions, comment bodies,
358
+ project agent context) are awkward to inline in shell commands. Each of these
359
+ fields has a companion file flag that reads **only that field** from a file —
360
+ never the whole record — following the standard `--body-file` convention:
361
+
362
+ ```bash
363
+ onus issue create --title "Fix login bug" --team ONUS --description-file ./body.md
364
+ onus issue update ENG-42 --description-file ./notes.md
365
+ onus issue comment create ENG-42 --body-file ./comment.md
366
+ onus comment create ENG-42 --body-file ./comment.md
367
+ onus project create --name "Migration" --team ONUS --description-file ./brief.md
368
+ onus project update <projectId> --description-file ./brief.md
369
+ onus project update <projectId> --agent-context-file AGENTS.md
370
+ ```
371
+
372
+ Pass `-` as the path to read the field value from stdin, so agents can pipe
373
+ content directly:
374
+
375
+ ```bash
376
+ generate-body | onus issue create --title "Fix login bug" --team ONUS --description-file -
377
+ ```
378
+
379
+ Passing both the inline flag and its file variant (e.g. `--body` together with
380
+ `--body-file`) is an error; the file never silently overrides the inline value.
381
+ Comment bodies must be non-empty: an empty or whitespace-only `--body` or
382
+ `--body-file` is rejected before the request is sent.
383
+
355
384
  ---
356
385
 
357
386
  ## How the CLI is generated from OpenAPI
package/dist/bin.js CHANGED
@@ -557,15 +557,33 @@ function registerAnalyticsCommands(program2) {
557
557
  );
558
558
  }
559
559
 
560
- // src/commands/project.ts
560
+ // src/file-input.ts
561
561
  import { readFileSync } from "fs";
562
- function resolveAgentContext(text, file) {
563
- if (file !== void 0 && text !== void 0) {
564
- throw new Error("--agent-context and --agent-context-file cannot be used together");
562
+ async function resolveTextValue(opts) {
563
+ if (opts.file !== void 0 && opts.value !== void 0) {
564
+ throw new Error(`${opts.inlineFlag} and ${opts.fileFlag} cannot be used together`);
565
565
  }
566
- if (file !== void 0) return readFileSync(file, "utf8");
567
- return text;
566
+ if (opts.file === void 0) return opts.value;
567
+ if (opts.file === "-") return readStdin();
568
+ return readFileSync(opts.file, "utf8");
569
+ }
570
+ function readStdin() {
571
+ return new Promise((resolve, reject) => {
572
+ const chunks = [];
573
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
574
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
575
+ process.stdin.on("error", reject);
576
+ });
568
577
  }
578
+ async function resolveRequiredTextValue(opts) {
579
+ const value = await resolveTextValue(opts);
580
+ if (value === void 0 || value.trim().length === 0) {
581
+ throw new Error(`${opts.subject} is required (provide ${opts.inlineFlag} or ${opts.fileFlag})`);
582
+ }
583
+ return value;
584
+ }
585
+
586
+ // src/commands/project.ts
569
587
  function previewAgentContext(value) {
570
588
  if (!value) return null;
571
589
  const firstLine = value.split("\n")[0].trim();
@@ -623,15 +641,29 @@ function registerProjectCommands(program2) {
623
641
  await handleApiError(err, globals.json);
624
642
  }
625
643
  });
626
- project.command("create").description("Create a new project").requiredOption("--name <name>", "Project name").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--status <status>", "Project status (backlog|planned|in_progress|completed|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--lead <leadId>", "Lead user UUID").option("--target-date <date>", "Target completion date (ISO)").option("--description <text>", "Project description (rendered under the title)").option(
644
+ project.command("create").description("Create a new project").requiredOption("--name <name>", "Project name").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("--status <status>", "Project status (backlog|planned|in_progress|completed|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--lead <leadId>", "Lead user UUID").option("--target-date <date>", "Target completion date (ISO)").option("--description <text>", "Project description (rendered under the title)").option("--description-file <path>", 'Read the project description from a file ("-" for stdin)').option(
627
645
  "--agent-context <text>",
628
646
  "Agent context markdown (agents.md-style guidance; '' clears)"
629
- ).option("--agent-context-file <path>", "Read agent context markdown from a file").option("-j, --json", "Output raw JSON").action(async function(opts) {
647
+ ).option(
648
+ "--agent-context-file <path>",
649
+ 'Read agent context markdown from a file ("-" for stdin)'
650
+ ).option("-j, --json", "Output raw JSON").action(async function(opts) {
630
651
  const globals = this.optsWithGlobals();
631
652
  try {
632
653
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
633
654
  const teamId = await resolveTeamId(client, opts.team);
634
- const agentContext = resolveAgentContext(opts.agentContext, opts.agentContextFile);
655
+ const description = await resolveTextValue({
656
+ value: opts.description,
657
+ file: opts.descriptionFile,
658
+ inlineFlag: "--description",
659
+ fileFlag: "--description-file"
660
+ });
661
+ const agentContext = await resolveTextValue({
662
+ value: opts.agentContext,
663
+ file: opts.agentContextFile,
664
+ inlineFlag: "--agent-context",
665
+ fileFlag: "--agent-context-file"
666
+ });
635
667
  const project2 = await client.projectsPost({
636
668
  projectsPostRequest: {
637
669
  team_id: teamId,
@@ -640,7 +672,7 @@ function registerProjectCommands(program2) {
640
672
  priority: opts.priority,
641
673
  lead_id: opts.lead,
642
674
  target_date: opts.targetDate,
643
- description: opts.description,
675
+ description,
644
676
  agent_context: agentContext
645
677
  }
646
678
  });
@@ -651,14 +683,28 @@ function registerProjectCommands(program2) {
651
683
  await handleApiError(err, globals.json);
652
684
  }
653
685
  });
654
- project.command("update <id>").description("Update an existing project by UUID").option("--name <name>", "Project name").option("--status <status>", "Project status (backlog|planned|in_progress|completed|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--lead <leadId>", "Lead user UUID").option("--target-date <date>", "Target completion date (ISO)").option("--description <text>", "Project description (rendered under the title)").option(
686
+ project.command("update <id>").description("Update an existing project by UUID").option("--name <name>", "Project name").option("--status <status>", "Project status (backlog|planned|in_progress|completed|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--lead <leadId>", "Lead user UUID").option("--target-date <date>", "Target completion date (ISO)").option("--description <text>", "Project description (rendered under the title)").option("--description-file <path>", 'Read the project description from a file ("-" for stdin)').option(
655
687
  "--agent-context <text>",
656
688
  "Agent context markdown (agents.md-style guidance; '' clears)"
657
- ).option("--agent-context-file <path>", "Read agent context markdown from a file").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
689
+ ).option(
690
+ "--agent-context-file <path>",
691
+ 'Read agent context markdown from a file ("-" for stdin)'
692
+ ).option("-j, --json", "Output raw JSON").action(async function(id, opts) {
658
693
  const globals = this.optsWithGlobals();
659
694
  try {
660
695
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
661
- const agentContext = resolveAgentContext(opts.agentContext, opts.agentContextFile);
696
+ const description = await resolveTextValue({
697
+ value: opts.description,
698
+ file: opts.descriptionFile,
699
+ inlineFlag: "--description",
700
+ fileFlag: "--description-file"
701
+ });
702
+ const agentContext = await resolveTextValue({
703
+ value: opts.agentContext,
704
+ file: opts.agentContextFile,
705
+ inlineFlag: "--agent-context",
706
+ fileFlag: "--agent-context-file"
707
+ });
662
708
  const project2 = await client.projectsIdPatch({
663
709
  id,
664
710
  projectsIdPatchRequest: {
@@ -667,7 +713,7 @@ function registerProjectCommands(program2) {
667
713
  priority: opts.priority,
668
714
  lead_id: opts.lead,
669
715
  target_date: opts.targetDate,
670
- description: opts.description,
716
+ description,
671
717
  agent_context: agentContext
672
718
  }
673
719
  });
@@ -718,6 +764,7 @@ function registerProjectCommands(program2) {
718
764
  [
719
765
  'onus project create --name "Website" --team ONUS',
720
766
  'onus project create --name "Mobile" --team ONUS --priority high --target-date <YYYY-MM-DD>',
767
+ 'onus project create --name "Migration" --team ONUS --description-file ./brief.md',
721
768
  'onus project create --name "Migration" --team ONUS --agent-context-file AGENTS.md'
722
769
  ]
723
770
  );
@@ -725,6 +772,7 @@ function registerProjectCommands(program2) {
725
772
  project.commands.find((c) => c.name() === "update"),
726
773
  [
727
774
  "onus project update <projectId> --status in_progress",
775
+ "onus project update <projectId> --description-file ./brief.md",
728
776
  "onus project update <projectId> --agent-context 'Add debug logging to all new code'",
729
777
  "onus project update <projectId> --agent-context '' # clears agent context"
730
778
  ]
@@ -883,7 +931,7 @@ Comments (${issue2.comments.length}):`);
883
931
  await handleApiError(err, globals.json);
884
932
  }
885
933
  });
886
- issue.command("create").description("Create a new issue").option("--title <title>", "Issue title").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("-d, --description <description>", "Issue description (markdown)").option("--status <status>", "Status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "Assignee UUID").option("--project <projectId>", "Project UUID").option("--parent <parentId>", "Parent issue UUID").option("--label <labelId>", "Label UUID to attach (repeatable)", collect, []).option(
934
+ issue.command("create").description("Create a new issue").option("--title <title>", "Issue title").requiredOption("--team <team>", "Team UUID or KEY (e.g. ONUS)").option("-d, --description <description>", "Issue description (markdown)").option("--description-file <path>", 'Read the issue description from a file ("-" for stdin)').option("--status <status>", "Status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "Assignee UUID").option("--project <projectId>", "Project UUID").option("--parent <parentId>", "Parent issue UUID").option("--label <labelId>", "Label UUID to attach (repeatable)", collect, []).option(
887
935
  "--template <nameOrId>",
888
936
  "Issue template: UUID or exact name within the team; fills omitted fields"
889
937
  ).option("-j, --json", "Output raw JSON").action(async function(opts) {
@@ -891,8 +939,13 @@ Comments (${issue2.comments.length}):`);
891
939
  try {
892
940
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
893
941
  const teamId = await resolveTeamId(client, opts.team);
942
+ let description = await resolveTextValue({
943
+ value: opts.description,
944
+ file: opts.descriptionFile,
945
+ inlineFlag: "--description",
946
+ fileFlag: "--description-file"
947
+ });
894
948
  let title = opts.title;
895
- let description = opts.description;
896
949
  let priority = opts.priority;
897
950
  let labels = (opts.label ?? []).length > 0 ? opts.label : void 0;
898
951
  if (opts.template) {
@@ -946,7 +999,7 @@ Comments (${issue2.comments.length}):`);
946
999
  await handleApiError(err, globals.json);
947
1000
  }
948
1001
  });
949
- issue.command("update <ref>").alias("edit").description("Update (or edit) an existing issue by reference").option("--title <title>", "Issue title").option("-d, --description <description>", "Issue description").option("--status <status>", "Status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "Assignee UUID").option("--project <projectId>", "Project UUID").option("--parent <parentId>", 'Parent issue UUID (pass "null", "none", or "" to clear)').option("--add-label <labelId>", "Label UUID to add (repeatable)", collect, []).option(
1002
+ issue.command("update <ref>").alias("edit").description("Update (or edit) an existing issue by reference").option("--title <title>", "Issue title").option("-d, --description <description>", "Issue description").option("--description-file <path>", 'Read the issue description from a file ("-" for stdin)').option("--status <status>", "Status (backlog|todo|in_progress|done|canceled)").option("--priority <priority>", "Priority (none|urgent|high|medium|low)").option("--assignee <assigneeId>", "Assignee UUID").option("--project <projectId>", "Project UUID").option("--parent <parentId>", 'Parent issue UUID (pass "null", "none", or "" to clear)').option("--add-label <labelId>", "Label UUID to add (repeatable)", collect, []).option(
950
1003
  "--remove-label <labelId>",
951
1004
  "Label UUID to remove (repeatable)",
952
1005
  collect,
@@ -955,9 +1008,15 @@ Comments (${issue2.comments.length}):`);
955
1008
  const globals = this.optsWithGlobals();
956
1009
  try {
957
1010
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1011
+ const description = await resolveTextValue({
1012
+ value: opts.description,
1013
+ file: opts.descriptionFile,
1014
+ inlineFlag: "--description",
1015
+ fileFlag: "--description-file"
1016
+ });
958
1017
  const patch = {};
959
1018
  if (opts.title !== void 0) patch.title = opts.title;
960
- if (opts.description !== void 0) patch.description = opts.description;
1019
+ if (description !== void 0) patch.description = description;
961
1020
  if (opts.status !== void 0) patch.status = opts.status;
962
1021
  if (opts.priority !== void 0) patch.priority = opts.priority;
963
1022
  if (opts.assignee !== void 0) patch.assignee_id = opts.assignee;
@@ -1017,14 +1076,16 @@ Comments (${issue2.comments.length}):`);
1017
1076
  issue.commands.find((c) => c.name() === "create"),
1018
1077
  [
1019
1078
  'onus issue create --title "Fix login bug" --team ONUS',
1020
- 'onus issue create --title "Ship v2" --team ONUS --priority high --project <projectId>'
1079
+ 'onus issue create --title "Ship v2" --team ONUS --priority high --project <projectId>',
1080
+ 'onus issue create --title "Refactor auth" --team ONUS --description-file ./body.md'
1021
1081
  ]
1022
1082
  );
1023
1083
  withExamples(
1024
1084
  issue.commands.find((c) => c.name() === "update"),
1025
1085
  [
1026
1086
  "onus issue update <issueId> --status done",
1027
- "onus issue update <issueId> --priority urgent --assignee <assigneeId>"
1087
+ "onus issue update <issueId> --priority urgent --assignee <assigneeId>",
1088
+ "onus issue update <issueId> --description-file ./notes.md"
1028
1089
  ]
1029
1090
  );
1030
1091
  withExamples(
@@ -1039,13 +1100,20 @@ Comments (${issue2.comments.length}):`);
1039
1100
  }
1040
1101
  function registerIssueCommentCommands(issue) {
1041
1102
  const comment = issue.command("comment").description("Create, list, edit, and delete issue comments");
1042
- comment.command("create <ref>").description("Post a comment on an issue").requiredOption("--body <body>", "Comment body text (markdown)").option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1103
+ comment.command("create <ref>").description("Post a comment on an issue").option("--body <body>", "Comment body text (markdown)").option("--body-file <path>", 'Read the comment body from a file ("-" for stdin)').option("-j, --json", "Output raw JSON").action(async function(id, opts) {
1043
1104
  const globals = this.optsWithGlobals();
1044
1105
  try {
1045
1106
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1107
+ const body = await resolveRequiredTextValue({
1108
+ value: opts.body,
1109
+ file: opts.bodyFile,
1110
+ inlineFlag: "--body",
1111
+ fileFlag: "--body-file",
1112
+ subject: "comment body"
1113
+ });
1046
1114
  const c = await client.issuesIdCommentsPost({
1047
1115
  id,
1048
- commentsCommentIdPatchRequest: { body: opts.body }
1116
+ commentsCommentIdPatchRequest: { body }
1049
1117
  });
1050
1118
  printJsonOrFormatted(c, globals.json, () => formatSuccess(`Posted comment ${c.id}.`));
1051
1119
  } catch (err) {
@@ -1078,13 +1146,20 @@ function registerIssueCommentCommands(issue) {
1078
1146
  await handleApiError(err, globals.json);
1079
1147
  }
1080
1148
  });
1081
- comment.command("edit <ref>").description("Edit an existing comment").requiredOption("--comment <commentId>", "Comment UUID").requiredOption("--body <body>", "New comment body text").option("-j, --json", "Output raw JSON").action(async function(_id, opts) {
1149
+ comment.command("edit <ref>").description("Edit an existing comment").requiredOption("--comment <commentId>", "Comment UUID").option("--body <body>", "New comment body text").option("--body-file <path>", 'Read the new comment body from a file ("-" for stdin)').option("-j, --json", "Output raw JSON").action(async function(_id, opts) {
1082
1150
  const globals = this.optsWithGlobals();
1083
1151
  try {
1084
1152
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1153
+ const body = await resolveRequiredTextValue({
1154
+ value: opts.body,
1155
+ file: opts.bodyFile,
1156
+ inlineFlag: "--body",
1157
+ fileFlag: "--body-file",
1158
+ subject: "comment body"
1159
+ });
1085
1160
  const c = await client.commentsCommentIdPatch({
1086
1161
  commentId: opts.comment,
1087
- commentsCommentIdPatchRequest: { body: opts.body }
1162
+ commentsCommentIdPatchRequest: { body }
1088
1163
  });
1089
1164
  printJsonOrFormatted(c, globals.json, () => formatSuccess(`Updated comment ${c.id}.`));
1090
1165
  } catch (err) {
@@ -1107,7 +1182,10 @@ function registerIssueCommentCommands(issue) {
1107
1182
  });
1108
1183
  withExamples(
1109
1184
  comment.commands.find((c) => c.name() === "create"),
1110
- ['onus issue comment create <issueId> --body "Thanks for the fix!"']
1185
+ [
1186
+ 'onus issue comment create <issueId> --body "Thanks for the fix!"',
1187
+ "onus issue comment create <issueId> --body-file ./comment.md"
1188
+ ]
1111
1189
  );
1112
1190
  withExamples(
1113
1191
  comment.commands.find((c) => c.name() === "list"),
@@ -1467,13 +1545,20 @@ function registerCommentCommands(program2) {
1467
1545
  await handleApiError(err, globals.json);
1468
1546
  }
1469
1547
  });
1470
- comment.command("create <issueId>").description("Post a comment on an issue").requiredOption("--body <body>", "Comment body text (markdown)").option("-j, --json", "Output raw JSON").action(async function(issueId, opts) {
1548
+ comment.command("create <issueId>").description("Post a comment on an issue").option("--body <body>", "Comment body text (markdown)").option("--body-file <path>", 'Read the comment body from a file ("-" for stdin)').option("-j, --json", "Output raw JSON").action(async function(issueId, opts) {
1471
1549
  const globals = this.optsWithGlobals();
1472
1550
  try {
1473
1551
  const client = createApiClient({ token: globals.token, apiUrl: globals.apiUrl });
1552
+ const body = await resolveRequiredTextValue({
1553
+ value: opts.body,
1554
+ file: opts.bodyFile,
1555
+ inlineFlag: "--body",
1556
+ fileFlag: "--body-file",
1557
+ subject: "comment body"
1558
+ });
1474
1559
  const comment2 = await client.issuesIdCommentsPost({
1475
1560
  id: issueId,
1476
- commentsCommentIdPatchRequest: { body: opts.body }
1561
+ commentsCommentIdPatchRequest: { body }
1477
1562
  });
1478
1563
  printJsonOrFormatted(comment2, globals.json, () => {
1479
1564
  formatSuccess(`Posted comment ${comment2.id}.`);
@@ -1488,7 +1573,10 @@ function registerCommentCommands(program2) {
1488
1573
  );
1489
1574
  withExamples(
1490
1575
  comment.commands.find((c) => c.name() === "create"),
1491
- ['onus comment create <issueId> --body "Looks good to me"']
1576
+ [
1577
+ 'onus comment create <issueId> --body "Looks good to me"',
1578
+ "onus comment create <issueId> --body-file ./comment.md"
1579
+ ]
1492
1580
  );
1493
1581
  }
1494
1582
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunyee-studio/onus-cli",
3
- "version": "2.28.0",
3
+ "version": "2.30.0",
4
4
  "description": "Official CLI and TypeScript SDK for Onus issue tracking platform",
5
5
  "keywords": [
6
6
  "agent",
@@ -22,6 +22,7 @@ onus --api-url <url> … # custom API base URL
22
22
 
23
23
  - **Starting work on an issue: transition it to `in_progress` immediately.** As soon as you pick up an issue to implement it — before writing any code — run `onus issue update <ref> --status in_progress --json`. No update needed if `onus issue get <ref> --json` already shows the status is `in_progress` or `done`.
24
24
  - Add `--json` to every command you intend to parse.
25
+ - Multi-line text (issue descriptions, comment bodies, agent context) goes through the field's file flag — `--description-file`, `--body-file`, `--agent-context-file` — never inline in the shell command. See "Long text via file input" below.
25
26
  - Whenever referencing an Onus issue in any response, include its link: `https://onus.brunyeestudio.com/team/{TEAM}/issues/{NUM}` where `{TEAM}` is the team key (the tag before the dash in the ref, e.g. `ONUS`) and `{NUM}` is the issue number (the number after the dash). Example: for ref `ONUS-1`, `TEAM = ONUS`, `NUM = 1` → https://onus.brunyeestudio.com/team/ONUS/issues/1
26
27
  - Exit codes: `0` success · `1` general error · `2` validation error · `3` auth error · `4` not found.
27
28
  - Issue commands take a `<ref>` = human identifier (e.g. `ENG-42`) or UUID. Every `--team <team>` option accepts a team KEY (e.g. `ONUS`) or a team UUID — prefer the KEY, it's stable and readable.
@@ -65,7 +66,8 @@ reading alongside the issue itself.
65
66
  (`issue create/update --project`).
66
67
  - Editing a project (name, status, description, `agent_context`) is
67
68
  permission-gated; never guess project UUIDs — `onus project list` first.
68
- Set context from a file: `onus project update <id> --agent-context-file AGENTS.md`.
69
+ Set context from a file: `onus project update <id> --agent-context-file AGENTS.md`
70
+ (or `-` to read stdin).
69
71
 
70
72
  ## Create and update issues
71
73
 
@@ -80,6 +82,28 @@ onus issue update ENG-42 --remove-label <labelId> --assignee <userId> --json
80
82
  onus issue delete ENG-42 --yes # skip confirmation
81
83
  ```
82
84
 
85
+ ## Long text via file input
86
+
87
+ Multi-line values (issue and project descriptions, comment bodies, project
88
+ agent context) must not be inlined in the shell command — quoting, escaping,
89
+ and shell length limits make the transcript fragile. Each long-text field has
90
+ a companion file flag that reads exactly that one field from a file; pass `-`
91
+ to read the value from stdin:
92
+
93
+ ```bash
94
+ onus issue create --title "Refactor auth" --team ONUS --description-file ./body.md --json
95
+ onus issue update ENG-42 --description-file ./notes.md --json
96
+ onus issue comment create ENG-42 --body-file ./comment.md --json
97
+ onus issue comment edit ENG-42 --comment <commentId> --body-file ./comment.md --json
98
+ onus project update <projectId> --description-file ./brief.md --json
99
+ onus project update <projectId> --agent-context-file AGENTS.md --json
100
+ generate-body | onus comment create ENG-42 --body-file - --json
101
+ ```
102
+
103
+ - The file flag covers exactly one field — never a whole-record file.
104
+ - Inline value and file flag are mutually exclusive: passing both (e.g.
105
+ `--body` together with `--body-file`) is an error.
106
+
83
107
  ## Comments
84
108
 
85
109
  ```bash
@@ -121,24 +145,34 @@ onus dsn create --team ONUS --label ci # prints the full secret exactly once
121
145
  ## Pull requests and issues
122
146
 
123
147
  Linking a PR to an issue means **the issue is auto-closed when the PR merges** —
124
- there is no "related" link type. How links are created:
125
-
126
- - **Auto-linking is GitHub-style.** The integration scans the PR title, body,
127
- and branch name for `KEY-123` refs introduced by a closing keyword —
128
- `Fixes` / `Closes` / `Resolves` (case-insensitive, optional colon, comma/
129
- `and` lists). A plain prose mention of an issue ID does NOT link and never
130
- auto-closes, so avoid writing bare issue IDs in PR text: use the issue's
131
- name or its Onus URL instead.
132
- - **Manual linking is deliberate.** `onus issue link-pr` creates a bot link
133
- that survives body edits and closes on merge — use it when the PR fully
134
- resolves an issue but its text has no closing keyword.
135
- - **Only link PRs that actually fix/close the issue.** If a PR merely touches
136
- related code, discusses the issue, or implements part of it without completing
137
- it, do NOT link it (no keyword in the PR text, no `link-pr`). Use a relation
138
- (`onus issue relation add … --type related`) or a comment mention instead.
139
- - Never guess or assume: before linking, verify the PR's changes fully resolve
140
- the issue (e.g. you implemented the fix in that PR, or the PR description
141
- says "Fixes ONUS-42").
148
+ there is no "related" link type.
149
+
150
+ **Always link manually with `onus issue link-pr`. Never rely on the fix
151
+ keyword.** The integration also scans the PR title, body, and branch name for
152
+ `KEY-123` refs introduced by a closing keyword (`Fixes` / `Closes` /
153
+ `Resolves` — case-insensitive, optional colon, comma/`and` lists), but agents
154
+ must never depend on that scan and must never write a closing keyword before
155
+ an Onus issue ref in PR text or a branch name. Reference issues in PR text by
156
+ name or their Onus URL, never as bare issue IDs.
157
+
158
+ Required PR flow, in order:
159
+
160
+ 1. **Create the PR** (push the branch, open the PR with `gh`).
161
+ 2. **Determine whether the PR closes the issue.** It closes the issue only if
162
+ the PR fully resolves it — you implemented the whole fix in that PR. A PR
163
+ that merely touches related code, discusses the issue, or implements part
164
+ of it without completing it does not close it.
165
+ 3. **Link it.** If (and only if) step 2 says yes, run:
166
+
167
+ ```bash
168
+ onus issue link-pr <ref> --repo owner/repo --pr <number> --json
169
+ ```
170
+
171
+ The bot link survives PR-body edits and closes the issue on merge. If the
172
+ PR does not close the issue, leave it unlinked — use a relation
173
+ (`onus issue relation add … --type related`) or a comment mention instead.
174
+
175
+ - Never guess or assume: link only issues you know the PR fully resolves.
142
176
  - Link at most once per PR per issue; don't spam links across partially-related
143
177
  issues.
144
178