@atollhq/cli 0.1.3 → 0.1.5

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/dist/index.js CHANGED
@@ -13,6 +13,7 @@ var __export = (target, all) => {
13
13
  // src/lib/config.ts
14
14
  var config_exports = {};
15
15
  __export(config_exports, {
16
+ CONFIG_PATH: () => CONFIG_PATH,
16
17
  deleteConfig: () => deleteConfig,
17
18
  ensureProfile: () => ensureProfile,
18
19
  getActiveProfile: () => getActiveProfile,
@@ -81,9 +82,9 @@ var init_config = __esm({
81
82
  });
82
83
 
83
84
  // src/index.ts
84
- var import_commander8 = require("commander");
85
- var import_node_fs2 = require("fs");
86
- var import_node_path2 = require("path");
85
+ var import_commander11 = require("commander");
86
+ var import_node_fs4 = require("fs");
87
+ var import_node_path4 = require("path");
87
88
 
88
89
  // src/commands/auth.ts
89
90
  var import_commander = require("commander");
@@ -97,6 +98,9 @@ function isJsonMode() {
97
98
  if (process.env.OUTPUT_FORMAT === "json") return true;
98
99
  return !process.stdout.isTTY;
99
100
  }
101
+ function enableJsonMode() {
102
+ process.env.OUTPUT_FORMAT = "json";
103
+ }
100
104
  function output(data, humanReadable) {
101
105
  if (isJsonMode()) {
102
106
  process.stdout.write(JSON.stringify(data) + "\n");
@@ -104,13 +108,34 @@ function output(data, humanReadable) {
104
108
  console.log(humanReadable);
105
109
  }
106
110
  }
111
+ function outputJson(data) {
112
+ process.stdout.write(JSON.stringify(data) + "\n");
113
+ }
107
114
  function outputError(message) {
108
115
  if (isJsonMode()) {
109
- process.stdout.write(JSON.stringify({ error: message }) + "\n");
116
+ process.stderr.write(JSON.stringify({ error: message }) + "\n");
110
117
  } else {
111
118
  console.error(`Error: ${message}`);
112
119
  }
113
120
  }
121
+ var DEFAULT_LIST_LIMIT = 25;
122
+ var MAX_LIST_LIMIT = 100;
123
+ function normalizeLimit(limit, defaultLimit = DEFAULT_LIST_LIMIT) {
124
+ if (limit === void 0 || Number.isNaN(limit)) return defaultLimit;
125
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
126
+ outputError(`--limit must be an integer from 1 to ${MAX_LIST_LIMIT} (got: "${limit}")`);
127
+ process.exit(2);
128
+ }
129
+ return limit;
130
+ }
131
+ function normalizeOffset(offset) {
132
+ if (offset === void 0 || Number.isNaN(offset)) return 0;
133
+ if (!Number.isInteger(offset) || offset < 0) {
134
+ outputError(`--offset must be a non-negative integer (got: "${offset}")`);
135
+ process.exit(2);
136
+ }
137
+ return offset;
138
+ }
114
139
 
115
140
  // src/lib/client.ts
116
141
  var DEFAULT_BASE_URL = "https://atollhq.com";
@@ -140,7 +165,10 @@ var AtollClient = class {
140
165
  const body = await res.text();
141
166
  throw new Error(`API ${res.status}: ${body}`);
142
167
  }
143
- return res.json();
168
+ if (res.status === 204) return void 0;
169
+ const text = await res.text();
170
+ if (!text) return void 0;
171
+ return JSON.parse(text);
144
172
  }
145
173
  async get(path) {
146
174
  return this.request(path, { method: "GET" });
@@ -464,15 +492,26 @@ Examples:
464
492
  $ atoll issue create --title "Fix login" --priority 1 --status todo
465
493
  $ atoll issue update ATOLL-42 --status done
466
494
  $ atoll issue assign ATOLL-42 --to <user-id>`);
467
- issueCommand.command("list").description("List issues").option("--status <status>", `Filter by status (${VALID_STATUSES.join(", ")})`).option("--assignee <user>", "Filter by assignee ID").option("--priority <n>", "Filter by priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
495
+ issueCommand.command("list").description("List issues").option("--status <status>", `Filter by status (${VALID_STATUSES.join(", ")})`).option("--assignee <user>", "Filter by assignee ID").option("--priority <n>", "Filter by priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).option("--limit <n>", "Max results (1-100)", parseInt).option("--offset <n>", "Results offset for pagination", parseInt).action(async (opts) => {
468
496
  try {
497
+ if (opts.status && !VALID_STATUSES.includes(opts.status)) {
498
+ outputError(`Invalid status "${opts.status}". Must be one of: ${VALID_STATUSES.join(", ")}`);
499
+ process.exit(2);
500
+ }
501
+ if (opts.priority !== void 0 && ![0, 1, 2, 3].includes(opts.priority)) {
502
+ outputError("Priority must be 0 (urgent), 1 (high), 2 (medium), or 3 (low).");
503
+ process.exit(2);
504
+ }
469
505
  const client = new AtollClient();
470
506
  const org = await resolveOrg(client);
507
+ const limit = normalizeLimit(opts.limit);
508
+ const offset = normalizeOffset(opts.offset);
471
509
  const params = new URLSearchParams();
472
510
  if (opts.status) params.set("status", opts.status);
473
511
  if (opts.assignee) params.set("assigneeId", opts.assignee);
474
512
  if (opts.priority !== void 0) params.set("priority", String(opts.priority));
475
- if (opts.limit !== void 0) params.set("limit", String(opts.limit));
513
+ params.set("limit", String(limit));
514
+ if (offset > 0) params.set("offset", String(offset));
476
515
  const qs = params.toString();
477
516
  const [data, projects] = await Promise.all([
478
517
  client.get(`/api/orgs/${org.id}/issues${qs ? `?${qs}` : ""}`),
@@ -481,10 +520,17 @@ issueCommand.command("list").description("List issues").option("--status <status
481
520
  const enriched = data.issues.map(
482
521
  (issue) => attachIssueUrl(issue, client.baseUrl, org.slug, projects)
483
522
  );
484
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
485
- for (const issue of enriched) {
486
- process.stdout.write(JSON.stringify(issue) + "\n");
487
- }
523
+ if (isJsonMode()) {
524
+ outputJson({
525
+ resource: "issues",
526
+ items: enriched,
527
+ total: data.total,
528
+ limit: data.limit,
529
+ offset: data.offset,
530
+ nextOffset: data.offset + data.limit < data.total ? data.offset + data.limit : null,
531
+ truncated: data.offset + data.limit < data.total,
532
+ hint: data.offset + data.limit < data.total ? "Use --offset with nextOffset to continue." : null
533
+ });
488
534
  return;
489
535
  }
490
536
  if (enriched.length === 0) {
@@ -502,11 +548,14 @@ issueCommand.command("list").description("List issues").option("--status <status
502
548
  console.log(`${padEnd(id, 10)} ${statusColor(issue.status, padEnd(issue.status, 14))} ${icon} ${padEnd(pri, 8)} ${title}`);
503
549
  }
504
550
  console.log(dim(`${enriched.length} of ${data.total} issues`));
551
+ if (data.offset + data.limit < data.total) {
552
+ console.log(dim(`Next: atoll issue list --offset ${data.offset + data.limit} --limit ${data.limit}`));
553
+ }
505
554
  } catch (err) {
506
555
  handleApiError(err);
507
556
  }
508
557
  });
509
- issueCommand.command("view <identifier>").description("View issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(async (identifier) => {
558
+ async function viewIssue(identifier) {
510
559
  try {
511
560
  const client = new AtollClient();
512
561
  const org = await resolveOrg(client);
@@ -516,8 +565,8 @@ issueCommand.command("view <identifier>").description("View issue details (UUID
516
565
  fetchProjectMap(client, org.id)
517
566
  ]);
518
567
  const enriched = attachIssueUrl(issue, client.baseUrl, org.slug, projects);
519
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
520
- process.stdout.write(JSON.stringify(enriched) + "\n");
568
+ if (isJsonMode()) {
569
+ outputJson(enriched);
521
570
  return;
522
571
  }
523
572
  const id = formatIdentifier(enriched, projects);
@@ -544,7 +593,9 @@ Subtasks: ${enriched.sub_tasks.length}`);
544
593
  } catch (err) {
545
594
  handleApiError(err);
546
595
  }
547
- });
596
+ }
597
+ issueCommand.command("view <identifier>").description("View issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
598
+ issueCommand.command("get <identifier>").description("Get issue details (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(viewIssue);
548
599
  issueCommand.command("create").description("Create a new issue").requiredOption("--title <title>", "Issue title").option("--description <text>", "Issue description").option("--status <status>", `Status (${VALID_STATUSES.join(", ")})`).option("--priority <n>", "Priority (0=urgent, 1=high, 2=medium, 3=low)", parseInt).action(async (opts) => {
549
600
  try {
550
601
  if (opts.status && !VALID_STATUSES.includes(opts.status)) {
@@ -605,11 +656,22 @@ issueCommand.command("update <identifier>").description("Update an issue").optio
605
656
  handleApiError(err);
606
657
  }
607
658
  });
608
- issueCommand.command("delete <identifier>").description("Delete an issue (admin/owner only)").action(async (identifier) => {
659
+ issueCommand.command("delete <identifier>").description("Delete an issue (admin/owner only)").option("--force", "Permanently delete the issue").option("--dry-run", "Show what would be deleted without changing anything").action(async (identifier, opts) => {
609
660
  try {
661
+ if (!opts.dryRun && !opts.force) {
662
+ outputError("Permanent issue deletion requires --force. Prefer `atoll issue archive <identifier>` for reversible removal.");
663
+ process.exit(2);
664
+ }
610
665
  const client = new AtollClient();
611
666
  const orgId = await resolveOrgId(client);
612
667
  const issueId = await resolveIssueId(client, orgId, identifier);
668
+ if (opts.dryRun) {
669
+ output(
670
+ { dryRun: true, wouldDelete: { type: "issue", id: issueId, identifier } },
671
+ `Would permanently delete issue ${identifier} (${issueId})`
672
+ );
673
+ return;
674
+ }
613
675
  await client.delete(`/api/orgs/${orgId}/issues/${issueId}`);
614
676
  output(
615
677
  { success: true, id: issueId },
@@ -619,6 +681,48 @@ issueCommand.command("delete <identifier>").description("Delete an issue (admin/
619
681
  handleApiError(err);
620
682
  }
621
683
  });
684
+ issueCommand.command("archive <identifier>").description("Archive an issue (reversible soft delete)").option("--dry-run", "Show what would be archived without changing anything").action(async (identifier, opts) => {
685
+ try {
686
+ const client = new AtollClient();
687
+ const orgId = await resolveOrgId(client);
688
+ const issueId = await resolveIssueId(client, orgId, identifier);
689
+ if (opts.dryRun) {
690
+ output(
691
+ { dryRun: true, wouldArchive: { type: "issue", id: issueId, identifier } },
692
+ `Would archive issue ${identifier} (${issueId})`
693
+ );
694
+ return;
695
+ }
696
+ await client.post(`/api/orgs/${orgId}/issues/${issueId}/archive`);
697
+ output(
698
+ { success: true, id: issueId, archived: true },
699
+ success(`Archived issue ${identifier}`)
700
+ );
701
+ } catch (err) {
702
+ handleApiError(err);
703
+ }
704
+ });
705
+ issueCommand.command("unarchive <identifier>").description("Unarchive an issue").option("--dry-run", "Show what would be unarchived without changing anything").action(async (identifier, opts) => {
706
+ try {
707
+ const client = new AtollClient();
708
+ const orgId = await resolveOrgId(client);
709
+ const issueId = await resolveIssueId(client, orgId, identifier);
710
+ if (opts.dryRun) {
711
+ output(
712
+ { dryRun: true, wouldUnarchive: { type: "issue", id: issueId, identifier } },
713
+ `Would unarchive issue ${identifier} (${issueId})`
714
+ );
715
+ return;
716
+ }
717
+ await client.delete(`/api/orgs/${orgId}/issues/${issueId}/archive`);
718
+ output(
719
+ { success: true, id: issueId, archived: false },
720
+ success(`Unarchived issue ${identifier}`)
721
+ );
722
+ } catch (err) {
723
+ handleApiError(err);
724
+ }
725
+ });
622
726
  issueCommand.command("assign <identifier>").description("Assign an issue to a user or agent").requiredOption("--to <user>", 'User/agent ID or "self" for yourself').action(async (identifier, opts) => {
623
727
  try {
624
728
  const client = new AtollClient();
@@ -693,32 +797,44 @@ function formatTimestamp(ts) {
693
797
  return d.toLocaleString();
694
798
  }
695
799
  var commentCommand = new import_commander3.Command("comment").description("Manage issue comments");
696
- commentCommand.command("list <identifier>").description("List comments on an issue (UUID or PREFIX-NUMBER e.g. ATOLL-42)").action(async (identifier) => {
800
+ commentCommand.command("list <identifier>").description("List comments on an issue (UUID or PREFIX-NUMBER e.g. ATOLL-42)").option("--limit <n>", "Max results (1-100)", parseInt).action(async (identifier, opts) => {
697
801
  try {
802
+ const limit = normalizeLimit(opts.limit);
698
803
  const client = new AtollClient();
699
804
  const orgId = await resolveOrgId(client);
700
805
  const issueId = await resolveIssueId(client, orgId, identifier);
701
806
  const data = await client.get(
702
807
  `/api/orgs/${orgId}/issues/${issueId}/comments`
703
808
  );
704
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
705
- for (const comment of data.comments) {
706
- process.stdout.write(JSON.stringify(comment) + "\n");
707
- }
809
+ const allComments = data.comments ?? [];
810
+ const comments = allComments.slice(0, limit);
811
+ const truncated = allComments.length > comments.length;
812
+ if (isJsonMode()) {
813
+ outputJson({
814
+ resource: "comments",
815
+ items: comments,
816
+ total: allComments.length,
817
+ limit,
818
+ offset: 0,
819
+ nextOffset: null,
820
+ truncated,
821
+ hint: truncated ? "Increase --limit up to 100 to show more comments." : null
822
+ });
708
823
  return;
709
824
  }
710
- if (data.comments.length === 0) {
825
+ if (comments.length === 0) {
711
826
  console.log("No comments found.");
712
827
  return;
713
828
  }
714
- for (const comment of data.comments) {
829
+ for (const comment of comments) {
715
830
  const author = formatAuthor(comment);
716
831
  const time = formatTimestamp(comment.created_at);
717
832
  console.log(`${BOLD2}${CYAN}${author}${RESET2} ${DIM2}${time}${RESET2}`);
718
833
  console.log(comment.body);
719
834
  console.log();
720
835
  }
721
- console.log(`${DIM2}${data.comments.length} comment(s)${RESET2}`);
836
+ console.log(`${DIM2}${comments.length} of ${allComments.length} comment(s)${RESET2}`);
837
+ if (truncated) console.log(`${DIM2}${allComments.length - comments.length} more comment(s) hidden; increase --limit to show more.${RESET2}`);
722
838
  } catch (err) {
723
839
  handleApiError(err);
724
840
  }
@@ -757,11 +873,22 @@ commentCommand.command("update <comment-id>").description("Update a comment").re
757
873
  handleApiError(err);
758
874
  }
759
875
  });
760
- commentCommand.command("delete <comment-id>").description("Delete a comment").requiredOption("--issue <identifier>", "Issue identifier (e.g. UUID or PREFIX-NUMBER)").action(async (commentId, opts) => {
876
+ commentCommand.command("delete <comment-id>").description("Delete a comment").requiredOption("--issue <identifier>", "Issue identifier (e.g. UUID or PREFIX-NUMBER)").option("--force", "Delete the comment").option("--dry-run", "Show what would be deleted without changing anything").action(async (commentId, opts) => {
761
877
  try {
878
+ if (!opts.dryRun && !opts.force) {
879
+ outputError("Comment deletion requires --force.");
880
+ process.exit(2);
881
+ }
762
882
  const client = new AtollClient();
763
883
  const orgId = await resolveOrgId(client);
764
884
  const issueId = await resolveIssueId(client, orgId, opts.issue);
885
+ if (opts.dryRun) {
886
+ output(
887
+ { dryRun: true, wouldDelete: { type: "comment", id: commentId, issueId } },
888
+ `Would delete comment ${commentId} on ${opts.issue}`
889
+ );
890
+ return;
891
+ }
765
892
  await client.delete(`/api/orgs/${orgId}/issues/${issueId}/comments/${commentId}`);
766
893
  output(
767
894
  { success: true, id: commentId },
@@ -787,16 +914,26 @@ Examples:
787
914
  $ atoll project list
788
915
  $ atoll project create --name "My Project" --icon \u{1F680}
789
916
  $ atoll project view <project-id>`);
790
- projectCommand.command("list").description("List all projects with progress").action(async () => {
917
+ projectCommand.command("list").description("List all projects with progress").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
791
918
  const client = new AtollClient();
792
919
  try {
920
+ const limit = normalizeLimit(opts.limit);
793
921
  const org = await resolveOrg(client);
794
922
  const data = await client.get(`/api/orgs/${org.id}/projects`);
795
- const projects = (data.projects ?? []).map((p) => attachProjectUrl(p, client.baseUrl, org.slug));
796
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
797
- for (const p of projects) {
798
- process.stdout.write(JSON.stringify(p) + "\n");
799
- }
923
+ const allProjects = (data.projects ?? []).map((p) => attachProjectUrl(p, client.baseUrl, org.slug));
924
+ const projects = allProjects.slice(0, limit);
925
+ const truncated = allProjects.length > projects.length;
926
+ if (isJsonMode()) {
927
+ outputJson({
928
+ resource: "projects",
929
+ items: projects,
930
+ total: allProjects.length,
931
+ limit,
932
+ offset: 0,
933
+ nextOffset: null,
934
+ truncated,
935
+ hint: truncated ? "Narrow the request in the API or increase --limit up to 100." : null
936
+ });
800
937
  return;
801
938
  }
802
939
  if (projects.length === 0) {
@@ -813,6 +950,7 @@ projectCommand.command("list").description("List all projects with progress").ac
813
950
  if (p.status === "archived") console.log(` ${dim("[archived]")}`);
814
951
  console.log("");
815
952
  }
953
+ if (truncated) console.log(dim(`${allProjects.length - projects.length} more project(s) hidden; increase --limit to show more.`));
816
954
  } catch (err) {
817
955
  handleApiError(err);
818
956
  }
@@ -837,7 +975,7 @@ ${project.url}` : ""}`)
837
975
  handleApiError(err);
838
976
  }
839
977
  });
840
- projectCommand.command("view <projectId>").description("View project details and issues").action(async (projectId) => {
978
+ async function viewProject(projectId) {
841
979
  const client = new AtollClient();
842
980
  try {
843
981
  const org = await resolveOrg(client);
@@ -845,8 +983,8 @@ projectCommand.command("view <projectId>").description("View project details and
845
983
  `/api/orgs/${org.id}/projects/${projectId}`
846
984
  );
847
985
  const p = attachProjectUrl(data.project, client.baseUrl, org.slug);
848
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
849
- process.stdout.write(JSON.stringify(p) + "\n");
986
+ if (isJsonMode()) {
987
+ outputJson(p);
850
988
  return;
851
989
  }
852
990
  const progress = p.progress ?? 0;
@@ -869,7 +1007,9 @@ projectCommand.command("view <projectId>").description("View project details and
869
1007
  } catch (err) {
870
1008
  handleApiError(err);
871
1009
  }
872
- });
1010
+ }
1011
+ projectCommand.command("view <projectId>").description("View project details and issues").action(viewProject);
1012
+ projectCommand.command("get <projectId>").description("Get project details and issues").action(viewProject);
873
1013
 
874
1014
  // src/commands/milestone.ts
875
1015
  var import_commander5 = require("commander");
@@ -882,18 +1022,28 @@ var milestoneCommand = new import_commander5.Command("milestone").description("M
882
1022
  Examples:
883
1023
  $ atoll milestone list --project <project-id>
884
1024
  $ atoll milestone create --project <project-id> --name "v1.0" --date 2026-06-01`);
885
- milestoneCommand.command("list").description("List milestones for a project").requiredOption("--project <id>", "Project ID").action(async (opts) => {
1025
+ milestoneCommand.command("list").description("List milestones for a project").requiredOption("--project <id>", "Project ID").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
886
1026
  const client = new AtollClient();
887
1027
  try {
1028
+ const limit = normalizeLimit(opts.limit);
888
1029
  const orgId = await resolveOrgId(client);
889
1030
  const data = await client.get(
890
1031
  `/api/orgs/${orgId}/projects/${opts.project}/milestones`
891
1032
  );
892
- const milestones = data.milestones ?? [];
893
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
894
- for (const m of milestones) {
895
- process.stdout.write(JSON.stringify(m) + "\n");
896
- }
1033
+ const allMilestones = data.milestones ?? [];
1034
+ const milestones = allMilestones.slice(0, limit);
1035
+ const truncated = allMilestones.length > milestones.length;
1036
+ if (isJsonMode()) {
1037
+ outputJson({
1038
+ resource: "milestones",
1039
+ items: milestones,
1040
+ total: allMilestones.length,
1041
+ limit,
1042
+ offset: 0,
1043
+ nextOffset: null,
1044
+ truncated,
1045
+ hint: truncated ? "Narrow the request by project or increase --limit up to 100." : null
1046
+ });
897
1047
  return;
898
1048
  }
899
1049
  if (milestones.length === 0) {
@@ -911,6 +1061,7 @@ milestoneCommand.command("list").description("List milestones for a project").re
911
1061
  if (m.status === "closed") console.log(` ${dim("[closed]")}`);
912
1062
  console.log("");
913
1063
  }
1064
+ if (truncated) console.log(dim(`${allMilestones.length - milestones.length} more milestone(s) hidden; increase --limit to show more.`));
914
1065
  } catch (err) {
915
1066
  handleApiError(err);
916
1067
  }
@@ -942,6 +1093,18 @@ milestoneCommand.command("create").description("Create a new milestone").require
942
1093
  // src/commands/config.ts
943
1094
  var import_commander6 = require("commander");
944
1095
  init_config();
1096
+ function configLocation(profileName) {
1097
+ return profileName ? `${CONFIG_PATH} profile "${profileName}"` : CONFIG_PATH;
1098
+ }
1099
+ function sourceForField(cfg, profileName, field, envVar) {
1100
+ if (process.env[envVar]) return `$${envVar}`;
1101
+ if (profileName && cfg.profiles?.[profileName]?.[field]) return configLocation(profileName);
1102
+ if (!profileName && cfg[field]) return configLocation();
1103
+ return null;
1104
+ }
1105
+ function withSource(value, source) {
1106
+ return source ? `${bold(value)} ${dim(`from ${source}`)}` : bold(value);
1107
+ }
945
1108
  var configCommand = new import_commander6.Command("config").description("Manage CLI configuration (org, team, API key)").addHelpText("after", `
946
1109
  Examples:
947
1110
  $ atoll config show
@@ -953,6 +1116,12 @@ configCommand.command("show").description("Display current configuration").optio
953
1116
  const activeProfile = cfg.activeProfile;
954
1117
  const resolved = resolveConfig({ profile: opts.profile });
955
1118
  const apiKeySet = !!resolved.apiKey;
1119
+ const sources = {
1120
+ apiKey: sourceForField(cfg, resolved.profile, "apiKey", "ATOLL_API_KEY"),
1121
+ orgSlug: sourceForField(cfg, resolved.profile, "orgSlug", "ATOLL_ORG"),
1122
+ defaultTeam: sourceForField(cfg, resolved.profile, "defaultTeam", "ATOLL_TEAM"),
1123
+ baseUrl: sourceForField(cfg, resolved.profile, "baseUrl", "ATOLL_BASE_URL")
1124
+ };
956
1125
  if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
957
1126
  output(
958
1127
  {
@@ -962,6 +1131,7 @@ configCommand.command("show").description("Display current configuration").optio
962
1131
  defaultTeam: resolved.defaultTeam ?? null,
963
1132
  baseUrl: resolved.baseUrl ?? null,
964
1133
  apiKeySet,
1134
+ sources,
965
1135
  profiles: Object.keys(cfg.profiles ?? {}).sort()
966
1136
  },
967
1137
  ""
@@ -971,10 +1141,10 @@ configCommand.command("show").description("Display current configuration").optio
971
1141
  console.log(`${bold("Atoll CLI Configuration")}`);
972
1142
  console.log(` ${dim("Active profile:")} ${activeProfile ? bold(activeProfile) : gray("(not set)")}`);
973
1143
  console.log(` ${dim("Selected profile:")} ${resolved.profile ? bold(resolved.profile) : gray("(legacy config)")}`);
974
- console.log(` ${dim("Org slug:")} ${resolved.orgSlug ? bold(resolved.orgSlug) : gray("(not set)")}`);
975
- console.log(` ${dim("Default team:")} ${resolved.defaultTeam ? bold(resolved.defaultTeam) : gray("(not set)")}`);
976
- console.log(` ${dim("Base URL:")} ${resolved.baseUrl ? bold(resolved.baseUrl) : gray("(default)")}`);
977
- console.log(` ${dim("API key:")} ${apiKeySet ? bold("set") : gray("not set \u2014 run `atoll auth login`")}`);
1144
+ console.log(` ${dim("Org slug:")} ${resolved.orgSlug ? withSource(resolved.orgSlug, sources.orgSlug) : gray("(not set)")}`);
1145
+ console.log(` ${dim("Default team:")} ${resolved.defaultTeam ? withSource(resolved.defaultTeam, sources.defaultTeam) : gray("(not set)")}`);
1146
+ console.log(` ${dim("Base URL:")} ${resolved.baseUrl ? withSource(resolved.baseUrl, sources.baseUrl) : gray("(default)")}`);
1147
+ console.log(` ${dim("API key:")} ${apiKeySet ? withSource("set", sources.apiKey) : gray("not set \u2014 run `atoll auth login`")}`);
978
1148
  });
979
1149
  configCommand.command("set-org <slug>").description("Set the default organisation slug").option("--profile <name>", "Profile name to update").action((slug, opts) => {
980
1150
  const cfg = readConfig();
@@ -1059,32 +1229,44 @@ Examples:
1059
1229
  $ atoll webhook list
1060
1230
  $ atoll webhook create --url https://example.com/hook --events issue.created,issue.updated
1061
1231
  $ atoll webhook delete <id>`);
1062
- webhookCommand.command("list").description("List webhooks for the current org").action(async () => {
1232
+ webhookCommand.command("list").description("List webhooks for the current org").option("--limit <n>", "Max results (1-100)", parseInt).action(async (opts) => {
1063
1233
  try {
1234
+ const limit = normalizeLimit(opts.limit);
1064
1235
  const client = new AtollClient();
1065
1236
  const orgId = await resolveOrgId2(client);
1066
1237
  const data = await client.get(`/api/webhooks?orgId=${orgId}`);
1067
- if (!process.stdout.isTTY || process.env.OUTPUT_FORMAT === "json") {
1068
- for (const wh of data.webhooks) {
1069
- process.stdout.write(JSON.stringify(wh) + "\n");
1070
- }
1238
+ const allWebhooks = data.webhooks ?? [];
1239
+ const webhooks = allWebhooks.slice(0, limit);
1240
+ const truncated = allWebhooks.length > webhooks.length;
1241
+ if (isJsonMode()) {
1242
+ outputJson({
1243
+ resource: "webhooks",
1244
+ items: webhooks,
1245
+ total: allWebhooks.length,
1246
+ limit,
1247
+ offset: 0,
1248
+ nextOffset: null,
1249
+ truncated,
1250
+ hint: truncated ? "Increase --limit up to 100 to show more webhooks." : null
1251
+ });
1071
1252
  return;
1072
1253
  }
1073
- if (data.webhooks.length === 0) {
1254
+ if (webhooks.length === 0) {
1074
1255
  console.log("No webhooks configured.");
1075
1256
  return;
1076
1257
  }
1077
1258
  const header = bold(`${padEnd2("ID", 38)} ${padEnd2("URL", 40)} ${padEnd2("EVENTS", 20)} ACTIVE`);
1078
1259
  console.log(header);
1079
1260
  console.log("\u2500".repeat(100));
1080
- for (const wh of data.webhooks) {
1261
+ for (const wh of webhooks) {
1081
1262
  const evts = wh.events.length === 0 ? "all" : wh.events.join(",");
1082
1263
  const active = wh.enabled ? green("yes") : gray("no");
1083
1264
  const url = wh.url.length > 38 ? wh.url.slice(0, 35) + "\u2026" : wh.url;
1084
1265
  const evtStr = evts.length > 18 ? evts.slice(0, 15) + "\u2026" : evts;
1085
1266
  console.log(`${padEnd2(wh.id.slice(0, 36), 38)} ${padEnd2(url, 40)} ${padEnd2(evtStr, 20)} ${active}`);
1086
1267
  }
1087
- console.log(dim(`${data.webhooks.length} webhook(s)`));
1268
+ console.log(dim(`${webhooks.length} of ${allWebhooks.length} webhook(s)`));
1269
+ if (truncated) console.log(dim(`${allWebhooks.length - webhooks.length} more webhook(s) hidden; increase --limit to show more.`));
1088
1270
  } catch (err) {
1089
1271
  handleApiError2(err);
1090
1272
  }
@@ -1115,9 +1297,20 @@ webhookCommand.command("create").description("Create a new webhook").requiredOpt
1115
1297
  handleApiError2(err);
1116
1298
  }
1117
1299
  });
1118
- webhookCommand.command("delete <id>").description("Delete a webhook").action(async (id) => {
1300
+ webhookCommand.command("delete <id>").description("Delete a webhook").option("--force", "Delete the webhook").option("--dry-run", "Show what would be deleted without changing anything").action(async (id, opts) => {
1119
1301
  try {
1302
+ if (!opts.dryRun && !opts.force) {
1303
+ outputError("Webhook deletion requires --force.");
1304
+ process.exit(2);
1305
+ }
1120
1306
  const client = new AtollClient();
1307
+ if (opts.dryRun) {
1308
+ output(
1309
+ { dryRun: true, wouldDelete: { type: "webhook", id } },
1310
+ `Would delete webhook ${id}`
1311
+ );
1312
+ return;
1313
+ }
1121
1314
  await client.delete(`/api/webhooks/${id}`);
1122
1315
  output(
1123
1316
  { success: true, id },
@@ -1128,24 +1321,405 @@ webhookCommand.command("delete <id>").description("Delete a webhook").action(asy
1128
1321
  }
1129
1322
  });
1130
1323
 
1324
+ // src/commands/heartbeat.ts
1325
+ var import_commander8 = require("commander");
1326
+ var VALID_SEVERITIES = ["critical", "warning", "info"];
1327
+ var heartbeatCommand = new import_commander8.Command("heartbeat").description("Get the current agent heartbeat briefing").option("--signals-only", "Only show heartbeat signals").option("--severity <severity>", `Filter signals by severity (${VALID_SEVERITIES.join(", ")})`).action(async (opts) => {
1328
+ try {
1329
+ if (opts.severity && !VALID_SEVERITIES.includes(opts.severity)) {
1330
+ outputError(`Invalid severity "${opts.severity}". Must be one of: ${VALID_SEVERITIES.join(", ")}`);
1331
+ process.exit(2);
1332
+ }
1333
+ const client = new AtollClient();
1334
+ const org = await resolveOrg(client);
1335
+ const context = await client.get(`/api/orgs/${org.id}/heartbeat`);
1336
+ const signals = filterSignals(context.signals ?? [], opts.severity);
1337
+ if (isJsonMode()) {
1338
+ if (opts.signalsOnly) {
1339
+ outputJson({ signals });
1340
+ return;
1341
+ }
1342
+ outputJson({ ...context, signals });
1343
+ return;
1344
+ }
1345
+ renderHeartbeat(context, signals, opts.signalsOnly ?? false);
1346
+ } catch (err) {
1347
+ handleApiError(err);
1348
+ }
1349
+ });
1350
+ function filterSignals(signals, severity) {
1351
+ if (!severity) return signals;
1352
+ return signals.filter((signal) => signal.severity === severity);
1353
+ }
1354
+ function renderHeartbeat(context, signals, signalsOnly) {
1355
+ const agentName = context.agent?.display_name || context.agent?.id || "agent";
1356
+ console.log(bold(`Heartbeat for ${agentName}`));
1357
+ if (context.timestamp) console.log(dim(context.timestamp));
1358
+ console.log("");
1359
+ renderSignals(signals);
1360
+ if (signalsOnly) return;
1361
+ const assigned = context.assigned_issues ?? [];
1362
+ if (assigned.length > 0) {
1363
+ console.log("");
1364
+ console.log(bold("Assigned"));
1365
+ for (const issue of assigned.slice(0, 10)) {
1366
+ const id = issue.number == null ? issue.id.slice(0, 8) : `ATOLL-${issue.number}`;
1367
+ console.log(` ${pad(id, 10)} ${pad(issue.status, 13)} P${issue.priority} ${issue.title}`);
1368
+ }
1369
+ if (assigned.length > 10) console.log(dim(` ${assigned.length - 10} more assigned issue(s) hidden`));
1370
+ }
1371
+ const goals = context.goals ?? [];
1372
+ if (goals.length > 0) {
1373
+ console.log("");
1374
+ console.log(bold("Goals"));
1375
+ for (const goal of goals.slice(0, 5)) {
1376
+ const title = goal.goal?.title ?? "(untitled goal)";
1377
+ const days = goal.days_remaining == null ? "no deadline" : `${goal.days_remaining} days left`;
1378
+ const kpis = goal.kpis ?? [];
1379
+ const initiatives = goal.initiatives ?? [];
1380
+ const offPace = kpis.filter((kpi) => kpi.is_off_pace).length;
1381
+ const stale = kpis.filter((kpi) => kpi.is_stale).length;
1382
+ const stalledOrBlocked = initiatives.filter(
1383
+ (initiative) => (initiative.stalled_issues ?? 0) > 0 || (initiative.blocked_issues ?? 0) > 0
1384
+ ).length;
1385
+ console.log(` ${title} ${dim(days)}`);
1386
+ console.log(dim(` KPIs: ${offPace} off pace, ${stale} stale`));
1387
+ console.log(dim(` Initiatives: ${stalledOrBlocked} stalled/blocked`));
1388
+ }
1389
+ if (goals.length > 5) console.log(dim(` ${goals.length - 5} more goal(s) hidden`));
1390
+ }
1391
+ }
1392
+ function renderSignals(signals) {
1393
+ if (signals.length === 0) {
1394
+ console.log(gray("No heartbeat signals."));
1395
+ return;
1396
+ }
1397
+ const groups = [
1398
+ ["critical", "Critical"],
1399
+ ["warning", "Warnings"],
1400
+ ["info", "Info"]
1401
+ ];
1402
+ for (const [severity, label] of groups) {
1403
+ const group = signals.filter((signal) => signal.severity === severity);
1404
+ if (group.length === 0) continue;
1405
+ console.log(formatSeverityHeading(severity, label));
1406
+ for (const signal of group) {
1407
+ console.log(` ${pad(signal.type, 18)} ${signal.message}`);
1408
+ }
1409
+ if (severity !== "info") console.log("");
1410
+ }
1411
+ }
1412
+ function formatSeverityHeading(severity, label) {
1413
+ if (severity === "critical") return red(bold(label));
1414
+ if (severity === "warning") return yellow(bold(label));
1415
+ return cyan(bold(label));
1416
+ }
1417
+ function pad(value, length) {
1418
+ return value.length >= length ? value.slice(0, length) : value + " ".repeat(length - value.length);
1419
+ }
1420
+
1421
+ // src/commands/agent-context.ts
1422
+ var import_node_fs2 = require("fs");
1423
+ var import_node_path2 = require("path");
1424
+ var import_commander9 = require("commander");
1425
+ init_config();
1426
+ var ISSUE_STATUSES = ["backlog", "todo", "in_progress", "done", "cancelled"];
1427
+ var PRIORITIES = [0, 1, 2, 3];
1428
+ var SEVERITIES = ["critical", "warning", "info"];
1429
+ var SIGNAL_TYPES = [
1430
+ "kpi_off_pace",
1431
+ "kpi_stale",
1432
+ "issue_stale",
1433
+ "issue_blocked",
1434
+ "milestone_overdue",
1435
+ "initiative_stalled",
1436
+ "webhook_failing"
1437
+ ];
1438
+ var agentContextCommand = new import_commander9.Command("agent-context").description("Output machine-readable CLI context for agents").action(() => {
1439
+ const config = readConfig();
1440
+ const resolved = resolveConfig();
1441
+ const profiles = Object.entries(config.profiles ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, profile]) => ({
1442
+ name,
1443
+ active: name === config.activeProfile,
1444
+ apiKeySet: Boolean(profile.apiKey),
1445
+ orgSlug: profile.orgSlug ?? null,
1446
+ defaultTeam: profile.defaultTeam ?? null,
1447
+ baseUrl: profile.baseUrl ?? null
1448
+ }));
1449
+ outputJson({
1450
+ schema_version: "1",
1451
+ cli: {
1452
+ name: "atoll",
1453
+ json_flag: "--json",
1454
+ output: {
1455
+ stdout: "data",
1456
+ stderr: "diagnostics_and_errors",
1457
+ list_shape: "{ resource, items, total, limit, offset, nextOffset, truncated, hint }"
1458
+ }
1459
+ },
1460
+ selected_context: {
1461
+ activeProfile: config.activeProfile ?? null,
1462
+ selectedProfile: resolved.profile ?? null,
1463
+ orgSlug: resolved.orgSlug ?? null,
1464
+ defaultTeam: resolved.defaultTeam ?? null,
1465
+ baseUrl: resolved.baseUrl ?? null,
1466
+ apiKeySet: Boolean(resolved.apiKey)
1467
+ },
1468
+ available_profiles: profiles,
1469
+ commands: buildCommandContext(),
1470
+ skill_manifests: findSkillManifests()
1471
+ });
1472
+ });
1473
+ var skillPathCommand = new import_commander9.Command("skill-path").description("Output local Atoll skill manifest paths when available").action(() => {
1474
+ outputJson({ skill_manifests: findSkillManifests() });
1475
+ });
1476
+ function buildCommandContext() {
1477
+ return {
1478
+ heartbeat: {
1479
+ description: "Primary agent orientation command. Reads current goals, KPI pace, initiative health, assigned work, and prioritized signals.",
1480
+ subcommands: {},
1481
+ flags: {
1482
+ "--signals-only": { type: "boolean", default: false },
1483
+ "--severity": { type: "enum", values: SEVERITIES },
1484
+ "--json": { type: "boolean", default: false }
1485
+ },
1486
+ signal_types: SIGNAL_TYPES
1487
+ },
1488
+ issue: {
1489
+ subcommands: {
1490
+ list: {
1491
+ flags: {
1492
+ "--status": { type: "enum", values: ISSUE_STATUSES },
1493
+ "--assignee": { type: "string" },
1494
+ "--priority": { type: "enum", values: PRIORITIES },
1495
+ "--limit": { type: "integer", min: 1, max: 100, default: 25 },
1496
+ "--offset": { type: "integer", min: 0, default: 0 },
1497
+ "--json": { type: "boolean", default: false }
1498
+ }
1499
+ },
1500
+ get: { alias_for: "view", args: ["identifier"], flags: { "--json": { type: "boolean", default: false } } },
1501
+ view: { args: ["identifier"], flags: { "--json": { type: "boolean", default: false } } },
1502
+ create: {
1503
+ flags: {
1504
+ "--title": { type: "string", required: true },
1505
+ "--description": { type: "string" },
1506
+ "--status": { type: "enum", values: ISSUE_STATUSES },
1507
+ "--priority": { type: "enum", values: PRIORITIES },
1508
+ "--json": { type: "boolean", default: false }
1509
+ }
1510
+ },
1511
+ update: {
1512
+ args: ["identifier"],
1513
+ flags: {
1514
+ "--title": { type: "string" },
1515
+ "--status": { type: "enum", values: ISSUE_STATUSES },
1516
+ "--priority": { type: "enum", values: PRIORITIES },
1517
+ "--json": { type: "boolean", default: false }
1518
+ }
1519
+ },
1520
+ archive: { args: ["identifier"], flags: { "--dry-run": { type: "boolean" }, "--json": { type: "boolean" } } },
1521
+ unarchive: { args: ["identifier"], flags: { "--dry-run": { type: "boolean" }, "--json": { type: "boolean" } } },
1522
+ delete: {
1523
+ args: ["identifier"],
1524
+ flags: {
1525
+ "--force": { type: "boolean", required: true },
1526
+ "--dry-run": { type: "boolean" },
1527
+ "--json": { type: "boolean", default: false }
1528
+ }
1529
+ },
1530
+ assign: { args: ["identifier"], flags: { "--to": { type: "string", required: true }, "--json": { type: "boolean" } } },
1531
+ unassign: { args: ["identifier"], flags: { "--json": { type: "boolean" } } }
1532
+ }
1533
+ },
1534
+ project: {
1535
+ subcommands: {
1536
+ list: { flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } },
1537
+ get: { alias_for: "view", args: ["projectId"], flags: { "--json": { type: "boolean" } } },
1538
+ view: { args: ["projectId"], flags: { "--json": { type: "boolean" } } },
1539
+ create: {
1540
+ flags: {
1541
+ "--name": { type: "string", required: true },
1542
+ "--description": { type: "string" },
1543
+ "--color": { type: "string", default: "#6366f1" },
1544
+ "--icon": { type: "string", default: "folder" },
1545
+ "--json": { type: "boolean" }
1546
+ }
1547
+ }
1548
+ }
1549
+ },
1550
+ comment: {
1551
+ subcommands: {
1552
+ list: { args: ["identifier"], flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } },
1553
+ add: { args: ["identifier"], flags: { "--body": { type: "string", required: true }, "--json": { type: "boolean" } } },
1554
+ update: {
1555
+ args: ["comment-id"],
1556
+ flags: { "--issue": { type: "string", required: true }, "--body": { type: "string", required: true }, "--json": { type: "boolean" } }
1557
+ },
1558
+ delete: {
1559
+ args: ["comment-id"],
1560
+ flags: {
1561
+ "--issue": { type: "string", required: true },
1562
+ "--force": { type: "boolean", required: true },
1563
+ "--dry-run": { type: "boolean" },
1564
+ "--json": { type: "boolean" }
1565
+ }
1566
+ }
1567
+ }
1568
+ },
1569
+ feedback: {
1570
+ description: "Record CLI or platform friction locally and optionally submit it upstream.",
1571
+ subcommands: {
1572
+ add: {
1573
+ alias: "feedback <text>",
1574
+ flags: {
1575
+ "--type": { type: "enum", values: ["bug", "feature"], default: "bug" },
1576
+ "--url": { type: "string" },
1577
+ "--send": { type: "boolean", default: false },
1578
+ "--json": { type: "boolean" }
1579
+ }
1580
+ },
1581
+ list: { flags: { "--limit": { type: "integer", min: 1, max: 100, default: 25 }, "--json": { type: "boolean" } } }
1582
+ }
1583
+ }
1584
+ };
1585
+ }
1586
+ function findSkillManifests() {
1587
+ const repoRoot = (0, import_node_path2.resolve)(__dirname, "..", "..", "..");
1588
+ const candidates = [
1589
+ ["codex", (0, import_node_path2.join)(repoRoot, "packages", "skill-codex", "skill", "SKILL.md")],
1590
+ ["claude", (0, import_node_path2.join)(repoRoot, "packages", "skill-claude", "skill", "SKILL.md")],
1591
+ ["gemini", (0, import_node_path2.join)(repoRoot, "packages", "skill-gemini", "skill", "SKILL.md")],
1592
+ ["clawhub", (0, import_node_path2.join)(repoRoot, "clawhub", "atoll-api", "SKILL.md")]
1593
+ ];
1594
+ return candidates.map(([name, path]) => ({ name, path, exists: (0, import_node_fs2.existsSync)(path) }));
1595
+ }
1596
+
1597
+ // src/commands/feedback.ts
1598
+ var import_node_fs3 = require("fs");
1599
+ var import_node_os2 = require("os");
1600
+ var import_node_path3 = require("path");
1601
+ var import_commander10 = require("commander");
1602
+ init_config();
1603
+ var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".atoll");
1604
+ var FEEDBACK_PATH = (0, import_node_path3.join)(CONFIG_DIR2, "feedback.jsonl");
1605
+ var VALID_TYPES = ["bug", "feature"];
1606
+ var DEFAULT_BASE_URL2 = "https://atollhq.com";
1607
+ var feedbackCommand = new import_commander10.Command("feedback").description("Record CLI or platform feedback").argument("[text...]", "Feedback text").option("--type <type>", `Feedback type (${VALID_TYPES.join(", ")})`, "bug").option("--url <url>", "Related page or endpoint URL").option("--send", "Also submit to the configured Atoll feedback endpoint").action(async (text, opts) => {
1608
+ if (text.length === 0) {
1609
+ outputError('Feedback text is required. Usage: atoll feedback "what went wrong"');
1610
+ process.exit(2);
1611
+ }
1612
+ await recordFeedback(text.join(" "), opts);
1613
+ });
1614
+ feedbackCommand.command("add <text...>").description("Record feedback").option("--type <type>", `Feedback type (${VALID_TYPES.join(", ")})`, "bug").option("--url <url>", "Related page or endpoint URL").option("--send", "Also submit to the configured Atoll feedback endpoint").action(async (text, opts) => {
1615
+ await recordFeedback(text.join(" "), opts);
1616
+ });
1617
+ feedbackCommand.command("list").description("List locally recorded feedback").option("--limit <n>", "Max results (1-100)", parseInt).action((opts) => {
1618
+ const limit = normalizeLimit(opts.limit);
1619
+ const entries = readFeedbackEntries().slice(-limit).reverse();
1620
+ if (isJsonMode()) {
1621
+ outputJson({
1622
+ resource: "feedback",
1623
+ items: entries,
1624
+ total: readFeedbackEntries().length,
1625
+ limit,
1626
+ offset: 0,
1627
+ nextOffset: null,
1628
+ truncated: readFeedbackEntries().length > entries.length,
1629
+ hint: null
1630
+ });
1631
+ return;
1632
+ }
1633
+ if (entries.length === 0) {
1634
+ console.log("No local feedback recorded.");
1635
+ return;
1636
+ }
1637
+ for (const entry of entries) {
1638
+ const sent = entry.sent_upstream ? `sent:${entry.upstream_status}` : "local";
1639
+ console.log(`${entry.created_at} ${entry.type} ${sent}`);
1640
+ console.log(` ${entry.description}`);
1641
+ if (entry.url) console.log(dim(` ${entry.url}`));
1642
+ }
1643
+ });
1644
+ async function recordFeedback(description, opts) {
1645
+ if (!VALID_TYPES.includes(opts.type)) {
1646
+ outputError(`Invalid feedback type "${opts.type}". Must be one of: ${VALID_TYPES.join(", ")}`);
1647
+ process.exit(2);
1648
+ }
1649
+ const entry = {
1650
+ id: makeId(),
1651
+ type: opts.type,
1652
+ description,
1653
+ url: opts.url ?? null,
1654
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1655
+ sent_upstream: false,
1656
+ upstream_status: null,
1657
+ upstream_error: null
1658
+ };
1659
+ const endpoint = process.env.ATOLL_FEEDBACK_ENDPOINT || (opts.send ? `${resolveConfig().baseUrl || DEFAULT_BASE_URL2}/api/feedback` : null);
1660
+ if (endpoint) {
1661
+ try {
1662
+ const response = await fetch(endpoint, {
1663
+ method: "POST",
1664
+ headers: { "Content-Type": "application/json" },
1665
+ body: JSON.stringify({
1666
+ type: entry.type,
1667
+ description: entry.description,
1668
+ url: entry.url ?? void 0,
1669
+ userName: getReporterName()
1670
+ })
1671
+ });
1672
+ entry.sent_upstream = response.ok;
1673
+ entry.upstream_status = response.status;
1674
+ if (!response.ok) entry.upstream_error = await response.text();
1675
+ } catch (err) {
1676
+ entry.upstream_error = err.message ?? String(err);
1677
+ }
1678
+ }
1679
+ appendFeedbackEntry(entry);
1680
+ output(
1681
+ { feedback: entry },
1682
+ entry.sent_upstream ? success(`Feedback recorded locally and sent upstream (${entry.upstream_status})`) : success("Feedback recorded locally")
1683
+ );
1684
+ }
1685
+ function readFeedbackEntries() {
1686
+ if (!(0, import_node_fs3.existsSync)(FEEDBACK_PATH)) return [];
1687
+ return (0, import_node_fs3.readFileSync)(FEEDBACK_PATH, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
1688
+ }
1689
+ function appendFeedbackEntry(entry) {
1690
+ if (!(0, import_node_fs3.existsSync)(CONFIG_DIR2)) (0, import_node_fs3.mkdirSync)(CONFIG_DIR2, { recursive: true });
1691
+ (0, import_node_fs3.appendFileSync)(FEEDBACK_PATH, JSON.stringify(entry) + "\n", "utf-8");
1692
+ }
1693
+ function getReporterName() {
1694
+ const config = readConfig();
1695
+ return process.env.ATOLL_PROFILE || config.activeProfile || void 0;
1696
+ }
1697
+ function makeId() {
1698
+ return `fb_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1699
+ }
1700
+
1131
1701
  // src/index.ts
1132
1702
  function readCliVersion() {
1133
1703
  try {
1134
- const packageJson = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(__dirname, "..", "package.json"), "utf-8"));
1704
+ const packageJson = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(__dirname, "..", "package.json"), "utf-8"));
1135
1705
  return packageJson.version ?? "0.0.0";
1136
1706
  } catch {
1137
1707
  return "0.0.0";
1138
1708
  }
1139
1709
  }
1140
- var program = new import_commander8.Command("atoll").description("Atoll CLI \u2014 project management from the terminal").version(readCliVersion()).addHelpText("after", `
1710
+ var program = new import_commander11.Command("atoll").description("Atoll CLI \u2014 project management from the terminal").version(readCliVersion()).addHelpText("after", `
1141
1711
  Examples:
1142
1712
  $ atoll issue list
1143
1713
  $ atoll issue create --title "Fix login bug" --priority 1
1144
1714
  $ atoll issue view ATOLL-42
1715
+ $ atoll heartbeat
1145
1716
  $ atoll project list
1146
1717
  $ atoll milestone list --project <id>
1147
- $ atoll config show`).option("--profile <name>", "Use a saved auth profile (env: ATOLL_PROFILE)").option("--org <slug>", "Override default org slug (env: ATOLL_ORG)").option("--team <id>", "Override default team slug/ID (env: ATOLL_TEAM)").hook("preAction", (thisCommand) => {
1148
- const opts = thisCommand.opts();
1718
+ $ atoll config show`).option("--profile <name>", "Use a saved auth profile (env: ATOLL_PROFILE)").option("--org <slug>", "Override default org slug (env: ATOLL_ORG)").option("--team <id>", "Override default team slug/ID (env: ATOLL_TEAM)").option("--json", "Emit machine-readable JSON").hook("preAction", (_thisCommand, actionCommand) => {
1719
+ const opts = actionCommand.optsWithGlobals();
1720
+ if (opts.json) {
1721
+ enableJsonMode();
1722
+ }
1149
1723
  if (opts.profile) {
1150
1724
  process.env.ATOLL_PROFILE = opts.profile;
1151
1725
  }
@@ -1165,7 +1739,11 @@ program.addCommand(projectCommand);
1165
1739
  program.addCommand(milestoneCommand);
1166
1740
  program.addCommand(configCommand);
1167
1741
  program.addCommand(webhookCommand);
1168
- var completionCommand = new import_commander8.Command("completion").description("Output shell completion scripts").addHelpText("after", `
1742
+ program.addCommand(heartbeatCommand);
1743
+ program.addCommand(agentContextCommand);
1744
+ program.addCommand(skillPathCommand);
1745
+ program.addCommand(feedbackCommand);
1746
+ var completionCommand = new import_commander11.Command("completion").description("Output shell completion scripts").addHelpText("after", `
1169
1747
  Examples:
1170
1748
  $ atoll completion bash >> ~/.bashrc
1171
1749
  $ atoll completion zsh >> ~/.zshrc`);
@@ -1176,6 +1754,7 @@ completionCommand.command("zsh").description("Output zsh completion script").act
1176
1754
  process.stdout.write(zshCompletion());
1177
1755
  });
1178
1756
  program.addCommand(completionCommand);
1757
+ addJsonOptionToSubcommands(program);
1179
1758
  program.parse();
1180
1759
  function bashCompletion() {
1181
1760
  return `# Atoll CLI bash completion
@@ -1185,13 +1764,14 @@ _atoll_completions() {
1185
1764
  local cur prev words cword
1186
1765
  _init_completion || return
1187
1766
 
1188
- local commands="auth issue comment project milestone config webhook completion"
1189
- local issue_cmds="list view create update delete assign unassign"
1190
- local project_cmds="list create view"
1767
+ local commands="auth issue comment project milestone config webhook heartbeat agent-context skill-path feedback completion"
1768
+ local issue_cmds="list view get create update archive unarchive delete assign unassign"
1769
+ local project_cmds="list create view get"
1191
1770
  local milestone_cmds="list create"
1192
1771
  local config_cmds="show set-org set-team set-base-url"
1193
1772
  local auth_cmds="login logout status profiles use"
1194
1773
  local webhook_cmds="list create delete"
1774
+ local feedback_cmds="add list"
1195
1775
  local completion_cmds="bash zsh"
1196
1776
 
1197
1777
  if [ "\${COMP_CWORD}" -eq 1 ]; then
@@ -1230,6 +1810,11 @@ _atoll_completions() {
1230
1810
  COMPREPLY=( $(compgen -W "\${webhook_cmds}" -- "\${cur}") )
1231
1811
  fi
1232
1812
  ;;
1813
+ feedback)
1814
+ if [ "\${COMP_CWORD}" -eq 2 ]; then
1815
+ COMPREPLY=( $(compgen -W "\${feedback_cmds}" -- "\${cur}") )
1816
+ fi
1817
+ ;;
1233
1818
  completion)
1234
1819
  if [ "\${COMP_CWORD}" -eq 2 ]; then
1235
1820
  COMPREPLY=( $(compgen -W "\${completion_cmds}" -- "\${cur}") )
@@ -1263,6 +1848,10 @@ _atoll() {
1263
1848
  'milestone[Manage milestones]' \\
1264
1849
  'config[Manage CLI configuration]' \\
1265
1850
  'webhook[Manage outbound webhooks]' \\
1851
+ 'heartbeat[Get agent heartbeat briefing]' \\
1852
+ 'agent-context[Output machine-readable CLI context]' \\
1853
+ 'skill-path[Output local Atoll skill manifest paths]' \\
1854
+ 'feedback[Record CLI or platform feedback]' \\
1266
1855
  'completion[Output shell completion scripts]'
1267
1856
  ;;
1268
1857
  args)
@@ -1271,7 +1860,10 @@ _atoll() {
1271
1860
  _values 'subcommand' \\
1272
1861
  'list[List issues]' \\
1273
1862
  'view[View issue details]' \\
1863
+ 'get[Get issue details]' \\
1274
1864
  'create[Create a new issue]' \\
1865
+ 'archive[Archive an issue]' \\
1866
+ 'unarchive[Unarchive an issue]' \\
1275
1867
  'update[Update an issue]' \\
1276
1868
  'delete[Delete an issue]' \\
1277
1869
  'assign[Assign an issue]' \\
@@ -1281,7 +1873,8 @@ _atoll() {
1281
1873
  _values 'subcommand' \\
1282
1874
  'list[List projects]' \\
1283
1875
  'create[Create a project]' \\
1284
- 'view[View project details]'
1876
+ 'view[View project details]' \\
1877
+ 'get[Get project details]'
1285
1878
  ;;
1286
1879
  milestone)
1287
1880
  _values 'subcommand' \\
@@ -1309,6 +1902,11 @@ _atoll() {
1309
1902
  'create[Create a webhook]' \\
1310
1903
  'delete[Delete a webhook]'
1311
1904
  ;;
1905
+ feedback)
1906
+ _values 'subcommand' \\
1907
+ 'add[Record feedback]' \\
1908
+ 'list[List local feedback]'
1909
+ ;;
1312
1910
  completion)
1313
1911
  _values 'shell' 'bash' 'zsh'
1314
1912
  ;;
@@ -1320,4 +1918,12 @@ _atoll() {
1320
1918
  _atoll "$@"
1321
1919
  `;
1322
1920
  }
1921
+ function addJsonOptionToSubcommands(command) {
1922
+ for (const subcommand of command.commands) {
1923
+ if (!subcommand.options.some((option) => option.long === "--json")) {
1924
+ subcommand.option("--json", "Emit machine-readable JSON");
1925
+ }
1926
+ addJsonOptionToSubcommands(subcommand);
1927
+ }
1928
+ }
1323
1929
  //# sourceMappingURL=index.js.map