@ainyc/canonry 1.39.4 → 1.40.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/assets/assets/{index-DWD1vfsF.js → index-Du0I3brY.js} +39 -39
- package/assets/index.html +1 -1
- package/dist/chunk-FOWWBLXD.js +1218 -0
- package/dist/{chunk-H6FO4LHC.js → chunk-FXHVGU5S.js} +626 -1593
- package/dist/cli.js +321 -83
- package/dist/index.js +2 -1
- package/dist/intelligence-service-PDZOIB7L.js +6 -0
- package/package.json +7 -7
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node --import tsx
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
ProviderNames,
|
|
4
|
+
RunKinds,
|
|
5
|
+
computeCompetitorOverlap,
|
|
5
6
|
configExists,
|
|
6
|
-
createClient,
|
|
7
7
|
createServer,
|
|
8
8
|
determineAnswerMentioned,
|
|
9
|
+
determineCitationState,
|
|
9
10
|
effectiveDomains,
|
|
11
|
+
extractRecommendedCompetitors,
|
|
10
12
|
formatAuditFactorScore,
|
|
11
13
|
getConfigDir,
|
|
12
14
|
getConfigPath,
|
|
@@ -14,20 +16,29 @@ import {
|
|
|
14
16
|
isFirstRun,
|
|
15
17
|
isTelemetryEnabled,
|
|
16
18
|
loadConfig,
|
|
17
|
-
migrate,
|
|
18
19
|
notificationEventSchema,
|
|
19
|
-
parseJsonColumn,
|
|
20
|
-
projects,
|
|
21
20
|
providerQuotaPolicySchema,
|
|
22
|
-
|
|
21
|
+
reparseStoredResult,
|
|
22
|
+
reparseStoredResult2,
|
|
23
|
+
reparseStoredResult3,
|
|
24
|
+
reparseStoredResult4,
|
|
23
25
|
resolveProviderInput,
|
|
24
|
-
runs,
|
|
25
26
|
saveConfig,
|
|
26
27
|
saveConfigPatch,
|
|
27
28
|
setGoogleAuthConfig,
|
|
28
29
|
showFirstRunNotice,
|
|
29
30
|
trackEvent
|
|
30
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-FXHVGU5S.js";
|
|
32
|
+
import {
|
|
33
|
+
apiKeys,
|
|
34
|
+
competitors,
|
|
35
|
+
createClient,
|
|
36
|
+
migrate,
|
|
37
|
+
parseJsonColumn,
|
|
38
|
+
projects,
|
|
39
|
+
querySnapshots,
|
|
40
|
+
runs
|
|
41
|
+
} from "./chunk-FOWWBLXD.js";
|
|
31
42
|
|
|
32
43
|
// src/cli.ts
|
|
33
44
|
import { pathToFileURL } from "url";
|
|
@@ -107,9 +118,9 @@ import { parseArgs } from "util";
|
|
|
107
118
|
function commandId(spec) {
|
|
108
119
|
return spec.path.join(".");
|
|
109
120
|
}
|
|
110
|
-
function matchesPath(args,
|
|
111
|
-
if (args.length <
|
|
112
|
-
return
|
|
121
|
+
function matchesPath(args, path6) {
|
|
122
|
+
if (args.length < path6.length) return false;
|
|
123
|
+
return path6.every((segment, index) => args[index] === segment);
|
|
113
124
|
}
|
|
114
125
|
function withFormatOption(options) {
|
|
115
126
|
if (!options) {
|
|
@@ -202,7 +213,7 @@ Usage: ${spec.usage}`, {
|
|
|
202
213
|
}
|
|
203
214
|
|
|
204
215
|
// src/commands/backfill.ts
|
|
205
|
-
import { eq, inArray } from "drizzle-orm";
|
|
216
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
206
217
|
var SNAPSHOT_BATCH_SIZE = 500;
|
|
207
218
|
async function backfillAnswerVisibilityCommand(opts) {
|
|
208
219
|
const config = loadConfig();
|
|
@@ -213,8 +224,13 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
213
224
|
let examined = 0;
|
|
214
225
|
let updated = 0;
|
|
215
226
|
let visible = 0;
|
|
227
|
+
let reparsed = 0;
|
|
228
|
+
let providerErrors = 0;
|
|
216
229
|
if (scopedProjects.length > 0) {
|
|
217
|
-
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
230
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and(
|
|
231
|
+
eq(runs.kind, RunKinds["answer-visibility"]),
|
|
232
|
+
inArray(runs.projectId, scopedProjects.map((project) => project.id))
|
|
233
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq(runs.kind, RunKinds["answer-visibility"])).all();
|
|
218
234
|
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
219
235
|
for (const run of runRows) {
|
|
220
236
|
const existing = runIdsByProject.get(run.projectId);
|
|
@@ -222,31 +238,95 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
222
238
|
else runIdsByProject.set(run.projectId, [run.id]);
|
|
223
239
|
}
|
|
224
240
|
for (const project of scopedProjects) {
|
|
241
|
+
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
225
242
|
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
226
243
|
if (runIds.length === 0) continue;
|
|
244
|
+
const projectDomains = effectiveDomains({
|
|
245
|
+
canonicalDomain: project.canonicalDomain,
|
|
246
|
+
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
247
|
+
});
|
|
227
248
|
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
228
249
|
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
229
250
|
const snapshotRows = db.select({
|
|
230
251
|
id: querySnapshots.id,
|
|
252
|
+
provider: querySnapshots.provider,
|
|
253
|
+
citationState: querySnapshots.citationState,
|
|
231
254
|
answerMentioned: querySnapshots.answerMentioned,
|
|
232
|
-
answerText: querySnapshots.answerText
|
|
255
|
+
answerText: querySnapshots.answerText,
|
|
256
|
+
citedDomains: querySnapshots.citedDomains,
|
|
257
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
258
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
259
|
+
rawResponse: querySnapshots.rawResponse
|
|
233
260
|
}).from(querySnapshots).where(inArray(querySnapshots.runId, batchRunIds)).all();
|
|
261
|
+
const pendingUpdates = [];
|
|
234
262
|
for (const snapshot of snapshotRows) {
|
|
235
263
|
examined++;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
ownedDomains: parseJsonColumn(project.ownedDomains, [])
|
|
242
|
-
})
|
|
243
|
-
);
|
|
264
|
+
const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
|
|
265
|
+
if (reparsedResult) reparsed++;
|
|
266
|
+
if (reparsedResult?.providerError) providerErrors++;
|
|
267
|
+
const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
|
|
268
|
+
const nextValue = determineAnswerMentioned(answerText, project.displayName, projectDomains);
|
|
244
269
|
if (nextValue) visible++;
|
|
270
|
+
const nextPatch = {};
|
|
245
271
|
if (snapshot.answerMentioned !== nextValue) {
|
|
246
|
-
|
|
247
|
-
|
|
272
|
+
nextPatch.answerMentioned = nextValue;
|
|
273
|
+
}
|
|
274
|
+
if ((snapshot.answerText ?? "") !== answerText) {
|
|
275
|
+
nextPatch.answerText = answerText;
|
|
276
|
+
}
|
|
277
|
+
if (reparsedResult) {
|
|
278
|
+
const normalized = {
|
|
279
|
+
provider: snapshot.provider,
|
|
280
|
+
answerText,
|
|
281
|
+
citedDomains: reparsedResult.citedDomains,
|
|
282
|
+
groundingSources: reparsedResult.groundingSources,
|
|
283
|
+
searchQueries: reparsedResult.searchQueries
|
|
284
|
+
};
|
|
285
|
+
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
286
|
+
const nextCitedDomains = JSON.stringify(reparsedResult.citedDomains);
|
|
287
|
+
const nextCompetitorOverlap = JSON.stringify(
|
|
288
|
+
computeCompetitorOverlap(normalized, competitorDomains)
|
|
289
|
+
);
|
|
290
|
+
const nextRecommendedCompetitors = JSON.stringify(
|
|
291
|
+
extractRecommendedCompetitors(
|
|
292
|
+
normalized.answerText,
|
|
293
|
+
projectDomains,
|
|
294
|
+
normalized.citedDomains,
|
|
295
|
+
competitorDomains
|
|
296
|
+
)
|
|
297
|
+
);
|
|
298
|
+
const nextRawResponse = stringifyStoredSnapshotEnvelope(
|
|
299
|
+
snapshot.rawResponse,
|
|
300
|
+
reparsedResult
|
|
301
|
+
);
|
|
302
|
+
if (snapshot.citationState !== nextCitationState) {
|
|
303
|
+
nextPatch.citationState = nextCitationState;
|
|
304
|
+
}
|
|
305
|
+
if (snapshot.citedDomains !== nextCitedDomains) {
|
|
306
|
+
nextPatch.citedDomains = nextCitedDomains;
|
|
307
|
+
}
|
|
308
|
+
if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
|
|
309
|
+
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
310
|
+
}
|
|
311
|
+
if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
|
|
312
|
+
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
313
|
+
}
|
|
314
|
+
if (snapshot.rawResponse !== nextRawResponse) {
|
|
315
|
+
nextPatch.rawResponse = nextRawResponse;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (Object.keys(nextPatch).length > 0) {
|
|
319
|
+
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
248
320
|
}
|
|
249
321
|
}
|
|
322
|
+
if (pendingUpdates.length > 0) {
|
|
323
|
+
db.transaction((tx) => {
|
|
324
|
+
for (const update of pendingUpdates) {
|
|
325
|
+
tx.update(querySnapshots).set(update.patch).where(eq(querySnapshots.id, update.id)).run();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
updated += pendingUpdates.length;
|
|
329
|
+
}
|
|
250
330
|
}
|
|
251
331
|
}
|
|
252
332
|
}
|
|
@@ -255,7 +335,9 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
255
335
|
projects: scopedProjects.length,
|
|
256
336
|
examined,
|
|
257
337
|
updated,
|
|
258
|
-
visible
|
|
338
|
+
visible,
|
|
339
|
+
reparsed,
|
|
340
|
+
providerErrors
|
|
259
341
|
};
|
|
260
342
|
if (opts?.format === "json") {
|
|
261
343
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -269,8 +351,11 @@ async function backfillAnswerVisibilityCommand(opts) {
|
|
|
269
351
|
console.log(` Examined: ${examined}`);
|
|
270
352
|
console.log(` Updated: ${updated}`);
|
|
271
353
|
console.log(` Visible: ${visible}`);
|
|
354
|
+
console.log(` Reparsed: ${reparsed}`);
|
|
355
|
+
console.log(` Errors: ${providerErrors}`);
|
|
272
356
|
}
|
|
273
357
|
async function backfillInsightsCommand(project, opts) {
|
|
358
|
+
const { IntelligenceService } = await import("./intelligence-service-PDZOIB7L.js");
|
|
274
359
|
const config = loadConfig();
|
|
275
360
|
const db = createClient(config.database);
|
|
276
361
|
migrate(db);
|
|
@@ -305,6 +390,53 @@ Backfill complete.`);
|
|
|
305
390
|
console.log(` Skipped: ${result.skipped}`);
|
|
306
391
|
console.log(` Insights: ${result.totalInsights}`);
|
|
307
392
|
}
|
|
393
|
+
function reparseProviderSnapshot(provider, rawResponse) {
|
|
394
|
+
const envelope = parseJsonColumn(rawResponse, {});
|
|
395
|
+
const apiResponse = resolveStoredApiResponse(envelope);
|
|
396
|
+
if (!apiResponse) return null;
|
|
397
|
+
switch (provider) {
|
|
398
|
+
case ProviderNames.openai:
|
|
399
|
+
return reparseStoredResult(apiResponse);
|
|
400
|
+
case ProviderNames.claude:
|
|
401
|
+
return reparseStoredResult2(apiResponse);
|
|
402
|
+
case ProviderNames.gemini:
|
|
403
|
+
return reparseStoredResult3(apiResponse);
|
|
404
|
+
case ProviderNames.perplexity:
|
|
405
|
+
return reparseStoredResult4(apiResponse);
|
|
406
|
+
default:
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function resolveStoredApiResponse(parsed) {
|
|
411
|
+
const nested = parsed.apiResponse;
|
|
412
|
+
if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
413
|
+
return nested;
|
|
414
|
+
}
|
|
415
|
+
if (looksLikeProviderApiResponse(parsed)) {
|
|
416
|
+
return parsed;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
function looksLikeProviderApiResponse(value) {
|
|
421
|
+
return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
|
|
422
|
+
}
|
|
423
|
+
function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
|
|
424
|
+
const parsed = parseJsonColumn(rawResponse, {});
|
|
425
|
+
const apiResponse = resolveStoredApiResponse(parsed);
|
|
426
|
+
const envelope = apiResponse === parsed ? {} : { ...parsed };
|
|
427
|
+
delete envelope.answerText;
|
|
428
|
+
delete envelope.citedDomains;
|
|
429
|
+
delete envelope.competitorOverlap;
|
|
430
|
+
delete envelope.recommendedCompetitors;
|
|
431
|
+
delete envelope.providerError;
|
|
432
|
+
return JSON.stringify({
|
|
433
|
+
...envelope,
|
|
434
|
+
groundingSources: reparsed.groundingSources,
|
|
435
|
+
searchQueries: reparsed.searchQueries,
|
|
436
|
+
...reparsed.providerError ? { providerError: reparsed.providerError } : {},
|
|
437
|
+
...apiResponse ? { apiResponse } : {}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
308
440
|
|
|
309
441
|
// src/cli-command-helpers.ts
|
|
310
442
|
function getString(values, key) {
|
|
@@ -481,9 +613,9 @@ var ApiClient = class {
|
|
|
481
613
|
}
|
|
482
614
|
return this.probePromise;
|
|
483
615
|
}
|
|
484
|
-
async request(method,
|
|
616
|
+
async request(method, path6, body) {
|
|
485
617
|
await this.probeBasePath();
|
|
486
|
-
const url = `${this.baseUrl}${
|
|
618
|
+
const url = `${this.baseUrl}${path6}`;
|
|
487
619
|
const serializedBody = body != null ? JSON.stringify(body) : void 0;
|
|
488
620
|
const headers = {
|
|
489
621
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
@@ -550,8 +682,8 @@ var ApiClient = class {
|
|
|
550
682
|
async appendKeywords(project, keywords) {
|
|
551
683
|
await this.request("POST", `/projects/${encodeURIComponent(project)}/keywords`, { keywords });
|
|
552
684
|
}
|
|
553
|
-
async putCompetitors(project,
|
|
554
|
-
await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors });
|
|
685
|
+
async putCompetitors(project, competitors2) {
|
|
686
|
+
await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors: competitors2 });
|
|
555
687
|
}
|
|
556
688
|
async listCompetitors(project) {
|
|
557
689
|
return this.request("GET", `/projects/${encodeURIComponent(project)}/competitors`);
|
|
@@ -1509,9 +1641,9 @@ async function gaConnect(project, opts) {
|
|
|
1509
1641
|
propertyId: opts.propertyId
|
|
1510
1642
|
};
|
|
1511
1643
|
if (opts.keyFile) {
|
|
1512
|
-
const
|
|
1644
|
+
const fs8 = await import("fs");
|
|
1513
1645
|
try {
|
|
1514
|
-
const content =
|
|
1646
|
+
const content = fs8.readFileSync(opts.keyFile, "utf-8");
|
|
1515
1647
|
JSON.parse(content);
|
|
1516
1648
|
body.keyJson = content;
|
|
1517
1649
|
} catch (e) {
|
|
@@ -3259,10 +3391,10 @@ Brand Gap Analysis`);
|
|
|
3259
3391
|
console.log(`
|
|
3260
3392
|
Opportunity Gaps (competitors cited, you're not):`);
|
|
3261
3393
|
for (const kw of data.gap) {
|
|
3262
|
-
const
|
|
3394
|
+
const competitors2 = kw.competitorsCiting.join(", ");
|
|
3263
3395
|
const cons = kw.consistency.totalRuns > 0 ? ` [cited ${kw.consistency.citedRuns}/${kw.consistency.totalRuns} runs]` : "";
|
|
3264
3396
|
console.log(` \u2022 ${kw.keyword}${cons}`);
|
|
3265
|
-
console.log(` Competitors: ${
|
|
3397
|
+
console.log(` Competitors: ${competitors2}`);
|
|
3266
3398
|
}
|
|
3267
3399
|
}
|
|
3268
3400
|
if (data.cited.length > 0) {
|
|
@@ -4525,6 +4657,10 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
|
|
|
4525
4657
|
}
|
|
4526
4658
|
];
|
|
4527
4659
|
|
|
4660
|
+
// src/commands/snapshot.ts
|
|
4661
|
+
import fs4 from "fs";
|
|
4662
|
+
import path2 from "path";
|
|
4663
|
+
|
|
4528
4664
|
// src/snapshot-pdf.ts
|
|
4529
4665
|
import fs3 from "fs";
|
|
4530
4666
|
import path from "path";
|
|
@@ -4806,8 +4942,8 @@ function renderQueries(pdf, report) {
|
|
|
4806
4942
|
for (const result of query.providerResults) {
|
|
4807
4943
|
const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
|
|
4808
4944
|
const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
|
|
4809
|
-
const
|
|
4810
|
-
const line = `${result.displayName}: ${status}${accuracy}${
|
|
4945
|
+
const competitors2 = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
|
|
4946
|
+
const line = `${result.displayName}: ${status}${accuracy}${competitors2}`;
|
|
4811
4947
|
pdf.bullet(line);
|
|
4812
4948
|
if (result.error) {
|
|
4813
4949
|
pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
|
|
@@ -4856,6 +4992,13 @@ function wrapText(font, text, size, maxWidth) {
|
|
|
4856
4992
|
function getClient16() {
|
|
4857
4993
|
return createApiClient();
|
|
4858
4994
|
}
|
|
4995
|
+
function slugify(value) {
|
|
4996
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
4997
|
+
}
|
|
4998
|
+
function autoOutputPath(companyName, ext) {
|
|
4999
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5000
|
+
return `${slugify(companyName)}-snapshot-${date}.${ext}`;
|
|
5001
|
+
}
|
|
4859
5002
|
async function createSnapshotReport(companyName, opts) {
|
|
4860
5003
|
const client = getClient16();
|
|
4861
5004
|
const report = await client.createSnapshot({
|
|
@@ -4864,23 +5007,110 @@ async function createSnapshotReport(companyName, opts) {
|
|
|
4864
5007
|
...opts.phrases && opts.phrases.length > 0 ? { phrases: opts.phrases } : {},
|
|
4865
5008
|
...opts.competitors && opts.competitors.length > 0 ? { competitors: opts.competitors } : {}
|
|
4866
5009
|
});
|
|
5010
|
+
let savedMdPath;
|
|
5011
|
+
if (opts.md) {
|
|
5012
|
+
const mdPath = opts.outputPath ?? autoOutputPath(companyName, "md");
|
|
5013
|
+
savedMdPath = writeSnapshotMarkdown(report, mdPath);
|
|
5014
|
+
}
|
|
4867
5015
|
let savedPdfPath;
|
|
4868
5016
|
if (opts.pdf) {
|
|
4869
|
-
|
|
5017
|
+
const pdfPath = opts.outputPath && !opts.md ? opts.outputPath : autoOutputPath(companyName, "pdf");
|
|
5018
|
+
savedPdfPath = await writeSnapshotPdf(report, pdfPath);
|
|
4870
5019
|
}
|
|
4871
5020
|
if (opts.format === "json") {
|
|
4872
5021
|
console.log(JSON.stringify(report, null, 2));
|
|
4873
|
-
if (
|
|
4874
|
-
|
|
5022
|
+
if (savedMdPath) process.stderr.write(`Saved markdown: ${savedMdPath}
|
|
5023
|
+
`);
|
|
5024
|
+
if (savedPdfPath) process.stderr.write(`Saved PDF: ${savedPdfPath}
|
|
4875
5025
|
`);
|
|
4876
|
-
}
|
|
4877
5026
|
return;
|
|
4878
5027
|
}
|
|
4879
5028
|
console.log(formatSnapshotText(report));
|
|
4880
|
-
if (
|
|
4881
|
-
|
|
5029
|
+
if (savedMdPath) console.log(`
|
|
5030
|
+
Markdown saved: ${savedMdPath}`);
|
|
5031
|
+
if (savedPdfPath) console.log(`
|
|
4882
5032
|
PDF saved: ${savedPdfPath}`);
|
|
5033
|
+
}
|
|
5034
|
+
function writeSnapshotMarkdown(report, outputPath) {
|
|
5035
|
+
const resolvedPath = path2.resolve(outputPath);
|
|
5036
|
+
fs4.mkdirSync(path2.dirname(resolvedPath), { recursive: true });
|
|
5037
|
+
fs4.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
|
|
5038
|
+
return resolvedPath;
|
|
5039
|
+
}
|
|
5040
|
+
function formatSnapshotMarkdown(report) {
|
|
5041
|
+
const lines = [];
|
|
5042
|
+
lines.push(`# AI Perception Snapshot: ${report.companyName}`);
|
|
5043
|
+
lines.push("");
|
|
5044
|
+
lines.push(`**Domain:** ${report.domain}`);
|
|
5045
|
+
lines.push(`**Generated:** ${new Date(report.generatedAt).toLocaleString("en-US", { year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "2-digit" })}`);
|
|
5046
|
+
lines.push(`**AEO Audit Score:** ${report.audit.overallScore}/100 (${report.audit.overallGrade})`);
|
|
5047
|
+
lines.push("");
|
|
5048
|
+
lines.push("## Visibility Gap");
|
|
5049
|
+
lines.push("");
|
|
5050
|
+
lines.push(report.summary.visibilityGap);
|
|
5051
|
+
lines.push("");
|
|
5052
|
+
if (report.summary.whatThisMeans.length > 0) {
|
|
5053
|
+
lines.push("## What This Means");
|
|
5054
|
+
lines.push("");
|
|
5055
|
+
for (const item of report.summary.whatThisMeans) {
|
|
5056
|
+
lines.push(`- ${item}`);
|
|
5057
|
+
}
|
|
5058
|
+
lines.push("");
|
|
5059
|
+
}
|
|
5060
|
+
if (report.summary.recommendedActions.length > 0) {
|
|
5061
|
+
lines.push("## Recommended Actions");
|
|
5062
|
+
lines.push("");
|
|
5063
|
+
for (const action of report.summary.recommendedActions) {
|
|
5064
|
+
lines.push(`- ${action}`);
|
|
5065
|
+
}
|
|
5066
|
+
lines.push("");
|
|
5067
|
+
}
|
|
5068
|
+
if (report.summary.topCompetitors.length > 0) {
|
|
5069
|
+
lines.push("## Competitors AI Recommends Instead");
|
|
5070
|
+
lines.push("");
|
|
5071
|
+
lines.push("| Competitor | Mentions |");
|
|
5072
|
+
lines.push("|------------|----------|");
|
|
5073
|
+
for (const entry of report.summary.topCompetitors) {
|
|
5074
|
+
lines.push(`| ${entry.name} | ${entry.count} |`);
|
|
5075
|
+
}
|
|
5076
|
+
lines.push("");
|
|
5077
|
+
}
|
|
5078
|
+
lines.push("## Provider Comparison");
|
|
5079
|
+
lines.push("");
|
|
5080
|
+
for (const query of report.queryResults) {
|
|
5081
|
+
lines.push(`### "${query.phrase}"`);
|
|
5082
|
+
lines.push("");
|
|
5083
|
+
lines.push("| Provider | Mentioned | Cited | Accuracy | Competitors Recommended |");
|
|
5084
|
+
lines.push("|----------|-----------|-------|----------|------------------------|");
|
|
5085
|
+
for (const result of query.providerResults) {
|
|
5086
|
+
if (result.error) {
|
|
5087
|
+
lines.push(`| ${result.displayName} | ERROR | - | - | ${result.error} |`);
|
|
5088
|
+
continue;
|
|
5089
|
+
}
|
|
5090
|
+
const mentioned = result.mentioned ? "Yes" : "No";
|
|
5091
|
+
const cited = result.cited ? "Yes" : "No";
|
|
5092
|
+
const accuracy = result.describedAccurately === "not-mentioned" ? "-" : result.describedAccurately;
|
|
5093
|
+
const competitors2 = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
|
|
5094
|
+
lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors2} |`);
|
|
5095
|
+
}
|
|
5096
|
+
lines.push("");
|
|
5097
|
+
}
|
|
5098
|
+
if (report.audit.factors.length > 0) {
|
|
5099
|
+
lines.push("## Audit Factors");
|
|
5100
|
+
lines.push("");
|
|
5101
|
+
lines.push(report.audit.summary);
|
|
5102
|
+
lines.push("");
|
|
5103
|
+
lines.push("| Factor | Score | Weight | Status |");
|
|
5104
|
+
lines.push("|--------|-------|--------|--------|");
|
|
5105
|
+
const sorted = [...report.audit.factors].sort((a, b) => a.score - b.score);
|
|
5106
|
+
for (const factor of sorted) {
|
|
5107
|
+
lines.push(`| ${factor.name} | ${factor.score} | ${factor.weight} | ${factor.status} |`);
|
|
5108
|
+
}
|
|
5109
|
+
lines.push("");
|
|
4883
5110
|
}
|
|
5111
|
+
lines.push("---");
|
|
5112
|
+
lines.push(`*Generated by [Canonry](https://github.com/AINYC/canonry)*`);
|
|
5113
|
+
return lines.join("\n");
|
|
4884
5114
|
}
|
|
4885
5115
|
function formatSnapshotText(report) {
|
|
4886
5116
|
const lines = [];
|
|
@@ -4948,15 +5178,17 @@ function parseCsvOption(value) {
|
|
|
4948
5178
|
var SNAPSHOT_CLI_COMMANDS = [
|
|
4949
5179
|
{
|
|
4950
5180
|
path: ["snapshot"],
|
|
4951
|
-
usage: 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--
|
|
5181
|
+
usage: 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--md] [--output <path>] [--pdf] [--format table|json]',
|
|
4952
5182
|
options: {
|
|
4953
5183
|
domain: stringOption(),
|
|
4954
5184
|
phrases: stringOption(),
|
|
4955
5185
|
competitors: stringOption(),
|
|
4956
|
-
|
|
5186
|
+
md: { type: "boolean" },
|
|
5187
|
+
pdf: { type: "boolean" },
|
|
5188
|
+
output: stringOption()
|
|
4957
5189
|
},
|
|
4958
5190
|
run: async (input) => {
|
|
4959
|
-
const usage = 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--
|
|
5191
|
+
const usage = 'canonry snapshot <company-name> --domain <domain> [--phrases "a,b"] [--competitors "x,y"] [--md] [--output <path>] [--pdf] [--format table|json]';
|
|
4960
5192
|
const companyName = requirePositional(input, 0, {
|
|
4961
5193
|
command: "snapshot",
|
|
4962
5194
|
usage,
|
|
@@ -4967,11 +5199,17 @@ var SNAPSHOT_CLI_COMMANDS = [
|
|
|
4967
5199
|
usage,
|
|
4968
5200
|
message: "--domain is required"
|
|
4969
5201
|
});
|
|
5202
|
+
const outputPath = getString(input.values, "output");
|
|
5203
|
+
const explicitMd = getBoolean(input.values, "md");
|
|
5204
|
+
const wantsPdf = getBoolean(input.values, "pdf");
|
|
5205
|
+
const wantsMd = explicitMd || !!outputPath && !wantsPdf;
|
|
4970
5206
|
await createSnapshotReport(companyName, {
|
|
4971
5207
|
domain,
|
|
4972
5208
|
phrases: parseCsvOption(getString(input.values, "phrases")),
|
|
4973
5209
|
competitors: parseCsvOption(getString(input.values, "competitors")),
|
|
4974
|
-
|
|
5210
|
+
md: wantsMd,
|
|
5211
|
+
pdf: wantsPdf,
|
|
5212
|
+
outputPath,
|
|
4975
5213
|
format: input.format
|
|
4976
5214
|
});
|
|
4977
5215
|
}
|
|
@@ -5100,7 +5338,7 @@ var INTELLIGENCE_CLI_COMMANDS = [
|
|
|
5100
5338
|
|
|
5101
5339
|
// src/commands/bootstrap.ts
|
|
5102
5340
|
import crypto from "crypto";
|
|
5103
|
-
import
|
|
5341
|
+
import path3 from "path";
|
|
5104
5342
|
import { eq as eq2 } from "drizzle-orm";
|
|
5105
5343
|
|
|
5106
5344
|
// ../config/src/index.ts
|
|
@@ -5247,7 +5485,7 @@ async function bootstrapCommand(_opts) {
|
|
|
5247
5485
|
);
|
|
5248
5486
|
}
|
|
5249
5487
|
const configDir = getConfigDir();
|
|
5250
|
-
const databasePath = env.databasePath ||
|
|
5488
|
+
const databasePath = env.databasePath || path3.join(configDir, "data.db");
|
|
5251
5489
|
const existing = configExists();
|
|
5252
5490
|
const existingConfig = existing ? loadConfig() : void 0;
|
|
5253
5491
|
let rawApiKey;
|
|
@@ -5317,10 +5555,10 @@ async function bootstrapCommand(_opts) {
|
|
|
5317
5555
|
|
|
5318
5556
|
// src/commands/daemon.ts
|
|
5319
5557
|
import { spawn } from "child_process";
|
|
5320
|
-
import
|
|
5321
|
-
import
|
|
5558
|
+
import fs5 from "fs";
|
|
5559
|
+
import path4 from "path";
|
|
5322
5560
|
function getPidPath() {
|
|
5323
|
-
return
|
|
5561
|
+
return path4.join(getConfigDir(), "canonry.pid");
|
|
5324
5562
|
}
|
|
5325
5563
|
function isProcessAlive(pid) {
|
|
5326
5564
|
try {
|
|
@@ -5347,8 +5585,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
|
|
|
5347
5585
|
async function startDaemon(opts) {
|
|
5348
5586
|
const pidPath = getPidPath();
|
|
5349
5587
|
const format = opts.format ?? "text";
|
|
5350
|
-
if (
|
|
5351
|
-
const existingPid = parseInt(
|
|
5588
|
+
if (fs5.existsSync(pidPath)) {
|
|
5589
|
+
const existingPid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
|
|
5352
5590
|
if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
|
|
5353
5591
|
throw new CliError({
|
|
5354
5592
|
code: "DAEMON_ALREADY_RUNNING",
|
|
@@ -5359,9 +5597,9 @@ async function startDaemon(opts) {
|
|
|
5359
5597
|
}
|
|
5360
5598
|
});
|
|
5361
5599
|
}
|
|
5362
|
-
|
|
5600
|
+
fs5.unlinkSync(pidPath);
|
|
5363
5601
|
}
|
|
5364
|
-
const cliPath =
|
|
5602
|
+
const cliPath = path4.resolve(new URL(import.meta.url).pathname);
|
|
5365
5603
|
const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
|
|
5366
5604
|
const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
|
|
5367
5605
|
if (opts.port) args.push("--port", opts.port);
|
|
@@ -5380,10 +5618,10 @@ async function startDaemon(opts) {
|
|
|
5380
5618
|
});
|
|
5381
5619
|
}
|
|
5382
5620
|
const configDir = getConfigDir();
|
|
5383
|
-
if (!
|
|
5384
|
-
|
|
5621
|
+
if (!fs5.existsSync(configDir)) {
|
|
5622
|
+
fs5.mkdirSync(configDir, { recursive: true });
|
|
5385
5623
|
}
|
|
5386
|
-
|
|
5624
|
+
fs5.writeFileSync(pidPath, String(child.pid), "utf-8");
|
|
5387
5625
|
const port = opts.port ?? "4100";
|
|
5388
5626
|
const host = opts.host ?? "127.0.0.1";
|
|
5389
5627
|
if (format !== "json") {
|
|
@@ -5392,7 +5630,7 @@ async function startDaemon(opts) {
|
|
|
5392
5630
|
const ready = await waitForReady(host, port);
|
|
5393
5631
|
if (!ready) {
|
|
5394
5632
|
try {
|
|
5395
|
-
|
|
5633
|
+
fs5.unlinkSync(pidPath);
|
|
5396
5634
|
} catch {
|
|
5397
5635
|
}
|
|
5398
5636
|
throw new CliError({
|
|
@@ -5424,7 +5662,7 @@ async function startDaemon(opts) {
|
|
|
5424
5662
|
}
|
|
5425
5663
|
function stopDaemon(format = "text") {
|
|
5426
5664
|
const pidPath = getPidPath();
|
|
5427
|
-
if (!
|
|
5665
|
+
if (!fs5.existsSync(pidPath)) {
|
|
5428
5666
|
if (format === "json") {
|
|
5429
5667
|
console.log(JSON.stringify({
|
|
5430
5668
|
stopped: false,
|
|
@@ -5435,7 +5673,7 @@ function stopDaemon(format = "text") {
|
|
|
5435
5673
|
console.log("Canonry is not running (no PID file found)");
|
|
5436
5674
|
return;
|
|
5437
5675
|
}
|
|
5438
|
-
const pid = parseInt(
|
|
5676
|
+
const pid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
|
|
5439
5677
|
if (isNaN(pid)) {
|
|
5440
5678
|
if (format === "json") {
|
|
5441
5679
|
console.log(JSON.stringify({
|
|
@@ -5446,7 +5684,7 @@ function stopDaemon(format = "text") {
|
|
|
5446
5684
|
} else {
|
|
5447
5685
|
console.error("Invalid PID file. Removing it.");
|
|
5448
5686
|
}
|
|
5449
|
-
|
|
5687
|
+
fs5.unlinkSync(pidPath);
|
|
5450
5688
|
return;
|
|
5451
5689
|
}
|
|
5452
5690
|
if (!isProcessAlive(pid)) {
|
|
@@ -5460,12 +5698,12 @@ function stopDaemon(format = "text") {
|
|
|
5460
5698
|
} else {
|
|
5461
5699
|
console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
|
|
5462
5700
|
}
|
|
5463
|
-
|
|
5701
|
+
fs5.unlinkSync(pidPath);
|
|
5464
5702
|
return;
|
|
5465
5703
|
}
|
|
5466
5704
|
try {
|
|
5467
5705
|
process.kill(pid, "SIGTERM");
|
|
5468
|
-
|
|
5706
|
+
fs5.unlinkSync(pidPath);
|
|
5469
5707
|
if (format === "json") {
|
|
5470
5708
|
console.log(JSON.stringify({
|
|
5471
5709
|
stopped: true,
|
|
@@ -5489,9 +5727,9 @@ function stopDaemon(format = "text") {
|
|
|
5489
5727
|
|
|
5490
5728
|
// src/commands/init.ts
|
|
5491
5729
|
import crypto2 from "crypto";
|
|
5492
|
-
import
|
|
5730
|
+
import fs6 from "fs";
|
|
5493
5731
|
import readline from "readline";
|
|
5494
|
-
import
|
|
5732
|
+
import path5 from "path";
|
|
5495
5733
|
function prompt(question) {
|
|
5496
5734
|
const rl = readline.createInterface({
|
|
5497
5735
|
input: process.stdin,
|
|
@@ -5528,8 +5766,8 @@ async function initCommand(opts) {
|
|
|
5528
5766
|
return;
|
|
5529
5767
|
}
|
|
5530
5768
|
const configDir = getConfigDir();
|
|
5531
|
-
if (!
|
|
5532
|
-
|
|
5769
|
+
if (!fs6.existsSync(configDir)) {
|
|
5770
|
+
fs6.mkdirSync(configDir, { recursive: true });
|
|
5533
5771
|
}
|
|
5534
5772
|
const bootstrapEnv = getBootstrapEnv(process.env, {
|
|
5535
5773
|
GEMINI_API_KEY: opts?.geminiKey,
|
|
@@ -5644,7 +5882,7 @@ async function initCommand(opts) {
|
|
|
5644
5882
|
const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
|
|
5645
5883
|
const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
|
|
5646
5884
|
const keyPrefix = rawApiKey.slice(0, 9);
|
|
5647
|
-
const databasePath =
|
|
5885
|
+
const databasePath = path5.join(configDir, "data.db");
|
|
5648
5886
|
const db = createClient(databasePath);
|
|
5649
5887
|
migrate(db);
|
|
5650
5888
|
db.insert(apiKeys).values({
|
|
@@ -6006,7 +6244,7 @@ var SYSTEM_CLI_COMMANDS = [
|
|
|
6006
6244
|
];
|
|
6007
6245
|
|
|
6008
6246
|
// src/cli-commands/wordpress.ts
|
|
6009
|
-
import
|
|
6247
|
+
import fs7 from "fs";
|
|
6010
6248
|
|
|
6011
6249
|
// src/commands/wordpress.ts
|
|
6012
6250
|
function getClient17() {
|
|
@@ -6242,12 +6480,12 @@ async function wordpressSetMeta(project, body) {
|
|
|
6242
6480
|
printPageDetail(result);
|
|
6243
6481
|
}
|
|
6244
6482
|
async function wordpressBulkSetMeta(project, opts) {
|
|
6245
|
-
const
|
|
6246
|
-
const
|
|
6247
|
-
const filePath =
|
|
6483
|
+
const fs8 = await import("fs/promises");
|
|
6484
|
+
const path6 = await import("path");
|
|
6485
|
+
const filePath = path6.resolve(opts.from);
|
|
6248
6486
|
let raw;
|
|
6249
6487
|
try {
|
|
6250
|
-
raw = await
|
|
6488
|
+
raw = await fs8.readFile(filePath, "utf8");
|
|
6251
6489
|
} catch {
|
|
6252
6490
|
throw new CliError({
|
|
6253
6491
|
code: "FILE_READ_ERROR",
|
|
@@ -6344,13 +6582,13 @@ async function wordpressSetSchema(project, body) {
|
|
|
6344
6582
|
printManualAssist(`Schema update for "${body.slug}"`, result);
|
|
6345
6583
|
}
|
|
6346
6584
|
async function wordpressSchemaDeploy(project, opts) {
|
|
6347
|
-
const
|
|
6348
|
-
const
|
|
6585
|
+
const fs8 = await import("fs/promises");
|
|
6586
|
+
const path6 = await import("path");
|
|
6349
6587
|
const yaml = await import("yaml").catch(() => null);
|
|
6350
|
-
const filePath =
|
|
6588
|
+
const filePath = path6.resolve(opts.profile);
|
|
6351
6589
|
let raw;
|
|
6352
6590
|
try {
|
|
6353
|
-
raw = await
|
|
6591
|
+
raw = await fs8.readFile(filePath, "utf8");
|
|
6354
6592
|
} catch {
|
|
6355
6593
|
throw new CliError({
|
|
6356
6594
|
code: "FILE_READ_ERROR",
|
|
@@ -6455,13 +6693,13 @@ async function wordpressOnboard(project, opts) {
|
|
|
6455
6693
|
}
|
|
6456
6694
|
let profileData;
|
|
6457
6695
|
if (opts.profile) {
|
|
6458
|
-
const
|
|
6459
|
-
const
|
|
6696
|
+
const fs8 = await import("fs/promises");
|
|
6697
|
+
const path6 = await import("path");
|
|
6460
6698
|
const yaml = await import("yaml").catch(() => null);
|
|
6461
|
-
const filePath =
|
|
6699
|
+
const filePath = path6.resolve(opts.profile);
|
|
6462
6700
|
let raw;
|
|
6463
6701
|
try {
|
|
6464
|
-
raw = await
|
|
6702
|
+
raw = await fs8.readFile(filePath, "utf8");
|
|
6465
6703
|
} catch {
|
|
6466
6704
|
throw new CliError({
|
|
6467
6705
|
code: "FILE_READ_ERROR",
|
|
@@ -6610,7 +6848,7 @@ function resolveContent(input, command, usage, options) {
|
|
|
6610
6848
|
}
|
|
6611
6849
|
if (contentFile) {
|
|
6612
6850
|
try {
|
|
6613
|
-
return
|
|
6851
|
+
return fs7.readFileSync(contentFile, "utf-8");
|
|
6614
6852
|
} catch (error) {
|
|
6615
6853
|
const message = error instanceof Error ? error.message : String(error);
|
|
6616
6854
|
throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
|