@opsrev/acculynx-cli 1.16.0 → 1.17.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/README.md +28 -0
- package/dist/{chunk-MQDU6DIH.js → chunk-STEYTGZE.js} +3 -0
- package/dist/index.js +33 -9
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,6 +140,34 @@ JOB_ID=$(acculynx jobs list --limit 1 | jq -r '.[0].id')
|
|
|
140
140
|
acculynx jobs get "$JOB_ID" | jq .
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
### Monitoring scans: payments with strict coverage
|
|
144
|
+
|
|
145
|
+
Scan every job in a set of milestones and fetch each one's received payments in
|
|
146
|
+
one process, with a coverage line a script can check:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
acculynx jobs scan --milestones Approved,Completed,Invoiced \
|
|
150
|
+
--enrich payments --strict --format jsonl --out payments.jsonl
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Each job line carries `payments`: the job's full received-payment objects
|
|
154
|
+
(`id`, `from`, `amount`, `paymentDate`, `checkNumber`, `notes`, …). The last
|
|
155
|
+
line is the coverage receipt:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{"type":"coverage","scanned":299,"serverCount":299,"complete":true,"pageError":null,"enrichErrors":0}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `--strict` (implied by `--enrich payments`) treats any pagination anomaly
|
|
162
|
+
(changing total, duplicate job IDs, overfull or malformed pages) as a partial
|
|
163
|
+
scan. Partial scans exit 3 and report `complete: false`. Never treat them as
|
|
164
|
+
a full inventory.
|
|
165
|
+
- A per-job payments failure doesn't abort the scan. It appears in that job's
|
|
166
|
+
`errors` (`{"jobId", "source": "payments", "message"}`) and counts toward
|
|
167
|
+
`enrichErrors`, so retry just those jobs with `acculynx jobs payments <id>`.
|
|
168
|
+
- Without `--strict` and without `payments`, `jsonl` output is unchanged: job
|
|
169
|
+
lines only.
|
|
170
|
+
|
|
143
171
|
## Development
|
|
144
172
|
|
|
145
173
|
```bash
|
|
@@ -179,6 +179,9 @@ async function paginate(client, path, params, limit = DEFAULT_LIMIT) {
|
|
|
179
179
|
if (data.items.length < DEFAULT_PAGE_SIZE) {
|
|
180
180
|
break;
|
|
181
181
|
}
|
|
182
|
+
if (typeof data.count === "number" && pageStartIndex + DEFAULT_PAGE_SIZE >= data.count) {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
182
185
|
pageStartIndex += DEFAULT_PAGE_SIZE;
|
|
183
186
|
}
|
|
184
187
|
return results;
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
resolveTradeTypeIds,
|
|
41
41
|
resolveWorkTypeId,
|
|
42
42
|
usersList
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-STEYTGZE.js";
|
|
44
44
|
|
|
45
45
|
// src/index.ts
|
|
46
46
|
import { createRequire } from "module";
|
|
@@ -139,7 +139,7 @@ async function scanJobs(client, filters, options = {}) {
|
|
|
139
139
|
});
|
|
140
140
|
return { jobs, scanned: fetched.length, serverCount, complete, ...pageError ? { pageError } : {} };
|
|
141
141
|
}
|
|
142
|
-
var ENRICHERS = ["financials", "reps", "dates", "messages"];
|
|
142
|
+
var ENRICHERS = ["financials", "reps", "dates", "messages", "payments"];
|
|
143
143
|
var asRecord = (v) => v !== null && typeof v === "object" ? v : {};
|
|
144
144
|
async function defaultLoadMessagesTool() {
|
|
145
145
|
const { createRequire: createRequire2 } = await import("module");
|
|
@@ -201,6 +201,12 @@ async function enrichJobs(client, jobs, enrich, concurrency = 5, deps = {}) {
|
|
|
201
201
|
by: String(msg.createdBy ?? ""),
|
|
202
202
|
text: String(msg.message ?? "").replace(/\s+/g, " ").trim().slice(0, 200)
|
|
203
203
|
}));
|
|
204
|
+
} else if (source === "payments") {
|
|
205
|
+
const received = asRecord(asRecord(await jobPayments(client, jobId)).receivedPayments);
|
|
206
|
+
if (!Array.isArray(received.payments) || received.payments.some((p) => p === null || typeof p !== "object")) {
|
|
207
|
+
throw new Error("invalid_payments");
|
|
208
|
+
}
|
|
209
|
+
entry.payments = received.payments;
|
|
204
210
|
}
|
|
205
211
|
} catch (error) {
|
|
206
212
|
entry.errors.push({ jobId, source, message: error instanceof Error ? error.message : String(error) });
|
|
@@ -288,7 +294,7 @@ function jobLine(entry, enrich) {
|
|
|
288
294
|
const date = mmdd(job.milestoneDate);
|
|
289
295
|
let line = `${id8} | ${name} \u2014 ${street}, ${city} | ${milestone} ${date}`;
|
|
290
296
|
const erroredSources = new Set(entry.errors.map((e) => e.source));
|
|
291
|
-
for (const source of ["reps", "financials", "dates"]) {
|
|
297
|
+
for (const source of ["reps", "financials", "dates", "payments"]) {
|
|
292
298
|
if (!enrich.includes(source)) continue;
|
|
293
299
|
if (erroredSources.has(source)) {
|
|
294
300
|
line += ` | ERR:${source}`;
|
|
@@ -305,6 +311,10 @@ function jobLine(entry, enrich) {
|
|
|
305
311
|
} else if (source === "dates") {
|
|
306
312
|
const approved = entry.dates?.find((d) => d.name === "Approved");
|
|
307
313
|
line += ` | appr\u2019d ${approved ? mmdd(approved.date) : "-"}`;
|
|
314
|
+
} else if (source === "payments") {
|
|
315
|
+
const payments = entry.payments ?? [];
|
|
316
|
+
const total = payments.reduce((sum, p) => sum + (typeof p.amount === "number" ? p.amount : 0), 0);
|
|
317
|
+
line += ` | paid ${money(total)} (${payments.length})`;
|
|
308
318
|
}
|
|
309
319
|
}
|
|
310
320
|
return line;
|
|
@@ -331,8 +341,20 @@ function formatScanDigest(report) {
|
|
|
331
341
|
}
|
|
332
342
|
return lines.join("\n");
|
|
333
343
|
}
|
|
334
|
-
function
|
|
335
|
-
return
|
|
344
|
+
function coverageLine(report) {
|
|
345
|
+
return JSON.stringify({
|
|
346
|
+
type: "coverage",
|
|
347
|
+
scanned: report.scanned,
|
|
348
|
+
serverCount: report.serverCount ?? null,
|
|
349
|
+
complete: report.complete,
|
|
350
|
+
pageError: report.pageError ?? null,
|
|
351
|
+
enrichErrors: report.jobs.reduce((sum, j) => sum + j.errors.length, 0)
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function formatScanJsonl(report, options = {}) {
|
|
355
|
+
const lines = report.jobs.map((j) => JSON.stringify(j));
|
|
356
|
+
if (options.receipt) lines.push(coverageLine(report));
|
|
357
|
+
return lines.join("\n");
|
|
336
358
|
}
|
|
337
359
|
|
|
338
360
|
// src/ops/scan-marketing.ts
|
|
@@ -415,7 +437,7 @@ function registerJobsCommands(parentCmd, getClient2) {
|
|
|
415
437
|
});
|
|
416
438
|
console.log(JSON.stringify(result));
|
|
417
439
|
});
|
|
418
|
-
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,messages").option("--format <fmt>", "digest, jsonl, or marketing-evidence (private operator data)", "digest").option("--out <path>", "Also write full jsonl to this file").action(async (opts) => {
|
|
440
|
+
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,messages,payments").option("--format <fmt>", "digest, jsonl, or marketing-evidence (private operator data)", "digest").option("--out <path>", "Also write full jsonl to this file").option("--strict", 'Strict coverage: any pagination anomaly makes the scan partial (exit 3), and jsonl output ends with a {"type":"coverage"} line. Implied by --enrich payments').action(async (opts) => {
|
|
419
441
|
const enrich = String(opts.enrich ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
420
442
|
const badEnricher = enrich.find((e) => !ENRICHERS.includes(e));
|
|
421
443
|
if (badEnricher) {
|
|
@@ -441,15 +463,17 @@ function registerJobsCommands(parentCmd, getClient2) {
|
|
|
441
463
|
assignment: opts.assignment,
|
|
442
464
|
tradeType: opts.tradeType.length ? opts.tradeType : void 0
|
|
443
465
|
};
|
|
444
|
-
const
|
|
466
|
+
const strict = Boolean(opts.strict) || opts.format === "marketing-evidence" || enrich.includes("payments");
|
|
467
|
+
const result = await scanJobs(getClient2(), filters, { strict });
|
|
445
468
|
const enrichedJobs = await enrichJobs(getClient2(), result.jobs, enrich);
|
|
446
469
|
const report = { filters, enrich, jobs: enrichedJobs, scanned: result.scanned, serverCount: result.serverCount, complete: result.complete, ...result.pageError ? { pageError: result.pageError } : {} };
|
|
447
470
|
const evidence = opts.format === "marketing-evidence" ? marketingEvidence(report) : null;
|
|
448
|
-
|
|
471
|
+
const jsonl = formatScanJsonl(report, { receipt: strict });
|
|
472
|
+
console.log(evidence ? JSON.stringify(evidence) : opts.format === "jsonl" ? jsonl : formatScanDigest(report));
|
|
449
473
|
if (!result.complete || evidence && (!evidence.coverage.queryComplete || !evidence.coverage.enrichmentComplete)) process.exitCode = 3;
|
|
450
474
|
if (opts.out) {
|
|
451
475
|
try {
|
|
452
|
-
writeFileSync(opts.out,
|
|
476
|
+
writeFileSync(opts.out, jsonl + "\n");
|
|
453
477
|
} catch (error) {
|
|
454
478
|
console.error(`out: FAILED - ${error instanceof Error ? error.message : String(error)}`);
|
|
455
479
|
}
|
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.17.1",
|
|
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",
|