@opsrev/acculynx-cli 1.13.1 → 1.14.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 +1 -0
- package/dist/{chunk-AKUPC2EI.js → chunk-MQDU6DIH.js} +1 -0
- package/dist/index.js +239 -1
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,7 @@ acculynx ping # verify API key
|
|
|
121
121
|
acculynx jobs list --limit 5 # 25 results by default; --all to fetch everything
|
|
122
122
|
acculynx jobs get <jobId>
|
|
123
123
|
acculynx jobs search --query "smith"
|
|
124
|
+
acculynx jobs scan --milestones Approved # scan all jobs → digest + coverage receipt
|
|
124
125
|
acculynx contacts list --limit 10
|
|
125
126
|
acculynx estimates get <estimateId>
|
|
126
127
|
```
|
package/dist/index.js
CHANGED
|
@@ -34,12 +34,13 @@ import {
|
|
|
34
34
|
jobSetWorkType,
|
|
35
35
|
jobUploadDocument,
|
|
36
36
|
jobsList,
|
|
37
|
+
paginate,
|
|
37
38
|
ping,
|
|
38
39
|
readStdin,
|
|
39
40
|
resolveTradeTypeIds,
|
|
40
41
|
resolveWorkTypeId,
|
|
41
42
|
usersList
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-MQDU6DIH.js";
|
|
43
44
|
|
|
44
45
|
// src/index.ts
|
|
45
46
|
import { createRequire } from "module";
|
|
@@ -53,6 +54,209 @@ function registerPingCommand(parentCmd, getClient2) {
|
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
// src/commands/jobs.ts
|
|
58
|
+
import { writeFileSync } from "fs";
|
|
59
|
+
|
|
60
|
+
// src/ops/scan.ts
|
|
61
|
+
var PAGE_SIZE = 25;
|
|
62
|
+
async function scanJobs(client, filters) {
|
|
63
|
+
const params = { includes: "contact", sortBy: "CreatedDate" };
|
|
64
|
+
if (filters.startDate) params.startDate = filters.startDate;
|
|
65
|
+
if (filters.endDate) params.endDate = filters.endDate;
|
|
66
|
+
if (filters.dateFilterType) params.dateFilterType = filters.dateFilterType;
|
|
67
|
+
if (filters.milestones) params.milestones = filters.milestones;
|
|
68
|
+
if (filters.assignment) params.assignment = filters.assignment;
|
|
69
|
+
const fetched = [];
|
|
70
|
+
let serverCount;
|
|
71
|
+
let pageError;
|
|
72
|
+
let pageStartIndex = 0;
|
|
73
|
+
while (true) {
|
|
74
|
+
let data;
|
|
75
|
+
try {
|
|
76
|
+
data = await client.get("/jobs", {
|
|
77
|
+
...params,
|
|
78
|
+
pageSize: String(PAGE_SIZE),
|
|
79
|
+
pageStartIndex: String(pageStartIndex)
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
pageError = error instanceof Error ? error.message : String(error);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (serverCount === void 0 && typeof data.count === "number") serverCount = data.count;
|
|
86
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
87
|
+
fetched.push(...items);
|
|
88
|
+
if (items.length < PAGE_SIZE) break;
|
|
89
|
+
pageStartIndex += PAGE_SIZE;
|
|
90
|
+
}
|
|
91
|
+
const complete = pageError === void 0 && (serverCount === void 0 || fetched.length === serverCount);
|
|
92
|
+
const wanted = (filters.tradeType ?? []).map((t) => t.toLowerCase());
|
|
93
|
+
const jobs = wanted.length === 0 ? fetched : fetched.filter((j) => {
|
|
94
|
+
const types = j.tradeTypes;
|
|
95
|
+
if (!Array.isArray(types)) return false;
|
|
96
|
+
return types.some((t) => {
|
|
97
|
+
const name = t !== null && typeof t === "object" ? t.name : void 0;
|
|
98
|
+
if (typeof name !== "string") return false;
|
|
99
|
+
const lower = name.toLowerCase();
|
|
100
|
+
return wanted.some((w) => lower.includes(w));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return { jobs, scanned: fetched.length, serverCount, complete, ...pageError ? { pageError } : {} };
|
|
104
|
+
}
|
|
105
|
+
var ENRICHERS = ["financials", "reps", "dates"];
|
|
106
|
+
var asRecord = (v) => v !== null && typeof v === "object" ? v : {};
|
|
107
|
+
async function enrichJobs(client, jobs, enrich, concurrency = 5) {
|
|
108
|
+
let userNames = /* @__PURE__ */ new Map();
|
|
109
|
+
if (enrich.includes("reps")) {
|
|
110
|
+
try {
|
|
111
|
+
const users = await paginate(client, "/users", {}, Infinity);
|
|
112
|
+
userNames = new Map(users.map((u) => [String(u.id), String(u.displayName ?? "")]));
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const nameOf = (id) => userNames.get(String(id)) || String(id ?? "").slice(0, 8);
|
|
117
|
+
const out = jobs.map((job) => ({ job, errors: [] }));
|
|
118
|
+
let next = 0;
|
|
119
|
+
async function enrichOne(entry) {
|
|
120
|
+
const jobId = String(entry.job.id ?? "");
|
|
121
|
+
for (const source of enrich) {
|
|
122
|
+
try {
|
|
123
|
+
if (source === "financials") {
|
|
124
|
+
const f = asRecord(await jobFinancials(client, jobId));
|
|
125
|
+
const totals = asRecord(f.worksheetSectionTotals);
|
|
126
|
+
entry.financials = {
|
|
127
|
+
approvedJobValue: f.approvedJobValue,
|
|
128
|
+
balanceDue: f.balanceDue,
|
|
129
|
+
worksheetTotal: totals.worksheetTotal
|
|
130
|
+
};
|
|
131
|
+
} else if (source === "reps") {
|
|
132
|
+
const r = asRecord(await jobReps(client, jobId));
|
|
133
|
+
const reps = {};
|
|
134
|
+
for (const item of r.items ?? []) {
|
|
135
|
+
const userId = asRecord(item.user).id;
|
|
136
|
+
if (item.type === "SalesOwner") reps.salesOwner = nameOf(userId);
|
|
137
|
+
else if (item.type === "CompanyRepresentative") reps.company = nameOf(userId);
|
|
138
|
+
}
|
|
139
|
+
entry.reps = reps;
|
|
140
|
+
} else if (source === "dates") {
|
|
141
|
+
const m = asRecord(await jobMilestones(client, jobId));
|
|
142
|
+
entry.dates = (m.items ?? []).map((i) => ({
|
|
143
|
+
name: String(i.name ?? ""),
|
|
144
|
+
date: String(i.date ?? "")
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
entry.errors.push({ jobId, source, message: error instanceof Error ? error.message : String(error) });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function worker() {
|
|
153
|
+
while (next < out.length) await enrichOne(out[next++]);
|
|
154
|
+
}
|
|
155
|
+
await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/ops/scan-digest.ts
|
|
160
|
+
function money(n) {
|
|
161
|
+
if (n === void 0 || n === null || Number.isNaN(n)) return "-";
|
|
162
|
+
return "$" + Math.round(n).toLocaleString("en-US");
|
|
163
|
+
}
|
|
164
|
+
function mmdd(iso) {
|
|
165
|
+
if (!iso) return "-";
|
|
166
|
+
const d = new Date(iso);
|
|
167
|
+
if (Number.isNaN(d.getTime())) return "-";
|
|
168
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
169
|
+
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
170
|
+
return `${mm}-${dd}`;
|
|
171
|
+
}
|
|
172
|
+
function customerName(job) {
|
|
173
|
+
const raw = job.contacts;
|
|
174
|
+
const contacts = Array.isArray(raw) ? raw : [];
|
|
175
|
+
const primary = contacts.find((c) => c !== null && typeof c === "object" && c.isPrimary === true);
|
|
176
|
+
if (primary) {
|
|
177
|
+
const contact = primary.contact ?? {};
|
|
178
|
+
const full = `${String(contact.firstName ?? "").trim()} ${String(contact.lastName ?? "").trim()}`.trim();
|
|
179
|
+
if (full) return full;
|
|
180
|
+
}
|
|
181
|
+
return String(job.jobName ?? "");
|
|
182
|
+
}
|
|
183
|
+
function headerLine1(report) {
|
|
184
|
+
const prefix = report.complete ? "SCAN" : "SCAN PARTIAL";
|
|
185
|
+
const f = report.filters;
|
|
186
|
+
const parts = [];
|
|
187
|
+
if (f.milestones) parts.push(`milestones=${f.milestones}`);
|
|
188
|
+
if (f.startDate) parts.push(`since=${f.startDate}`);
|
|
189
|
+
if (f.endDate) parts.push(`until=${f.endDate}`);
|
|
190
|
+
if (f.dateFilterType) parts.push(`dateField=${f.dateFilterType}`);
|
|
191
|
+
if (f.assignment) parts.push(`assignment=${f.assignment}`);
|
|
192
|
+
if (f.tradeType && f.tradeType.length > 0) parts.push(`trade=${f.tradeType.join(",")}`);
|
|
193
|
+
if (report.enrich.length > 0) parts.push(`enrich=${report.enrich.join(",")}`);
|
|
194
|
+
return parts.length > 0 ? `${prefix} ${parts.join(" ")}` : prefix;
|
|
195
|
+
}
|
|
196
|
+
function headerLine2(report) {
|
|
197
|
+
const scanned = report.scanned ?? report.jobs.length;
|
|
198
|
+
const totalLabel = report.serverCount !== void 0 ? String(report.serverCount) : "?";
|
|
199
|
+
let line = `jobs ${scanned}/${totalLabel}`;
|
|
200
|
+
if (report.serverCount !== void 0) line += ` (server count ${report.serverCount})`;
|
|
201
|
+
const tradeFiltered = (report.filters.tradeType ?? []).length > 0;
|
|
202
|
+
if (tradeFiltered && report.jobs.length !== scanned) line += ` \xB7 trade matched ${report.jobs.length}`;
|
|
203
|
+
if (report.enrich.length > 0) {
|
|
204
|
+
const enrichedCount = report.jobs.filter((j) => j.errors.length === 0).length;
|
|
205
|
+
const errorCount = report.jobs.reduce((sum, j) => sum + j.errors.length, 0);
|
|
206
|
+
line += ` \xB7 enriched ${enrichedCount} \xB7 errors ${errorCount}`;
|
|
207
|
+
}
|
|
208
|
+
if (!report.complete) {
|
|
209
|
+
const note = report.pageError ?? `fetched ${scanned} of ${totalLabel}`;
|
|
210
|
+
line += ` \xB7 PARTIAL: ${note}`;
|
|
211
|
+
}
|
|
212
|
+
return line;
|
|
213
|
+
}
|
|
214
|
+
function jobLine(entry, enrich) {
|
|
215
|
+
const job = entry.job;
|
|
216
|
+
const id8 = String(job.id ?? "").slice(0, 8);
|
|
217
|
+
const name = customerName(job);
|
|
218
|
+
const addr = job.locationAddress ?? {};
|
|
219
|
+
const street = String(addr.street1 || "-");
|
|
220
|
+
const city = String(addr.city || "-");
|
|
221
|
+
const milestone = String(job.currentMilestone ?? "-");
|
|
222
|
+
const date = mmdd(job.milestoneDate);
|
|
223
|
+
let line = `${id8} | ${name} \u2014 ${street}, ${city} | ${milestone} ${date}`;
|
|
224
|
+
const erroredSources = new Set(entry.errors.map((e) => e.source));
|
|
225
|
+
for (const source of ["reps", "financials", "dates"]) {
|
|
226
|
+
if (!enrich.includes(source)) continue;
|
|
227
|
+
if (erroredSources.has(source)) {
|
|
228
|
+
line += ` | ERR:${source}`;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (source === "reps") {
|
|
232
|
+
const salesOwner = entry.reps?.salesOwner || "-";
|
|
233
|
+
const company = entry.reps?.company || "";
|
|
234
|
+
line += ` | rep ${salesOwner}`;
|
|
235
|
+
if (company && company !== entry.reps?.salesOwner) line += ` (co ${company})`;
|
|
236
|
+
} else if (source === "financials") {
|
|
237
|
+
const f = entry.financials;
|
|
238
|
+
line += ` | wk ${money(f?.worksheetTotal)} | appr ${money(f?.approvedJobValue)} | bal ${money(f?.balanceDue)}`;
|
|
239
|
+
} else if (source === "dates") {
|
|
240
|
+
const approved = entry.dates?.find((d) => d.name === "Approved");
|
|
241
|
+
line += ` | appr\u2019d ${approved ? mmdd(approved.date) : "-"}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return line;
|
|
245
|
+
}
|
|
246
|
+
function formatScanDigest(report) {
|
|
247
|
+
const lines = [headerLine1(report), headerLine2(report)];
|
|
248
|
+
for (const entry of report.jobs) lines.push(jobLine(entry, report.enrich));
|
|
249
|
+
const allErrors = report.jobs.flatMap((j) => j.errors);
|
|
250
|
+
if (allErrors.length > 0) {
|
|
251
|
+
lines.push("errors:");
|
|
252
|
+
for (const e of allErrors) lines.push(` ${e.jobId.slice(0, 8)} ${e.source}: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
return lines.join("\n");
|
|
255
|
+
}
|
|
256
|
+
function formatScanJsonl(report) {
|
|
257
|
+
return report.jobs.map((j) => JSON.stringify(j)).join("\n");
|
|
258
|
+
}
|
|
259
|
+
|
|
56
260
|
// src/commands/jobs.ts
|
|
57
261
|
function collect(value, previous) {
|
|
58
262
|
return [...previous, value];
|
|
@@ -74,6 +278,40 @@ function registerJobsCommands(parentCmd, getClient2) {
|
|
|
74
278
|
});
|
|
75
279
|
console.log(JSON.stringify(result));
|
|
76
280
|
});
|
|
281
|
+
jobs.command("scan").description("Fetch ALL matching jobs and print a digest with a coverage receipt").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--date-filter-type <type>", "Date field to filter on").option("--milestones <milestones>", "Filter by milestones (comma-separated)").option("--assignment <type>", "Filter by assignment: assigned, unassigned").option("--trade-type <name>", "Client-side trade-type filter (repeatable)", collect, []).option("--enrich <list>", "Comma-separated: financials,reps,dates").option("--format <fmt>", "digest or jsonl", "digest").option("--out <path>", "Also write full jsonl to this file").action(async (opts) => {
|
|
282
|
+
const enrich = String(opts.enrich ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
283
|
+
const badEnricher = enrich.find((e) => !ENRICHERS.includes(e));
|
|
284
|
+
if (badEnricher) {
|
|
285
|
+
console.error(`Unknown enricher "${badEnricher}" (valid: ${ENRICHERS.join(", ")})`);
|
|
286
|
+
process.exitCode = 2;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (opts.format !== "digest" && opts.format !== "jsonl") {
|
|
290
|
+
console.error("Unknown --format (valid: digest, jsonl)");
|
|
291
|
+
process.exitCode = 2;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const filters = {
|
|
295
|
+
startDate: opts.startDate,
|
|
296
|
+
endDate: opts.endDate,
|
|
297
|
+
dateFilterType: opts.dateFilterType,
|
|
298
|
+
milestones: opts.milestones,
|
|
299
|
+
assignment: opts.assignment,
|
|
300
|
+
tradeType: opts.tradeType.length ? opts.tradeType : void 0
|
|
301
|
+
};
|
|
302
|
+
const result = await scanJobs(getClient2(), filters);
|
|
303
|
+
const enrichedJobs = await enrichJobs(getClient2(), result.jobs, enrich);
|
|
304
|
+
const report = { filters, enrich, jobs: enrichedJobs, scanned: result.scanned, serverCount: result.serverCount, complete: result.complete, ...result.pageError ? { pageError: result.pageError } : {} };
|
|
305
|
+
console.log(opts.format === "jsonl" ? formatScanJsonl(report) : formatScanDigest(report));
|
|
306
|
+
if (!result.complete) process.exitCode = 3;
|
|
307
|
+
if (opts.out) {
|
|
308
|
+
try {
|
|
309
|
+
writeFileSync(opts.out, formatScanJsonl(report) + "\n");
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(`out: FAILED - ${error instanceof Error ? error.message : String(error)}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
77
315
|
jobs.command("get").argument("<jobId>", "Job ID").description("Get job details").action(async (jobId) => {
|
|
78
316
|
console.log(JSON.stringify(await jobGet(getClient2(), jobId)));
|
|
79
317
|
});
|
package/dist/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opsrev/acculynx-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "AccuLynx MCP server & CLI — use the AccuLynx roofing CRM in Claude Desktop, Cursor, and any MCP client. Also a JSON CLI for scripts and AI agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|