@ainyc/canonry 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-67L2E6T5.js → chunk-CKC26GOH.js} +176 -35
- package/dist/cli.js +102 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/package.json +4 -4
|
@@ -57,15 +57,87 @@ function configExists() {
|
|
|
57
57
|
return fs.existsSync(getConfigPath());
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// src/
|
|
60
|
+
// src/telemetry.ts
|
|
61
|
+
import crypto from "crypto";
|
|
61
62
|
import { createRequire } from "module";
|
|
63
|
+
var _require = createRequire(import.meta.url);
|
|
64
|
+
var { version: VERSION } = _require("../package.json");
|
|
65
|
+
var TELEMETRY_ENDPOINT = "https://ainyc.ai/api/telemetry";
|
|
66
|
+
var TIMEOUT_MS = 3e3;
|
|
67
|
+
function isTelemetryEnabled() {
|
|
68
|
+
if (process.env.CANONRY_TELEMETRY_DISABLED === "1") return false;
|
|
69
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
70
|
+
if (process.env.CI) return false;
|
|
71
|
+
if (!configExists()) return true;
|
|
72
|
+
try {
|
|
73
|
+
const config = loadConfig();
|
|
74
|
+
return config.telemetry !== false;
|
|
75
|
+
} catch {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function getOrCreateAnonymousId() {
|
|
80
|
+
if (!configExists()) return void 0;
|
|
81
|
+
try {
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
if (config.anonymousId) return config.anonymousId;
|
|
84
|
+
const id = crypto.randomUUID();
|
|
85
|
+
config.anonymousId = id;
|
|
86
|
+
saveConfig(config);
|
|
87
|
+
return id;
|
|
88
|
+
} catch {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function isFirstRun() {
|
|
93
|
+
if (!configExists()) return false;
|
|
94
|
+
try {
|
|
95
|
+
const config = loadConfig();
|
|
96
|
+
return !config.anonymousId;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function showFirstRunNotice() {
|
|
102
|
+
process.stderr.write(
|
|
103
|
+
"\nCanonry collects anonymous telemetry to prioritize features.\nDisable any time: canonry telemetry disable\nLearn more: https://ainyc.ai/telemetry\n\n"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function trackEvent(event, properties) {
|
|
107
|
+
if (!isTelemetryEnabled()) return;
|
|
108
|
+
const anonymousId = getOrCreateAnonymousId();
|
|
109
|
+
if (!anonymousId) return;
|
|
110
|
+
const payload = {
|
|
111
|
+
anonymousId,
|
|
112
|
+
event,
|
|
113
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
114
|
+
version: VERSION,
|
|
115
|
+
nodeVersion: process.versions.node,
|
|
116
|
+
os: process.platform,
|
|
117
|
+
arch: process.arch,
|
|
118
|
+
properties
|
|
119
|
+
};
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
122
|
+
timeout.unref();
|
|
123
|
+
fetch(TELEMETRY_ENDPOINT, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
body: JSON.stringify(payload),
|
|
127
|
+
signal: controller.signal
|
|
128
|
+
}).catch(() => {
|
|
129
|
+
}).finally(() => clearTimeout(timeout));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/server.ts
|
|
133
|
+
import { createRequire as createRequire2 } from "module";
|
|
62
134
|
import fs2 from "fs";
|
|
63
135
|
import path2 from "path";
|
|
64
136
|
import { fileURLToPath } from "url";
|
|
65
137
|
import Fastify from "fastify";
|
|
66
138
|
|
|
67
139
|
// ../api-routes/src/auth.ts
|
|
68
|
-
import
|
|
140
|
+
import crypto2 from "crypto";
|
|
69
141
|
import { eq } from "drizzle-orm";
|
|
70
142
|
|
|
71
143
|
// ../db/src/client.ts
|
|
@@ -575,7 +647,7 @@ var scheduleDtoSchema = z6.object({
|
|
|
575
647
|
|
|
576
648
|
// ../api-routes/src/auth.ts
|
|
577
649
|
function hashKey(key) {
|
|
578
|
-
return
|
|
650
|
+
return crypto2.createHash("sha256").update(key).digest("hex");
|
|
579
651
|
}
|
|
580
652
|
var SKIP_PATHS = ["/health"];
|
|
581
653
|
function shouldSkipAuth(url) {
|
|
@@ -609,11 +681,11 @@ async function authPlugin(app) {
|
|
|
609
681
|
}
|
|
610
682
|
|
|
611
683
|
// ../api-routes/src/projects.ts
|
|
612
|
-
import
|
|
684
|
+
import crypto4 from "crypto";
|
|
613
685
|
import { eq as eq3 } from "drizzle-orm";
|
|
614
686
|
|
|
615
687
|
// ../api-routes/src/helpers.ts
|
|
616
|
-
import
|
|
688
|
+
import crypto3 from "crypto";
|
|
617
689
|
import { eq as eq2, and } from "drizzle-orm";
|
|
618
690
|
function resolveProject(db, name) {
|
|
619
691
|
const project = db.select().from(projects).where(eq2(projects.name, name)).get();
|
|
@@ -625,7 +697,7 @@ function resolveProject(db, name) {
|
|
|
625
697
|
function writeAuditLog(db, entry) {
|
|
626
698
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
627
699
|
db.insert(auditLog).values({
|
|
628
|
-
id:
|
|
700
|
+
id: crypto3.randomUUID(),
|
|
629
701
|
projectId: entry.projectId ?? null,
|
|
630
702
|
actor: entry.actor,
|
|
631
703
|
action: entry.action,
|
|
@@ -670,7 +742,7 @@ async function projectRoutes(app, opts) {
|
|
|
670
742
|
const updated = app.db.select().from(projects).where(eq3(projects.id, existing.id)).get();
|
|
671
743
|
return reply.status(200).send(formatProject(updated));
|
|
672
744
|
}
|
|
673
|
-
const id =
|
|
745
|
+
const id = crypto4.randomUUID();
|
|
674
746
|
app.db.insert(projects).values({
|
|
675
747
|
id,
|
|
676
748
|
name,
|
|
@@ -803,7 +875,7 @@ function formatProject(row) {
|
|
|
803
875
|
}
|
|
804
876
|
|
|
805
877
|
// ../api-routes/src/keywords.ts
|
|
806
|
-
import
|
|
878
|
+
import crypto5 from "crypto";
|
|
807
879
|
import { eq as eq4 } from "drizzle-orm";
|
|
808
880
|
async function keywordRoutes(app, opts) {
|
|
809
881
|
app.get("/projects/:name/keywords", async (request, reply) => {
|
|
@@ -825,7 +897,7 @@ async function keywordRoutes(app, opts) {
|
|
|
825
897
|
tx.delete(keywords).where(eq4(keywords.projectId, project.id)).run();
|
|
826
898
|
for (const kw of body.keywords) {
|
|
827
899
|
tx.insert(keywords).values({
|
|
828
|
-
id:
|
|
900
|
+
id: crypto5.randomUUID(),
|
|
829
901
|
projectId: project.id,
|
|
830
902
|
keyword: kw,
|
|
831
903
|
createdAt: now
|
|
@@ -857,7 +929,7 @@ async function keywordRoutes(app, opts) {
|
|
|
857
929
|
for (const kw of body.keywords) {
|
|
858
930
|
if (!existingSet.has(kw)) {
|
|
859
931
|
app.db.insert(keywords).values({
|
|
860
|
-
id:
|
|
932
|
+
id: crypto5.randomUUID(),
|
|
861
933
|
projectId: project.id,
|
|
862
934
|
keyword: kw,
|
|
863
935
|
createdAt: now
|
|
@@ -932,7 +1004,7 @@ function resolveProjectSafe(app, name, reply) {
|
|
|
932
1004
|
}
|
|
933
1005
|
|
|
934
1006
|
// ../api-routes/src/competitors.ts
|
|
935
|
-
import
|
|
1007
|
+
import crypto6 from "crypto";
|
|
936
1008
|
import { eq as eq5 } from "drizzle-orm";
|
|
937
1009
|
async function competitorRoutes(app) {
|
|
938
1010
|
app.get("/projects/:name/competitors", async (request, reply) => {
|
|
@@ -954,7 +1026,7 @@ async function competitorRoutes(app) {
|
|
|
954
1026
|
tx.delete(competitors).where(eq5(competitors.projectId, project.id)).run();
|
|
955
1027
|
for (const domain of body.competitors) {
|
|
956
1028
|
tx.insert(competitors).values({
|
|
957
|
-
id:
|
|
1029
|
+
id: crypto6.randomUUID(),
|
|
958
1030
|
projectId: project.id,
|
|
959
1031
|
domain,
|
|
960
1032
|
createdAt: now
|
|
@@ -989,13 +1061,13 @@ function resolveProjectSafe2(app, name, reply) {
|
|
|
989
1061
|
import { eq as eq7, asc } from "drizzle-orm";
|
|
990
1062
|
|
|
991
1063
|
// ../api-routes/src/run-queue.ts
|
|
992
|
-
import
|
|
1064
|
+
import crypto7 from "crypto";
|
|
993
1065
|
import { and as and2, eq as eq6, or } from "drizzle-orm";
|
|
994
1066
|
function queueRunIfProjectIdle(db, params) {
|
|
995
1067
|
const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
996
1068
|
const kind = params.kind ?? "answer-visibility";
|
|
997
1069
|
const trigger = params.trigger ?? "manual";
|
|
998
|
-
const runId =
|
|
1070
|
+
const runId = crypto7.randomUUID();
|
|
999
1071
|
return db.transaction((tx) => {
|
|
1000
1072
|
const activeRun = tx.select().from(runs).where(
|
|
1001
1073
|
and2(
|
|
@@ -1152,7 +1224,7 @@ function resolveProjectSafe3(app, name, reply) {
|
|
|
1152
1224
|
}
|
|
1153
1225
|
|
|
1154
1226
|
// ../api-routes/src/apply.ts
|
|
1155
|
-
import
|
|
1227
|
+
import crypto9 from "crypto";
|
|
1156
1228
|
import { eq as eq8 } from "drizzle-orm";
|
|
1157
1229
|
|
|
1158
1230
|
// ../api-routes/src/schedule-utils.ts
|
|
@@ -1244,7 +1316,7 @@ function isValidTimezone(tz) {
|
|
|
1244
1316
|
}
|
|
1245
1317
|
|
|
1246
1318
|
// ../api-routes/src/webhooks.ts
|
|
1247
|
-
import
|
|
1319
|
+
import crypto8 from "crypto";
|
|
1248
1320
|
import dns from "dns/promises";
|
|
1249
1321
|
import http from "http";
|
|
1250
1322
|
import https from "https";
|
|
@@ -1296,7 +1368,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
|
|
|
1296
1368
|
"User-Agent": "Canonry/0.1.0"
|
|
1297
1369
|
};
|
|
1298
1370
|
if (webhookSecret) {
|
|
1299
|
-
headers["X-Canonry-Signature"] = "sha256=" +
|
|
1371
|
+
headers["X-Canonry-Signature"] = "sha256=" + crypto8.createHmac("sha256", webhookSecret).update(body).digest("hex");
|
|
1300
1372
|
}
|
|
1301
1373
|
return await new Promise((resolve) => {
|
|
1302
1374
|
const requestOptions = {
|
|
@@ -1438,7 +1510,7 @@ async function applyRoutes(app, opts) {
|
|
|
1438
1510
|
entityId: projectId
|
|
1439
1511
|
});
|
|
1440
1512
|
} else {
|
|
1441
|
-
projectId =
|
|
1513
|
+
projectId = crypto9.randomUUID();
|
|
1442
1514
|
app.db.insert(projects).values({
|
|
1443
1515
|
id: projectId,
|
|
1444
1516
|
name,
|
|
@@ -1466,7 +1538,7 @@ async function applyRoutes(app, opts) {
|
|
|
1466
1538
|
tx.delete(keywords).where(eq8(keywords.projectId, projectId)).run();
|
|
1467
1539
|
for (const kw of config.spec.keywords) {
|
|
1468
1540
|
tx.insert(keywords).values({
|
|
1469
|
-
id:
|
|
1541
|
+
id: crypto9.randomUUID(),
|
|
1470
1542
|
projectId,
|
|
1471
1543
|
keyword: kw,
|
|
1472
1544
|
createdAt: now
|
|
@@ -1482,7 +1554,7 @@ async function applyRoutes(app, opts) {
|
|
|
1482
1554
|
tx.delete(competitors).where(eq8(competitors.projectId, projectId)).run();
|
|
1483
1555
|
for (const domain of config.spec.competitors) {
|
|
1484
1556
|
tx.insert(competitors).values({
|
|
1485
|
-
id:
|
|
1557
|
+
id: crypto9.randomUUID(),
|
|
1486
1558
|
projectId,
|
|
1487
1559
|
domain,
|
|
1488
1560
|
createdAt: now
|
|
@@ -1538,7 +1610,7 @@ async function applyRoutes(app, opts) {
|
|
|
1538
1610
|
}).where(eq8(schedules.id, existingSched.id)).run();
|
|
1539
1611
|
} else {
|
|
1540
1612
|
app.db.insert(schedules).values({
|
|
1541
|
-
id:
|
|
1613
|
+
id: crypto9.randomUUID(),
|
|
1542
1614
|
projectId,
|
|
1543
1615
|
cronExpr,
|
|
1544
1616
|
preset,
|
|
@@ -1570,11 +1642,11 @@ async function applyRoutes(app, opts) {
|
|
|
1570
1642
|
app.db.delete(notifications).where(eq8(notifications.projectId, projectId)).run();
|
|
1571
1643
|
for (const notif of config.spec.notifications) {
|
|
1572
1644
|
app.db.insert(notifications).values({
|
|
1573
|
-
id:
|
|
1645
|
+
id: crypto9.randomUUID(),
|
|
1574
1646
|
projectId,
|
|
1575
1647
|
channel: notif.channel,
|
|
1576
1648
|
config: JSON.stringify({ url: notif.url, events: notif.events }),
|
|
1577
|
-
webhookSecret:
|
|
1649
|
+
webhookSecret: crypto9.randomBytes(32).toString("hex"),
|
|
1578
1650
|
enabled: 1,
|
|
1579
1651
|
createdAt: now,
|
|
1580
1652
|
updatedAt: now
|
|
@@ -1848,8 +1920,37 @@ async function settingsRoutes(app, opts) {
|
|
|
1848
1920
|
});
|
|
1849
1921
|
}
|
|
1850
1922
|
|
|
1923
|
+
// ../api-routes/src/telemetry.ts
|
|
1924
|
+
async function telemetryRoutes(app, opts) {
|
|
1925
|
+
app.get("/telemetry", async (_request, reply) => {
|
|
1926
|
+
if (!opts.getTelemetryStatus) {
|
|
1927
|
+
return reply.status(501).send({ error: "Telemetry status is not available in this deployment" });
|
|
1928
|
+
}
|
|
1929
|
+
const status = opts.getTelemetryStatus();
|
|
1930
|
+
return {
|
|
1931
|
+
enabled: status.enabled,
|
|
1932
|
+
anonymousId: status.anonymousId ? status.anonymousId.slice(0, 8) + "..." : void 0
|
|
1933
|
+
};
|
|
1934
|
+
});
|
|
1935
|
+
app.put("/telemetry", async (request, reply) => {
|
|
1936
|
+
if (!opts.setTelemetryEnabled) {
|
|
1937
|
+
return reply.status(501).send({ error: "Telemetry configuration is not available in this deployment" });
|
|
1938
|
+
}
|
|
1939
|
+
const { enabled } = request.body ?? {};
|
|
1940
|
+
if (typeof enabled !== "boolean") {
|
|
1941
|
+
return reply.status(400).send({ error: "enabled (boolean) is required" });
|
|
1942
|
+
}
|
|
1943
|
+
opts.setTelemetryEnabled(enabled);
|
|
1944
|
+
const status = opts.getTelemetryStatus?.();
|
|
1945
|
+
return {
|
|
1946
|
+
enabled: status?.enabled ?? enabled,
|
|
1947
|
+
anonymousId: status?.anonymousId ? status.anonymousId.slice(0, 8) + "..." : void 0
|
|
1948
|
+
};
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1851
1952
|
// ../api-routes/src/schedules.ts
|
|
1852
|
-
import
|
|
1953
|
+
import crypto10 from "crypto";
|
|
1853
1954
|
import { eq as eq10 } from "drizzle-orm";
|
|
1854
1955
|
async function scheduleRoutes(app, opts) {
|
|
1855
1956
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
@@ -1896,7 +1997,7 @@ async function scheduleRoutes(app, opts) {
|
|
|
1896
1997
|
}).where(eq10(schedules.id, existing.id)).run();
|
|
1897
1998
|
} else {
|
|
1898
1999
|
app.db.insert(schedules).values({
|
|
1899
|
-
id:
|
|
2000
|
+
id: crypto10.randomUUID(),
|
|
1900
2001
|
projectId: project.id,
|
|
1901
2002
|
cronExpr,
|
|
1902
2003
|
preset: preset ?? null,
|
|
@@ -1975,7 +2076,7 @@ function resolveProjectSafe5(app, name, reply) {
|
|
|
1975
2076
|
}
|
|
1976
2077
|
|
|
1977
2078
|
// ../api-routes/src/notifications.ts
|
|
1978
|
-
import
|
|
2079
|
+
import crypto11 from "crypto";
|
|
1979
2080
|
import { eq as eq11 } from "drizzle-orm";
|
|
1980
2081
|
var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed"];
|
|
1981
2082
|
async function notificationRoutes(app) {
|
|
@@ -2006,8 +2107,8 @@ async function notificationRoutes(app) {
|
|
|
2006
2107
|
});
|
|
2007
2108
|
}
|
|
2008
2109
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2009
|
-
const id =
|
|
2010
|
-
const webhookSecret =
|
|
2110
|
+
const id = crypto11.randomUUID();
|
|
2111
|
+
const webhookSecret = crypto11.randomBytes(32).toString("hex");
|
|
2011
2112
|
app.db.insert(notifications).values({
|
|
2012
2113
|
id,
|
|
2013
2114
|
projectId: project.id,
|
|
@@ -2152,6 +2253,10 @@ async function apiRoutes(app, opts) {
|
|
|
2152
2253
|
onScheduleUpdated: opts.onScheduleUpdated
|
|
2153
2254
|
});
|
|
2154
2255
|
await api.register(notificationRoutes);
|
|
2256
|
+
await api.register(telemetryRoutes, {
|
|
2257
|
+
getTelemetryStatus: opts.getTelemetryStatus,
|
|
2258
|
+
setTelemetryEnabled: opts.setTelemetryEnabled
|
|
2259
|
+
});
|
|
2155
2260
|
}, { prefix: "/api/v1" });
|
|
2156
2261
|
}
|
|
2157
2262
|
|
|
@@ -3072,7 +3177,7 @@ var localAdapter = {
|
|
|
3072
3177
|
};
|
|
3073
3178
|
|
|
3074
3179
|
// src/job-runner.ts
|
|
3075
|
-
import
|
|
3180
|
+
import crypto12 from "crypto";
|
|
3076
3181
|
import { eq as eq12, inArray as inArray2 } from "drizzle-orm";
|
|
3077
3182
|
var JobRunner = class {
|
|
3078
3183
|
db;
|
|
@@ -3093,6 +3198,7 @@ var JobRunner = class {
|
|
|
3093
3198
|
}
|
|
3094
3199
|
async executeRun(runId, projectId, providerOverride) {
|
|
3095
3200
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3201
|
+
const startTime = Date.now();
|
|
3096
3202
|
try {
|
|
3097
3203
|
this.db.update(runs).set({ status: "running", startedAt: now }).where(eq12(runs.id, runId)).run();
|
|
3098
3204
|
const project = this.db.select().from(projects).where(eq12(projects.id, projectId)).get();
|
|
@@ -3148,7 +3254,7 @@ var JobRunner = class {
|
|
|
3148
3254
|
const citationState = determineCitationState(normalized, project.canonicalDomain);
|
|
3149
3255
|
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
3150
3256
|
this.db.insert(querySnapshots).values({
|
|
3151
|
-
id:
|
|
3257
|
+
id: crypto12.randomUUID(),
|
|
3152
3258
|
runId,
|
|
3153
3259
|
keywordId: kw.id,
|
|
3154
3260
|
provider: providerName,
|
|
@@ -3185,6 +3291,14 @@ var JobRunner = class {
|
|
|
3185
3291
|
} else {
|
|
3186
3292
|
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, runId)).run();
|
|
3187
3293
|
}
|
|
3294
|
+
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
3295
|
+
trackEvent("run.completed", {
|
|
3296
|
+
status: finalStatus,
|
|
3297
|
+
providerCount: activeProviders.length,
|
|
3298
|
+
providers: activeProviders.map((p) => p.adapter.name),
|
|
3299
|
+
keywordCount: projectKeywords.length,
|
|
3300
|
+
durationMs: Date.now() - startTime
|
|
3301
|
+
});
|
|
3188
3302
|
for (const p of activeProviders) {
|
|
3189
3303
|
this.incrementUsage(`${projectId}:${p.adapter.name}`, "queries", queriesPerProvider);
|
|
3190
3304
|
}
|
|
@@ -3201,6 +3315,13 @@ var JobRunner = class {
|
|
|
3201
3315
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3202
3316
|
error: errorMessage
|
|
3203
3317
|
}).where(eq12(runs.id, runId)).run();
|
|
3318
|
+
trackEvent("run.completed", {
|
|
3319
|
+
status: "failed",
|
|
3320
|
+
providerCount: 0,
|
|
3321
|
+
providers: [],
|
|
3322
|
+
keywordCount: 0,
|
|
3323
|
+
durationMs: Date.now() - startTime
|
|
3324
|
+
});
|
|
3204
3325
|
if (this.onRunCompleted) {
|
|
3205
3326
|
this.onRunCompleted(runId, projectId).catch((notifErr) => {
|
|
3206
3327
|
console.error("[JobRunner] Notification callback failed:", notifErr);
|
|
@@ -3229,7 +3350,7 @@ var JobRunner = class {
|
|
|
3229
3350
|
incrementUsage(scope, metric, count) {
|
|
3230
3351
|
const now = /* @__PURE__ */ new Date();
|
|
3231
3352
|
const period = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
3232
|
-
const id =
|
|
3353
|
+
const id = crypto12.randomUUID();
|
|
3233
3354
|
const existing = this.db.select().from(usageCounters).where(eq12(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
|
|
3234
3355
|
if (existing) {
|
|
3235
3356
|
this.db.update(usageCounters).set({ count: existing.count + count, updatedAt: now.toISOString() }).where(eq12(usageCounters.id, existing.id)).run();
|
|
@@ -3479,7 +3600,7 @@ var Scheduler = class {
|
|
|
3479
3600
|
|
|
3480
3601
|
// src/notifier.ts
|
|
3481
3602
|
import { eq as eq14, desc as desc2, and as and3, or as or2 } from "drizzle-orm";
|
|
3482
|
-
import
|
|
3603
|
+
import crypto13 from "crypto";
|
|
3483
3604
|
var Notifier = class {
|
|
3484
3605
|
db;
|
|
3485
3606
|
serverUrl;
|
|
@@ -3617,7 +3738,7 @@ var Notifier = class {
|
|
|
3617
3738
|
}
|
|
3618
3739
|
logDelivery(projectId, notificationId, event, status, error) {
|
|
3619
3740
|
this.db.insert(auditLog).values({
|
|
3620
|
-
id:
|
|
3741
|
+
id: crypto13.randomUUID(),
|
|
3621
3742
|
projectId,
|
|
3622
3743
|
actor: "scheduler",
|
|
3623
3744
|
action: `notification.${status}`,
|
|
@@ -3741,8 +3862,8 @@ function stripHtml(html) {
|
|
|
3741
3862
|
}
|
|
3742
3863
|
|
|
3743
3864
|
// src/server.ts
|
|
3744
|
-
var
|
|
3745
|
-
var { version: PKG_VERSION } =
|
|
3865
|
+
var _require2 = createRequire2(import.meta.url);
|
|
3866
|
+
var { version: PKG_VERSION } = _require2("../package.json");
|
|
3746
3867
|
var DEFAULT_QUOTA = {
|
|
3747
3868
|
maxConcurrency: 2,
|
|
3748
3869
|
maxRequestsPerMinute: 10,
|
|
@@ -3882,6 +4003,21 @@ async function createServer(opts) {
|
|
|
3882
4003
|
onProjectDeleted: (projectId) => {
|
|
3883
4004
|
scheduler.remove(projectId);
|
|
3884
4005
|
},
|
|
4006
|
+
getTelemetryStatus: () => {
|
|
4007
|
+
const enabled = isTelemetryEnabled();
|
|
4008
|
+
return {
|
|
4009
|
+
enabled,
|
|
4010
|
+
// Only read/create the anonymous ID if telemetry is enabled.
|
|
4011
|
+
// Don't mutate config for opted-out users.
|
|
4012
|
+
anonymousId: enabled ? getOrCreateAnonymousId() : void 0
|
|
4013
|
+
};
|
|
4014
|
+
},
|
|
4015
|
+
setTelemetryEnabled: (enabled) => {
|
|
4016
|
+
const config = loadConfig();
|
|
4017
|
+
config.telemetry = enabled;
|
|
4018
|
+
saveConfig(config);
|
|
4019
|
+
opts.config.telemetry = enabled;
|
|
4020
|
+
},
|
|
3885
4021
|
onGenerateKeywords: async (providerName, count, project) => {
|
|
3886
4022
|
const provider = registry.get(providerName);
|
|
3887
4023
|
if (!provider) throw new Error(`Provider "${providerName}" is not configured`);
|
|
@@ -4001,5 +4137,10 @@ export {
|
|
|
4001
4137
|
loadConfig,
|
|
4002
4138
|
saveConfig,
|
|
4003
4139
|
configExists,
|
|
4140
|
+
isTelemetryEnabled,
|
|
4141
|
+
getOrCreateAnonymousId,
|
|
4142
|
+
isFirstRun,
|
|
4143
|
+
showFirstRunNotice,
|
|
4144
|
+
trackEvent,
|
|
4004
4145
|
createServer
|
|
4005
4146
|
};
|
package/dist/cli.js
CHANGED
|
@@ -6,11 +6,16 @@ import {
|
|
|
6
6
|
createServer,
|
|
7
7
|
getConfigDir,
|
|
8
8
|
getConfigPath,
|
|
9
|
+
getOrCreateAnonymousId,
|
|
10
|
+
isFirstRun,
|
|
11
|
+
isTelemetryEnabled,
|
|
9
12
|
loadConfig,
|
|
10
13
|
migrate,
|
|
11
14
|
providerQuotaPolicySchema,
|
|
12
|
-
saveConfig
|
|
13
|
-
|
|
15
|
+
saveConfig,
|
|
16
|
+
showFirstRunNotice,
|
|
17
|
+
trackEvent
|
|
18
|
+
} from "./chunk-CKC26GOH.js";
|
|
14
19
|
|
|
15
20
|
// src/cli.ts
|
|
16
21
|
import { parseArgs } from "util";
|
|
@@ -254,13 +259,18 @@ async function initCommand(opts) {
|
|
|
254
259
|
apiKey: rawApiKey,
|
|
255
260
|
providers
|
|
256
261
|
});
|
|
257
|
-
const providerNames = Object.keys(providers)
|
|
262
|
+
const providerNames = Object.keys(providers);
|
|
258
263
|
console.log(`
|
|
259
264
|
Config saved to ${getConfigPath()}`);
|
|
260
265
|
console.log(`Database created at ${databasePath}`);
|
|
261
266
|
console.log(`API key: ${rawApiKey}`);
|
|
262
|
-
console.log(`Providers: ${providerNames}`);
|
|
263
|
-
|
|
267
|
+
console.log(`Providers: ${providerNames.join(", ")}`);
|
|
268
|
+
showFirstRunNotice();
|
|
269
|
+
console.log('Run "canonry serve" to start the server.');
|
|
270
|
+
trackEvent("cli.init", {
|
|
271
|
+
providerCount: providerNames.length,
|
|
272
|
+
providers: providerNames
|
|
273
|
+
});
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
// src/commands/serve.ts
|
|
@@ -276,6 +286,13 @@ async function serveCommand() {
|
|
|
276
286
|
console.log(`
|
|
277
287
|
Canonry server running at http://${host === "0.0.0.0" ? "localhost" : host}:${port}`);
|
|
278
288
|
console.log("Press Ctrl+C to stop.\n");
|
|
289
|
+
const providerNames = Object.keys(config.providers ?? {}).filter(
|
|
290
|
+
(k) => config.providers?.[k]?.apiKey || config.providers?.[k]?.baseUrl
|
|
291
|
+
);
|
|
292
|
+
trackEvent("serve.started", {
|
|
293
|
+
providerCount: providerNames.length,
|
|
294
|
+
providers: providerNames
|
|
295
|
+
});
|
|
279
296
|
} catch (err) {
|
|
280
297
|
app.log.error(err);
|
|
281
298
|
process.exit(1);
|
|
@@ -394,6 +411,12 @@ var ApiClient = class {
|
|
|
394
411
|
async testNotification(project, id) {
|
|
395
412
|
return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}/test`);
|
|
396
413
|
}
|
|
414
|
+
async getTelemetry() {
|
|
415
|
+
return this.request("GET", "/telemetry");
|
|
416
|
+
}
|
|
417
|
+
async updateTelemetry(enabled) {
|
|
418
|
+
return this.request("PUT", "/telemetry", { enabled });
|
|
419
|
+
}
|
|
397
420
|
async generateKeywords(project, provider, count) {
|
|
398
421
|
return this.request(
|
|
399
422
|
"POST",
|
|
@@ -869,6 +892,64 @@ function printNotification(n) {
|
|
|
869
892
|
console.log(` Enabled: ${n.enabled ? "yes" : "no"}`);
|
|
870
893
|
}
|
|
871
894
|
|
|
895
|
+
// src/commands/telemetry.ts
|
|
896
|
+
function telemetryCommand(subcommand) {
|
|
897
|
+
switch (subcommand) {
|
|
898
|
+
case "status": {
|
|
899
|
+
if (process.env.CANONRY_TELEMETRY_DISABLED === "1") {
|
|
900
|
+
console.log("Telemetry: disabled (CANONRY_TELEMETRY_DISABLED=1)");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (process.env.DO_NOT_TRACK === "1") {
|
|
904
|
+
console.log("Telemetry: disabled (DO_NOT_TRACK=1)");
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (process.env.CI) {
|
|
908
|
+
console.log("Telemetry: disabled (CI environment detected)");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!configExists()) {
|
|
912
|
+
console.log('Telemetry: enabled (no config yet \u2014 run "canonry init" first)');
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const config = loadConfig();
|
|
916
|
+
const enabled = config.telemetry !== false;
|
|
917
|
+
console.log(`Telemetry: ${enabled ? "enabled" : "disabled"}`);
|
|
918
|
+
if (config.anonymousId) {
|
|
919
|
+
const masked = config.anonymousId.slice(0, 8) + "...";
|
|
920
|
+
console.log(`Anonymous ID: ${masked}`);
|
|
921
|
+
}
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
case "enable": {
|
|
925
|
+
if (!configExists()) {
|
|
926
|
+
console.error('No config found. Run "canonry init" first.');
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
const config = loadConfig();
|
|
930
|
+
config.telemetry = true;
|
|
931
|
+
saveConfig(config);
|
|
932
|
+
console.log("Telemetry enabled.");
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
case "disable": {
|
|
936
|
+
if (!configExists()) {
|
|
937
|
+
console.error('No config found. Run "canonry init" first.');
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
const config = loadConfig();
|
|
941
|
+
config.telemetry = false;
|
|
942
|
+
saveConfig(config);
|
|
943
|
+
console.log("Telemetry disabled. No data will be sent.");
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
default:
|
|
947
|
+
console.error(`Unknown telemetry subcommand: ${subcommand ?? "(none)"}`);
|
|
948
|
+
console.log("Available: status, enable, disable");
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
872
953
|
// src/cli.ts
|
|
873
954
|
import { createRequire } from "module";
|
|
874
955
|
var USAGE = `
|
|
@@ -907,6 +988,9 @@ Usage:
|
|
|
907
988
|
canonry notify test <project> <id> Send test webhook
|
|
908
989
|
canonry settings Show active provider and quota settings
|
|
909
990
|
canonry settings provider <name> Update a provider config (--api-key, --base-url, --model)
|
|
991
|
+
canonry telemetry status Show telemetry status
|
|
992
|
+
canonry telemetry enable Enable anonymous telemetry
|
|
993
|
+
canonry telemetry disable Disable anonymous telemetry
|
|
910
994
|
canonry --help Show this help
|
|
911
995
|
canonry --version Show version
|
|
912
996
|
|
|
@@ -937,6 +1021,15 @@ async function main() {
|
|
|
937
1021
|
return;
|
|
938
1022
|
}
|
|
939
1023
|
const command = args[0];
|
|
1024
|
+
if (command !== "telemetry" && command !== "init" && isTelemetryEnabled() && isFirstRun()) {
|
|
1025
|
+
showFirstRunNotice();
|
|
1026
|
+
getOrCreateAnonymousId();
|
|
1027
|
+
}
|
|
1028
|
+
const SUBCOMMAND_COMMANDS = /* @__PURE__ */ new Set(["project", "keyword", "competitor", "schedule", "notify", "settings", "telemetry"]);
|
|
1029
|
+
const resolvedCommand = SUBCOMMAND_COMMANDS.has(command) && args[1] && !args[1].startsWith("-") ? `${command}.${args[1]}` : command;
|
|
1030
|
+
if (command !== "telemetry") {
|
|
1031
|
+
trackEvent("cli.command", { command: resolvedCommand });
|
|
1032
|
+
}
|
|
940
1033
|
try {
|
|
941
1034
|
switch (command) {
|
|
942
1035
|
case "init": {
|
|
@@ -1337,6 +1430,10 @@ async function main() {
|
|
|
1337
1430
|
}
|
|
1338
1431
|
break;
|
|
1339
1432
|
}
|
|
1433
|
+
case "telemetry": {
|
|
1434
|
+
telemetryCommand(args[1]);
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1340
1437
|
default:
|
|
1341
1438
|
console.error(`Unknown command: ${command}`);
|
|
1342
1439
|
console.log('Run "canonry --help" for usage.');
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
|
|
6
6
|
"license": "FSL-1.1-ALv2",
|
|
@@ -51,13 +51,13 @@
|
|
|
51
51
|
"@types/node-cron": "^3.0.11",
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"tsx": "^4.19.0",
|
|
54
|
-
"@ainyc/canonry-api-routes": "0.0.0",
|
|
55
54
|
"@ainyc/canonry-config": "0.0.0",
|
|
56
|
-
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
57
55
|
"@ainyc/canonry-contracts": "0.0.0",
|
|
56
|
+
"@ainyc/canonry-api-routes": "0.0.0",
|
|
57
|
+
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
58
58
|
"@ainyc/canonry-db": "0.0.0",
|
|
59
|
-
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
60
59
|
"@ainyc/canonry-provider-local": "0.0.0",
|
|
60
|
+
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
61
61
|
"@ainyc/canonry-provider-openai": "0.0.0"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|