conduyt 1.63.0 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,11 @@ conduyt contacts get <id> # fetch one contact (carries `revision`)
36
36
  conduyt contacts update <id> --first Ann --expected-revision <n> # PATCH; a contact changed since revision n is refused: 409 contact_changed, exit 1, code and current revision printed
37
37
  conduyt contacts collaborators add <contactId> <userId> [--expected-revision <n>] # POST /contacts/{id}/collaborators; prints { added, revision }
38
38
  conduyt contacts collaborators remove <contactId> <userId> [--expected-revision <n>] # DELETE with a body; prints { removed, revision }; 404 when not a collaborator
39
+ conduyt chat channels # GET /chat/channels: your channels; also CREATES your auto-channel memberships the first time
40
+ conduyt chat unread # GET /chat/unread: per channel { channelId, unread, soundUnread, newestSeq, latestSeq }
41
+ conduyt chat messages <channelId> [--limit 1-100] [--cursor <messageId>] # newest first; prints { data, hasMore, nextCursor }; every message carries `seq`; does not move the read marker; a 403 "Not a member of this channel" is retried once after `chat channels`
42
+ conduyt chat mark-read <channelId> <messageId> # readUpTo: marker moves to that message's seq, forward only (older or foreign message = no-op, exit 0); prints the channel's newest message; same one-retry membership bootstrap
43
+ conduyt notifications list [--page n] [--per-page 1-200] # prints { data, meta }: meta.latestByType = { type: { latest, count } } over the last 24 h, next to unreadCount / unreadByType
39
44
  conduyt deals list --pipeline <id> # list deals
40
45
  conduyt deals list --health-summary --pipeline <id> # Pipeline health strip for the filtered set (totalOpenDeals, atRiskValue, checks[]); add --health-check stale --health-limit 20 for one check's deals, --health-cursor <nextCursor> to page, --health-rows-only for just the page
41
46
  conduyt deals health --pipeline <id> --check needs_action # GET /deals/inspection: the six checks over open deals; --check adds its deals, --cursor/--offset pages
@@ -48,7 +53,7 @@ conduyt pipelines archive <id> --move-open-deals-to <pipelineId|none> [--expecte
48
53
  conduyt pipelines settings-revision # the account settingsRevision to pass as --expected-settings-revision to archive / delete-stage (409 stale_settings_revision when settings changed since)
49
54
  conduyt pipelines restore <id> # bring an archived pipeline back (last in the menu, never the default; 409 name_taken)
50
55
  conduyt pipelines reorder --order id1,id2 --expected-revision id1,id2 # board-menu order in one transaction (409 order_changed prints the current revision); `reorder-stages <pipelineId>` does the same for a pipeline's columns
51
- conduyt pipelines delete-stage <pipelineId> <stageId> [--move-to <stageId>] [--expected-settings-revision N] # move the live deals to another stage and delete this one; a stale revision is 409 stale_settings_revision
56
+ conduyt pipelines delete-stage <pipelineId> <stageId> [--move-to <stageId>] [--expected-settings-revision N] # soft delete: an empty stage always deletes (hidden everywhere, then 404, no restore, name reusable); --move-to re-homes live deals first; 409s print the server message (deals in it, used by N active automation(s), DPP map, pipeline_archived, intake_target, stale_settings_revision); a run parked before the delete fails at that step
52
57
  conduyt pipelines rot-days set <pipelineId> 21 # days without activity (1-90) before a deal reads "Stale in stage"; `rot-days get [pipelineId]` reads
53
58
  conduyt status-metadata list --stage-id <id> # the status codes + buckets attached to stages; `get | create | update | delete | import` (update / delete take --expected-updated-at; 409 metadata_changed)
54
59
  conduyt status-metadata import --file rows.json --mode replace # set-based import (max 1000; an item key outside stageId, code, bucket, lifecycleStage, dripEligible, dripTrackName, isTerminal, notes is refused before any request); per-item errors print and exit 1; `--mode replace` with no items is refused unless `--confirm-empty-replace` (it deletes every row). Timeouts: archive 75 s, import and the Mailchimp write / action commands 60 s; on a timeout the server may have applied it, so read the state first
package/dist/index.js CHANGED
@@ -937,7 +937,7 @@ stageRules
937
937
  // isSystem included (a system holding stage automations route deals through; hidden from rep pickers, never won or lost)
938
938
  pipelines
939
939
  .command("update-stage <pipelineId> <stageId>")
940
- .description("Update one stage (PATCH — only the fields you pass change). --is-system marks a system holding stage: automations route deals through it, reps never see it in a picker, and it can never be won or lost")
940
+ .description("Update one stage (PATCH — only the fields you pass change). --is-system marks a system holding stage: automations route deals through it, reps never see it in a picker, and it can never be won or lost. A soft-deleted stage (`pipelines delete-stage`) answers 404")
941
941
  .option("--name <name>", "stage name")
942
942
  .option("--color <hex>", "stage colour")
943
943
  .option("--is-won <bool>", "true|false — entering this stage closes the deal as won")
@@ -1030,7 +1030,7 @@ pipelines
1030
1030
  }));
1031
1031
  pipelines
1032
1032
  .command("delete-stage <pipelineId> <stageId>")
1033
- .description("Delete a stage (owner / admin key). Without --move-to the stage must have no deals; with it the live deals move to another stage of the SAME pipeline (same won / lost setting, not a system stage) and the stage is deleted in one transaction. A stage a soft-deleted deal still points at is refused. With --expected-settings-revision the answer is { id, deleted: true, settingsRevision }. Refusals exit 1 with the code printed: stale_settings_revision, pipeline_archived, intake_target (other 409s name the cause)")
1033
+ .description("Soft-delete a stage (owner / admin key). An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history and reports keep its name, but it is hidden from every pipeline / stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage; its stage rule, outcome mappings and status metadata go with it. With --move-to the live deals move to another stage of the SAME pipeline (same won / lost setting, not a system stage) and the stage is deleted in one transaction. With --expected-settings-revision the answer is { id, deleted: true, settingsRevision }. A refusal exits 1 with the server's message printed verbatim and its code when it has one (never retried): deals currently in the stage and no --move-to; 'Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.' (a published automation uses the stage in an action, trigger, condition or wait-until); the DPP status map selects it; stale_settings_revision, pipeline_archived, intake_target. A --move-to that is not a valid target is a 400. Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log")
1034
1034
  .option("--move-to <stageId>", "another stage of the same pipeline the live deals move to")
1035
1035
  .option("--expected-settings-revision <n>", "the settingsRevision from `pipelines settings-revision`; refused 409 stale_settings_revision when settings changed since")
1036
1036
  .action(run(async (client, pipelineId, stageId, opts) => {
@@ -3289,6 +3289,66 @@ ai
3289
3289
  Object.assign(body, jsonArg(opts.json, "--json"));
3290
3290
  return client.post("/api/v1/ai/enrich-contact", body);
3291
3291
  }));
3292
+ // ---- team chat (app 10-09, live sha 3869c79ba) ----
3293
+ const chat = program.command("chat").description("Team chat: unread vector, messages (each carries `seq`), read acknowledgement");
3294
+ /** The messages route's membership refusal (403 "Not a member of this channel"); any other 403 (a permission) is not this one. */
3295
+ function isMembershipRefusal(err) {
3296
+ return err instanceof Error && /\b403\b/.test(err.message) && /Not a member of this channel/.test(err.message);
3297
+ }
3298
+ /** `chat unread` lists AUTO channels the caller has no membership row in yet; the messages route answers 403 for them until GET /chat/channels
3299
+ * creates it. On that one refusal: call /chat/channels ONCE, retry ONCE. A second refusal, or a 403 for any other reason, is the caller's. */
3300
+ async function withChatMembership(client, read) {
3301
+ try {
3302
+ return await read();
3303
+ }
3304
+ catch (err) {
3305
+ if (!isMembershipRefusal(err))
3306
+ throw err;
3307
+ await client.get("/api/v1/chat/channels");
3308
+ return read();
3309
+ }
3310
+ }
3311
+ chat
3312
+ .command("channels")
3313
+ .description("GET /chat/channels: your channels (id, name, kind, members, last message). SIDE EFFECT: also creates your memberships in the account's auto channels (needs-a-hand, new-leads, wins) the first time. `chat messages` and `chat mark-read` do this once for you on a 403 'Not a member of this channel'")
3314
+ .action(run(async (client) => client.get("/api/v1/chat/channels")));
3315
+ chat
3316
+ .command("unread")
3317
+ .description("GET /chat/unread: per channel { channelId, unread, soundUnread, newestSeq, latestSeq }; a row can be an AUTO channel you have not opened yet (no membership row: reading it first answers 403, which `chat messages` / `chat mark-read` recover from once). unread is the badge count (auto channels 0 by design); soundUnread counts every channel; newestSeq = seq of the newest message not written by you (null = none), latestSeq the same including panic cards. Read-only")
3318
+ .action(run(async (client) => client.get("/api/v1/chat/unread")));
3319
+ chat
3320
+ .command("messages <channelId>")
3321
+ .description("List a channel's messages, newest first (GET /chat/channels/{id}/messages); every message carries `seq`, its commit-ordered position in the channel. Prints { data, hasMore, nextCursor }. Needs channel membership (else 403). Does NOT move your read marker: use `chat mark-read`. On a 403 'Not a member of this channel' it calls GET /chat/channels once and retries ONCE (a second refusal or any other 403 exits 1 as is)")
3322
+ .option("--limit <n>", "messages per page, 1-100 (default 50)")
3323
+ .option("--cursor <messageId>", "nextCursor from the previous page (a message UUID)")
3324
+ .action(run(async (client, channelId, opts) => {
3325
+ assertUuid(channelId, "channel id");
3326
+ const limit = opts.limit !== undefined ? intFlag(opts.limit, "--limit", 1, 100) : undefined;
3327
+ const cursor = opts.cursor !== undefined ? uuidOf(opts.cursor, "--cursor") : undefined;
3328
+ // the live route answers { data: { data, hasMore, nextCursor } }: print() unwraps the outer envelope, leaving { data, hasMore, nextCursor }
3329
+ return withChatMembership(client, () => client.get(`/api/v1/chat/channels/${encodeURIComponent(channelId)}/messages${buildQuery({ limit, cursor })}`));
3330
+ }));
3331
+ chat
3332
+ .command("mark-read <channelId> <messageId>")
3333
+ .description("Acknowledge the channel as read up to one message you were served (GET /chat/channels/{id}/messages?readUpTo=<messageId>&limit=1). The marker moves to that message's seq and only FORWARD: an older message, or one outside this channel, is a no-op (still exit 0: re-check `chat unread`); a malformed id is refused here; a 403 'Not a member of this channel' is retried ONCE after GET /chat/channels (a second refusal or any other 403 exits 1). Prints the channel's newest message, not a confirmation of the move")
3334
+ .action(run(async (client, channelId, messageId) => {
3335
+ assertUuid(channelId, "channel id");
3336
+ const readUpTo = uuidOf(messageId, "message id");
3337
+ return withChatMembership(client, () => client.get(`/api/v1/chat/channels/${encodeURIComponent(channelId)}/messages${buildQuery({ limit: 1, readUpTo })}`));
3338
+ }));
3339
+ // ---- notifications (read side; meta.latestByType, app 10-09) ----
3340
+ const notifications = program.command("notifications").description("Your in-app notifications");
3341
+ notifications
3342
+ .command("list")
3343
+ .description("List your notifications (GET /notifications). Prints { data, meta } whole: meta carries page, per_page, total, unreadCount, unreadByType { type: n } and latestByType { type: { latest, count } } (per type over the last 24 h: newest createdAt as an ISO string and the row count, read state ignored)")
3344
+ .option("--page <n>", "page number (default 1)")
3345
+ .option("--per-page <n>", "results per page, 1-200 (default 50)")
3346
+ .action(run(async (client, opts) => {
3347
+ const page = opts.page !== undefined ? intFlag(opts.page, "--page", 1, 1_000_000) : undefined;
3348
+ const per_page = opts.perPage !== undefined ? intFlag(opts.perPage, "--per-page", 1, 200) : undefined;
3349
+ // the live route answers { data: { data, meta } }: print() unwraps the outer envelope, leaving { data, meta }
3350
+ return client.get(`/api/v1/notifications${buildQuery({ page, per_page })}`);
3351
+ }));
3292
3352
  // ---- raw escape hatch ----
3293
3353
  program
3294
3354
  .command("api <method> <path>")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.63.0",
3
+ "version": "1.65.0",
4
4
  "description": "Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",