@opsrev/acculynx-cli 1.0.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/dist/index.d.ts +1 -0
- package/dist/index.js +255 -0
- package/package.json +40 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
var BASE_URL = "https://api.acculynx.com/api/v2";
|
|
8
|
+
function getConfig(options) {
|
|
9
|
+
const apiKey = options.apiKey ?? process.env.ACCULYNX_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Missing required credential: ACCULYNX_API_KEY (set env var or pass --api-key)"
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
baseUrl: BASE_URL,
|
|
17
|
+
apiKey
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/api-client.ts
|
|
22
|
+
var MAX_RETRIES = 3;
|
|
23
|
+
var INITIAL_DELAY_MS = 1e3;
|
|
24
|
+
async function sleep(ms) {
|
|
25
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
}
|
|
27
|
+
async function fetchWithRetry(url, init, retries = MAX_RETRIES) {
|
|
28
|
+
const response = await fetch(url, init);
|
|
29
|
+
if (response.status === 429 && retries > 0) {
|
|
30
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
31
|
+
const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_DELAY_MS * Math.pow(2, MAX_RETRIES - retries);
|
|
32
|
+
await sleep(delay);
|
|
33
|
+
return fetchWithRetry(url, init, retries - 1);
|
|
34
|
+
}
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
function createApiClient(config) {
|
|
38
|
+
const headers = {
|
|
39
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
40
|
+
Accept: "application/json"
|
|
41
|
+
};
|
|
42
|
+
async function doGet(path, params) {
|
|
43
|
+
let url = `${config.baseUrl}${path}`;
|
|
44
|
+
if (params && Object.keys(params).length > 0) {
|
|
45
|
+
url += `?${new URLSearchParams(params).toString()}`;
|
|
46
|
+
}
|
|
47
|
+
const response = await fetchWithRetry(url, { headers });
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
throw new Error(
|
|
51
|
+
`API error (${response.status} ${response.statusText}): ${text}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
async function doPost(path, body) {
|
|
57
|
+
const url = `${config.baseUrl}${path}`;
|
|
58
|
+
const response = await fetchWithRetry(url, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify(body)
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const text = await response.text();
|
|
65
|
+
throw new Error(
|
|
66
|
+
`API error (${response.status} ${response.statusText}): ${text}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return response.json();
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
get: doGet,
|
|
73
|
+
post: doPost
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/commands/ping.ts
|
|
78
|
+
function registerPingCommand(parentCmd, getClient2) {
|
|
79
|
+
parentCmd.command("ping").description("Health check \u2014 verify API key and connectivity").action(async () => {
|
|
80
|
+
const result = await getClient2().get("/diagnostics/ping");
|
|
81
|
+
console.log(JSON.stringify(result));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/api-helpers.ts
|
|
86
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
87
|
+
async function paginate(client, path, params, limit) {
|
|
88
|
+
const results = [];
|
|
89
|
+
let pageStartIndex = 0;
|
|
90
|
+
while (true) {
|
|
91
|
+
const pageParams = {
|
|
92
|
+
...params,
|
|
93
|
+
pageSize: String(DEFAULT_PAGE_SIZE),
|
|
94
|
+
pageStartIndex: String(pageStartIndex)
|
|
95
|
+
};
|
|
96
|
+
const data = await client.get(path, pageParams);
|
|
97
|
+
results.push(...data.items);
|
|
98
|
+
if (limit && results.length >= limit) {
|
|
99
|
+
return results.slice(0, limit);
|
|
100
|
+
}
|
|
101
|
+
if (data.items.length < DEFAULT_PAGE_SIZE) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
pageStartIndex += DEFAULT_PAGE_SIZE;
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
async function readStdin(stream = process.stdin) {
|
|
109
|
+
const chunks = [];
|
|
110
|
+
for await (const chunk of stream) {
|
|
111
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
112
|
+
}
|
|
113
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
114
|
+
if (!raw) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"No input provided on stdin. Pipe JSON body, e.g.: echo '{...}' | acculynx jobs create"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
} catch {
|
|
122
|
+
throw new Error("Invalid JSON on stdin");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/commands/jobs.ts
|
|
127
|
+
function registerJobsCommands(parentCmd, getClient2) {
|
|
128
|
+
const jobs = parentCmd.command("jobs").description("Job operations");
|
|
129
|
+
jobs.command("list").description("List jobs (paginated)").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").option("--sort-by <field>", "Sort by: CreatedDate, MilestoneDate, ModifiedDate").option("--sort-order <order>", "Ascending or Descending").option("--includes <fields>", "Include: contact, initialAppointment").option("--limit <n>", "Max total results").action(async (opts) => {
|
|
130
|
+
const params = {};
|
|
131
|
+
if (opts.startDate) params.startDate = opts.startDate;
|
|
132
|
+
if (opts.endDate) params.endDate = opts.endDate;
|
|
133
|
+
if (opts.dateFilterType) params.dateFilterType = opts.dateFilterType;
|
|
134
|
+
if (opts.milestones) params.milestones = opts.milestones;
|
|
135
|
+
if (opts.sortBy) params.sortBy = opts.sortBy;
|
|
136
|
+
if (opts.sortOrder) params.sortOrder = opts.sortOrder;
|
|
137
|
+
if (opts.includes) params.includes = opts.includes;
|
|
138
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
139
|
+
const result = await paginate(getClient2(), "/jobs", params, limit);
|
|
140
|
+
console.log(JSON.stringify(result));
|
|
141
|
+
});
|
|
142
|
+
jobs.command("get").argument("<jobId>", "Job ID").description("Get job details").action(async (jobId) => {
|
|
143
|
+
const result = await getClient2().get(`/jobs/${jobId}`);
|
|
144
|
+
console.log(JSON.stringify(result));
|
|
145
|
+
});
|
|
146
|
+
jobs.command("create").description("Create a job (pipe JSON body to stdin)").action(async () => {
|
|
147
|
+
const body = await readStdin();
|
|
148
|
+
const result = await getClient2().post("/jobs", body);
|
|
149
|
+
console.log(JSON.stringify(result));
|
|
150
|
+
});
|
|
151
|
+
jobs.command("search").description("Search jobs (pipe JSON search criteria to stdin)").action(async () => {
|
|
152
|
+
const body = await readStdin();
|
|
153
|
+
const result = await getClient2().post("/jobs/search", body);
|
|
154
|
+
console.log(JSON.stringify(result));
|
|
155
|
+
});
|
|
156
|
+
const subResources = [
|
|
157
|
+
{ name: "contacts", path: "contacts", desc: "List job contacts" },
|
|
158
|
+
{ name: "estimates", path: "estimates", desc: "List job estimates" },
|
|
159
|
+
{ name: "financials", path: "financials", desc: "Get job financials" },
|
|
160
|
+
{ name: "invoices", path: "invoices", desc: "List job invoices" },
|
|
161
|
+
{ name: "milestones", path: "milestone-history", desc: "List job milestone history" },
|
|
162
|
+
{ name: "payments", path: "payments", desc: "List job payments" },
|
|
163
|
+
{ name: "history", path: "history", desc: "Get job history" }
|
|
164
|
+
];
|
|
165
|
+
for (const sub of subResources) {
|
|
166
|
+
jobs.command(sub.name).argument("<jobId>", "Job ID").description(sub.desc).action(async (jobId) => {
|
|
167
|
+
const result = await getClient2().get(`/jobs/${jobId}/${sub.path}`);
|
|
168
|
+
console.log(JSON.stringify(result));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/commands/contacts.ts
|
|
174
|
+
function registerContactsCommands(parentCmd, getClient2) {
|
|
175
|
+
const contacts = parentCmd.command("contacts").description("Contact operations");
|
|
176
|
+
contacts.command("list").description("List contacts (paginated)").option("--limit <n>", "Max total results").action(async (opts) => {
|
|
177
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
178
|
+
const result = await paginate(getClient2(), "/contacts", {}, limit);
|
|
179
|
+
console.log(JSON.stringify(result));
|
|
180
|
+
});
|
|
181
|
+
contacts.command("get").argument("<contactId>", "Contact ID").description("Get contact details").action(async (contactId) => {
|
|
182
|
+
const result = await getClient2().get(`/contacts/${contactId}`);
|
|
183
|
+
console.log(JSON.stringify(result));
|
|
184
|
+
});
|
|
185
|
+
contacts.command("create").description("Create a contact (pipe JSON body to stdin)").action(async () => {
|
|
186
|
+
const body = await readStdin();
|
|
187
|
+
const result = await getClient2().post("/contacts", body);
|
|
188
|
+
console.log(JSON.stringify(result));
|
|
189
|
+
});
|
|
190
|
+
contacts.command("search").description("Search contacts (pipe JSON search criteria to stdin)").action(async () => {
|
|
191
|
+
const body = await readStdin();
|
|
192
|
+
const result = await getClient2().post("/contacts/search", body);
|
|
193
|
+
console.log(JSON.stringify(result));
|
|
194
|
+
});
|
|
195
|
+
contacts.command("emails").argument("<contactId>", "Contact ID").description("List contact email addresses").action(async (contactId) => {
|
|
196
|
+
const result = await getClient2().get(`/contacts/${contactId}/email-addresses`);
|
|
197
|
+
console.log(JSON.stringify(result));
|
|
198
|
+
});
|
|
199
|
+
contacts.command("phones").argument("<contactId>", "Contact ID").description("List contact phone numbers").action(async (contactId) => {
|
|
200
|
+
const result = await getClient2().get(`/contacts/${contactId}/phone-numbers`);
|
|
201
|
+
console.log(JSON.stringify(result));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/commands/estimates.ts
|
|
206
|
+
function registerEstimatesCommands(parentCmd, getClient2) {
|
|
207
|
+
const estimates = parentCmd.command("estimates").description("Estimate operations");
|
|
208
|
+
estimates.command("list").description("List estimates (paginated)").option("--limit <n>", "Max total results").action(async (opts) => {
|
|
209
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : void 0;
|
|
210
|
+
const result = await paginate(getClient2(), "/estimates", {}, limit);
|
|
211
|
+
console.log(JSON.stringify(result));
|
|
212
|
+
});
|
|
213
|
+
estimates.command("get").argument("<estimateId>", "Estimate ID").description("Get estimate details").action(async (estimateId) => {
|
|
214
|
+
const result = await getClient2().get(`/estimates/${estimateId}`);
|
|
215
|
+
console.log(JSON.stringify(result));
|
|
216
|
+
});
|
|
217
|
+
estimates.command("sections").argument("<estimateId>", "Estimate ID").description("List sections for an estimate").action(async (estimateId) => {
|
|
218
|
+
const result = await getClient2().get(`/estimates/${estimateId}/sections`);
|
|
219
|
+
console.log(JSON.stringify(result));
|
|
220
|
+
});
|
|
221
|
+
estimates.command("section").argument("<estimateId>", "Estimate ID").argument("<sectionId>", "Section ID").description("Get estimate section details").action(async (estimateId, sectionId) => {
|
|
222
|
+
const result = await getClient2().get(`/estimates/${estimateId}/sections/${sectionId}`);
|
|
223
|
+
console.log(JSON.stringify(result));
|
|
224
|
+
});
|
|
225
|
+
estimates.command("items").argument("<estimateId>", "Estimate ID").argument("<sectionId>", "Section ID").description("List items in an estimate section").action(async (estimateId, sectionId) => {
|
|
226
|
+
const result = await getClient2().get(`/estimates/${estimateId}/sections/${sectionId}/items`);
|
|
227
|
+
console.log(JSON.stringify(result));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/index.ts
|
|
232
|
+
var program = new Command();
|
|
233
|
+
program.name("acculynx").description("AccuLynx CRM CLI for AI agents").version("0.1.0").exitOverride().configureOutput({
|
|
234
|
+
writeOut: (str) => process.stdout.write(str),
|
|
235
|
+
writeErr: (str) => process.stderr.write(JSON.stringify({ error: str.trim() }) + "\n")
|
|
236
|
+
}).option("--api-key <key>", "AccuLynx API key (env: ACCULYNX_API_KEY)");
|
|
237
|
+
var _client = null;
|
|
238
|
+
function getConfigFromProgram() {
|
|
239
|
+
const opts = program.opts();
|
|
240
|
+
return getConfig({ apiKey: opts.apiKey });
|
|
241
|
+
}
|
|
242
|
+
function getClient() {
|
|
243
|
+
if (!_client) {
|
|
244
|
+
_client = createApiClient(getConfigFromProgram());
|
|
245
|
+
}
|
|
246
|
+
return _client;
|
|
247
|
+
}
|
|
248
|
+
registerPingCommand(program, getClient);
|
|
249
|
+
registerJobsCommands(program, getClient);
|
|
250
|
+
registerContactsCommands(program, getClient);
|
|
251
|
+
registerEstimatesCommands(program, getClient);
|
|
252
|
+
program.parseAsync().catch((err) => {
|
|
253
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opsrev/acculynx-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for the AccuLynx roofing CRM API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"acculynx": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"acculynx",
|
|
18
|
+
"cli",
|
|
19
|
+
"roofing",
|
|
20
|
+
"crm",
|
|
21
|
+
"api"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^14.0.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^25.4.0",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"tsx": "^4.21.0",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vitest": "^4.0.18"
|
|
39
|
+
}
|
|
40
|
+
}
|