@opsrev/acculynx-cli 1.10.0 → 1.12.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.
@@ -0,0 +1,351 @@
1
+ // src/config.ts
2
+ var BASE_URL = "https://api.acculynx.com/api/v2";
3
+ function getConfig(options) {
4
+ const apiKey = options.apiKey ?? process.env.ACCULYNX_API_KEY;
5
+ if (!apiKey) {
6
+ throw new Error(
7
+ "Missing required credential: ACCULYNX_API_KEY (set env var or pass --api-key)"
8
+ );
9
+ }
10
+ return {
11
+ baseUrl: BASE_URL,
12
+ apiKey
13
+ };
14
+ }
15
+
16
+ // src/sanitize.ts
17
+ var PUNCTUATION = {
18
+ "\u2012": "-",
19
+ // figure dash
20
+ "\u2013": "-",
21
+ // en dash
22
+ "\u2014": "-",
23
+ // em dash
24
+ "\u2015": "-",
25
+ // horizontal bar
26
+ "\u2018": "'",
27
+ // left single quote
28
+ "\u2019": "'",
29
+ // right single quote
30
+ "\u2032": "'",
31
+ // prime
32
+ "\u201C": '"',
33
+ // left double quote
34
+ "\u201D": '"',
35
+ // right double quote
36
+ "\u2033": '"'
37
+ // double prime
38
+ };
39
+ var PUNCTUATION_RE = new RegExp(`[${Object.keys(PUNCTUATION).join("")}]`, "g");
40
+ function toAscii(s) {
41
+ return s.replace(PUNCTUATION_RE, (ch) => PUNCTUATION[ch]).normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^\x00-\x7F]/g, "").replace(/ {2,}/g, " ").trim();
42
+ }
43
+ function sanitizeDeep(value) {
44
+ if (typeof value === "string") return toAscii(value);
45
+ if (Array.isArray(value)) return value.map(sanitizeDeep);
46
+ if (value !== null && typeof value === "object") {
47
+ const out = {};
48
+ for (const [key, v] of Object.entries(value)) out[key] = sanitizeDeep(v);
49
+ return out;
50
+ }
51
+ return value;
52
+ }
53
+
54
+ // src/api-client.ts
55
+ var MAX_RETRIES = 3;
56
+ var INITIAL_DELAY_MS = 1e3;
57
+ async function sleep(ms) {
58
+ return new Promise((resolve) => setTimeout(resolve, ms));
59
+ }
60
+ function sanitizeForm(form) {
61
+ const out = new FormData();
62
+ for (const [key, value] of form.entries()) {
63
+ if (typeof value === "string") {
64
+ out.append(key, toAscii(value));
65
+ } else {
66
+ out.append(key, value, toAscii(value.name) || "file");
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ async function fetchWithRetry(url, init, retries = MAX_RETRIES) {
72
+ const response = await fetch(url, init);
73
+ if (response.status === 429 && retries > 0) {
74
+ const retryAfter = response.headers.get("Retry-After");
75
+ const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_DELAY_MS * Math.pow(2, MAX_RETRIES - retries);
76
+ await sleep(delay);
77
+ return fetchWithRetry(url, init, retries - 1);
78
+ }
79
+ return response;
80
+ }
81
+ function createApiClient(config) {
82
+ const headers = {
83
+ Authorization: `Bearer ${config.apiKey}`,
84
+ Accept: "application/json"
85
+ };
86
+ async function doGet(path, params) {
87
+ let url = `${config.baseUrl}${path}`;
88
+ if (params && Object.keys(params).length > 0) {
89
+ url += `?${new URLSearchParams(params).toString()}`;
90
+ }
91
+ const response = await fetchWithRetry(url, { headers });
92
+ if (!response.ok) {
93
+ const text = await response.text();
94
+ throw new Error(
95
+ `API error (${response.status} ${response.statusText}): ${text}`
96
+ );
97
+ }
98
+ return response.json();
99
+ }
100
+ async function doPost(path, body) {
101
+ const url = `${config.baseUrl}${path}`;
102
+ const response = await fetchWithRetry(url, {
103
+ method: "POST",
104
+ headers: { ...headers, "Content-Type": "application/json" },
105
+ body: JSON.stringify(sanitizeDeep(body))
106
+ });
107
+ if (!response.ok) {
108
+ const text2 = await response.text();
109
+ throw new Error(
110
+ `API error (${response.status} ${response.statusText}): ${text2}`
111
+ );
112
+ }
113
+ const text = await response.text();
114
+ return text ? JSON.parse(text) : { status: response.status };
115
+ }
116
+ async function doPostForm(path, body) {
117
+ const url = `${config.baseUrl}${path}`;
118
+ const { "Content-Type": _, ...formHeaders } = headers;
119
+ const response = await fetchWithRetry(url, {
120
+ method: "POST",
121
+ headers: formHeaders,
122
+ body: sanitizeForm(body)
123
+ });
124
+ if (!response.ok) {
125
+ const text2 = await response.text();
126
+ throw new Error(
127
+ `API error (${response.status} ${response.statusText}): ${text2}`
128
+ );
129
+ }
130
+ const text = await response.text();
131
+ return text ? JSON.parse(text) : { status: response.status };
132
+ }
133
+ return {
134
+ get: doGet,
135
+ post: doPost,
136
+ postForm: doPostForm
137
+ };
138
+ }
139
+
140
+ // src/ops/ping.ts
141
+ function ping(client) {
142
+ return client.get("/diagnostics/ping");
143
+ }
144
+
145
+ // src/api-helpers.ts
146
+ var DEFAULT_PAGE_SIZE = 25;
147
+ var DEFAULT_LIMIT = 25;
148
+ async function paginate(client, path, params, limit = DEFAULT_LIMIT) {
149
+ const results = [];
150
+ let pageStartIndex = 0;
151
+ while (true) {
152
+ const pageParams = {
153
+ ...params,
154
+ pageSize: String(DEFAULT_PAGE_SIZE),
155
+ pageStartIndex: String(pageStartIndex)
156
+ };
157
+ const data = await client.get(path, pageParams);
158
+ results.push(...data.items);
159
+ if (limit !== Infinity && results.length >= limit) {
160
+ return results.slice(0, limit);
161
+ }
162
+ if (data.items.length < DEFAULT_PAGE_SIZE) {
163
+ break;
164
+ }
165
+ pageStartIndex += DEFAULT_PAGE_SIZE;
166
+ }
167
+ return results;
168
+ }
169
+ async function readStdin(stream = process.stdin) {
170
+ const chunks = [];
171
+ for await (const chunk of stream) {
172
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
173
+ }
174
+ const raw = Buffer.concat(chunks).toString("utf-8").trim();
175
+ if (!raw) {
176
+ throw new Error(
177
+ "No input provided on stdin. Pipe JSON body, e.g.: echo '{...}' | acculynx jobs create"
178
+ );
179
+ }
180
+ try {
181
+ return JSON.parse(raw);
182
+ } catch {
183
+ throw new Error("Invalid JSON on stdin");
184
+ }
185
+ }
186
+
187
+ // src/ops/jobs.ts
188
+ import { readFileSync } from "fs";
189
+ import { basename, extname } from "path";
190
+ var DISALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
191
+ ".exe",
192
+ ".com",
193
+ ".dll",
194
+ ".msi",
195
+ ".bat",
196
+ ".cmd",
197
+ ".sh",
198
+ ".pl",
199
+ ".vbs",
200
+ ".py",
201
+ ".php"
202
+ ]);
203
+ function jobsList(client, opts = {}) {
204
+ const params = {};
205
+ if (opts.startDate) params.startDate = opts.startDate;
206
+ if (opts.endDate) params.endDate = opts.endDate;
207
+ if (opts.dateFilterType) params.dateFilterType = opts.dateFilterType;
208
+ if (opts.milestones) params.milestones = opts.milestones;
209
+ params.sortBy = opts.sortBy ?? "CreatedDate";
210
+ if (opts.sortOrder) params.sortOrder = opts.sortOrder;
211
+ if (opts.includes) params.includes = opts.includes;
212
+ if (opts.assignment) params.assignment = opts.assignment;
213
+ return paginate(client, "/jobs", params, opts.limit);
214
+ }
215
+ function jobGet(client, jobId) {
216
+ return client.get(`/jobs/${jobId}`);
217
+ }
218
+ function jobCreate(client, body) {
219
+ return client.post("/jobs", body);
220
+ }
221
+ function jobSearch(client, query) {
222
+ return client.post("/jobs/search", { SearchTerm: query });
223
+ }
224
+ var jobContacts = (client, jobId) => client.get(`/jobs/${jobId}/contacts`);
225
+ var jobEstimates = (client, jobId) => client.get(`/jobs/${jobId}/estimates`);
226
+ var jobFinancials = (client, jobId) => client.get(`/jobs/${jobId}/financials`);
227
+ var jobInvoices = (client, jobId) => client.get(`/jobs/${jobId}/invoices`);
228
+ var jobMilestones = (client, jobId) => client.get(`/jobs/${jobId}/milestone-history`);
229
+ var jobPayments = (client, jobId) => client.get(`/jobs/${jobId}/payments`);
230
+ var jobHistory = (client, jobId) => client.get(`/jobs/${jobId}/history`);
231
+ var jobReps = (client, jobId) => client.get(`/jobs/${jobId}/representatives`);
232
+ function jobRepsAssign(client, jobId, opts) {
233
+ const type = opts.type ?? "company";
234
+ return client.post(`/jobs/${jobId}/representatives/${type}`, { id: opts.userId });
235
+ }
236
+ function documentFolders(client, opts = {}) {
237
+ const params = {
238
+ recordStartIndex: opts.recordStartIndex ?? "0",
239
+ sortOrder: opts.sortOrder ?? "Ascending"
240
+ };
241
+ if (opts.pageSize) params.pageSize = opts.pageSize;
242
+ return client.get("/company-settings/job-file-settings/document-folders", params);
243
+ }
244
+ function jobAddExpense(client, jobId, body) {
245
+ return client.post(`/jobs/${jobId}/payments/expense`, body);
246
+ }
247
+ function jobUploadDocument(client, jobId, filePath, opts) {
248
+ const ext = extname(filePath).toLowerCase();
249
+ if (DISALLOWED_EXTENSIONS.has(ext)) {
250
+ throw new Error(`File type ${ext} is not allowed`);
251
+ }
252
+ const fileBuffer = readFileSync(filePath);
253
+ const fileName = basename(filePath);
254
+ const file = new File([fileBuffer], fileName);
255
+ const form = new FormData();
256
+ form.append("file", file);
257
+ form.append("documentFolderId", opts.folderId);
258
+ if (opts.description) form.append("description", opts.description);
259
+ if (opts.externalId) form.append("externalId", opts.externalId);
260
+ if (opts.externalSource) form.append("externalSource", opts.externalSource);
261
+ return client.postForm(`/jobs/${jobId}/documents`, form);
262
+ }
263
+
264
+ // src/ops/contacts.ts
265
+ function contactsList(client, limit) {
266
+ return paginate(client, "/contacts", {}, limit);
267
+ }
268
+ function contactGet(client, contactId) {
269
+ return client.get(`/contacts/${contactId}`);
270
+ }
271
+ function contactCreate(client, body) {
272
+ return client.post("/contacts", body);
273
+ }
274
+ function contactsSearch(client, opts) {
275
+ return client.post("/contacts/search", {
276
+ searchTerm: opts.query,
277
+ startDate: opts.startDate,
278
+ endDate: opts.endDate,
279
+ sort: {
280
+ sortColumn: opts.sortBy ?? "lastName",
281
+ sortDirection: opts.sortOrder ?? "Ascending"
282
+ }
283
+ });
284
+ }
285
+ var contactEmails = (client, contactId) => client.get(`/contacts/${contactId}/email-addresses`);
286
+ var contactPhones = (client, contactId) => client.get(`/contacts/${contactId}/phone-numbers`);
287
+ function contactPhoneAdd(client, contactId, opts) {
288
+ return client.post(`/contacts/${contactId}/phone-numbers`, {
289
+ type: opts.type,
290
+ number: opts.number,
291
+ smsOptOut: Boolean(opts.smsOptOut)
292
+ });
293
+ }
294
+
295
+ // src/ops/estimates.ts
296
+ function estimatesList(client, limit) {
297
+ return paginate(client, "/estimates", {}, limit);
298
+ }
299
+ var estimateGet = (client, estimateId) => client.get(`/estimates/${estimateId}`);
300
+ var estimateSections = (client, estimateId) => client.get(`/estimates/${estimateId}/sections`);
301
+ var estimateSection = (client, estimateId, sectionId) => client.get(`/estimates/${estimateId}/sections/${sectionId}`);
302
+ var estimateItems = (client, estimateId, sectionId) => client.get(`/estimates/${estimateId}/sections/${sectionId}/items`);
303
+
304
+ // src/ops/users.ts
305
+ async function usersList(client, opts = {}) {
306
+ if (opts.search) {
307
+ const all = await paginate(client, "/users", {}, Infinity);
308
+ const term = opts.search.toLowerCase();
309
+ return all.filter((u) => {
310
+ const fields = [u.displayName, u.firstName, u.lastName, u.email];
311
+ return fields.some((f) => f && f.toLowerCase().includes(term));
312
+ });
313
+ }
314
+ return paginate(client, "/users", {}, opts.limit);
315
+ }
316
+
317
+ export {
318
+ getConfig,
319
+ createApiClient,
320
+ ping,
321
+ readStdin,
322
+ jobsList,
323
+ jobGet,
324
+ jobCreate,
325
+ jobSearch,
326
+ jobContacts,
327
+ jobEstimates,
328
+ jobFinancials,
329
+ jobInvoices,
330
+ jobMilestones,
331
+ jobPayments,
332
+ jobHistory,
333
+ jobReps,
334
+ jobRepsAssign,
335
+ documentFolders,
336
+ jobAddExpense,
337
+ jobUploadDocument,
338
+ contactsList,
339
+ contactGet,
340
+ contactCreate,
341
+ contactsSearch,
342
+ contactEmails,
343
+ contactPhones,
344
+ contactPhoneAdd,
345
+ estimatesList,
346
+ estimateGet,
347
+ estimateSections,
348
+ estimateSection,
349
+ estimateItems,
350
+ usersList
351
+ };
package/dist/index.js CHANGED
@@ -1,262 +1,120 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ contactCreate,
4
+ contactEmails,
5
+ contactGet,
6
+ contactPhoneAdd,
7
+ contactPhones,
8
+ contactsList,
9
+ contactsSearch,
10
+ createApiClient,
11
+ documentFolders,
12
+ estimateGet,
13
+ estimateItems,
14
+ estimateSection,
15
+ estimateSections,
16
+ estimatesList,
17
+ getConfig,
18
+ jobAddExpense,
19
+ jobContacts,
20
+ jobCreate,
21
+ jobEstimates,
22
+ jobFinancials,
23
+ jobGet,
24
+ jobHistory,
25
+ jobInvoices,
26
+ jobMilestones,
27
+ jobPayments,
28
+ jobReps,
29
+ jobRepsAssign,
30
+ jobSearch,
31
+ jobUploadDocument,
32
+ jobsList,
33
+ ping,
34
+ readStdin,
35
+ usersList
36
+ } from "./chunk-GAJNPAO3.js";
2
37
 
3
38
  // src/index.ts
4
39
  import { createRequire } from "module";
5
40
  import { Command } from "commander";
6
41
 
7
- // src/config.ts
8
- var BASE_URL = "https://api.acculynx.com/api/v2";
9
- function getConfig(options) {
10
- const apiKey = options.apiKey ?? process.env.ACCULYNX_API_KEY;
11
- if (!apiKey) {
12
- throw new Error(
13
- "Missing required credential: ACCULYNX_API_KEY (set env var or pass --api-key)"
14
- );
15
- }
16
- return {
17
- baseUrl: BASE_URL,
18
- apiKey
19
- };
20
- }
21
-
22
- // src/api-client.ts
23
- var MAX_RETRIES = 3;
24
- var INITIAL_DELAY_MS = 1e3;
25
- async function sleep(ms) {
26
- return new Promise((resolve) => setTimeout(resolve, ms));
27
- }
28
- async function fetchWithRetry(url, init, retries = MAX_RETRIES) {
29
- const response = await fetch(url, init);
30
- if (response.status === 429 && retries > 0) {
31
- const retryAfter = response.headers.get("Retry-After");
32
- const delay = retryAfter ? parseInt(retryAfter, 10) * 1e3 : INITIAL_DELAY_MS * Math.pow(2, MAX_RETRIES - retries);
33
- await sleep(delay);
34
- return fetchWithRetry(url, init, retries - 1);
35
- }
36
- return response;
37
- }
38
- function createApiClient(config) {
39
- const headers = {
40
- Authorization: `Bearer ${config.apiKey}`,
41
- Accept: "application/json"
42
- };
43
- async function doGet(path, params) {
44
- let url = `${config.baseUrl}${path}`;
45
- if (params && Object.keys(params).length > 0) {
46
- url += `?${new URLSearchParams(params).toString()}`;
47
- }
48
- const response = await fetchWithRetry(url, { headers });
49
- if (!response.ok) {
50
- const text = await response.text();
51
- throw new Error(
52
- `API error (${response.status} ${response.statusText}): ${text}`
53
- );
54
- }
55
- return response.json();
56
- }
57
- async function doPost(path, body) {
58
- const url = `${config.baseUrl}${path}`;
59
- const response = await fetchWithRetry(url, {
60
- method: "POST",
61
- headers: { ...headers, "Content-Type": "application/json" },
62
- body: JSON.stringify(body)
63
- });
64
- if (!response.ok) {
65
- const text2 = await response.text();
66
- throw new Error(
67
- `API error (${response.status} ${response.statusText}): ${text2}`
68
- );
69
- }
70
- const text = await response.text();
71
- return text ? JSON.parse(text) : { status: response.status };
72
- }
73
- async function doPostForm(path, body) {
74
- const url = `${config.baseUrl}${path}`;
75
- const { "Content-Type": _, ...formHeaders } = headers;
76
- const response = await fetchWithRetry(url, {
77
- method: "POST",
78
- headers: formHeaders,
79
- body
80
- });
81
- if (!response.ok) {
82
- const text2 = await response.text();
83
- throw new Error(
84
- `API error (${response.status} ${response.statusText}): ${text2}`
85
- );
86
- }
87
- const text = await response.text();
88
- return text ? JSON.parse(text) : { status: response.status };
89
- }
90
- return {
91
- get: doGet,
92
- post: doPost,
93
- postForm: doPostForm
94
- };
95
- }
96
-
97
42
  // src/commands/ping.ts
98
43
  function registerPingCommand(parentCmd, getClient2) {
99
44
  parentCmd.command("ping").description("Health check \u2014 verify API key and connectivity").action(async () => {
100
- const result = await getClient2().get("/diagnostics/ping");
45
+ const result = await ping(getClient2());
101
46
  console.log(JSON.stringify(result));
102
47
  });
103
48
  }
104
49
 
105
50
  // src/commands/jobs.ts
106
- import { readFileSync } from "fs";
107
- import { basename, extname } from "path";
108
-
109
- // src/api-helpers.ts
110
- var DEFAULT_PAGE_SIZE = 25;
111
- var DEFAULT_LIMIT = 25;
112
- async function paginate(client, path, params, limit = DEFAULT_LIMIT) {
113
- const results = [];
114
- let pageStartIndex = 0;
115
- while (true) {
116
- const pageParams = {
117
- ...params,
118
- pageSize: String(DEFAULT_PAGE_SIZE),
119
- pageStartIndex: String(pageStartIndex)
120
- };
121
- const data = await client.get(path, pageParams);
122
- results.push(...data.items);
123
- if (limit !== Infinity && results.length >= limit) {
124
- return results.slice(0, limit);
125
- }
126
- if (data.items.length < DEFAULT_PAGE_SIZE) {
127
- break;
128
- }
129
- pageStartIndex += DEFAULT_PAGE_SIZE;
130
- }
131
- return results;
132
- }
133
- async function readStdin(stream = process.stdin) {
134
- const chunks = [];
135
- for await (const chunk of stream) {
136
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
137
- }
138
- const raw = Buffer.concat(chunks).toString("utf-8").trim();
139
- if (!raw) {
140
- throw new Error(
141
- "No input provided on stdin. Pipe JSON body, e.g.: echo '{...}' | acculynx jobs create"
142
- );
143
- }
144
- try {
145
- return JSON.parse(raw);
146
- } catch {
147
- throw new Error("Invalid JSON on stdin");
148
- }
149
- }
150
-
151
- // src/commands/jobs.ts
152
- var DISALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
153
- ".exe",
154
- ".com",
155
- ".dll",
156
- ".msi",
157
- ".bat",
158
- ".cmd",
159
- ".sh",
160
- ".pl",
161
- ".vbs",
162
- ".py",
163
- ".php"
164
- ]);
165
51
  function registerJobsCommands(parentCmd, getClient2) {
166
52
  const jobs = parentCmd.command("jobs").description("Job operations");
167
53
  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("--assignment <type>", "Filter by assignment: assigned, unassigned").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
168
- const params = {};
169
- if (opts.startDate) params.startDate = opts.startDate;
170
- if (opts.endDate) params.endDate = opts.endDate;
171
- if (opts.dateFilterType) params.dateFilterType = opts.dateFilterType;
172
- if (opts.milestones) params.milestones = opts.milestones;
173
- params.sortBy = opts.sortBy ?? "CreatedDate";
174
- if (opts.sortOrder) params.sortOrder = opts.sortOrder;
175
- if (opts.includes) params.includes = opts.includes;
176
- if (opts.assignment) params.assignment = opts.assignment;
177
54
  const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
178
- const result = await paginate(getClient2(), "/jobs", params, limit);
55
+ const result = await jobsList(getClient2(), {
56
+ startDate: opts.startDate,
57
+ endDate: opts.endDate,
58
+ dateFilterType: opts.dateFilterType,
59
+ milestones: opts.milestones,
60
+ sortBy: opts.sortBy,
61
+ sortOrder: opts.sortOrder,
62
+ includes: opts.includes,
63
+ assignment: opts.assignment,
64
+ limit
65
+ });
179
66
  console.log(JSON.stringify(result));
180
67
  });
181
68
  jobs.command("get").argument("<jobId>", "Job ID").description("Get job details").action(async (jobId) => {
182
- const result = await getClient2().get(`/jobs/${jobId}`);
183
- console.log(JSON.stringify(result));
69
+ console.log(JSON.stringify(await jobGet(getClient2(), jobId)));
184
70
  });
185
71
  jobs.command("create").description("Create a job (pipe JSON body to stdin)").action(async () => {
186
72
  const body = await readStdin();
187
- const result = await getClient2().post("/jobs", body);
188
- console.log(JSON.stringify(result));
73
+ console.log(JSON.stringify(await jobCreate(getClient2(), body)));
189
74
  });
190
75
  jobs.command("search").description("Search jobs").requiredOption("--query <text>", "Search term (required)").action(async (opts) => {
191
- const result = await getClient2().post("/jobs/search", {
192
- SearchTerm: opts.query
193
- });
194
- console.log(JSON.stringify(result));
76
+ console.log(JSON.stringify(await jobSearch(getClient2(), opts.query)));
195
77
  });
196
78
  const subResources = [
197
- { name: "contacts", path: "contacts", desc: "List job contacts" },
198
- { name: "estimates", path: "estimates", desc: "List job estimates" },
199
- { name: "financials", path: "financials", desc: "Get job financials" },
200
- { name: "invoices", path: "invoices", desc: "List job invoices" },
201
- { name: "milestones", path: "milestone-history", desc: "List job milestone history" },
202
- { name: "payments", path: "payments", desc: "List job payments" },
203
- { name: "history", path: "history", desc: "Get job history" }
79
+ { name: "contacts", fn: jobContacts, desc: "List job contacts" },
80
+ { name: "estimates", fn: jobEstimates, desc: "List job estimates" },
81
+ { name: "financials", fn: jobFinancials, desc: "Get job financials" },
82
+ { name: "invoices", fn: jobInvoices, desc: "List job invoices" },
83
+ { name: "milestones", fn: jobMilestones, desc: "List job milestone history" },
84
+ { name: "payments", fn: jobPayments, desc: "List job payments" },
85
+ { name: "history", fn: jobHistory, desc: "Get job history" }
204
86
  ];
205
87
  for (const sub of subResources) {
206
88
  jobs.command(sub.name).argument("<jobId>", "Job ID").description(sub.desc).action(async (jobId) => {
207
- const result = await getClient2().get(`/jobs/${jobId}/${sub.path}`);
208
- console.log(JSON.stringify(result));
89
+ console.log(JSON.stringify(await sub.fn(getClient2(), jobId)));
209
90
  });
210
91
  }
211
92
  jobs.command("reps").argument("<jobId>", "Job ID").description("List all representatives for a job").action(async (jobId) => {
212
- const result = await getClient2().get(`/jobs/${jobId}/representatives`);
213
- console.log(JSON.stringify(result));
93
+ console.log(JSON.stringify(await jobReps(getClient2(), jobId)));
214
94
  });
215
95
  jobs.command("reps-assign").argument("<jobId>", "Job ID").description("Assign a representative to a job").requiredOption("--user-id <id>", "User ID to assign").option("--type <type>", "Rep type: company, sales-owner, ar-owner", "company").action(async (jobId, opts) => {
216
- const result = await getClient2().post(
217
- `/jobs/${jobId}/representatives/${opts.type}`,
218
- { id: opts.userId }
219
- );
96
+ const result = await jobRepsAssign(getClient2(), jobId, { userId: opts.userId, type: opts.type });
220
97
  console.log(JSON.stringify(result));
221
98
  });
222
99
  jobs.command("document-folders").description("List document folders for the company").option("--page-size <n>", "Number of items per page").option("--record-start-index <n>", "Index of first element to return", "0").option("--sort-order <order>", "Ascending or Descending", "Ascending").action(async (opts) => {
223
- const params = {
100
+ const result = await documentFolders(getClient2(), {
101
+ pageSize: opts.pageSize,
224
102
  recordStartIndex: opts.recordStartIndex,
225
103
  sortOrder: opts.sortOrder
226
- };
227
- if (opts.pageSize) params.pageSize = opts.pageSize;
228
- const result = await getClient2().get(
229
- "/company-settings/job-file-settings/document-folders",
230
- params
231
- );
104
+ });
232
105
  console.log(JSON.stringify(result));
233
106
  });
234
107
  jobs.command("add-expense").argument("<jobId>", "Job ID").description("Record an additional expense on a job (pipe JSON body to stdin)").action(async (jobId) => {
235
108
  const body = await readStdin();
236
- const result = await getClient2().post(
237
- `/jobs/${jobId}/payments/expense`,
238
- body
239
- );
240
- console.log(JSON.stringify(result));
109
+ console.log(JSON.stringify(await jobAddExpense(getClient2(), jobId, body)));
241
110
  });
242
111
  jobs.command("upload-document").argument("<jobId>", "Job ID").argument("<filePath>", "Path to the file to upload").requiredOption("--folder-id <id>", "Document folder ID (required)").option("--description <text>", "Brief description of the file").option("--external-id <id>", "External reference identifier").option("--external-source <source>", "External reference source").description("Upload a document to a job").action(async (jobId, filePath, opts) => {
243
- const ext = extname(filePath).toLowerCase();
244
- if (DISALLOWED_EXTENSIONS.has(ext)) {
245
- throw new Error(`File type ${ext} is not allowed`);
246
- }
247
- const fileBuffer = readFileSync(filePath);
248
- const fileName = basename(filePath);
249
- const file = new File([fileBuffer], fileName);
250
- const form = new FormData();
251
- form.append("file", file);
252
- form.append("documentFolderId", opts.folderId);
253
- if (opts.description) form.append("description", opts.description);
254
- if (opts.externalId) form.append("externalId", opts.externalId);
255
- if (opts.externalSource) form.append("externalSource", opts.externalSource);
256
- const result = await getClient2().postForm(
257
- `/jobs/${jobId}/documents`,
258
- form
259
- );
112
+ const result = await jobUploadDocument(getClient2(), jobId, filePath, {
113
+ folderId: opts.folderId,
114
+ description: opts.description,
115
+ externalId: opts.externalId,
116
+ externalSource: opts.externalSource
117
+ });
260
118
  console.log(JSON.stringify(result));
261
119
  });
262
120
  }
@@ -266,47 +124,37 @@ function registerContactsCommands(parentCmd, getClient2) {
266
124
  const contacts = parentCmd.command("contacts").description("Contact operations");
267
125
  contacts.command("list").description("List contacts (paginated)").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
268
126
  const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
269
- const result = await paginate(getClient2(), "/contacts", {}, limit);
270
- console.log(JSON.stringify(result));
127
+ console.log(JSON.stringify(await contactsList(getClient2(), limit)));
271
128
  });
272
129
  contacts.command("get").argument("<contactId>", "Contact ID").description("Get contact details").action(async (contactId) => {
273
- const result = await getClient2().get(`/contacts/${contactId}`);
274
- console.log(JSON.stringify(result));
130
+ console.log(JSON.stringify(await contactGet(getClient2(), contactId)));
275
131
  });
276
132
  contacts.command("create").description("Create a contact (pipe JSON body to stdin)").action(async () => {
277
133
  const body = await readStdin();
278
- const result = await getClient2().post("/contacts", body);
279
- console.log(JSON.stringify(result));
134
+ console.log(JSON.stringify(await contactCreate(getClient2(), body)));
280
135
  });
281
136
  contacts.command("search").description("Search contacts").requiredOption("--query <text>", "Search term (required)").requiredOption("--start-date <date>", "Start date (YYYY-MM-DD, required)").requiredOption("--end-date <date>", "End date (YYYY-MM-DD, required)").option("--sort-by <field>", "Sort by: CreatedDate, firstName, lastName", "lastName").option("--sort-order <order>", "Ascending or Descending", "Ascending").action(async (opts) => {
282
- const result = await getClient2().post("/contacts/search", {
283
- searchTerm: opts.query,
137
+ const result = await contactsSearch(getClient2(), {
138
+ query: opts.query,
284
139
  startDate: opts.startDate,
285
140
  endDate: opts.endDate,
286
- sort: {
287
- sortColumn: opts.sortBy,
288
- sortDirection: opts.sortOrder
289
- }
141
+ sortBy: opts.sortBy,
142
+ sortOrder: opts.sortOrder
290
143
  });
291
144
  console.log(JSON.stringify(result));
292
145
  });
293
146
  contacts.command("emails").argument("<contactId>", "Contact ID").description("List contact email addresses").action(async (contactId) => {
294
- const result = await getClient2().get(`/contacts/${contactId}/email-addresses`);
295
- console.log(JSON.stringify(result));
147
+ console.log(JSON.stringify(await contactEmails(getClient2(), contactId)));
296
148
  });
297
149
  contacts.command("phones").argument("<contactId>", "Contact ID").description("List contact phone numbers").action(async (contactId) => {
298
- const result = await getClient2().get(`/contacts/${contactId}/phone-numbers`);
299
- console.log(JSON.stringify(result));
150
+ console.log(JSON.stringify(await contactPhones(getClient2(), contactId)));
300
151
  });
301
152
  contacts.command("phone-add").argument("<contactId>", "Contact ID").description("Add a phone number to a contact (omit --sms-opt-out to opt in to SMS)").requiredOption("--type <type>", "Phone type: Mobile, Home, Work").requiredOption("--number <number>", "Phone number").option("--sms-opt-out", "Opt this number out of SMS").action(async (contactId, opts) => {
302
- const result = await getClient2().post(
303
- `/contacts/${contactId}/phone-numbers`,
304
- {
305
- type: opts.type,
306
- number: opts.number,
307
- smsOptOut: Boolean(opts.smsOptOut)
308
- }
309
- );
153
+ const result = await contactPhoneAdd(getClient2(), contactId, {
154
+ type: opts.type,
155
+ number: opts.number,
156
+ smsOptOut: Boolean(opts.smsOptOut)
157
+ });
310
158
  console.log(JSON.stringify(result));
311
159
  });
312
160
  }
@@ -316,24 +164,19 @@ function registerEstimatesCommands(parentCmd, getClient2) {
316
164
  const estimates = parentCmd.command("estimates").description("Estimate operations");
317
165
  estimates.command("list").description("List estimates (paginated)").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
318
166
  const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
319
- const result = await paginate(getClient2(), "/estimates", {}, limit);
320
- console.log(JSON.stringify(result));
167
+ console.log(JSON.stringify(await estimatesList(getClient2(), limit)));
321
168
  });
322
169
  estimates.command("get").argument("<estimateId>", "Estimate ID").description("Get estimate details").action(async (estimateId) => {
323
- const result = await getClient2().get(`/estimates/${estimateId}`);
324
- console.log(JSON.stringify(result));
170
+ console.log(JSON.stringify(await estimateGet(getClient2(), estimateId)));
325
171
  });
326
172
  estimates.command("sections").argument("<estimateId>", "Estimate ID").description("List sections for an estimate").action(async (estimateId) => {
327
- const result = await getClient2().get(`/estimates/${estimateId}/sections`);
328
- console.log(JSON.stringify(result));
173
+ console.log(JSON.stringify(await estimateSections(getClient2(), estimateId)));
329
174
  });
330
175
  estimates.command("section").argument("<estimateId>", "Estimate ID").argument("<sectionId>", "Section ID").description("Get estimate section details").action(async (estimateId, sectionId) => {
331
- const result = await getClient2().get(`/estimates/${estimateId}/sections/${sectionId}`);
332
- console.log(JSON.stringify(result));
176
+ console.log(JSON.stringify(await estimateSection(getClient2(), estimateId, sectionId)));
333
177
  });
334
178
  estimates.command("items").argument("<estimateId>", "Estimate ID").argument("<sectionId>", "Section ID").description("List items in an estimate section").action(async (estimateId, sectionId) => {
335
- const result = await getClient2().get(`/estimates/${estimateId}/sections/${sectionId}/items`);
336
- console.log(JSON.stringify(result));
179
+ console.log(JSON.stringify(await estimateItems(getClient2(), estimateId, sectionId)));
337
180
  });
338
181
  }
339
182
 
@@ -341,18 +184,8 @@ function registerEstimatesCommands(parentCmd, getClient2) {
341
184
  function registerUsersCommands(parentCmd, getClient2) {
342
185
  const users = parentCmd.command("users").description("User operations");
343
186
  users.command("list").description("List users (paginated)").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").option("--search <text>", "Filter users by name or email (client-side)").action(async (opts) => {
344
- if (opts.search) {
345
- const all = await paginate(getClient2(), "/users", {}, Infinity);
346
- const term = opts.search.toLowerCase();
347
- const filtered = all.filter((u) => {
348
- const fields = [u.displayName, u.firstName, u.lastName, u.email];
349
- return fields.some((f) => f && f.toLowerCase().includes(term));
350
- });
351
- console.log(JSON.stringify(filtered));
352
- return;
353
- }
354
187
  const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
355
- const result = await paginate(getClient2(), "/users", {}, limit);
188
+ const result = await usersList(getClient2(), { limit, search: opts.search });
356
189
  console.log(JSON.stringify(result));
357
190
  });
358
191
  }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ interface AccuLynxConfig {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ }
6
+
7
+ /**
8
+ * Fail fast at boot if the API key is missing/unresolvable (getConfig throws).
9
+ * Exported with injectable deps so it can be unit-tested without booting the server.
10
+ */
11
+ declare function bootSelfCheck(getCfg?: () => AccuLynxConfig, logFn?: (msg: string) => void, exit?: (code: number) => never): Promise<void>;
12
+
13
+ export { bootSelfCheck };
package/dist/mcp.js ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ contactCreate,
4
+ contactEmails,
5
+ contactGet,
6
+ contactPhoneAdd,
7
+ contactPhones,
8
+ contactsList,
9
+ contactsSearch,
10
+ createApiClient,
11
+ documentFolders,
12
+ estimateGet,
13
+ estimateItems,
14
+ estimateSection,
15
+ estimateSections,
16
+ estimatesList,
17
+ getConfig,
18
+ jobAddExpense,
19
+ jobContacts,
20
+ jobCreate,
21
+ jobEstimates,
22
+ jobFinancials,
23
+ jobGet,
24
+ jobHistory,
25
+ jobInvoices,
26
+ jobMilestones,
27
+ jobPayments,
28
+ jobReps,
29
+ jobRepsAssign,
30
+ jobSearch,
31
+ jobUploadDocument,
32
+ jobsList,
33
+ ping,
34
+ usersList
35
+ } from "./chunk-GAJNPAO3.js";
36
+
37
+ // src/mcp.ts
38
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
39
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
40
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
41
+ import { createRequire } from "module";
42
+
43
+ // src/mcp/tools.ts
44
+ var LIMIT = { type: "number", description: "Max total results (default 25). Omit and set all=true to fetch everything." };
45
+ var ALL = { type: "boolean", description: "Fetch all results (ignores limit)." };
46
+ var DATE = { type: "string", description: "Date in YYYY-MM-DD format.", pattern: "^\\d{4}-\\d{2}-\\d{2}$" };
47
+ var BODY = { type: "object", description: "The full JSON request body for this resource.", additionalProperties: true };
48
+ var SORT_ORDER = { type: "string", enum: ["Ascending", "Descending"], description: "Sort direction." };
49
+ function ok(value) {
50
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
51
+ }
52
+ function fail(payload) {
53
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], isError: true };
54
+ }
55
+ var str = (v) => typeof v === "string" ? v : void 0;
56
+ var num = (v) => {
57
+ if (v === void 0 || v === null) return void 0;
58
+ const n = Number(v);
59
+ return Number.isNaN(n) ? void 0 : n;
60
+ };
61
+ var boolOf = (v) => typeof v === "boolean" ? v : void 0;
62
+ var resolveLimit = (args) => boolOf(args.all) ? Infinity : num(args.limit);
63
+ function defaultClient() {
64
+ return createApiClient(getConfig({}));
65
+ }
66
+ function buildTools() {
67
+ return [
68
+ { name: "acculynx_ping", description: "Health check \u2014 verify the API key and connectivity.", inputSchema: { type: "object", properties: {}, required: [] } },
69
+ { name: "acculynx_jobs_list", description: "List jobs (paginated). All filters optional.", inputSchema: { type: "object", properties: {
70
+ startDate: DATE,
71
+ endDate: DATE,
72
+ dateFilterType: { type: "string", description: "Date field to filter on." },
73
+ milestones: { type: "string", description: "Filter by milestones." },
74
+ sortBy: { type: "string", enum: ["CreatedDate", "MilestoneDate", "ModifiedDate"], description: "Sort field (default CreatedDate)." },
75
+ sortOrder: SORT_ORDER,
76
+ includes: { type: "string", enum: ["contact", "initialAppointment"], description: "Related data to include." },
77
+ assignment: { type: "string", enum: ["assigned", "unassigned"], description: "Filter by assignment status." },
78
+ limit: LIMIT,
79
+ all: ALL
80
+ }, required: [] } },
81
+ { name: "acculynx_jobs_get", description: "Get one job by id.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
82
+ { name: "acculynx_jobs_create", description: "Create a job from a JSON body.", inputSchema: { type: "object", properties: { body: BODY }, required: ["body"] } },
83
+ { name: "acculynx_jobs_search", description: "Search jobs by a free-text term.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search term." } }, required: ["query"] } },
84
+ { name: "acculynx_jobs_contacts", description: "List a job's contacts.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
85
+ { name: "acculynx_jobs_estimates", description: "List a job's estimates.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
86
+ { name: "acculynx_jobs_financials", description: "Get a job's financials.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
87
+ { name: "acculynx_jobs_invoices", description: "List a job's invoices.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
88
+ { name: "acculynx_jobs_milestones", description: "List a job's milestone history.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
89
+ { name: "acculynx_jobs_payments", description: "List a job's payments.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
90
+ { name: "acculynx_jobs_history", description: "Get a job's history.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
91
+ { name: "acculynx_jobs_reps", description: "List a job's representatives.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." } }, required: ["jobId"] } },
92
+ { name: "acculynx_jobs_reps_assign", description: "Assign a representative to a job.", inputSchema: { type: "object", properties: {
93
+ jobId: { type: "string", description: "Job id." },
94
+ userId: { type: "string", description: "User id to assign." },
95
+ type: { type: "string", enum: ["company", "sales-owner", "ar-owner"], description: "Rep type (default company)." }
96
+ }, required: ["jobId", "userId"] } },
97
+ { name: "acculynx_jobs_document_folders", description: "List the company's document folders.", inputSchema: { type: "object", properties: {
98
+ pageSize: { type: "number", description: "Items per page." },
99
+ recordStartIndex: { type: "number", description: "0-based start index (default 0)." },
100
+ sortOrder: SORT_ORDER
101
+ }, required: [] } },
102
+ { name: "acculynx_jobs_add_expense", description: "Record an additional expense on a job from a JSON body.", inputSchema: { type: "object", properties: { jobId: { type: "string", description: "Job id." }, body: BODY }, required: ["jobId", "body"] } },
103
+ { name: "acculynx_jobs_upload_document", description: "Upload a local file to a job. Blocked extensions: .exe .com .dll .msi .bat .cmd .sh .pl .vbs .py .php.", inputSchema: { type: "object", properties: {
104
+ jobId: { type: "string", description: "Job id." },
105
+ filePath: { type: "string", description: "Absolute path to a local file the server can read." },
106
+ folderId: { type: "string", description: "Destination document folder id (from acculynx_jobs_document_folders)." },
107
+ description: { type: "string", description: "Brief file description." },
108
+ externalId: { type: "string", description: "External reference id." },
109
+ externalSource: { type: "string", description: "External reference source." }
110
+ }, required: ["jobId", "filePath", "folderId"] } },
111
+ { name: "acculynx_contacts_list", description: "List contacts (paginated).", inputSchema: { type: "object", properties: { limit: LIMIT, all: ALL }, required: [] } },
112
+ { name: "acculynx_contacts_get", description: "Get one contact by id.", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "Contact id." } }, required: ["contactId"] } },
113
+ { name: "acculynx_contacts_create", description: "Create a contact from a JSON body.", inputSchema: { type: "object", properties: { body: BODY }, required: ["body"] } },
114
+ { name: "acculynx_contacts_search", description: "Search contacts within a date range.", inputSchema: { type: "object", properties: {
115
+ query: { type: "string", description: "Search term." },
116
+ startDate: DATE,
117
+ endDate: DATE,
118
+ sortBy: { type: "string", enum: ["CreatedDate", "firstName", "lastName"], description: "Sort field (default lastName)." },
119
+ sortOrder: SORT_ORDER
120
+ }, required: ["query", "startDate", "endDate"] } },
121
+ { name: "acculynx_contacts_emails", description: "List a contact's email addresses.", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "Contact id." } }, required: ["contactId"] } },
122
+ { name: "acculynx_contacts_phones", description: "List a contact's phone numbers.", inputSchema: { type: "object", properties: { contactId: { type: "string", description: "Contact id." } }, required: ["contactId"] } },
123
+ { name: "acculynx_contacts_phone_add", description: "Add a phone number to a contact. Omit smsOptOut to opt in to SMS.", inputSchema: { type: "object", properties: {
124
+ contactId: { type: "string", description: "Contact id." },
125
+ type: { type: "string", enum: ["Mobile", "Home", "Work"], description: "Phone type." },
126
+ number: { type: "string", description: "Phone number." },
127
+ smsOptOut: { type: "boolean", description: "Opt this number OUT of SMS (default false = opted in)." }
128
+ }, required: ["contactId", "type", "number"] } },
129
+ { name: "acculynx_estimates_list", description: "List estimates (paginated).", inputSchema: { type: "object", properties: { limit: LIMIT, all: ALL }, required: [] } },
130
+ { name: "acculynx_estimates_get", description: "Get one estimate by id.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." } }, required: ["estimateId"] } },
131
+ { name: "acculynx_estimates_sections", description: "List an estimate's sections.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." } }, required: ["estimateId"] } },
132
+ { name: "acculynx_estimates_section", description: "Get one estimate section.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." }, sectionId: { type: "string", description: "Section id." } }, required: ["estimateId", "sectionId"] } },
133
+ { name: "acculynx_estimates_items", description: "List items in an estimate section.", inputSchema: { type: "object", properties: { estimateId: { type: "string", description: "Estimate id." }, sectionId: { type: "string", description: "Section id." } }, required: ["estimateId", "sectionId"] } },
134
+ { name: "acculynx_users_list", description: "List users (paginated). Optional client-side search by name/email.", inputSchema: { type: "object", properties: { limit: LIMIT, all: ALL, search: { type: "string", description: "Filter users by name or email (case-insensitive substring)." } }, required: [] } }
135
+ ];
136
+ }
137
+ async function handleToolCall(call, makeClient = defaultClient) {
138
+ const args = call.args ?? {};
139
+ try {
140
+ const client = makeClient();
141
+ switch (call.name) {
142
+ case "acculynx_ping":
143
+ return ok(await ping(client));
144
+ case "acculynx_jobs_list":
145
+ return ok(await jobsList(client, {
146
+ startDate: str(args.startDate),
147
+ endDate: str(args.endDate),
148
+ dateFilterType: str(args.dateFilterType),
149
+ milestones: str(args.milestones),
150
+ sortBy: str(args.sortBy),
151
+ sortOrder: str(args.sortOrder),
152
+ includes: str(args.includes),
153
+ assignment: str(args.assignment),
154
+ limit: resolveLimit(args)
155
+ }));
156
+ case "acculynx_jobs_get":
157
+ return ok(await jobGet(client, str(args.jobId) ?? ""));
158
+ case "acculynx_jobs_create":
159
+ return ok(await jobCreate(client, args.body));
160
+ case "acculynx_jobs_search":
161
+ return ok(await jobSearch(client, str(args.query) ?? ""));
162
+ case "acculynx_jobs_contacts":
163
+ return ok(await jobContacts(client, str(args.jobId) ?? ""));
164
+ case "acculynx_jobs_estimates":
165
+ return ok(await jobEstimates(client, str(args.jobId) ?? ""));
166
+ case "acculynx_jobs_financials":
167
+ return ok(await jobFinancials(client, str(args.jobId) ?? ""));
168
+ case "acculynx_jobs_invoices":
169
+ return ok(await jobInvoices(client, str(args.jobId) ?? ""));
170
+ case "acculynx_jobs_milestones":
171
+ return ok(await jobMilestones(client, str(args.jobId) ?? ""));
172
+ case "acculynx_jobs_payments":
173
+ return ok(await jobPayments(client, str(args.jobId) ?? ""));
174
+ case "acculynx_jobs_history":
175
+ return ok(await jobHistory(client, str(args.jobId) ?? ""));
176
+ case "acculynx_jobs_reps":
177
+ return ok(await jobReps(client, str(args.jobId) ?? ""));
178
+ case "acculynx_jobs_reps_assign":
179
+ return ok(await jobRepsAssign(client, str(args.jobId) ?? "", { userId: str(args.userId) ?? "", type: str(args.type) }));
180
+ case "acculynx_jobs_document_folders":
181
+ return ok(await documentFolders(client, {
182
+ pageSize: args.pageSize !== void 0 ? String(num(args.pageSize)) : void 0,
183
+ recordStartIndex: args.recordStartIndex !== void 0 ? String(num(args.recordStartIndex)) : void 0,
184
+ sortOrder: str(args.sortOrder)
185
+ }));
186
+ case "acculynx_jobs_add_expense":
187
+ return ok(await jobAddExpense(client, str(args.jobId) ?? "", args.body));
188
+ case "acculynx_jobs_upload_document":
189
+ return ok(await jobUploadDocument(client, str(args.jobId) ?? "", str(args.filePath) ?? "", {
190
+ folderId: str(args.folderId) ?? "",
191
+ description: str(args.description),
192
+ externalId: str(args.externalId),
193
+ externalSource: str(args.externalSource)
194
+ }));
195
+ case "acculynx_contacts_list":
196
+ return ok(await contactsList(client, resolveLimit(args)));
197
+ case "acculynx_contacts_get":
198
+ return ok(await contactGet(client, str(args.contactId) ?? ""));
199
+ case "acculynx_contacts_create":
200
+ return ok(await contactCreate(client, args.body));
201
+ case "acculynx_contacts_search":
202
+ return ok(await contactsSearch(client, {
203
+ query: str(args.query) ?? "",
204
+ startDate: str(args.startDate) ?? "",
205
+ endDate: str(args.endDate) ?? "",
206
+ sortBy: str(args.sortBy),
207
+ sortOrder: str(args.sortOrder)
208
+ }));
209
+ case "acculynx_contacts_emails":
210
+ return ok(await contactEmails(client, str(args.contactId) ?? ""));
211
+ case "acculynx_contacts_phones":
212
+ return ok(await contactPhones(client, str(args.contactId) ?? ""));
213
+ case "acculynx_contacts_phone_add":
214
+ return ok(await contactPhoneAdd(client, str(args.contactId) ?? "", {
215
+ type: str(args.type) ?? "",
216
+ number: str(args.number) ?? "",
217
+ smsOptOut: boolOf(args.smsOptOut)
218
+ }));
219
+ case "acculynx_estimates_list":
220
+ return ok(await estimatesList(client, resolveLimit(args)));
221
+ case "acculynx_estimates_get":
222
+ return ok(await estimateGet(client, str(args.estimateId) ?? ""));
223
+ case "acculynx_estimates_sections":
224
+ return ok(await estimateSections(client, str(args.estimateId) ?? ""));
225
+ case "acculynx_estimates_section":
226
+ return ok(await estimateSection(client, str(args.estimateId) ?? "", str(args.sectionId) ?? ""));
227
+ case "acculynx_estimates_items":
228
+ return ok(await estimateItems(client, str(args.estimateId) ?? "", str(args.sectionId) ?? ""));
229
+ case "acculynx_users_list":
230
+ return ok(await usersList(client, { limit: resolveLimit(args), search: str(args.search) }));
231
+ default:
232
+ return fail({ error: "unknown_tool", message: `Unknown tool: ${call.name}` });
233
+ }
234
+ } catch (e) {
235
+ return fail({ error: "acculynx_error", message: e instanceof Error ? e.message : String(e) });
236
+ }
237
+ }
238
+
239
+ // src/mcp.ts
240
+ var require2 = createRequire(import.meta.url);
241
+ var { version } = require2("../package.json");
242
+ function log(msg) {
243
+ process.stderr.write(`[acculynx-mcp] ${msg}
244
+ `);
245
+ }
246
+ async function bootSelfCheck(getCfg = () => getConfig({}), logFn = log, exit = process.exit) {
247
+ try {
248
+ getCfg();
249
+ } catch (e) {
250
+ logFn(`fatal: ${e instanceof Error ? e.message : String(e)}`);
251
+ exit(1);
252
+ }
253
+ }
254
+ async function main() {
255
+ await bootSelfCheck();
256
+ let extendedTools = [];
257
+ let handleExtended = null;
258
+ try {
259
+ const resolved = require2.resolve("@opsrev/acculynx-cli-unofficial");
260
+ const mod = await import(resolved);
261
+ if (typeof mod.extendedTools === "function" && typeof mod.handleExtendedToolCall === "function") {
262
+ extendedTools = mod.extendedTools();
263
+ handleExtended = mod.handleExtendedToolCall;
264
+ }
265
+ } catch {
266
+ }
267
+ const publicNames = new Set(buildTools().map((t) => t.name));
268
+ const server = new Server({ name: "acculynx", version }, { capabilities: { tools: {} } });
269
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...buildTools(), ...extendedTools] }));
270
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
271
+ const call = { name: req.params.name, args: req.params.arguments };
272
+ if (publicNames.has(call.name) || !handleExtended) {
273
+ return await handleToolCall(call);
274
+ }
275
+ return await handleExtended(call);
276
+ });
277
+ await server.connect(new StdioServerTransport());
278
+ log("connected (stdio)");
279
+ }
280
+ if (process.env.VITEST === void 0) {
281
+ main().catch((e) => {
282
+ log(`fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}`);
283
+ process.exit(1);
284
+ });
285
+ }
286
+ export {
287
+ bootSelfCheck
288
+ };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@opsrev/acculynx-cli",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "CLI for the AccuLynx roofing CRM API",
5
5
  "type": "module",
6
6
  "bin": {
7
- "acculynx": "./dist/index.js"
7
+ "acculynx": "./dist/index.js",
8
+ "acculynx-mcp": "./dist/mcp.js"
8
9
  },
9
10
  "scripts": {
10
- "build": "tsup src/index.ts --format esm --dts --clean",
11
+ "build": "tsup src/index.ts src/mcp.ts --format esm --dts --clean",
11
12
  "dev": "tsx --env-file=.env src/index.ts",
12
13
  "test": "vitest run",
13
14
  "test:watch": "vitest",
@@ -28,6 +29,7 @@
28
29
  "dist"
29
30
  ],
30
31
  "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
31
33
  "commander": "^14.0.3"
32
34
  },
33
35
  "devDependencies": {