@skillrecordings/cli 0.9.0 → 0.10.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.
package/dist/index.js CHANGED
@@ -91673,6 +91673,51 @@ init_esm_shims();
91673
91673
 
91674
91674
  // src/commands/front/api.ts
91675
91675
  init_esm_shims();
91676
+
91677
+ // src/commands/front/json-output.ts
91678
+ init_esm_shims();
91679
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
91680
+ import { tmpdir as tmpdir2 } from "os";
91681
+ import { join as join12 } from "path";
91682
+ var STDOUT_LIMIT = 64 * 1024;
91683
+ function writeJsonOutput(data2) {
91684
+ const json = JSON.stringify(data2, null, 2);
91685
+ if (json.length <= STDOUT_LIMIT) {
91686
+ console.log(json);
91687
+ return;
91688
+ }
91689
+ const dir = join12(tmpdir2(), "skill-front");
91690
+ mkdirSync5(dir, { recursive: true });
91691
+ const filepath = join12(dir, `${Date.now()}.json`);
91692
+ writeFileSync8(filepath, json);
91693
+ const envelope = {
91694
+ _type: data2?._type ?? "result",
91695
+ _file: filepath,
91696
+ _size: `${(json.length / 1024).toFixed(1)}KB`,
91697
+ _hint: `cat ${filepath} | jq`
91698
+ };
91699
+ const d = data2;
91700
+ if (d.data && typeof d.data === "object") {
91701
+ const inner = d.data;
91702
+ if (inner.total !== void 0) envelope.total = inner.total;
91703
+ if (inner.query !== void 0) envelope.query = inner.query;
91704
+ if (Array.isArray(inner.conversations)) {
91705
+ envelope.conversations = inner.conversations.map(
91706
+ (c) => ({
91707
+ id: c.id,
91708
+ subject: c.subject,
91709
+ status: c.status
91710
+ })
91711
+ );
91712
+ }
91713
+ }
91714
+ if (Array.isArray(d._actions) && d._actions.length > 0) {
91715
+ envelope._actions = d._actions;
91716
+ }
91717
+ console.log(JSON.stringify(envelope, null, 2));
91718
+ }
91719
+
91720
+ // src/commands/front/api.ts
91676
91721
  function getFrontClient() {
91677
91722
  const apiToken = process.env.FRONT_API_TOKEN;
91678
91723
  if (!apiToken) {
@@ -91714,13 +91759,91 @@ async function apiPassthrough(method, endpoint, options) {
91714
91759
  `Unsupported method: ${method}. Use GET, POST, PATCH, PUT, or DELETE.`
91715
91760
  );
91716
91761
  }
91717
- console.log(JSON.stringify(result, null, 2));
91762
+ writeJsonOutput(result);
91718
91763
  }
91719
91764
  function registerApiCommand(frontCommand) {
91720
91765
  frontCommand.command("api").description("Raw Front API request (escape hatch)").argument("<method>", "HTTP method (GET, POST, PATCH, PUT, DELETE)").argument(
91721
91766
  "<endpoint>",
91722
91767
  "API endpoint path (e.g., /me, /conversations/cnv_xxx)"
91723
- ).option("--data <json>", "Request body as JSON string").action(
91768
+ ).option("--data <json>", "Request body as JSON string").addHelpText(
91769
+ "after",
91770
+ `
91771
+ \u2501\u2501\u2501 Raw Front API Passthrough \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
91772
+
91773
+ Escape hatch for making arbitrary Front API calls. Use this when no
91774
+ typed CLI command exists for what you need.
91775
+
91776
+ ARGUMENTS
91777
+ <method> HTTP method: GET, POST, PATCH, PUT, DELETE
91778
+ <endpoint> API path (leading / is optional \u2014 both work)
91779
+
91780
+ OPTIONS
91781
+ --data <json> Request body as a valid JSON string (for POST, PATCH, PUT)
91782
+
91783
+ COMMON ENDPOINTS
91784
+ Endpoint What it returns
91785
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
91786
+ /me Authenticated identity
91787
+ /inboxes All inboxes
91788
+ /conversations/cnv_xxx Conversation details
91789
+ /conversations/cnv_xxx/messages Messages in a conversation
91790
+ /tags All tags
91791
+ /teammates All teammates
91792
+ /contacts All contacts
91793
+ /accounts All accounts
91794
+ /channels All channels
91795
+ /rules All rules
91796
+
91797
+ ENDPOINT NORMALIZATION
91798
+ Leading slash is optional. These are equivalent:
91799
+ skill front api GET /me
91800
+ skill front api GET me
91801
+
91802
+ OUTPUT
91803
+ Always JSON. Pipe to jq for filtering.
91804
+
91805
+ EXAMPLES
91806
+ # Check authenticated identity
91807
+ skill front api GET /me
91808
+
91809
+ # List all inboxes
91810
+ skill front api GET /inboxes
91811
+
91812
+ # Get a specific conversation
91813
+ skill front api GET /conversations/cnv_abc123
91814
+
91815
+ # Archive a conversation
91816
+ skill front api PATCH /conversations/cnv_abc123 --data '{"status":"archived"}'
91817
+
91818
+ # Apply a tag to a conversation
91819
+ skill front api POST /conversations/cnv_abc123/tags --data '{"tag_ids":["tag_xxx"]}'
91820
+
91821
+ # Create a new tag
91822
+ skill front api POST /tags --data '{"name":"my-new-tag","highlight":"blue"}'
91823
+
91824
+ # Delete a tag
91825
+ skill front api DELETE /tags/tag_xxx
91826
+
91827
+ # List teammates and extract emails
91828
+ skill front api GET /teammates | jq '._results[].email'
91829
+
91830
+ # Get conversation + pipe to jq for specific fields
91831
+ skill front api GET /conversations/cnv_abc123 | jq '{subject, status, assignee: .assignee.email}'
91832
+
91833
+ WHEN TO USE THIS vs TYPED COMMANDS
91834
+ Prefer typed commands when available \u2014 they have better error handling,
91835
+ pagination, and output formatting:
91836
+ skill front search (not: skill front api GET /conversations/search/...)
91837
+ skill front inbox (not: skill front api GET /inboxes)
91838
+ skill front tags list (not: skill front api GET /tags)
91839
+ skill front conversation (not: skill front api GET /conversations/cnv_xxx)
91840
+
91841
+ Use "skill front api" for endpoints without a dedicated command, or when
91842
+ you need the raw response shape.
91843
+
91844
+ Full API docs: https://dev.frontapp.com/reference
91845
+ `
91846
+ ).action(
91724
91847
  async (method, endpoint, options) => {
91725
91848
  try {
91726
91849
  await apiPassthrough(method, endpoint, options);
@@ -92000,16 +92123,12 @@ async function archiveConversations(convId, additionalIds, options) {
92000
92123
  };
92001
92124
  })
92002
92125
  );
92003
- console.log(
92004
- JSON.stringify(
92005
- hateoasWrap({
92006
- type: "archive-result",
92007
- command: `skill front archive ${allIds.map(normalizeId).join(" ")} --json`,
92008
- data: results2
92009
- }),
92010
- null,
92011
- 2
92012
- )
92126
+ writeJsonOutput(
92127
+ hateoasWrap({
92128
+ type: "archive-result",
92129
+ command: `skill front archive ${allIds.map(normalizeId).join(" ")} --json`,
92130
+ data: results2
92131
+ })
92013
92132
  );
92014
92133
  return;
92015
92134
  }
@@ -92059,7 +92178,47 @@ async function archiveConversations(convId, additionalIds, options) {
92059
92178
  }
92060
92179
  }
92061
92180
  function registerArchiveCommand(frontCommand) {
92062
- frontCommand.command("archive").description("Archive one or more conversations by ID").argument("<id>", "Conversation ID (e.g., cnv_xxx)").argument("[ids...]", "Additional conversation IDs to archive").option("--json", "Output as JSON").action(archiveConversations);
92181
+ frontCommand.command("archive").description("Archive one or more conversations by ID").argument("<id>", "Conversation ID (e.g., cnv_xxx)").argument("[ids...]", "Additional conversation IDs to archive").option("--json", "Output as JSON").addHelpText(
92182
+ "after",
92183
+ `
92184
+ \u2501\u2501\u2501 Archive Conversations \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
92185
+
92186
+ Sets the status of one or more conversations to 'archived' in Front.
92187
+ Accepts conversation IDs (cnv_xxx) or full Front API URLs.
92188
+
92189
+ SINGLE CONVERSATION
92190
+ skill front archive cnv_abc123
92191
+
92192
+ MULTIPLE CONVERSATIONS
92193
+ skill front archive cnv_abc123 cnv_def456 cnv_ghi789
92194
+
92195
+ All IDs are archived concurrently. The summary shows success/failure counts.
92196
+
92197
+ JSON OUTPUT (for scripting)
92198
+ skill front archive cnv_abc123 --json
92199
+ skill front archive cnv_abc123 cnv_def456 --json
92200
+
92201
+ JSON envelope includes per-conversation success/error status.
92202
+
92203
+ BATCH PIPELINE (search \u2192 archive)
92204
+ # Archive all snoozed conversations in an inbox
92205
+ skill front search "is:snoozed" --inbox inb_4bj7r --json \\
92206
+ | jq -r '.data.conversations[].id' \\
92207
+ | xargs -I{} skill front archive {}
92208
+
92209
+ # Archive unassigned conversations older than 30 days
92210
+ skill front search "is:unassigned" --inbox inb_4bj7r --json \\
92211
+ | jq -r '.data.conversations[].id' \\
92212
+ | xargs -I{} skill front archive {}
92213
+
92214
+ For filter-based bulk archiving with built-in safety, use bulk-archive instead.
92215
+
92216
+ RELATED COMMANDS
92217
+ skill front bulk-archive Filter-based bulk archive (--dry-run, rate limiting)
92218
+ skill front conversation View conversation details before archiving
92219
+ skill front search Find conversations by query / filters
92220
+ `
92221
+ ).action(archiveConversations);
92063
92222
  }
92064
92223
 
92065
92224
  // src/commands/front/assign.ts
@@ -92089,20 +92248,16 @@ async function assignConversation(conversationId, teammateId, options) {
92089
92248
  const assigneeId = options.unassign ? "" : normalizeId2(teammateId);
92090
92249
  await front.conversations.updateAssignee(convId, assigneeId);
92091
92250
  if (options.json) {
92092
- console.log(
92093
- JSON.stringify(
92094
- hateoasWrap({
92095
- type: "assign-result",
92096
- command: options.unassign ? `skill front assign ${convId} --unassign --json` : `skill front assign ${convId} ${assigneeId} --json`,
92097
- data: {
92098
- id: convId,
92099
- assignee: options.unassign ? null : assigneeId,
92100
- success: true
92101
- }
92102
- }),
92103
- null,
92104
- 2
92105
- )
92251
+ writeJsonOutput(
92252
+ hateoasWrap({
92253
+ type: "assign-result",
92254
+ command: options.unassign ? `skill front assign ${convId} --unassign --json` : `skill front assign ${convId} ${assigneeId} --json`,
92255
+ data: {
92256
+ id: convId,
92257
+ assignee: options.unassign ? null : assigneeId,
92258
+ success: true
92259
+ }
92260
+ })
92106
92261
  );
92107
92262
  } else {
92108
92263
  if (options.unassign) {
@@ -92128,7 +92283,58 @@ async function assignConversation(conversationId, teammateId, options) {
92128
92283
  }
92129
92284
  }
92130
92285
  function registerAssignCommand(frontCommand) {
92131
- frontCommand.command("assign").description("Assign a conversation to a teammate").argument("<conversation-id>", "Conversation ID (cnv_xxx)").argument("[teammate-id]", "Teammate ID (tea_xxx) - omit with --unassign").option("--unassign", "Remove assignee").option("--json", "Output as JSON").action(assignConversation);
92286
+ frontCommand.command("assign").description("Assign a conversation to a teammate").argument("<conversation-id>", "Conversation ID (cnv_xxx)").argument("[teammate-id]", "Teammate ID (tea_xxx) - omit with --unassign").option("--unassign", "Remove assignee").option("--json", "Output as JSON").addHelpText(
92287
+ "after",
92288
+ `
92289
+ \u2501\u2501\u2501 Assign / Unassign Conversations \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
92290
+
92291
+ Assign a conversation to a teammate, or remove the current assignee.
92292
+ Accepts conversation IDs (cnv_xxx) and teammate IDs (tea_xxx).
92293
+
92294
+ ASSIGN
92295
+ skill front assign <conversation-id> <teammate-id>
92296
+
92297
+ The teammate must exist in your Front workspace.
92298
+
92299
+ UNASSIGN
92300
+ skill front assign <conversation-id> --unassign
92301
+
92302
+ Removes the current assignee. Cannot be combined with a teammate ID.
92303
+
92304
+ FINDING IDs
92305
+ Teammate IDs:
92306
+ skill front teammates # Human-readable list
92307
+ skill front teammates --json | jq '.[].id' # Just the IDs
92308
+
92309
+ Conversation IDs:
92310
+ skill front search --inbox inb_xxx --json | jq '.data.conversations[].id'
92311
+ skill front conversation <cnv_xxx> # Verify a conversation
92312
+
92313
+ JSON OUTPUT (--json)
92314
+ Returns a HATEOAS-wrapped object:
92315
+ { type: "assign-result", data: { id, assignee, success } }
92316
+
92317
+ EXAMPLES
92318
+ # Assign a conversation to a teammate
92319
+ skill front assign cnv_abc123 tea_def456
92320
+
92321
+ # Unassign (remove assignee)
92322
+ skill front assign cnv_abc123 --unassign
92323
+
92324
+ # Assign and get JSON output
92325
+ skill front assign cnv_abc123 tea_def456 --json
92326
+
92327
+ # Pipeline: assign all unassigned convos in an inbox to a teammate
92328
+ skill front search --inbox inb_4bj7r --status unassigned --json \\
92329
+ | jq -r '.data.conversations[].id' \\
92330
+ | xargs -I{} skill front assign {} tea_def456
92331
+
92332
+ RELATED COMMANDS
92333
+ skill front teammates List teammates and their IDs
92334
+ skill front conversation <id> View conversation details + current assignee
92335
+ skill front search Find conversations by filters
92336
+ `
92337
+ ).action(assignConversation);
92132
92338
  }
92133
92339
 
92134
92340
  // src/commands/front/bulk-archive.ts
@@ -92293,16 +92499,12 @@ Found ${result.matches.length} matching conversations`);
92293
92499
  }
92294
92500
  if (dryRun) {
92295
92501
  if (json) {
92296
- console.log(
92297
- JSON.stringify(
92298
- hateoasWrap({
92299
- type: "bulk-archive-result",
92300
- command: `skill front bulk-archive --inbox ${inbox} --dry-run --json`,
92301
- data: result
92302
- }),
92303
- null,
92304
- 2
92305
- )
92502
+ writeJsonOutput(
92503
+ hateoasWrap({
92504
+ type: "bulk-archive-result",
92505
+ command: `skill front bulk-archive --inbox ${inbox} --dry-run --json`,
92506
+ data: result
92507
+ })
92306
92508
  );
92307
92509
  } else {
92308
92510
  console.log("\nMatching conversations:");
@@ -92322,16 +92524,12 @@ Run without --dry-run to archive ${result.matches.length} conversation(s)`
92322
92524
  if (!json) {
92323
92525
  console.log("\nNo conversations to archive.");
92324
92526
  } else {
92325
- console.log(
92326
- JSON.stringify(
92327
- hateoasWrap({
92328
- type: "bulk-archive-result",
92329
- command: `skill front bulk-archive --inbox ${inbox} --json`,
92330
- data: result
92331
- }),
92332
- null,
92333
- 2
92334
- )
92527
+ writeJsonOutput(
92528
+ hateoasWrap({
92529
+ type: "bulk-archive-result",
92530
+ command: `skill front bulk-archive --inbox ${inbox} --json`,
92531
+ data: result
92532
+ })
92335
92533
  );
92336
92534
  }
92337
92535
  return;
@@ -92361,16 +92559,12 @@ Run without --dry-run to archive ${result.matches.length} conversation(s)`
92361
92559
  await new Promise((r) => setTimeout(r, 150));
92362
92560
  }
92363
92561
  if (json) {
92364
- console.log(
92365
- JSON.stringify(
92366
- hateoasWrap({
92367
- type: "bulk-archive-result",
92368
- command: `skill front bulk-archive --inbox ${inbox} --json`,
92369
- data: result
92370
- }),
92371
- null,
92372
- 2
92373
- )
92562
+ writeJsonOutput(
92563
+ hateoasWrap({
92564
+ type: "bulk-archive-result",
92565
+ command: `skill front bulk-archive --inbox ${inbox} --json`,
92566
+ data: result
92567
+ })
92374
92568
  );
92375
92569
  } else {
92376
92570
  console.log("\n\nBulk Archive Results:");
@@ -92415,7 +92609,67 @@ function registerBulkArchiveCommand(parent2) {
92415
92609
  ).option("--tag <name>", "Filter by tag name (contains)").option(
92416
92610
  "--older-than <duration>",
92417
92611
  "Filter by age (e.g., 30d, 7d, 24h, 60m)"
92418
- ).option("--dry-run", "Preview without archiving").option("--json", "JSON output").action(bulkArchiveConversations);
92612
+ ).option("--dry-run", "Preview without archiving").option("--json", "JSON output").addHelpText(
92613
+ "after",
92614
+ `
92615
+ \u2501\u2501\u2501 Bulk Archive Conversations \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
92616
+
92617
+ Archive conversations matching filter criteria from a specific inbox.
92618
+ Requires --inbox and at least one filter. Filters are AND-combined.
92619
+ Rate limiting is built in (100-150ms between API calls).
92620
+
92621
+ \u26A0\uFE0F ALWAYS use --dry-run first to preview what would be archived.
92622
+
92623
+ FILTER OPTIONS
92624
+ --sender <email> Sender email (substring match, case-insensitive)
92625
+ --subject <text> Subject line (substring match, case-insensitive)
92626
+ --status <status> Conversation status (unassigned, assigned, archived)
92627
+ --tag <name> Tag name (substring match, case-insensitive)
92628
+ --older-than <duration> Age filter \u2014 duration format: 30d, 7d, 24h, 60m
92629
+
92630
+ Filters combine with AND: --status unassigned --older-than 30d means
92631
+ conversations that are BOTH unassigned AND older than 30 days.
92632
+
92633
+ DRY RUN (preview first!)
92634
+ skill front bulk-archive --inbox inb_4bj7r --sender "mailer-daemon" --dry-run
92635
+
92636
+ Shows matching count + each conversation ID/subject/reason. No changes made.
92637
+
92638
+ EXECUTE (after verifying dry run)
92639
+ skill front bulk-archive --inbox inb_4bj7r --sender "mailer-daemon"
92640
+
92641
+ PRACTICAL EXAMPLES
92642
+ # Archive all noise from mailer-daemon
92643
+ skill front bulk-archive --inbox inb_4bj7r --sender "mailer-daemon" --dry-run
92644
+ skill front bulk-archive --inbox inb_4bj7r --sender "mailer-daemon"
92645
+
92646
+ # Archive unassigned conversations older than 30 days
92647
+ skill front bulk-archive --inbox inb_4bj7r --status unassigned --older-than 30d --dry-run
92648
+
92649
+ # Archive everything tagged "spam"
92650
+ skill front bulk-archive --inbox inb_4bj7r --tag "spam" --dry-run
92651
+
92652
+ # Archive old daily report emails
92653
+ skill front bulk-archive --inbox inb_4bj7r --subject "Daily Report" --older-than 7d --dry-run
92654
+
92655
+ # List available inboxes (omit --inbox)
92656
+ skill front bulk-archive
92657
+
92658
+ JSON OUTPUT (for scripting)
92659
+ skill front bulk-archive --inbox inb_4bj7r --status unassigned --dry-run --json
92660
+
92661
+ # Count matches
92662
+ skill front bulk-archive ... --dry-run --json | jq '.data.matches | length'
92663
+
92664
+ # Extract matched IDs
92665
+ skill front bulk-archive ... --dry-run --json | jq -r '.data.matches[].id'
92666
+
92667
+ RELATED COMMANDS
92668
+ skill front triage Categorize conversations before archiving
92669
+ skill front archive Archive specific conversations by ID
92670
+ skill front search Find conversations by query / filters
92671
+ `
92672
+ ).action(bulkArchiveConversations);
92419
92673
  }
92420
92674
 
92421
92675
  // src/commands/front/conversation-tag.ts
@@ -92454,21 +92708,17 @@ async function tagConversation(convId, tagNameOrId, options) {
92454
92708
  const tag = await resolveTag(front, tagNameOrId);
92455
92709
  await front.conversations.addTag(normalizedConvId, tag.id);
92456
92710
  if (options.json) {
92457
- console.log(
92458
- JSON.stringify(
92459
- hateoasWrap({
92460
- type: "tag-result",
92461
- command: `skill front tag ${normalizedConvId} ${tagNameOrId} --json`,
92462
- data: {
92463
- conversationId: normalizedConvId,
92464
- tagId: tag.id,
92465
- tagName: tag.name,
92466
- action: "added"
92467
- }
92468
- }),
92469
- null,
92470
- 2
92471
- )
92711
+ writeJsonOutput(
92712
+ hateoasWrap({
92713
+ type: "tag-result",
92714
+ command: `skill front tag ${normalizedConvId} ${tagNameOrId} --json`,
92715
+ data: {
92716
+ conversationId: normalizedConvId,
92717
+ tagId: tag.id,
92718
+ tagName: tag.name,
92719
+ action: "added"
92720
+ }
92721
+ })
92472
92722
  );
92473
92723
  return;
92474
92724
  }
@@ -92500,21 +92750,17 @@ async function untagConversation(convId, tagNameOrId, options) {
92500
92750
  const tag = await resolveTag(front, tagNameOrId);
92501
92751
  await front.conversations.removeTag(normalizedConvId, tag.id);
92502
92752
  if (options.json) {
92503
- console.log(
92504
- JSON.stringify(
92505
- hateoasWrap({
92506
- type: "untag-result",
92507
- command: `skill front untag ${normalizedConvId} ${tagNameOrId} --json`,
92508
- data: {
92509
- conversationId: normalizedConvId,
92510
- tagId: tag.id,
92511
- tagName: tag.name,
92512
- action: "removed"
92513
- }
92514
- }),
92515
- null,
92516
- 2
92517
- )
92753
+ writeJsonOutput(
92754
+ hateoasWrap({
92755
+ type: "untag-result",
92756
+ command: `skill front untag ${normalizedConvId} ${tagNameOrId} --json`,
92757
+ data: {
92758
+ conversationId: normalizedConvId,
92759
+ tagId: tag.id,
92760
+ tagName: tag.name,
92761
+ action: "removed"
92762
+ }
92763
+ })
92518
92764
  );
92519
92765
  return;
92520
92766
  }
@@ -92538,8 +92784,87 @@ async function untagConversation(convId, tagNameOrId, options) {
92538
92784
  }
92539
92785
  }
92540
92786
  function registerConversationTagCommands(frontCommand) {
92541
- frontCommand.command("tag").description("Add a tag to a conversation").argument("<conversation-id>", "Conversation ID (cnv_xxx)").argument("<tag-name-or-id>", "Tag name or ID (tag_xxx)").option("--json", "Output as JSON").action(tagConversation);
92542
- frontCommand.command("untag").description("Remove a tag from a conversation").argument("<conversation-id>", "Conversation ID (cnv_xxx)").argument("<tag-name-or-id>", "Tag name or ID (tag_xxx)").option("--json", "Output as JSON").action(untagConversation);
92787
+ frontCommand.command("tag").description("Add a tag to a conversation").argument("<conversation-id>", "Conversation ID (cnv_xxx)").argument("<tag-name-or-id>", "Tag name or ID (tag_xxx)").option("--json", "Output as JSON").addHelpText(
92788
+ "after",
92789
+ `
92790
+ \u2501\u2501\u2501 Tag a Conversation \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
92791
+
92792
+ Add a tag to a conversation. Accepts a tag name OR a tag ID (tag_xxx).
92793
+ Name lookup is case-insensitive \u2014 "Billing", "billing", and "BILLING" all work.
92794
+
92795
+ USAGE
92796
+ skill front tag <conversation-id> <tag-name-or-id>
92797
+
92798
+ TAG RESOLUTION
92799
+ By name: skill front tag cnv_abc123 "billing" # case-insensitive
92800
+ By ID: skill front tag cnv_abc123 tag_14nmdp # exact ID
92801
+
92802
+ If the name doesn't match any existing tag, the command errors with a hint
92803
+ to run \`skill front tags list\` to see available tags.
92804
+
92805
+ FINDING TAGS
92806
+ skill front tags list # Human-readable list
92807
+ skill front tags list --json | jq '.[].id' # Just the IDs
92808
+ skill front tags list --json | jq '.[] | {id, name}' # IDs + names
92809
+
92810
+ JSON OUTPUT (--json)
92811
+ Returns a HATEOAS-wrapped object:
92812
+ { type: "tag-result", data: { conversationId, tagId, tagName, action: "added" } }
92813
+
92814
+ EXAMPLES
92815
+ # Tag by name
92816
+ skill front tag cnv_abc123 "needs-review"
92817
+
92818
+ # Tag by ID
92819
+ skill front tag cnv_abc123 tag_14nmdp
92820
+
92821
+ # Tag and get JSON output
92822
+ skill front tag cnv_abc123 "billing" --json
92823
+
92824
+ RELATED COMMANDS
92825
+ skill front untag <id> <tag> Remove a tag from a conversation
92826
+ skill front tags list List all available tags
92827
+ skill front conversation <id> View conversation details + current tags
92828
+ `
92829
+ ).action(tagConversation);
92830
+ frontCommand.command("untag").description("Remove a tag from a conversation").argument("<conversation-id>", "Conversation ID (cnv_xxx)").argument("<tag-name-or-id>", "Tag name or ID (tag_xxx)").option("--json", "Output as JSON").addHelpText(
92831
+ "after",
92832
+ `
92833
+ \u2501\u2501\u2501 Untag a Conversation \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
92834
+
92835
+ Remove a tag from a conversation. Accepts a tag name OR a tag ID (tag_xxx).
92836
+ Name lookup is case-insensitive \u2014 "Billing", "billing", and "BILLING" all work.
92837
+
92838
+ USAGE
92839
+ skill front untag <conversation-id> <tag-name-or-id>
92840
+
92841
+ TAG RESOLUTION
92842
+ By name: skill front untag cnv_abc123 "billing" # case-insensitive
92843
+ By ID: skill front untag cnv_abc123 tag_14nmdp # exact ID
92844
+
92845
+ If the name doesn't match any existing tag, the command errors with a hint
92846
+ to run \`skill front tags list\` to see available tags.
92847
+
92848
+ JSON OUTPUT (--json)
92849
+ Returns a HATEOAS-wrapped object:
92850
+ { type: "untag-result", data: { conversationId, tagId, tagName, action: "removed" } }
92851
+
92852
+ EXAMPLES
92853
+ # Untag by name
92854
+ skill front untag cnv_abc123 "needs-review"
92855
+
92856
+ # Untag by ID
92857
+ skill front untag cnv_abc123 tag_14nmdp
92858
+
92859
+ # Untag and get JSON output
92860
+ skill front untag cnv_abc123 "billing" --json
92861
+
92862
+ RELATED COMMANDS
92863
+ skill front tag <id> <tag> Add a tag to a conversation
92864
+ skill front tags list List all available tags
92865
+ skill front conversation <id> View conversation details + current tags
92866
+ `
92867
+ ).action(untagConversation);
92543
92868
  }
92544
92869
 
92545
92870
  // src/commands/front/inbox.ts
@@ -92585,19 +92910,15 @@ async function listInboxes(options) {
92585
92910
  const inboxList = await front.inboxes.list();
92586
92911
  const inboxes = inboxList._results ?? [];
92587
92912
  if (options.json) {
92588
- console.log(
92589
- JSON.stringify(
92590
- hateoasWrap({
92591
- type: "inbox-list",
92592
- command: "skill front inbox --json",
92593
- data: inboxes,
92594
- links: inboxListLinks(
92595
- inboxes.map((i) => ({ id: i.id, name: i.name }))
92596
- )
92597
- }),
92598
- null,
92599
- 2
92600
- )
92913
+ writeJsonOutput(
92914
+ hateoasWrap({
92915
+ type: "inbox-list",
92916
+ command: "skill front inbox --json",
92917
+ data: inboxes,
92918
+ links: inboxListLinks(
92919
+ inboxes.map((i) => ({ id: i.id, name: i.name }))
92920
+ )
92921
+ })
92601
92922
  );
92602
92923
  return;
92603
92924
  }
@@ -92676,24 +92997,20 @@ async function listConversations(inboxNameOrId, options) {
92676
92997
  `);
92677
92998
  }
92678
92999
  if (options.json) {
92679
- console.log(
92680
- JSON.stringify(
92681
- hateoasWrap({
92682
- type: "conversation-list",
92683
- command: `skill front inbox ${inbox.id} --json`,
92684
- data: {
92685
- total: conversations.length,
92686
- conversations
92687
- },
92688
- links: conversationListLinks(
92689
- conversations.map((c) => ({ id: c.id, subject: c.subject })),
92690
- inbox.id
92691
- ),
92692
- actions: conversationListActions(inbox.id)
92693
- }),
92694
- null,
92695
- 2
92696
- )
93000
+ writeJsonOutput(
93001
+ hateoasWrap({
93002
+ type: "conversation-list",
93003
+ command: `skill front inbox ${inbox.id} --json`,
93004
+ data: {
93005
+ total: conversations.length,
93006
+ conversations
93007
+ },
93008
+ links: conversationListLinks(
93009
+ conversations.map((c) => ({ id: c.id, subject: c.subject })),
93010
+ inbox.id
93011
+ ),
93012
+ actions: conversationListActions(inbox.id)
93013
+ })
92697
93014
  );
92698
93015
  return;
92699
93016
  }
@@ -92748,7 +93065,100 @@ function registerInboxCommand(front) {
92748
93065
  ).option("--json", "Output as JSON").option(
92749
93066
  "--status <status>",
92750
93067
  "Filter by status (unassigned, assigned, archived)"
92751
- ).option("--tag <tag>", "Filter by tag name").option("--limit <n>", "Limit number of results", "50").action(async (inboxNameOrId, options) => {
93068
+ ).option("--tag <tag>", "Filter by tag name").option("--limit <n>", "Limit number of results", "50").addHelpText(
93069
+ "after",
93070
+ `
93071
+ \u2501\u2501\u2501 Front Inbox Command \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
93072
+
93073
+ Two modes: list all inboxes, or list conversations in a specific inbox.
93074
+
93075
+ MODE 1: LIST ALL INBOXES (no argument)
93076
+ skill front inbox Show all inboxes (ID, name, privacy)
93077
+ skill front inbox --json JSON output with HATEOAS links
93078
+
93079
+ MODE 2: LIST CONVERSATIONS IN AN INBOX (with argument)
93080
+ skill front inbox <inbox-name-or-id> List conversations in an inbox
93081
+ skill front inbox inb_4bj7r By inbox ID
93082
+ skill front inbox "AI Hero" By name (case-insensitive exact match)
93083
+
93084
+ INBOX NAME LOOKUP
93085
+ Pass a human-readable name instead of inb_xxx. The command fetches all
93086
+ inboxes and matches case-insensitively against the full name.
93087
+ Example: "ai hero" matches "AI Hero", "Total TypeScript" matches exactly.
93088
+ If no match is found, an error is thrown with the name you provided.
93089
+
93090
+ OPTIONS
93091
+ Flag Default Description
93092
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
93093
+ --status <status> (none) Filter conversations by status
93094
+ --tag <tag> (none) Filter by tag name or tag_xxx ID
93095
+ --limit <n> 50 Max conversations to return (pages auto)
93096
+ --json false Output as JSON (HATEOAS envelope)
93097
+
93098
+ STATUS VALUES (--status flag)
93099
+ open In the Open tab (not archived, trashed, or snoozed)
93100
+ archived In the Archived tab
93101
+ assigned Has an assignee
93102
+ unassigned No assignee
93103
+ unreplied Last message was inbound (no teammate reply yet)
93104
+ snoozed Snoozed (will reopen later)
93105
+ trashed In Trash
93106
+ waiting Waiting for response
93107
+
93108
+ TAG FILTER (--tag flag)
93109
+ Filter by tag name (human-readable) or tag ID (tag_xxx).
93110
+ The tag value is passed as a Front query filter: tag:"<value>"
93111
+ Examples:
93112
+ --tag "500 Error" Filter by tag name
93113
+ --tag tag_14nmdp Filter by tag ID
93114
+
93115
+ PAGINATION
93116
+ Page size is always 50. If --limit is higher than 50, the command
93117
+ automatically paginates through results until the limit is reached.
93118
+ Progress is shown in the terminal (e.g., "Fetched 150 conversations...").
93119
+
93120
+ JSON + jq PATTERNS
93121
+ # List all inbox IDs
93122
+ skill front inbox --json | jq '.data[].id'
93123
+
93124
+ # Get inbox names and IDs as a table
93125
+ skill front inbox --json | jq '.data[] | {id, name}'
93126
+
93127
+ # Get conversation IDs from an inbox
93128
+ skill front inbox inb_4bj7r --json | jq '.data.conversations[].id'
93129
+
93130
+ # Get conversation summaries
93131
+ skill front inbox inb_4bj7r --json | jq '.data.conversations[] | {id, subject, status}'
93132
+
93133
+ # Count conversations by status
93134
+ skill front inbox inb_4bj7r --json | jq '[.data.conversations[].status] | group_by(.) | map({status: .[0], count: length})'
93135
+
93136
+ EXAMPLES
93137
+ # Step 1: Find inbox IDs
93138
+ skill front inbox
93139
+
93140
+ # Step 2: Look up inbox by name
93141
+ skill front inbox "Total TypeScript"
93142
+
93143
+ # Step 3: Filter unassigned conversations
93144
+ skill front inbox inb_4bj7r --status unassigned
93145
+
93146
+ # Step 4: Filter by tag
93147
+ skill front inbox inb_4bj7r --tag "500 Error"
93148
+
93149
+ # Step 5: Pipe to jq for conversation IDs
93150
+ skill front inbox inb_4bj7r --status open --json | jq '.data.conversations[].id'
93151
+
93152
+ # Limit results to 10
93153
+ skill front inbox inb_4bj7r --limit 10
93154
+
93155
+ RELATED COMMANDS
93156
+ skill front conversation <id> View full conversation with messages
93157
+ skill front search <query> Search across all inboxes with filters
93158
+ skill front report Generate support metrics report
93159
+ skill front triage Triage unassigned conversations
93160
+ `
93161
+ ).action(async (inboxNameOrId, options) => {
92752
93162
  if (!inboxNameOrId) {
92753
93163
  await listInboxes(options || {});
92754
93164
  } else {
@@ -92759,7 +93169,7 @@ function registerInboxCommand(front) {
92759
93169
 
92760
93170
  // src/commands/front/pull-conversations.ts
92761
93171
  init_esm_shims();
92762
- import { writeFileSync as writeFileSync8 } from "fs";
93172
+ import { writeFileSync as writeFileSync9 } from "fs";
92763
93173
  async function pullConversations(options) {
92764
93174
  const { inbox, limit: limit2 = 50, output, filter: filter4, json = false } = options;
92765
93175
  const frontToken = process.env.FRONT_API_TOKEN;
@@ -92878,20 +93288,16 @@ Built ${samples.length} eval samples`);
92878
93288
  console.log(` ${cat}: ${count}`);
92879
93289
  }
92880
93290
  if (output) {
92881
- writeFileSync8(output, JSON.stringify(samples, null, 2));
93291
+ writeFileSync9(output, JSON.stringify(samples, null, 2));
92882
93292
  console.log(`
92883
93293
  Saved to ${output}`);
92884
93294
  } else if (json) {
92885
- console.log(
92886
- JSON.stringify(
92887
- hateoasWrap({
92888
- type: "eval-dataset",
92889
- command: `skill front pull --inbox ${inbox} --json`,
92890
- data: samples
92891
- }),
92892
- null,
92893
- 2
92894
- )
93295
+ writeJsonOutput(
93296
+ hateoasWrap({
93297
+ type: "eval-dataset",
93298
+ command: `skill front pull --inbox ${inbox} --json`,
93299
+ data: samples
93300
+ })
92895
93301
  );
92896
93302
  }
92897
93303
  } catch (error) {
@@ -92903,7 +93309,73 @@ Saved to ${output}`);
92903
93309
  }
92904
93310
  }
92905
93311
  function registerPullCommand(parent2) {
92906
- parent2.command("pull").description("Export conversations to JSON for eval datasets").option("-i, --inbox <id>", "Inbox ID to pull from").option("-l, --limit <n>", "Max conversations to pull", parseInt).option("-o, --output <file>", "Output file path").option("-f, --filter <term>", "Filter by subject/tag containing term").option("--json", "JSON output").action(pullConversations);
93312
+ parent2.command("pull").description("Export conversations to JSON for eval datasets").option("-i, --inbox <id>", "Inbox ID to pull from").option("-l, --limit <n>", "Max conversations to pull", parseInt).option("-o, --output <file>", "Output file path").option("-f, --filter <term>", "Filter by subject/tag containing term").option("--json", "JSON output").addHelpText(
93313
+ "after",
93314
+ `
93315
+ \u2501\u2501\u2501 Pull Conversations (Eval Dataset Export) \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
93316
+
93317
+ Export conversations from a Front inbox as structured EvalSample objects.
93318
+ Designed for building eval datasets for routing, classification, and
93319
+ canned-response testing.
93320
+
93321
+ OPTIONS
93322
+ -i, --inbox <id> Inbox ID to pull from (inb_xxx). Omit to list
93323
+ available inboxes and their IDs.
93324
+ -l, --limit <n> Max conversations to export (default: 50)
93325
+ -o, --output <file> Write output to a file instead of stdout
93326
+ -f, --filter <term> Only include conversations whose subject or tags
93327
+ contain this text (case-insensitive)
93328
+ --json Output as JSON (HATEOAS-wrapped)
93329
+
93330
+ OUTPUT FORMAT (EvalSample)
93331
+ Each sample includes:
93332
+ - id / conversationId Front conversation ID
93333
+ - subject Conversation subject
93334
+ - customerEmail Sender email address
93335
+ - status Conversation status
93336
+ - tags Array of tag names
93337
+ - triggerMessage Most recent inbound message (id, subject, body, timestamp)
93338
+ - conversationHistory Full message thread (direction, body, timestamp, author)
93339
+ - category Inferred category (see below)
93340
+
93341
+ CATEGORY INFERENCE
93342
+ Category Rule
93343
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
93344
+ refund Tag or subject contains "refund"
93345
+ access Tag contains "access" or subject contains "login"/"access"
93346
+ technical Tag contains "technical" or subject contains "error"/"bug"
93347
+ feedback Subject contains "feedback" or "suggestion"
93348
+ business Subject contains "partnership" or "collaborate"
93349
+ general Everything else (default)
93350
+
93351
+ RATE LIMITING
93352
+ Built-in 100ms delay between conversation message fetches to respect
93353
+ Front API limits. Large exports will take time proportional to --limit.
93354
+
93355
+ EXAMPLES
93356
+ # List available inboxes (no --inbox flag)
93357
+ skill front pull
93358
+
93359
+ # Pull 50 conversations (default limit)
93360
+ skill front pull --inbox inb_4bj7r
93361
+
93362
+ # Pull 200 conversations and save to file
93363
+ skill front pull --inbox inb_4bj7r --limit 200 --output data/eval-dataset.json
93364
+
93365
+ # Pull only refund-related conversations
93366
+ skill front pull --inbox inb_4bj7r --filter refund --output data/refund-samples.json
93367
+
93368
+ # Pipe JSON for analysis
93369
+ skill front pull --inbox inb_4bj7r --json | jq '[.data[] | {id, category, subject}]'
93370
+
93371
+ # Category breakdown
93372
+ skill front pull --inbox inb_4bj7r --json | jq '[.data[].category] | group_by(.) | map({(.[0]): length}) | add'
93373
+
93374
+ RELATED COMMANDS
93375
+ skill eval routing <file> Run routing eval against a dataset
93376
+ skill front inbox List inboxes
93377
+ `
93378
+ ).action(pullConversations);
92907
93379
  }
92908
93380
 
92909
93381
  // src/commands/front/reply.ts
@@ -92930,16 +93402,12 @@ async function replyToConversation(conversationId, options) {
92930
93402
  }
92931
93403
  );
92932
93404
  if (options.json) {
92933
- console.log(
92934
- JSON.stringify(
92935
- hateoasWrap({
92936
- type: "draft-reply",
92937
- command: `skill front reply ${normalizedId} --body ${JSON.stringify(options.body)}${options.author ? ` --author ${options.author}` : ""} --json`,
92938
- data: draft
92939
- }),
92940
- null,
92941
- 2
92942
- )
93405
+ writeJsonOutput(
93406
+ hateoasWrap({
93407
+ type: "draft-reply",
93408
+ command: `skill front reply ${normalizedId} --body ${JSON.stringify(options.body)}${options.author ? ` --author ${options.author}` : ""} --json`,
93409
+ data: draft
93410
+ })
92943
93411
  );
92944
93412
  return;
92945
93413
  }
@@ -92971,7 +93439,62 @@ async function replyToConversation(conversationId, options) {
92971
93439
  }
92972
93440
  }
92973
93441
  function registerReplyCommand(frontCommand) {
92974
- frontCommand.command("reply").description("Create a draft reply on a conversation").argument("<conversation-id>", "Conversation ID (cnv_xxx)").requiredOption("--body <text>", "Reply body text").option("--author <teammate-id>", "Author teammate ID").option("--json", "Output as JSON").action(replyToConversation);
93442
+ frontCommand.command("reply").description("Create a draft reply on a conversation").argument("<conversation-id>", "Conversation ID (cnv_xxx)").requiredOption("--body <text>", "Reply body text").option("--author <teammate-id>", "Author teammate ID").option("--json", "Output as JSON").addHelpText(
93443
+ "after",
93444
+ `
93445
+ \u2501\u2501\u2501 Draft Reply (HITL \u2014 Human-in-the-Loop) \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
93446
+
93447
+ \u26A0\uFE0F SAFETY: This command creates a DRAFT only. It NEVER auto-sends.
93448
+ The draft appears in Front for a human to review, edit, and send manually.
93449
+ This is by design \u2014 the HITL principle ensures no message goes out without
93450
+ human approval.
93451
+
93452
+ USAGE
93453
+ skill front reply <conversation-id> --body "Your reply text here"
93454
+
93455
+ OPTIONS
93456
+ --body <text> Required. The reply body text.
93457
+ Accepts plain text or HTML.
93458
+ --author <teammate-id> Optional. Teammate ID (tea_xxx) to set as sender.
93459
+ Defaults to the API token owner.
93460
+ --json Output as JSON.
93461
+
93462
+ BODY FORMAT
93463
+ Plain text: --body "Thanks for reaching out. We'll look into this."
93464
+ HTML: --body "<p>Hi there,</p><p>Your refund has been processed.</p>"
93465
+
93466
+ For multi-line plain text, the body will render as-is in the Front draft.
93467
+
93468
+ JSON OUTPUT (--json)
93469
+ Returns a HATEOAS-wrapped object:
93470
+ { type: "draft-reply", data: { id, ... } }
93471
+
93472
+ WORKFLOW
93473
+ 1. Read the conversation first:
93474
+ skill front conversation cnv_abc123 -m
93475
+ 2. Draft a reply:
93476
+ skill front reply cnv_abc123 --body "We've processed your request."
93477
+ 3. Open Front \u2192 review the draft \u2192 edit if needed \u2192 click Send.
93478
+
93479
+ EXAMPLES
93480
+ # Simple draft reply
93481
+ skill front reply cnv_abc123 --body "Thanks, we're looking into this now."
93482
+
93483
+ # HTML reply with specific author
93484
+ skill front reply cnv_abc123 \\
93485
+ --body "<p>Hi! Your license has been transferred.</p>" \\
93486
+ --author tea_def456
93487
+
93488
+ # Draft reply and capture the draft ID
93489
+ skill front reply cnv_abc123 --body "Processing your refund." --json \\
93490
+ | jq '.data.id'
93491
+
93492
+ RELATED COMMANDS
93493
+ skill front conversation <id> -m View conversation + message history
93494
+ skill front message <id> View a specific message body
93495
+ skill front search Find conversations to reply to
93496
+ `
93497
+ ).action(replyToConversation);
92975
93498
  }
92976
93499
 
92977
93500
  // src/commands/front/report.ts
@@ -93074,18 +93597,14 @@ async function generateReport(options) {
93074
93597
  );
93075
93598
  if (json) {
93076
93599
  const unresolvedIds = report.unresolvedIssues.map((i) => i.id);
93077
- console.log(
93078
- JSON.stringify(
93079
- hateoasWrap({
93080
- type: "report",
93081
- command: `skill front report --inbox ${inbox} --json`,
93082
- data: report,
93083
- links: reportLinks(inbox, unresolvedIds),
93084
- actions: reportActions(inbox)
93085
- }),
93086
- null,
93087
- 2
93088
- )
93600
+ writeJsonOutput(
93601
+ hateoasWrap({
93602
+ type: "report",
93603
+ command: `skill front report --inbox ${inbox} --json`,
93604
+ data: report,
93605
+ links: reportLinks(inbox, unresolvedIds),
93606
+ actions: reportActions(inbox)
93607
+ })
93089
93608
  );
93090
93609
  } else {
93091
93610
  printReport(report);
@@ -93176,7 +93695,64 @@ function registerReportCommand(front) {
93176
93695
  "Number of days to include in report",
93177
93696
  parseInt,
93178
93697
  30
93179
- ).option("--json", "Output as JSON").action(generateReport);
93698
+ ).option("--json", "Output as JSON").addHelpText(
93699
+ "after",
93700
+ `
93701
+ \u2501\u2501\u2501 Inbox Forensics Report \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
93702
+
93703
+ Generates a comprehensive report for a Front inbox covering the last N days.
93704
+ --inbox is required. --days defaults to 30.
93705
+
93706
+ WHAT THE REPORT INCLUDES
93707
+ - Overview: total conversations, status breakdown with percentages
93708
+ - Volume by week: bar chart of conversation volume per ISO week
93709
+ - Tag breakdown: top 15 tags by frequency
93710
+ - Top senders: top 10 sender email addresses
93711
+ - Unresolved issues: unassigned conversations (newest first, up to 10 shown)
93712
+
93713
+ BASIC USAGE
93714
+ skill front report --inbox inb_4bj7r
93715
+ skill front report --inbox inb_4bj7r --days 60
93716
+ skill front report --inbox inb_4bj7r --days 7
93717
+
93718
+ JSON OUTPUT (for scripting)
93719
+ skill front report --inbox inb_4bj7r --json
93720
+
93721
+ # Extract unresolved issues
93722
+ skill front report --inbox inb_4bj7r --json | jq '.data.unresolvedIssues'
93723
+
93724
+ # Top senders
93725
+ skill front report --inbox inb_4bj7r --json | jq '.data.topSenders'
93726
+
93727
+ # Volume by week
93728
+ skill front report --inbox inb_4bj7r --json | jq '.data.volumeByWeek'
93729
+
93730
+ # Status breakdown
93731
+ skill front report --inbox inb_4bj7r --json | jq '.data.overview.byStatus'
93732
+
93733
+ # Count of unassigned conversations
93734
+ skill front report --inbox inb_4bj7r --json | jq '.data.unresolvedIssues | length'
93735
+
93736
+ # Senders with more than 5 conversations
93737
+ skill front report --inbox inb_4bj7r --json \\
93738
+ | jq '[.data.topSenders[] | select(.count > 5)]'
93739
+
93740
+ WORKFLOW: REPORT \u2192 TRIAGE \u2192 ARCHIVE
93741
+ # 1. Run report to understand inbox state
93742
+ skill front report --inbox inb_4bj7r
93743
+
93744
+ # 2. Triage to categorize conversations
93745
+ skill front triage --inbox inb_4bj7r
93746
+
93747
+ # 3. Bulk archive the noise
93748
+ skill front bulk-archive --inbox inb_4bj7r --sender "noreply@" --dry-run
93749
+
93750
+ RELATED COMMANDS
93751
+ skill front triage Categorize conversations by intent
93752
+ skill front inbox List and inspect inboxes
93753
+ skill front bulk-archive Archive conversations matching filters
93754
+ `
93755
+ ).action(generateReport);
93180
93756
  }
93181
93757
 
93182
93758
  // src/commands/front/search.ts
@@ -93235,24 +93811,20 @@ async function searchConversations(query, options) {
93235
93811
  console.log("");
93236
93812
  }
93237
93813
  if (options.json) {
93238
- console.log(
93239
- JSON.stringify(
93240
- hateoasWrap({
93241
- type: "search-results",
93242
- command: `skill front search ${JSON.stringify(fullQuery)} --json`,
93243
- data: {
93244
- query: fullQuery,
93245
- total: conversations.length,
93246
- conversations
93247
- },
93248
- links: conversationListLinks(
93249
- conversations.map((c) => ({ id: c.id, subject: c.subject }))
93250
- ),
93251
- actions: options.inbox ? conversationListActions(options.inbox) : []
93252
- }),
93253
- null,
93254
- 2
93255
- )
93814
+ writeJsonOutput(
93815
+ hateoasWrap({
93816
+ type: "search-results",
93817
+ command: `skill front search ${JSON.stringify(fullQuery)} --json`,
93818
+ data: {
93819
+ query: fullQuery,
93820
+ total: conversations.length,
93821
+ conversations
93822
+ },
93823
+ links: conversationListLinks(
93824
+ conversations.map((c) => ({ id: c.id, subject: c.subject }))
93825
+ ),
93826
+ actions: options.inbox ? conversationListActions(options.inbox) : []
93827
+ })
93256
93828
  );
93257
93829
  return;
93258
93830
  }
@@ -93382,6 +93954,17 @@ EXAMPLES
93382
93954
  # Pipe JSON to jq for IDs only
93383
93955
  skill front search "is:unassigned" --inbox inb_4bj7r --json | jq '.data.conversations[].id'
93384
93956
 
93957
+ LARGE RESULTS
93958
+ When --json output exceeds 64KB (common with 25+ conversations), results are
93959
+ automatically written to a temp file. Stdout gets a summary with the file path:
93960
+ { "_file": "/tmp/skill-front/1738692000.json", "total": 50, ... }
93961
+
93962
+ To always get the full file:
93963
+ skill front search "..." --json > results.json
93964
+
93965
+ To process the spilled file:
93966
+ cat /tmp/skill-front/*.json | jq '.data.conversations[].id'
93967
+
93385
93968
  Full docs: https://dev.frontapp.com/docs/search-1
93386
93969
  `
93387
93970
  ).action((query, options) => {
@@ -93521,20 +94104,16 @@ async function listTags(options) {
93521
94104
  );
93522
94105
  const filteredTags = options.unused ? tagsWithCounts.filter((t2) => t2.conversation_count === 0) : tagsWithCounts;
93523
94106
  if (options.json) {
93524
- console.log(
93525
- JSON.stringify(
93526
- hateoasWrap({
93527
- type: "tag-list",
93528
- command: `skill front tags list${options.unused ? " --unused" : ""} --json`,
93529
- data: filteredTags,
93530
- links: tagListLinks(
93531
- filteredTags.map((t2) => ({ id: t2.id, name: t2.name }))
93532
- ),
93533
- actions: tagListActions()
93534
- }),
93535
- null,
93536
- 2
93537
- )
94107
+ writeJsonOutput(
94108
+ hateoasWrap({
94109
+ type: "tag-list",
94110
+ command: `skill front tags list${options.unused ? " --unused" : ""} --json`,
94111
+ data: filteredTags,
94112
+ links: tagListLinks(
94113
+ filteredTags.map((t2) => ({ id: t2.id, name: t2.name }))
94114
+ ),
94115
+ actions: tagListActions()
94116
+ })
93538
94117
  );
93539
94118
  return;
93540
94119
  }
@@ -93940,13 +94519,146 @@ async function cleanupTags(options) {
93940
94519
  }
93941
94520
  }
93942
94521
  function registerTagCommands(frontCommand) {
93943
- const tags = frontCommand.command("tags").description("List, rename, delete, and clean up Front tags");
93944
- tags.command("list").description("List all tags with conversation counts").option("--json", "Output as JSON").option("--unused", "Show only tags with 0 conversations").action(listTags);
93945
- tags.command("delete").description("Delete a tag by ID").argument("<id>", "Tag ID (e.g., tag_xxx)").option("-f, --force", "Skip confirmation prompt").action(deleteTag);
93946
- tags.command("rename").description("Rename a tag").argument("<id>", "Tag ID (e.g., tag_xxx)").argument("<name>", "New tag name").action(renameTag);
94522
+ const tags = frontCommand.command("tags").description("List, rename, delete, and clean up Front tags").addHelpText(
94523
+ "after",
94524
+ `
94525
+ \u2501\u2501\u2501 Tag Management \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
94526
+
94527
+ Manage Front tags: list with usage counts, delete unused, rename,
94528
+ and bulk-clean duplicates / case variants / obsolete tags.
94529
+
94530
+ SUBCOMMANDS
94531
+ list List all tags with conversation counts (--unused, --json)
94532
+ delete Delete a tag by ID (tag_xxx)
94533
+ rename Rename a tag (tag_xxx \u2192 new name)
94534
+ cleanup Find and fix duplicate, case-variant, and obsolete tags
94535
+
94536
+ EXAMPLES
94537
+ skill front tags list
94538
+ skill front tags list --unused --json
94539
+ skill front tags delete tag_abc123 --force
94540
+ skill front tags rename tag_abc123 "billing-issue"
94541
+ skill front tags cleanup
94542
+ skill front tags cleanup --execute
94543
+
94544
+ RELATED COMMANDS
94545
+ skill front tag <cnv_xxx> <tag_xxx> Apply a tag to a conversation
94546
+ skill front untag <cnv_xxx> <tag_xxx> Remove a tag from a conversation
94547
+ `
94548
+ );
94549
+ tags.command("list").description("List all tags with conversation counts").option("--json", "Output as JSON").option("--unused", "Show only tags with 0 conversations").addHelpText(
94550
+ "after",
94551
+ `
94552
+ \u2501\u2501\u2501 Tag List \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
94553
+
94554
+ Lists every tag in the Front workspace with its conversation count.
94555
+ Conversation counts are fetched per-tag (rate-limited, ~5 concurrent).
94556
+
94557
+ OPTIONS
94558
+ --unused Show only tags with 0 conversations (candidates for deletion)
94559
+ --json Output as JSON (HATEOAS-wrapped with links and actions)
94560
+
94561
+ OUTPUT COLUMNS (table mode)
94562
+ ID Tag ID (tag_xxx)
94563
+ Name Tag display name
94564
+ Color Highlight color
94565
+ Convos Number of conversations using this tag (0 shows warning)
94566
+
94567
+ JSON + jq PATTERNS
94568
+ # All unused tags
94569
+ skill front tags list --json | jq '.data[] | select(.conversation_count == 0)'
94570
+
94571
+ # Tag names only
94572
+ skill front tags list --json | jq '.data[].name'
94573
+
94574
+ # Tags sorted by usage (most \u2192 least)
94575
+ skill front tags list --json | jq '.data | sort_by(-.conversation_count)'
94576
+
94577
+ # Count of unused tags
94578
+ skill front tags list --json | jq '[.data[] | select(.conversation_count == 0)] | length'
94579
+
94580
+ NOTE
94581
+ Fetching counts for many tags can take a while due to Front API rate limits.
94582
+ The command batches requests (5 at a time, 100ms between batches).
94583
+ `
94584
+ ).action(listTags);
94585
+ tags.command("delete").description("Delete a tag by ID").argument("<id>", "Tag ID (e.g., tag_xxx)").option("-f, --force", "Skip confirmation prompt").addHelpText(
94586
+ "after",
94587
+ `
94588
+ \u2501\u2501\u2501 Tag Delete \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
94589
+
94590
+ Delete a single tag by its Front ID.
94591
+
94592
+ ARGUMENTS
94593
+ <id> Tag ID in tag_xxx format (find IDs via: skill front tags list --json)
94594
+
94595
+ OPTIONS
94596
+ -f, --force Skip the confirmation prompt (use in scripts)
94597
+
94598
+ BEHAVIOR
94599
+ - Shows tag name, ID, and conversation count before prompting
94600
+ - Warns if the tag is still applied to conversations
94601
+ - Deleting a tag removes it from all conversations that use it
94602
+ - This action is irreversible
94603
+
94604
+ EXAMPLES
94605
+ skill front tags delete tag_abc123
94606
+ skill front tags delete tag_abc123 --force
94607
+ `
94608
+ ).action(deleteTag);
94609
+ tags.command("rename").description("Rename a tag").argument("<id>", "Tag ID (e.g., tag_xxx)").argument("<name>", "New tag name").addHelpText(
94610
+ "after",
94611
+ `
94612
+ \u2501\u2501\u2501 Tag Rename \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
94613
+
94614
+ Rename a tag. The tag keeps its ID and stays applied to all conversations.
94615
+
94616
+ ARGUMENTS
94617
+ <id> Tag ID in tag_xxx format
94618
+ <name> New display name for the tag
94619
+
94620
+ EXAMPLES
94621
+ skill front tags rename tag_abc123 "billing-issue"
94622
+ skill front tags rename tag_abc123 "refund-request"
94623
+
94624
+ NOTE
94625
+ If a tag with the new name already exists, the API will return an error.
94626
+ Use "skill front tags cleanup" to merge duplicates and case variants.
94627
+ `
94628
+ ).action(renameTag);
93947
94629
  tags.command("cleanup").description(
93948
94630
  "Clean up tags: delete duplicates, merge case variants, remove obsolete, create missing standard tags"
93949
- ).option("--execute", "Actually apply changes (default is dry-run)", false).action(cleanupTags);
94631
+ ).option("--execute", "Actually apply changes (default is dry-run)", false).addHelpText(
94632
+ "after",
94633
+ `
94634
+ \u2501\u2501\u2501 Tag Cleanup \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
94635
+
94636
+ Analyze all tags for issues and optionally fix them. Default is dry-run.
94637
+
94638
+ WHAT IT FINDS
94639
+ - Exact duplicates (same name, multiple tag IDs)
94640
+ - Case variants ("Refund" vs "refund" vs "REFUND")
94641
+ - Obsolete tags (date-based like "jan-2022", Gmail imports like "INBOX")
94642
+ - Missing standard tags from the category registry
94643
+
94644
+ WHAT IT DOES (with --execute)
94645
+ - Deletes duplicate tags (keeps the one with most conversations)
94646
+ - Renames case variants to canonical lowercase-hyphenated form
94647
+ - Deletes obsolete/imported tags
94648
+ - Creates missing standard category tags
94649
+
94650
+ OPTIONS
94651
+ --execute Apply the cleanup plan. Without this flag, only a dry-run
94652
+ report is printed (safe to run anytime).
94653
+
94654
+ EXAMPLES
94655
+ # See what would be changed (safe, read-only)
94656
+ skill front tags cleanup
94657
+
94658
+ # Actually apply changes (prompts for confirmation)
94659
+ skill front tags cleanup --execute
94660
+ `
94661
+ ).action(cleanupTags);
93950
94662
  }
93951
94663
 
93952
94664
  // src/commands/front/triage.ts
@@ -94045,21 +94757,17 @@ Fetching ${status} conversations from inbox ${inbox}...`);
94045
94757
  });
94046
94758
  }
94047
94759
  if (json) {
94048
- console.log(
94049
- JSON.stringify(
94050
- hateoasWrap({
94051
- type: "triage-result",
94052
- command: `skill front triage --inbox ${inbox} --json`,
94053
- data: {
94054
- total: allConversations.length,
94055
- stats: stats4,
94056
- results
94057
- },
94058
- actions: triageActions(inbox)
94059
- }),
94060
- null,
94061
- 2
94062
- )
94760
+ writeJsonOutput(
94761
+ hateoasWrap({
94762
+ type: "triage-result",
94763
+ command: `skill front triage --inbox ${inbox} --json`,
94764
+ data: {
94765
+ total: allConversations.length,
94766
+ stats: stats4,
94767
+ results
94768
+ },
94769
+ actions: triageActions(inbox)
94770
+ })
94063
94771
  );
94064
94772
  return;
94065
94773
  }
@@ -94169,7 +94877,72 @@ function registerTriageCommand(front) {
94169
94877
  "-s, --status <status>",
94170
94878
  "Conversation status filter (unassigned, assigned, archived)",
94171
94879
  "unassigned"
94172
- ).option("--auto-archive", "Automatically archive noise and spam").option("--json", "JSON output").action(triageConversations);
94880
+ ).option("--auto-archive", "Automatically archive noise and spam").option("--json", "JSON output").addHelpText(
94881
+ "after",
94882
+ `
94883
+ \u2501\u2501\u2501 AI-Powered Triage \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
94884
+
94885
+ Categorize inbox conversations into actionable, noise, or spam using
94886
+ heuristic rules. Optionally auto-archive the junk.
94887
+
94888
+ OPTIONS
94889
+ -i, --inbox <id> (required) Inbox ID to triage (inb_xxx)
94890
+ -s, --status <status> Conversation status to filter (default: unassigned)
94891
+ Values: unassigned, assigned, archived
94892
+ --auto-archive Archive all noise + spam conversations automatically
94893
+ --json Output as JSON (HATEOAS-wrapped)
94894
+
94895
+ CATEGORIZATION RULES
94896
+ Category Signals
94897
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
94898
+ Noise mailer-daemon, noreply, no-reply, postmaster, newsletter
94899
+ delivery failures, auto-replies, out-of-office,
94900
+ automated reports (daily/weekly/monthly), cert notifications
94901
+ Spam partnership/sponsorship pitches, guest post / link exchange,
94902
+ backlink requests, marketing opportunities
94903
+ Actionable Everything else (real support issues)
94904
+
94905
+ WORKFLOW
94906
+ 1. Triage to see the breakdown:
94907
+ skill front triage --inbox inb_4bj7r
94908
+
94909
+ 2. Review categories in the output (actionable / noise / spam)
94910
+
94911
+ 3. If satisfied, auto-archive the junk:
94912
+ skill front triage --inbox inb_4bj7r --auto-archive
94913
+
94914
+ JSON + jq PATTERNS
94915
+ # Just the stats
94916
+ skill front triage --inbox inb_4bj7r --json | jq '.data.stats'
94917
+
94918
+ # All noise conversation IDs
94919
+ skill front triage --inbox inb_4bj7r --json | jq '[.data.results[] | select(.category == "noise") | .id]'
94920
+
94921
+ # Spam sender emails
94922
+ skill front triage --inbox inb_4bj7r --json | jq '[.data.results[] | select(.category == "spam") | .senderEmail]'
94923
+
94924
+ # Actionable count
94925
+ skill front triage --inbox inb_4bj7r --json | jq '.data.stats.actionable'
94926
+
94927
+ EXAMPLES
94928
+ # Triage unassigned conversations (default)
94929
+ skill front triage --inbox inb_4bj7r
94930
+
94931
+ # Triage assigned conversations
94932
+ skill front triage --inbox inb_4bj7r --status assigned
94933
+
94934
+ # Triage and auto-archive noise + spam
94935
+ skill front triage --inbox inb_4bj7r --auto-archive
94936
+
94937
+ # Pipe JSON for downstream processing
94938
+ skill front triage --inbox inb_4bj7r --json | jq '.data.results[] | select(.category == "actionable")'
94939
+
94940
+ RELATED COMMANDS
94941
+ skill front bulk-archive Bulk-archive conversations by query
94942
+ skill front report Inbox activity report
94943
+ skill front search Search conversations with filters
94944
+ `
94945
+ ).action(triageConversations);
94173
94946
  }
94174
94947
 
94175
94948
  // src/commands/front/index.ts
@@ -94207,17 +94980,13 @@ async function getMessage(id, options) {
94207
94980
  const front = getFrontClient9();
94208
94981
  const message = await front.messages.get(normalizeId6(id));
94209
94982
  if (options.json) {
94210
- console.log(
94211
- JSON.stringify(
94212
- hateoasWrap({
94213
- type: "message",
94214
- command: `skill front message ${normalizeId6(id)} --json`,
94215
- data: message,
94216
- links: messageLinks(message.id)
94217
- }),
94218
- null,
94219
- 2
94220
- )
94983
+ writeJsonOutput(
94984
+ hateoasWrap({
94985
+ type: "message",
94986
+ command: `skill front message ${normalizeId6(id)} --json`,
94987
+ data: message,
94988
+ links: messageLinks(message.id)
94989
+ })
94221
94990
  );
94222
94991
  return;
94223
94992
  }
@@ -94276,18 +95045,14 @@ async function getConversation2(id, options) {
94276
95045
  }
94277
95046
  if (options.json) {
94278
95047
  const convId = normalizeId6(id);
94279
- console.log(
94280
- JSON.stringify(
94281
- hateoasWrap({
94282
- type: "conversation",
94283
- command: `skill front conversation ${convId} --json`,
94284
- data: { conversation, messages },
94285
- links: conversationLinks(conversation.id),
94286
- actions: conversationActions(conversation.id)
94287
- }),
94288
- null,
94289
- 2
94290
- )
95048
+ writeJsonOutput(
95049
+ hateoasWrap({
95050
+ type: "conversation",
95051
+ command: `skill front conversation ${convId} --json`,
95052
+ data: { conversation, messages },
95053
+ links: conversationLinks(conversation.id),
95054
+ actions: conversationActions(conversation.id)
95055
+ })
94291
95056
  );
94292
95057
  return;
94293
95058
  }
@@ -94345,19 +95110,15 @@ async function listTeammates(options) {
94345
95110
  const front = getFrontSdkClient2();
94346
95111
  const result = await front.teammates.list();
94347
95112
  if (options.json) {
94348
- console.log(
94349
- JSON.stringify(
94350
- hateoasWrap({
94351
- type: "teammate-list",
94352
- command: "skill front teammates --json",
94353
- data: result._results,
94354
- links: teammateListLinks(
94355
- result._results.map((t2) => ({ id: t2.id, email: t2.email }))
94356
- )
94357
- }),
94358
- null,
94359
- 2
94360
- )
95113
+ writeJsonOutput(
95114
+ hateoasWrap({
95115
+ type: "teammate-list",
95116
+ command: "skill front teammates --json",
95117
+ data: result._results,
95118
+ links: teammateListLinks(
95119
+ result._results.map((t2) => ({ id: t2.id, email: t2.email }))
95120
+ )
95121
+ })
94361
95122
  );
94362
95123
  return;
94363
95124
  }
@@ -94398,17 +95159,13 @@ async function getTeammate(id, options) {
94398
95159
  const front = getFrontSdkClient2();
94399
95160
  const teammate = await front.teammates.get(id);
94400
95161
  if (options.json) {
94401
- console.log(
94402
- JSON.stringify(
94403
- hateoasWrap({
94404
- type: "teammate",
94405
- command: `skill front teammate ${id} --json`,
94406
- data: teammate,
94407
- links: teammateLinks(teammate.id)
94408
- }),
94409
- null,
94410
- 2
94411
- )
95162
+ writeJsonOutput(
95163
+ hateoasWrap({
95164
+ type: "teammate",
95165
+ command: `skill front teammate ${id} --json`,
95166
+ data: teammate,
95167
+ links: teammateLinks(teammate.id)
95168
+ })
94412
95169
  );
94413
95170
  return;
94414
95171
  }
@@ -94443,10 +95200,245 @@ async function getTeammate(id, options) {
94443
95200
  }
94444
95201
  function registerFrontCommands(program3) {
94445
95202
  const front = program3.command("front").description("Front conversations, inboxes, tags, archival, and reporting");
94446
- front.command("message").description("Get a message by ID (body, author, recipients, attachments)").argument("<id>", "Message ID (e.g., msg_xxx)").option("--json", "Output as JSON").action(getMessage);
94447
- front.command("conversation").description("Get a conversation by ID (status, tags, assignee, messages)").argument("<id>", "Conversation ID (e.g., cnv_xxx)").option("--json", "Output as JSON").option("-m, --messages", "Include message history").action(getConversation2);
94448
- front.command("teammates").description("List all teammates in the workspace").option("--json", "Output as JSON").action(listTeammates);
94449
- front.command("teammate").description("Get teammate details by ID").argument("<id>", "Teammate ID (e.g., tea_xxx or username)").option("--json", "Output as JSON").action(getTeammate);
95203
+ const messageCmd = front.command("message").description("Get a message by ID (body, author, recipients, attachments)").argument("<id>", "Message ID (e.g., msg_xxx)").option("--json", "Output as JSON").action(getMessage);
95204
+ messageCmd.addHelpText(
95205
+ "after",
95206
+ `
95207
+ \u2501\u2501\u2501 Message Details \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
95208
+
95209
+ Fetches a single message from Front by its ID. Returns the full message
95210
+ including HTML body, plaintext body, author, recipients, attachments,
95211
+ and metadata.
95212
+
95213
+ ID FORMAT
95214
+ msg_xxx Front message ID (prefixed with msg_)
95215
+ You can find message IDs from conversation message lists.
95216
+
95217
+ WHAT'S RETURNED
95218
+ Field Description
95219
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
95220
+ id Message ID (msg_xxx)
95221
+ type Message type (email, sms, custom, etc.)
95222
+ subject Message subject line
95223
+ body Full HTML body
95224
+ text Plaintext body (stripped HTML)
95225
+ author Author object (email, id) \u2014 teammate or contact
95226
+ recipients Array of {role, handle} \u2014 from/to/cc/bcc
95227
+ attachments Array of {filename, content_type, size, url}
95228
+ created_at Unix timestamp of message creation
95229
+ metadata Headers, external references
95230
+
95231
+ JSON + jq PATTERNS
95232
+ # Get the HTML body
95233
+ skill front message msg_xxx --json | jq '.data.body'
95234
+
95235
+ # Get the plaintext body
95236
+ skill front message msg_xxx --json | jq '.data.text'
95237
+
95238
+ # Get the author email
95239
+ skill front message msg_xxx --json | jq '.data.author.email'
95240
+
95241
+ # List all recipients
95242
+ skill front message msg_xxx --json | jq '.data.recipients[] | {role, handle}'
95243
+
95244
+ # List attachment filenames
95245
+ skill front message msg_xxx --json | jq '.data.attachments[].filename'
95246
+
95247
+ RELATED COMMANDS
95248
+ skill front conversation <id> -m Find message IDs from a conversation
95249
+ skill front search <query> Search conversations to find threads
95250
+
95251
+ EXAMPLES
95252
+ # Get full message details (human-readable)
95253
+ skill front message msg_1a2b3c
95254
+
95255
+ # Get message as JSON for piping
95256
+ skill front message msg_1a2b3c --json
95257
+
95258
+ # Extract just the body text
95259
+ skill front message msg_1a2b3c --json | jq -r '.data.text'
95260
+
95261
+ # Check who sent a message
95262
+ skill front message msg_1a2b3c --json | jq '{author: .data.author.email, recipients: [.data.recipients[].handle]}'
95263
+ `
95264
+ );
95265
+ const conversationCmd = front.command("conversation").description("Get a conversation by ID (status, tags, assignee, messages)").argument("<id>", "Conversation ID (e.g., cnv_xxx)").option("--json", "Output as JSON").option("-m, --messages", "Include message history").action(getConversation2);
95266
+ conversationCmd.addHelpText(
95267
+ "after",
95268
+ `
95269
+ \u2501\u2501\u2501 Conversation Details \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
95270
+
95271
+ Fetches a conversation from Front by its ID. Returns metadata, tags,
95272
+ assignee, recipient, and optionally the full message history.
95273
+
95274
+ ID FORMAT
95275
+ cnv_xxx Front conversation ID (prefixed with cnv_)
95276
+ Find conversation IDs via search or inbox listing.
95277
+
95278
+ FLAGS
95279
+ -m, --messages Include full message history in the response.
95280
+ Without this flag, only conversation metadata is returned.
95281
+
95282
+ WHAT'S RETURNED
95283
+ Field Description
95284
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
95285
+ id Conversation ID (cnv_xxx)
95286
+ subject Conversation subject line
95287
+ status Current status (see below)
95288
+ created_at Unix timestamp
95289
+ recipient Primary recipient {handle, role}
95290
+ assignee Assigned teammate {id, email} or null
95291
+ tags Array of {id, name} tags on this conversation
95292
+ messages (with -m) Array of full message objects
95293
+
95294
+ STATUS VALUES
95295
+ archived Conversation is archived
95296
+ unassigned Open, no assignee
95297
+ assigned Open, has an assignee
95298
+ deleted In trash
95299
+ waiting Waiting for response
95300
+
95301
+ JSON + jq PATTERNS
95302
+ # Get conversation metadata
95303
+ skill front conversation cnv_xxx --json | jq '.data.conversation'
95304
+
95305
+ # Get all messages (requires -m flag)
95306
+ skill front conversation cnv_xxx -m --json | jq '.data.messages[]'
95307
+
95308
+ # Get just message bodies as plaintext
95309
+ skill front conversation cnv_xxx -m --json | jq -r '.data.messages[].text'
95310
+
95311
+ # Extract tag names
95312
+ skill front conversation cnv_xxx --json | jq '[.data.conversation.tags[].name]'
95313
+
95314
+ # Get assignee email
95315
+ skill front conversation cnv_xxx --json | jq '.data.conversation.assignee.email'
95316
+
95317
+ # Get message count
95318
+ skill front conversation cnv_xxx -m --json | jq '.data.messages | length'
95319
+
95320
+ # Get inbound messages only
95321
+ skill front conversation cnv_xxx -m --json | jq '[.data.messages[] | select(.is_inbound)]'
95322
+
95323
+ RELATED COMMANDS
95324
+ skill front message <id> Get full details for a specific message
95325
+ skill front assign <cnv> <tea> Assign conversation to a teammate
95326
+ skill front tag <cnv> <tag> Add a tag to a conversation
95327
+ skill front reply <cnv> Send a reply to a conversation
95328
+ skill front search <query> Search for conversations
95329
+
95330
+ EXAMPLES
95331
+ # Get conversation overview
95332
+ skill front conversation cnv_abc123
95333
+
95334
+ # Get conversation with full message history
95335
+ skill front conversation cnv_abc123 -m
95336
+
95337
+ # Pipe to jq to extract tags and assignee
95338
+ skill front conversation cnv_abc123 --json | jq '{tags: [.data.conversation.tags[].name], assignee: .data.conversation.assignee.email}'
95339
+
95340
+ # Get the latest message text from a conversation
95341
+ skill front conversation cnv_abc123 -m --json | jq -r '.data.messages[-1].text'
95342
+ `
95343
+ );
95344
+ const teammatesCmd = front.command("teammates").description("List all teammates in the workspace").option("--json", "Output as JSON").action(listTeammates);
95345
+ teammatesCmd.addHelpText(
95346
+ "after",
95347
+ `
95348
+ \u2501\u2501\u2501 List Teammates \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
95349
+
95350
+ Lists all teammates in the Front workspace. Returns each teammate's ID,
95351
+ email, name, username, and availability status.
95352
+
95353
+ WHAT'S RETURNED (per teammate)
95354
+ Field Description
95355
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
95356
+ id Teammate ID (tea_xxx)
95357
+ email Teammate email address
95358
+ first_name First name
95359
+ last_name Last name
95360
+ username Front username
95361
+ is_available Whether teammate is currently available (true/false)
95362
+
95363
+ JSON + jq PATTERNS
95364
+ # Get all teammate IDs
95365
+ skill front teammates --json | jq '[.data[].id]'
95366
+
95367
+ # Get ID + email pairs
95368
+ skill front teammates --json | jq '.data[] | {id, email}'
95369
+
95370
+ # Find a teammate by email
95371
+ skill front teammates --json | jq '.data[] | select(.email == "joel@example.com")'
95372
+
95373
+ # List only available teammates
95374
+ skill front teammates --json | jq '[.data[] | select(.is_available)]'
95375
+
95376
+ # Get a count of teammates
95377
+ skill front teammates --json | jq '.data | length'
95378
+
95379
+ RELATED COMMANDS
95380
+ skill front teammate <id> Get details for a specific teammate
95381
+ skill front assign <cnv> <tea> Assign a conversation to a teammate
95382
+
95383
+ EXAMPLES
95384
+ # List all teammates (human-readable table)
95385
+ skill front teammates
95386
+
95387
+ # List as JSON for scripting
95388
+ skill front teammates --json
95389
+
95390
+ # Find teammate ID by email for use in assign
95391
+ skill front teammates --json | jq -r '.data[] | select(.email | contains("joel")) | .id'
95392
+ `
95393
+ );
95394
+ const teammateCmd = front.command("teammate").description("Get teammate details by ID").argument("<id>", "Teammate ID (e.g., tea_xxx or username)").option("--json", "Output as JSON").action(getTeammate);
95395
+ teammateCmd.addHelpText(
95396
+ "after",
95397
+ `
95398
+ \u2501\u2501\u2501 Teammate Details \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
95399
+
95400
+ Fetches details for a single teammate by their ID. Returns email, name,
95401
+ username, and current availability.
95402
+
95403
+ ID FORMAT
95404
+ tea_xxx Front teammate ID (prefixed with tea_)
95405
+ Find teammate IDs via: skill front teammates
95406
+
95407
+ WHAT'S RETURNED
95408
+ Field Description
95409
+ \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
95410
+ id Teammate ID (tea_xxx)
95411
+ email Teammate email address
95412
+ first_name First name
95413
+ last_name Last name
95414
+ username Front username
95415
+ is_available Whether teammate is currently available (true/false)
95416
+
95417
+ JSON + jq PATTERNS
95418
+ # Get teammate email
95419
+ skill front teammate tea_xxx --json | jq '.data.email'
95420
+
95421
+ # Get full name
95422
+ skill front teammate tea_xxx --json | jq '(.data.first_name + " " + .data.last_name)'
95423
+
95424
+ # Check availability
95425
+ skill front teammate tea_xxx --json | jq '.data.is_available'
95426
+
95427
+ RELATED COMMANDS
95428
+ skill front teammates List all teammates (to find IDs)
95429
+ skill front assign <cnv> <tea> Assign a conversation to this teammate
95430
+
95431
+ EXAMPLES
95432
+ # Get teammate details (human-readable)
95433
+ skill front teammate tea_1a2b3c
95434
+
95435
+ # Get as JSON
95436
+ skill front teammate tea_1a2b3c --json
95437
+
95438
+ # Quick check if teammate is available
95439
+ skill front teammate tea_1a2b3c --json | jq -r 'if .data.is_available then "available" else "away" end'
95440
+ `
95441
+ );
94450
95442
  registerInboxCommand(front);
94451
95443
  registerArchiveCommand(front);
94452
95444
  registerBulkArchiveCommand(front);
@@ -112631,7 +113623,7 @@ Total: ${result.totalDurationMs}ms`);
112631
113623
 
112632
113624
  // src/commands/responses.ts
112633
113625
  init_esm_shims();
112634
- import { writeFileSync as writeFileSync9 } from "fs";
113626
+ import { writeFileSync as writeFileSync10 } from "fs";
112635
113627
  function formatDate3(date) {
112636
113628
  return date.toLocaleString("en-US", {
112637
113629
  month: "short",
@@ -113132,7 +114124,7 @@ async function exportResponses(options) {
113132
114124
  }
113133
114125
  const outputJson = JSON.stringify(exportData, null, 2);
113134
114126
  if (options.output) {
113135
- writeFileSync9(options.output, outputJson, "utf-8");
114127
+ writeFileSync10(options.output, outputJson, "utf-8");
113136
114128
  console.log(
113137
114129
  `Exported ${exportData.length} responses to ${options.output}`
113138
114130
  );