wanas-zcrm-extractor 1.7.0 → 1.7.2
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 +3 -2
- package/bin/cli.js +34 -3
- package/package.json +1 -1
- package/src/services/auditLogService.js +21 -14
package/README.md
CHANGED
|
@@ -322,11 +322,12 @@ zcrm skill --ide claude --metadata-dir ./crm-meta
|
|
|
322
322
|
<details>
|
|
323
323
|
<summary><h3>7. <code>zcrm audit</code></h3></summary>
|
|
324
324
|
|
|
325
|
-
Triggers an asynchronous export of the Zoho CRM audit log, polls until it completes (with a bounded timeout), and downloads the resulting CSV/ZIP
|
|
325
|
+
Triggers an asynchronous export of the Zoho CRM audit log, polls until it completes (with a bounded timeout), and downloads the resulting CSV/ZIP into the result folder's CRM root — `metadata/crm/audit_logs/` by default (override with `-o, --output`). A subsequent `zcrm pull` never deletes this folder.
|
|
326
326
|
|
|
327
327
|
```bash
|
|
328
|
-
zcrm audit # last 3 years by default
|
|
328
|
+
zcrm audit # last 3 years by default → ./metadata/crm/audit_logs/
|
|
329
329
|
zcrm audit --filter input.json # custom export criteria
|
|
330
|
+
zcrm audit -o ./my-export # save into ./my-export/crm/audit_logs/
|
|
330
331
|
```
|
|
331
332
|
|
|
332
333
|
> **Requires the audit-log OAuth scopes** — `ZohoCRM.settings.audit_logs.CREATE` and `ZohoCRM.settings.audit_logs.READ` (both in the default scopes). If you authenticated *before* they were added you'll see `401 invalid oauth scope` — just re-authenticate: `zcrm logout` then `zcrm login`. *(If the download step later reports a file-scope error, add `ZohoFiles.files.READ` to `ZOHO_SCOPES` and log in again.)*
|
package/bin/cli.js
CHANGED
|
@@ -319,8 +319,13 @@ program
|
|
|
319
319
|
dc = dc || answers.dc;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
// Record the resolved, non-sensitive parameters of this run. Secrets
|
|
323
|
+
// (client secret, refresh token) are deliberately never written to disk.
|
|
324
|
+
logInfo(`Login config — DC: ${dc || 'com'}, redirect URI: ${redirectUri}, client ID set: ${!!clientId}.`, 'cli');
|
|
325
|
+
|
|
322
326
|
// 1. Headless flow using direct refresh token
|
|
323
327
|
if (refreshToken) {
|
|
328
|
+
logInfo('Using headless refresh-token flow (no browser callback).', 'cli');
|
|
324
329
|
console.log('\n\x1b[34mℹ\x1b[0m Bypassing browser callback. Verifying refresh token credentials...');
|
|
325
330
|
|
|
326
331
|
configStore.save({
|
|
@@ -336,6 +341,7 @@ program
|
|
|
336
341
|
// Initialize and refresh to verify key validity
|
|
337
342
|
authService.loadTokens();
|
|
338
343
|
await authService.refreshAccessToken();
|
|
344
|
+
logInfo('Refresh token verified; access token obtained successfully.', 'cli');
|
|
339
345
|
|
|
340
346
|
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
341
347
|
console.log('🎉 \x1b[1;32mCredentials and session authenticated successfully via Refresh Token!\x1b[0m');
|
|
@@ -344,7 +350,9 @@ program
|
|
|
344
350
|
}
|
|
345
351
|
|
|
346
352
|
// 2. Interactive Browser OAuth2 flow
|
|
353
|
+
logInfo('Starting interactive browser OAuth2 flow.', 'cli');
|
|
347
354
|
await runOAuthServer(clientId, clientSecret, redirectUri, dc);
|
|
355
|
+
logInfo('OAuth2 flow completed; tokens exchanged and saved.', 'cli');
|
|
348
356
|
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
349
357
|
console.log('🎉 \x1b[1;32mSuccessfully logged in! Access configurations saved.\x1b[0m');
|
|
350
358
|
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
@@ -359,7 +367,9 @@ program
|
|
|
359
367
|
.command('logout')
|
|
360
368
|
.description('Logout of Zoho CRM and clear stored configurations')
|
|
361
369
|
.action(() => {
|
|
370
|
+
logInfo('Clearing stored credentials and tokens.', 'cli');
|
|
362
371
|
configStore.clear();
|
|
372
|
+
logInfo('Stored configuration cleared.', 'cli');
|
|
363
373
|
console.log('🎉 Stored credentials cleared successfully.');
|
|
364
374
|
process.exit(0);
|
|
365
375
|
});
|
|
@@ -370,6 +380,7 @@ program
|
|
|
370
380
|
.description('Check currently authenticated Zoho CRM user and organization details')
|
|
371
381
|
.action(async () => {
|
|
372
382
|
console.log(getBanner());
|
|
383
|
+
logInfo('Loading stored tokens.', 'cli');
|
|
373
384
|
authService.loadTokens();
|
|
374
385
|
|
|
375
386
|
if (!authService.isAuthenticated()) {
|
|
@@ -378,14 +389,16 @@ program
|
|
|
378
389
|
|
|
379
390
|
try {
|
|
380
391
|
const apiClient = require('../src/utils/apiClient');
|
|
381
|
-
|
|
392
|
+
|
|
382
393
|
console.log(`\x1b[36mFetching active user and organization details...\x1b[0m\n`);
|
|
383
394
|
|
|
384
395
|
// Fetch user profile and org details concurrently
|
|
396
|
+
logInfo('Fetching current user (/crm/v8/users) and org (/crm/v8/org).', 'cli');
|
|
385
397
|
const [userRes, orgRes] = await Promise.all([
|
|
386
398
|
apiClient.get('/crm/v8/users', { type: 'CurrentUser' }),
|
|
387
399
|
apiClient.get('/crm/v8/org')
|
|
388
400
|
]);
|
|
401
|
+
logInfo('User and organization details fetched successfully.', 'cli');
|
|
389
402
|
|
|
390
403
|
const user = userRes?.users?.[0] || {};
|
|
391
404
|
const org = orgRes?.org?.[0] || {};
|
|
@@ -434,13 +447,16 @@ program
|
|
|
434
447
|
console.log(` Callback URI: \x1b[34m${storedConfig.redirect_uri || 'Not configured'}\x1b[0m`);
|
|
435
448
|
console.log(`--------------------------------------------------------------------`);
|
|
436
449
|
|
|
450
|
+
logInfo(`Auth state — has tokens: ${!!hasTokens}, DC: ${storedConfig.dc || 'none'}.`, 'cli');
|
|
451
|
+
|
|
437
452
|
// 2. Check Local Metadata Directory Status
|
|
438
453
|
const targetDir = path.resolve(options.dir);
|
|
439
454
|
const indexPath = path.join(targetDir, '.zcrm', '.store', 'index.json');
|
|
440
455
|
const functionsDir = path.join(targetDir, 'crm', 'functions');
|
|
441
456
|
|
|
442
457
|
console.log(`\x1b[1m📂 Local Metadata Database (${targetDir}):\x1b[0m`);
|
|
443
|
-
|
|
458
|
+
|
|
459
|
+
logInfo(`Inspecting local metadata directory: ${targetDir}`, 'cli');
|
|
444
460
|
if (fs.existsSync(indexPath)) {
|
|
445
461
|
try {
|
|
446
462
|
const index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
@@ -458,6 +474,7 @@ program
|
|
|
458
474
|
}
|
|
459
475
|
}
|
|
460
476
|
|
|
477
|
+
logInfo(`Metadata database found — ${index.modules_count || 0} modules, ${fnCount} deluge scripts.`, 'cli');
|
|
461
478
|
console.log(` Status: \x1b[1;32mDatabase Exists\x1b[0m`);
|
|
462
479
|
console.log(` Organization: \x1b[36m${index.crm_org || 'Unknown'}\x1b[0m`);
|
|
463
480
|
console.log(` Last Synced: \x1b[32m${index.generated_at ? new Date(index.generated_at).toLocaleString() : 'N/A'}\x1b[0m`);
|
|
@@ -467,6 +484,7 @@ program
|
|
|
467
484
|
console.log(` Status: \x1b[1;33mInvalid / Corrupted Database\x1b[0m (Error: ${err.message})`);
|
|
468
485
|
}
|
|
469
486
|
} else {
|
|
487
|
+
logInfo('No metadata database found at target directory.', 'cli');
|
|
470
488
|
console.log(` Status: \x1b[1;30mEmpty (No database found at target directory)\x1b[0m`);
|
|
471
489
|
console.log(` Tip: Run \x1b[36mzcrm pull -o "${options.dir}"\x1b[0m to fetch metadata.`);
|
|
472
490
|
}
|
|
@@ -616,6 +634,7 @@ program
|
|
|
616
634
|
.command('audit')
|
|
617
635
|
.description('Export the Zoho CRM audit log (asynchronous job)')
|
|
618
636
|
.option('-f, --filter <path>', 'Path to a JSON file containing the audit_log_export criteria')
|
|
637
|
+
.option('-o, --output <dir>', 'Result folder to save the audit log into (its crm/audit_logs/ subfolder)', './metadata')
|
|
619
638
|
.action(async (options) => {
|
|
620
639
|
console.log(getBanner());
|
|
621
640
|
// This command DOES read/export audit-log data, so it gets its own honest
|
|
@@ -632,10 +651,17 @@ program
|
|
|
632
651
|
}
|
|
633
652
|
|
|
634
653
|
try {
|
|
654
|
+
logInfo(`Audit output directory: ${path.resolve(options.output)}`, 'cli');
|
|
655
|
+
if (options.filter) logInfo(`Audit filter file: ${options.filter}`, 'cli');
|
|
656
|
+
logInfo('Verifying access token before export.', 'cli');
|
|
635
657
|
await authService.getAccessToken(); // Refresh if necessary
|
|
636
|
-
await auditLogService.exportAuditLog(options.filter);
|
|
658
|
+
const savedPath = await auditLogService.exportAuditLog(options.filter, options.output);
|
|
637
659
|
console.log('\n\x1b[1;32m====================================================================\x1b[0m');
|
|
638
660
|
console.log('🎉 \x1b[1;32mAudit Log Export Completed Successfully!\x1b[0m');
|
|
661
|
+
console.log('\x1b[1;32m====================================================================\x1b[0m');
|
|
662
|
+
if (savedPath) {
|
|
663
|
+
console.log(` \x1b[1mSaved To:\x1b[0m \x1b[34m${savedPath}\x1b[0m`);
|
|
664
|
+
}
|
|
639
665
|
console.log('\x1b[1;32m====================================================================\x1b[0m\n');
|
|
640
666
|
process.exit(0);
|
|
641
667
|
} catch (error) {
|
|
@@ -660,8 +686,10 @@ program
|
|
|
660
686
|
try {
|
|
661
687
|
const fs = require('fs');
|
|
662
688
|
const mdPath = path.join(__dirname, '../llm.md');
|
|
689
|
+
logInfo(`Reading LLM manual from ${mdPath}.`, 'cli');
|
|
663
690
|
if (fs.existsSync(mdPath)) {
|
|
664
691
|
const content = fs.readFileSync(mdPath, 'utf8');
|
|
692
|
+
logInfo(`LLM manual loaded (${content.length} chars); printing to console.`, 'cli');
|
|
665
693
|
console.log('\n\x1b[36m📘 Wanas Extractor - LLM System Manual:\x1b[0m\n');
|
|
666
694
|
console.log(content);
|
|
667
695
|
} else {
|
|
@@ -706,6 +734,8 @@ program
|
|
|
706
734
|
const projectDir = path.resolve(options.dir);
|
|
707
735
|
const packageDir = path.join(__dirname, '..');
|
|
708
736
|
|
|
737
|
+
logInfo(`Generating "${ide}" skill into ${projectDir} (metadata root: ${path.resolve(options.metadataDir)}).`, 'cli');
|
|
738
|
+
|
|
709
739
|
let result = skillGenerator.generateSkill({
|
|
710
740
|
ide,
|
|
711
741
|
projectDir,
|
|
@@ -713,6 +743,7 @@ program
|
|
|
713
743
|
packageDir,
|
|
714
744
|
force: options.force
|
|
715
745
|
});
|
|
746
|
+
logInfo(`Skill generation result: ${result.status} → ${result.filePath}`, 'cli');
|
|
716
747
|
|
|
717
748
|
// Standalone files that already exist require explicit confirmation.
|
|
718
749
|
if (result.status === 'exists') {
|
package/package.json
CHANGED
|
@@ -9,15 +9,16 @@ async function delay(ms) {
|
|
|
9
9
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async function exportAuditLog(filterFile) {
|
|
12
|
+
async function exportAuditLog(filterFile, outputDir) {
|
|
13
13
|
try {
|
|
14
|
-
// For a
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
14
|
+
// For a FULL export Zoho's docs say to "skip the input request" entirely —
|
|
15
|
+
// i.e. send an EMPTY body. We send `null` (empty body); the apiClient adds
|
|
16
|
+
// the required `Content-Type: application/json` header on POSTs. This avoids
|
|
17
|
+
// BOTH the 415 (missing content type) and the 400 MANDATORY_NOT_FOUND that a
|
|
18
|
+
// present-but-incomplete object (`{}` or `[{}]`) triggers at $.audit_log_export[0].
|
|
19
|
+
// A filter file (which must contain a valid `audit_log_export[0].criteria`)
|
|
20
|
+
// overrides it when supplied.
|
|
21
|
+
let requestBody = null;
|
|
21
22
|
|
|
22
23
|
if (filterFile) {
|
|
23
24
|
const filterPath = path.resolve(process.cwd(), filterFile);
|
|
@@ -49,7 +50,9 @@ async function exportAuditLog(filterFile) {
|
|
|
49
50
|
|
|
50
51
|
let downloadUrl = null;
|
|
51
52
|
let attempts = 0;
|
|
52
|
-
|
|
53
|
+
// Poll cadence is overridable (mainly so the test suite doesn't wait real
|
|
54
|
+
// seconds); defaults to the documented 15s between status checks.
|
|
55
|
+
const POLL_INTERVAL_MS = parseInt(process.env.ZCRM_AUDIT_POLL_MS, 10) || 15000;
|
|
53
56
|
const MAX_ATTEMPTS = 120; // 120 × 15s = 30 minutes, then give up rather than hang forever
|
|
54
57
|
|
|
55
58
|
// Poll until completion (bounded so a stuck job can never hang the CLI).
|
|
@@ -87,9 +90,11 @@ async function exportAuditLog(filterFile) {
|
|
|
87
90
|
throw new Error(`Audit log export did not finish within ${Math.round(MAX_ATTEMPTS * POLL_INTERVAL_MS / 60000)} minutes (job ${jobId}). Try a narrower filter or re-run later.`);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
// Download the file
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
// Download the file into the result folder's CRM root, alongside the
|
|
94
|
+
// metadata `zcrm pull` produces (e.g. ./metadata/crm/audit_logs/). The pull
|
|
95
|
+
// sync sweep never touches crm/audit_logs/, so re-pulls won't delete it.
|
|
96
|
+
const targetDir = path.join(path.resolve(outputDir || './metadata'), 'crm', 'audit_logs');
|
|
97
|
+
await fs.ensureDir(targetDir);
|
|
93
98
|
|
|
94
99
|
// Determine the extension from the URL *path* (signed URLs carry a query
|
|
95
100
|
// string, so endsWith('.zip') on the whole URL would always miss). Default
|
|
@@ -102,7 +107,7 @@ async function exportAuditLog(filterFile) {
|
|
|
102
107
|
} catch (urlErr) {
|
|
103
108
|
// Keep the .zip default if the URL can't be parsed.
|
|
104
109
|
}
|
|
105
|
-
const outputPath = path.join(
|
|
110
|
+
const outputPath = path.join(targetDir, `AuditLog_${jobId}${ext}`);
|
|
106
111
|
|
|
107
112
|
logInfo(`Downloading result to ${outputPath}...`);
|
|
108
113
|
|
|
@@ -133,7 +138,9 @@ async function exportAuditLog(filterFile) {
|
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
logInfo(`Audit log successfully downloaded and saved to: ${outputPath}`);
|
|
136
|
-
|
|
141
|
+
|
|
142
|
+
return outputPath;
|
|
143
|
+
|
|
137
144
|
} catch (error) {
|
|
138
145
|
logError(error, 'auditLogService.exportAuditLog');
|
|
139
146
|
throw error;
|