conduyt 1.62.0 → 1.64.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 +9 -1
- package/dist/client.js +11 -3
- package/dist/index.js +85 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,15 @@ The saved config lives at `~/.conduyt/config.json` and is written owner-only (`c
|
|
|
32
32
|
```bash
|
|
33
33
|
conduyt whoami # show the authenticated user
|
|
34
34
|
conduyt contacts list --limit 25 # list contacts (supports --cursor, --query)
|
|
35
|
-
conduyt contacts get <id> # fetch one contact
|
|
35
|
+
conduyt contacts get <id> # fetch one contact (carries `revision`)
|
|
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
|
+
conduyt contacts collaborators add <contactId> <userId> [--expected-revision <n>] # POST /contacts/{id}/collaborators; prints { added, revision }
|
|
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
|
|
36
44
|
conduyt deals list --pipeline <id> # list deals
|
|
37
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
|
|
38
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
|
package/dist/client.js
CHANGED
|
@@ -57,7 +57,14 @@ export class ConduytClient {
|
|
|
57
57
|
// STAGE_REQUIREMENTS 422 carries lostReasonOptions) keep the context —
|
|
58
58
|
// same principle as `structured` above, bounded against huge bodies.
|
|
59
59
|
if (json && typeof json === "object" && json.error) {
|
|
60
|
-
|
|
60
|
+
// Scalars first (code, revision), nested objects last: a 409 contact_changed also carries the whole current
|
|
61
|
+
// `contact`, and the bound below may cut that, never the code or the current revision.
|
|
62
|
+
const isScalar = (v) => v === null || typeof v !== "object";
|
|
63
|
+
const entries = Object.entries(json).filter(([k]) => k !== "error");
|
|
64
|
+
const extras = Object.fromEntries([
|
|
65
|
+
...entries.filter(([, v]) => isScalar(v)),
|
|
66
|
+
...entries.filter(([, v]) => !isScalar(v)),
|
|
67
|
+
]);
|
|
61
68
|
if (Object.keys(extras).length > 0) {
|
|
62
69
|
msg = `${msg} ${JSON.stringify(extras).slice(0, 1000)}`;
|
|
63
70
|
}
|
|
@@ -78,8 +85,9 @@ export class ConduytClient {
|
|
|
78
85
|
put(path, body, opts) {
|
|
79
86
|
return this.request("PUT", path, body, opts);
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
/** `body` is for the few DELETE routes that take one (contact collaborators with an --expected-revision pin). */
|
|
89
|
+
del(path, body) {
|
|
90
|
+
return this.request("DELETE", path, body);
|
|
83
91
|
}
|
|
84
92
|
/** A POST whose body comes back as text (a CSV or XLSX export), not JSON. */
|
|
85
93
|
async postText(path, body) {
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { splitList, stageRulePatch, validateCardFields, validateRuleChange } fro
|
|
|
9
9
|
import { ARCHIVE_PIPELINE_TIMEOUT_MS, MAILCHIMP_ADMIN_TIMEOUT_MS, STATUS_METADATA_IMPORT_TIMEOUT_MS, PIPELINE_COLORS, boolFlag, intFlag, isoDate, lifecycleStage as lifecycleStageArg, MAX_PIPELINE_ROT_DAYS, MIN_PIPELINE_ROT_DAYS, pipelineColor, settingsRevision, slug as slugArg, uuid as uuidOf, uuidList, validateImportItems, validateStatusMetadataBody } from "./pipeline-settings.js";
|
|
10
10
|
import { saveConfig, resolveConfig, configPath } from "./config.js";
|
|
11
11
|
import { print, fail, unwrap } from "./output.js";
|
|
12
|
+
/** the contact save revision bound (catalog: integer 0..2147483647) */
|
|
13
|
+
const CONTACT_REVISION_MAX = 2147483647;
|
|
12
14
|
// Exit cleanly when a downstream pipe closes early (e.g. `conduyt deals list |
|
|
13
15
|
// head`). Without this, the stdout write after the reader is gone throws an
|
|
14
16
|
// unhandled EPIPE and prints a Node stack trace — broken-pipe is normal Unix
|
|
@@ -77,7 +79,7 @@ contacts
|
|
|
77
79
|
.action(run(async (client, opts) => client.get(`/api/v1/contacts${buildQuery({ limit: opts.limit, cursor: opts.cursor, query: opts.query, master_status: opts.masterStatus, exclude_master_status: opts.excludeMasterStatus })}`)));
|
|
78
80
|
contacts
|
|
79
81
|
.command("get <id>")
|
|
80
|
-
.description("Get a single contact by id")
|
|
82
|
+
.description("Get a single contact by id (includes `revision`: pass it as --expected-revision on `contacts update` / `contacts collaborators`)")
|
|
81
83
|
.action(run(async (client, id) => client.get(`/api/v1/contacts/${encodeURIComponent(id)}`)));
|
|
82
84
|
contacts
|
|
83
85
|
.command("create")
|
|
@@ -135,10 +137,13 @@ contacts
|
|
|
135
137
|
.option("--company <company>", "company name (auto-creates/links a company; pass '' to clear)")
|
|
136
138
|
.option("--source <source>", "correct the lead's first-touch source (normally set once at creation — update only to fix bad attribution; pass 'null' to clear)")
|
|
137
139
|
.option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
|
|
140
|
+
.option("--expected-revision <n>", "the contact's `revision` as you read it (contacts get / list; 0-2147483647): a contact changed since is refused with 409 contact_changed, nothing is written, exit 1 with the code and the current revision printed")
|
|
138
141
|
.option("--json <json>", "full JSON body, merged over the flags above (for any field)")
|
|
139
142
|
.action(run(async (client, id, opts) => {
|
|
140
143
|
assertUuid(id, "contact id");
|
|
141
144
|
const body = {};
|
|
145
|
+
if (opts.expectedRevision !== undefined)
|
|
146
|
+
body.expectedRevision = intFlag(opts.expectedRevision, "--expected-revision", 0, CONTACT_REVISION_MAX);
|
|
142
147
|
if (opts.first !== undefined)
|
|
143
148
|
body.firstName = opts.first;
|
|
144
149
|
if (opts.last !== undefined)
|
|
@@ -175,6 +180,25 @@ contacts
|
|
|
175
180
|
assertContactSource(body.source, { allowNull: true });
|
|
176
181
|
return client.patch(`/api/v1/contacts/${encodeURIComponent(id)}`, body);
|
|
177
182
|
}));
|
|
183
|
+
const collaborators = contacts.command("collaborators").description("Team members collaborating on a contact");
|
|
184
|
+
for (const [verb, method] of [["add", "post"], ["remove", "del"]]) {
|
|
185
|
+
collaborators
|
|
186
|
+
.command(`${verb} <contactId> <userId>`)
|
|
187
|
+
.description(verb === "add"
|
|
188
|
+
? "Add a team member as a collaborator (POST /contacts/{id}/collaborators; needs edit authority). Prints { added, revision } with the contact's NEW revision. A stale --expected-revision is 409 contact_changed (exit 1, code and current revision printed); 409 without a code: already a collaborator"
|
|
189
|
+
: "Remove a collaborator (DELETE /contacts/{id}/collaborators with a body; needs edit authority). Prints { removed, revision } with the contact's NEW revision. A stale --expected-revision is 409 contact_changed (exit 1, code and current revision printed); 404 when the user is not a collaborator")
|
|
190
|
+
.option("--expected-revision <n>", "the contact's `revision` as you read it (0-2147483647); omit for no pin")
|
|
191
|
+
.action(run(async (client, contactId, userId, opts) => {
|
|
192
|
+
assertUuid(contactId, "contact id");
|
|
193
|
+
if (userId.trim() === "")
|
|
194
|
+
throw new Error("userId must not be blank.");
|
|
195
|
+
const body = { userId };
|
|
196
|
+
if (opts.expectedRevision !== undefined)
|
|
197
|
+
body.expectedRevision = intFlag(opts.expectedRevision, "--expected-revision", 0, CONTACT_REVISION_MAX);
|
|
198
|
+
const path = `/api/v1/contacts/${encodeURIComponent(contactId)}/collaborators`;
|
|
199
|
+
return method === "post" ? client.post(path, body) : client.del(path, body);
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
178
202
|
contacts
|
|
179
203
|
.command("delete <id>")
|
|
180
204
|
.description("Delete a contact by id (soft-delete; retained for audit, the contact's deals are unaffected)")
|
|
@@ -3265,6 +3289,66 @@ ai
|
|
|
3265
3289
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
3266
3290
|
return client.post("/api/v1/ai/enrich-contact", body);
|
|
3267
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
|
+
}));
|
|
3268
3352
|
// ---- raw escape hatch ----
|
|
3269
3353
|
program
|
|
3270
3354
|
.command("api <method> <path>")
|
package/package.json
CHANGED