@ainyc/canonry 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1101 @@
1
+ #!/usr/bin/env node --import tsx
2
+ import {
3
+ apiKeys,
4
+ configExists,
5
+ createClient,
6
+ createServer,
7
+ getConfigDir,
8
+ getConfigPath,
9
+ loadConfig,
10
+ migrate,
11
+ saveConfig
12
+ } from "./chunk-ONZDY6Q4.js";
13
+
14
+ // src/cli.ts
15
+ import { parseArgs } from "util";
16
+
17
+ // src/commands/init.ts
18
+ import crypto from "crypto";
19
+ import fs from "fs";
20
+ import readline from "readline";
21
+ import path from "path";
22
+ function prompt(question) {
23
+ const rl = readline.createInterface({
24
+ input: process.stdin,
25
+ output: process.stdout
26
+ });
27
+ return new Promise((resolve) => {
28
+ rl.question(question, (answer) => {
29
+ rl.close();
30
+ resolve(answer.trim());
31
+ });
32
+ });
33
+ }
34
+ var DEFAULT_QUOTA = {
35
+ maxConcurrency: 2,
36
+ maxRequestsPerMinute: 10,
37
+ maxRequestsPerDay: 500
38
+ };
39
+ async function initCommand() {
40
+ console.log("Initializing canonry...\n");
41
+ if (configExists()) {
42
+ console.log(`Config already exists at ${getConfigPath()}`);
43
+ console.log("To reinitialize, delete the config file first.");
44
+ return;
45
+ }
46
+ const configDir = getConfigDir();
47
+ if (!fs.existsSync(configDir)) {
48
+ fs.mkdirSync(configDir, { recursive: true });
49
+ }
50
+ const providers = {};
51
+ console.log("Configure AI providers (at least one required):\n");
52
+ const geminiApiKey = await prompt("Gemini API key (press Enter to skip): ");
53
+ if (geminiApiKey) {
54
+ const geminiModel = await prompt(" Gemini model [gemini-2.5-flash]: ") || "gemini-2.5-flash";
55
+ providers.gemini = { apiKey: geminiApiKey, model: geminiModel, quota: DEFAULT_QUOTA };
56
+ }
57
+ const openaiApiKey = await prompt("OpenAI API key (press Enter to skip): ");
58
+ if (openaiApiKey) {
59
+ const openaiModel = await prompt(" OpenAI model [gpt-4o]: ") || "gpt-4o";
60
+ providers.openai = { apiKey: openaiApiKey, model: openaiModel, quota: DEFAULT_QUOTA };
61
+ }
62
+ const claudeApiKey = await prompt("Anthropic API key (press Enter to skip): ");
63
+ if (claudeApiKey) {
64
+ const claudeModel = await prompt(" Claude model [claude-sonnet-4-6]: ") || "claude-sonnet-4-6";
65
+ providers.claude = { apiKey: claudeApiKey, model: claudeModel, quota: DEFAULT_QUOTA };
66
+ }
67
+ console.log("\nLocal LLM (Ollama, LM Studio, llama.cpp, vLLM \u2014 any OpenAI-compatible API):");
68
+ const localBaseUrl = await prompt("Local LLM base URL (press Enter to skip, e.g. http://localhost:11434/v1): ");
69
+ if (localBaseUrl) {
70
+ const localModel = await prompt(" Model name [llama3]: ") || "llama3";
71
+ const localApiKey = await prompt(" API key (press Enter if not needed): ") || void 0;
72
+ providers.local = { baseUrl: localBaseUrl, apiKey: localApiKey, model: localModel, quota: DEFAULT_QUOTA };
73
+ }
74
+ const hasProvider = providers.gemini || providers.openai || providers.claude || providers.local;
75
+ if (!hasProvider) {
76
+ console.error("\nAt least one provider is required.");
77
+ process.exit(1);
78
+ }
79
+ const rawApiKey = `cnry_${crypto.randomBytes(16).toString("hex")}`;
80
+ const keyHash = crypto.createHash("sha256").update(rawApiKey).digest("hex");
81
+ const keyPrefix = rawApiKey.slice(0, 9);
82
+ const databasePath = path.join(configDir, "data.db");
83
+ const db = createClient(databasePath);
84
+ migrate(db);
85
+ db.insert(apiKeys).values({
86
+ id: crypto.randomUUID(),
87
+ name: "default",
88
+ keyHash,
89
+ keyPrefix,
90
+ scopes: '["*"]',
91
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
92
+ }).run();
93
+ saveConfig({
94
+ apiUrl: "http://localhost:4100",
95
+ database: databasePath,
96
+ apiKey: rawApiKey,
97
+ providers
98
+ });
99
+ const providerNames = Object.keys(providers).join(", ");
100
+ console.log(`
101
+ Config saved to ${getConfigPath()}`);
102
+ console.log(`Database created at ${databasePath}`);
103
+ console.log(`API key: ${rawApiKey}`);
104
+ console.log(`Providers: ${providerNames}`);
105
+ console.log('\nRun "canonry serve" to start the server.');
106
+ }
107
+
108
+ // src/commands/serve.ts
109
+ async function serveCommand() {
110
+ const config = loadConfig();
111
+ const port = parseInt(process.env.CANONRY_PORT ?? "4100", 10);
112
+ const db = createClient(config.database);
113
+ migrate(db);
114
+ const app = await createServer({ config, db });
115
+ try {
116
+ await app.listen({ host: "0.0.0.0", port });
117
+ console.log(`
118
+ Canonry server running at http://localhost:${port}`);
119
+ console.log("Press Ctrl+C to stop.\n");
120
+ } catch (err) {
121
+ app.log.error(err);
122
+ process.exit(1);
123
+ }
124
+ }
125
+
126
+ // src/client.ts
127
+ var ApiClient = class {
128
+ baseUrl;
129
+ apiKey;
130
+ constructor(baseUrl, apiKey) {
131
+ this.baseUrl = baseUrl.replace(/\/$/, "") + "/api/v1";
132
+ this.apiKey = apiKey;
133
+ }
134
+ async request(method, path2, body) {
135
+ const url = `${this.baseUrl}${path2}`;
136
+ const headers = {
137
+ "Authorization": `Bearer ${this.apiKey}`,
138
+ "Content-Type": "application/json"
139
+ };
140
+ const res = await fetch(url, {
141
+ method,
142
+ headers,
143
+ body: body != null ? JSON.stringify(body) : void 0
144
+ });
145
+ if (!res.ok) {
146
+ let errorBody;
147
+ try {
148
+ errorBody = await res.json();
149
+ } catch {
150
+ errorBody = { error: { code: "UNKNOWN", message: res.statusText } };
151
+ }
152
+ const msg = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error && typeof errorBody.error === "object" && "message" in errorBody.error ? String(errorBody.error.message) : `HTTP ${res.status}: ${res.statusText}`;
153
+ throw new Error(msg);
154
+ }
155
+ if (res.status === 204) {
156
+ return void 0;
157
+ }
158
+ return await res.json();
159
+ }
160
+ async putProject(name, body) {
161
+ return this.request("PUT", `/projects/${encodeURIComponent(name)}`, body);
162
+ }
163
+ async listProjects() {
164
+ return this.request("GET", "/projects");
165
+ }
166
+ async getProject(name) {
167
+ return this.request("GET", `/projects/${encodeURIComponent(name)}`);
168
+ }
169
+ async deleteProject(name) {
170
+ await this.request("DELETE", `/projects/${encodeURIComponent(name)}`);
171
+ }
172
+ async putKeywords(project, keywords) {
173
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/keywords`, { keywords });
174
+ }
175
+ async listKeywords(project) {
176
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/keywords`);
177
+ }
178
+ async appendKeywords(project, keywords) {
179
+ await this.request("POST", `/projects/${encodeURIComponent(project)}/keywords`, { keywords });
180
+ }
181
+ async putCompetitors(project, competitors) {
182
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/competitors`, { competitors });
183
+ }
184
+ async listCompetitors(project) {
185
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/competitors`);
186
+ }
187
+ async triggerRun(project, body) {
188
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/runs`, body ?? {});
189
+ }
190
+ async listRuns(project) {
191
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/runs`);
192
+ }
193
+ async getRun(id) {
194
+ return this.request("GET", `/runs/${encodeURIComponent(id)}`);
195
+ }
196
+ async getTimeline(project) {
197
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/timeline`);
198
+ }
199
+ async getHistory(project) {
200
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/history`);
201
+ }
202
+ async getExport(project) {
203
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/export`);
204
+ }
205
+ async apply(config) {
206
+ return this.request("POST", "/apply", config);
207
+ }
208
+ async getStatus(project) {
209
+ return this.request("GET", `/projects/${encodeURIComponent(project)}`);
210
+ }
211
+ async getSettings() {
212
+ return this.request("GET", "/settings");
213
+ }
214
+ async updateProvider(name, body) {
215
+ return this.request("PUT", `/settings/providers/${encodeURIComponent(name)}`, body);
216
+ }
217
+ async putSchedule(project, body) {
218
+ return this.request("PUT", `/projects/${encodeURIComponent(project)}/schedule`, body);
219
+ }
220
+ async getSchedule(project) {
221
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/schedule`);
222
+ }
223
+ async deleteSchedule(project) {
224
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/schedule`);
225
+ }
226
+ async createNotification(project, body) {
227
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications`, body);
228
+ }
229
+ async listNotifications(project) {
230
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/notifications`);
231
+ }
232
+ async deleteNotification(project, id) {
233
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}`);
234
+ }
235
+ async testNotification(project, id) {
236
+ return this.request("POST", `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}/test`);
237
+ }
238
+ };
239
+
240
+ // src/commands/project.ts
241
+ function getClient() {
242
+ const config = loadConfig();
243
+ return new ApiClient(config.apiUrl, config.apiKey);
244
+ }
245
+ async function createProject(name, opts) {
246
+ const client = getClient();
247
+ const result = await client.putProject(name, {
248
+ displayName: opts.displayName,
249
+ canonicalDomain: opts.domain,
250
+ country: opts.country,
251
+ language: opts.language
252
+ });
253
+ console.log(`Project created: ${result.name} (${result.id})`);
254
+ }
255
+ async function listProjects() {
256
+ const client = getClient();
257
+ const projects = await client.listProjects();
258
+ if (projects.length === 0) {
259
+ console.log("No projects found.");
260
+ return;
261
+ }
262
+ console.log("Projects:\n");
263
+ const nameWidth = Math.max(4, ...projects.map((p) => p.name.length));
264
+ const domainWidth = Math.max(6, ...projects.map((p) => p.canonicalDomain.length));
265
+ console.log(
266
+ ` ${"NAME".padEnd(nameWidth)} ${"DOMAIN".padEnd(domainWidth)} COUNTRY LANGUAGE`
267
+ );
268
+ console.log(` ${"\u2500".repeat(nameWidth)} ${"\u2500".repeat(domainWidth)} \u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
269
+ for (const p of projects) {
270
+ console.log(
271
+ ` ${p.name.padEnd(nameWidth)} ${p.canonicalDomain.padEnd(domainWidth)} ${p.country.padEnd(7)} ${p.language}`
272
+ );
273
+ }
274
+ }
275
+ async function showProject(name) {
276
+ const client = getClient();
277
+ const project = await client.getProject(name);
278
+ console.log(`Project: ${project.displayName}
279
+ `);
280
+ console.log(` Name: ${project.name}`);
281
+ console.log(` ID: ${project.id}`);
282
+ console.log(` Domain: ${project.canonicalDomain}`);
283
+ console.log(` Country: ${project.country}`);
284
+ console.log(` Language: ${project.language}`);
285
+ console.log(` Config source: ${project.configSource}`);
286
+ console.log(` Config revision: ${project.configRevision}`);
287
+ console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(", ") : "(none)"}`);
288
+ const labelEntries = Object.entries(project.labels);
289
+ console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
290
+ console.log(` Created: ${project.createdAt}`);
291
+ console.log(` Updated: ${project.updatedAt}`);
292
+ }
293
+ async function deleteProject(name) {
294
+ const client = getClient();
295
+ await client.deleteProject(name);
296
+ console.log(`Project deleted: ${name}`);
297
+ }
298
+
299
+ // src/commands/keyword.ts
300
+ import fs2 from "fs";
301
+ function getClient2() {
302
+ const config = loadConfig();
303
+ return new ApiClient(config.apiUrl, config.apiKey);
304
+ }
305
+ async function addKeywords(project, keywords) {
306
+ const client = getClient2();
307
+ await client.appendKeywords(project, keywords);
308
+ console.log(`Added ${keywords.length} keyword(s) to "${project}".`);
309
+ }
310
+ async function listKeywords(project) {
311
+ const client = getClient2();
312
+ const kws = await client.listKeywords(project);
313
+ if (kws.length === 0) {
314
+ console.log(`No keywords found for "${project}".`);
315
+ return;
316
+ }
317
+ console.log(`Keywords for "${project}" (${kws.length}):
318
+ `);
319
+ for (const kw of kws) {
320
+ console.log(` ${kw.keyword}`);
321
+ }
322
+ }
323
+ async function importKeywords(project, filePath) {
324
+ if (!fs2.existsSync(filePath)) {
325
+ throw new Error(`File not found: ${filePath}`);
326
+ }
327
+ const content = fs2.readFileSync(filePath, "utf-8");
328
+ const keywords = content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
329
+ if (keywords.length === 0) {
330
+ console.log("No keywords found in file.");
331
+ return;
332
+ }
333
+ const client = getClient2();
334
+ await client.appendKeywords(project, keywords);
335
+ console.log(`Imported ${keywords.length} keyword(s) to "${project}".`);
336
+ }
337
+
338
+ // src/commands/competitor.ts
339
+ function getClient3() {
340
+ const config = loadConfig();
341
+ return new ApiClient(config.apiUrl, config.apiKey);
342
+ }
343
+ async function addCompetitors(project, domains) {
344
+ const client = getClient3();
345
+ const existing = await client.listCompetitors(project);
346
+ const existingDomains = existing.map((c) => c.domain);
347
+ const allDomains = [.../* @__PURE__ */ new Set([...existingDomains, ...domains])];
348
+ await client.putCompetitors(project, allDomains);
349
+ console.log(`Added ${domains.length} competitor(s) to "${project}".`);
350
+ }
351
+ async function listCompetitors(project) {
352
+ const client = getClient3();
353
+ const comps = await client.listCompetitors(project);
354
+ if (comps.length === 0) {
355
+ console.log(`No competitors found for "${project}".`);
356
+ return;
357
+ }
358
+ console.log(`Competitors for "${project}" (${comps.length}):
359
+ `);
360
+ for (const c of comps) {
361
+ console.log(` ${c.domain}`);
362
+ }
363
+ }
364
+
365
+ // src/commands/run.ts
366
+ function getClient4() {
367
+ const config = loadConfig();
368
+ return new ApiClient(config.apiUrl, config.apiKey);
369
+ }
370
+ async function triggerRun(project, opts) {
371
+ const client = getClient4();
372
+ const body = {};
373
+ if (opts?.provider) {
374
+ body.providers = [opts.provider];
375
+ }
376
+ const run = await client.triggerRun(project, body);
377
+ console.log(`Run created: ${run.id}`);
378
+ console.log(` Kind: ${run.kind}`);
379
+ console.log(` Status: ${run.status}`);
380
+ if (opts?.provider) {
381
+ console.log(` Provider: ${opts.provider}`);
382
+ }
383
+ }
384
+ async function listRuns(project) {
385
+ const client = getClient4();
386
+ const runs = await client.listRuns(project);
387
+ if (runs.length === 0) {
388
+ console.log(`No runs found for "${project}".`);
389
+ return;
390
+ }
391
+ console.log(`Runs for "${project}" (${runs.length}):
392
+ `);
393
+ console.log(" ID STATUS KIND TRIGGER CREATED");
394
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
395
+ for (const run of runs) {
396
+ console.log(
397
+ ` ${run.id} ${run.status.padEnd(10)} ${run.kind.padEnd(18)} ${run.trigger.padEnd(9)} ${run.createdAt}`
398
+ );
399
+ }
400
+ }
401
+
402
+ // src/commands/status.ts
403
+ function getClient5() {
404
+ const config = loadConfig();
405
+ return new ApiClient(config.apiUrl, config.apiKey);
406
+ }
407
+ async function showStatus(project) {
408
+ const client = getClient5();
409
+ const projectData = await client.getProject(project);
410
+ console.log(`Status: ${projectData.displayName} (${projectData.name})
411
+ `);
412
+ console.log(` Domain: ${projectData.canonicalDomain}`);
413
+ console.log(` Country: ${projectData.country}`);
414
+ console.log(` Language: ${projectData.language}`);
415
+ try {
416
+ const runs = await client.listRuns(project);
417
+ if (runs.length > 0) {
418
+ const latest = runs[runs.length - 1];
419
+ console.log(`
420
+ Latest run:`);
421
+ console.log(` ID: ${latest.id}`);
422
+ console.log(` Status: ${latest.status}`);
423
+ console.log(` Created: ${latest.createdAt}`);
424
+ if (latest.finishedAt) {
425
+ console.log(` Finished: ${latest.finishedAt}`);
426
+ }
427
+ console.log(`
428
+ Total runs: ${runs.length}`);
429
+ } else {
430
+ console.log('\n No runs yet. Use "canonry run" to trigger one.');
431
+ }
432
+ } catch {
433
+ console.log("\n Run info unavailable.");
434
+ }
435
+ }
436
+
437
+ // src/commands/evidence.ts
438
+ function getClient6() {
439
+ const config = loadConfig();
440
+ return new ApiClient(config.apiUrl, config.apiKey);
441
+ }
442
+ async function showEvidence(project) {
443
+ const client = getClient6();
444
+ const timeline = await client.getTimeline(project);
445
+ if (timeline.length === 0) {
446
+ console.log('No keyword evidence yet. Trigger a run first with "canonry run".');
447
+ return;
448
+ }
449
+ console.log(`Evidence: ${project}
450
+ `);
451
+ for (const entry of timeline) {
452
+ const latest = entry.runs[entry.runs.length - 1];
453
+ if (!latest) continue;
454
+ const state = latest.citationState === "cited" ? "\u2713 cited" : "\u2717 not-cited";
455
+ const transition = latest.transition !== latest.citationState ? ` (${latest.transition})` : "";
456
+ console.log(` ${state}${transition} ${entry.keyword}`);
457
+ }
458
+ console.log(`
459
+ Keywords: ${timeline.length}`);
460
+ const cited = timeline.filter((e) => e.runs[e.runs.length - 1]?.citationState === "cited").length;
461
+ console.log(` Cited: ${cited} / ${timeline.length}`);
462
+ }
463
+
464
+ // src/commands/history.ts
465
+ function getClient7() {
466
+ const config = loadConfig();
467
+ return new ApiClient(config.apiUrl, config.apiKey);
468
+ }
469
+ async function showHistory(project) {
470
+ const client = getClient7();
471
+ try {
472
+ const entries = await client.getHistory(project);
473
+ if (entries.length === 0) {
474
+ console.log(`No audit history for "${project}".`);
475
+ return;
476
+ }
477
+ console.log(`Audit history for "${project}" (${entries.length}):
478
+ `);
479
+ console.log(" TIMESTAMP ACTION ENTITY TYPE ACTOR");
480
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500");
481
+ for (const entry of entries) {
482
+ console.log(
483
+ ` ${entry.createdAt.padEnd(23)} ${entry.action.padEnd(18)} ${entry.entityType.padEnd(11)} ${entry.actor}`
484
+ );
485
+ }
486
+ } catch (err) {
487
+ const message = err instanceof Error ? err.message : String(err);
488
+ console.error(`Failed to fetch history: ${message}`);
489
+ process.exit(1);
490
+ }
491
+ }
492
+
493
+ // src/commands/apply.ts
494
+ import fs3 from "fs";
495
+ import { parse } from "yaml";
496
+ async function applyConfig(filePath) {
497
+ if (!fs3.existsSync(filePath)) {
498
+ throw new Error(`File not found: ${filePath}`);
499
+ }
500
+ const content = fs3.readFileSync(filePath, "utf-8");
501
+ const config = parse(content);
502
+ const clientConfig = loadConfig();
503
+ const client = new ApiClient(clientConfig.apiUrl, clientConfig.apiKey);
504
+ const result = await client.apply(config);
505
+ console.log(`Applied config for "${result.name}" (revision ${result.configRevision})`);
506
+ }
507
+
508
+ // src/commands/export-cmd.ts
509
+ import { stringify } from "yaml";
510
+ async function exportProject(project, opts) {
511
+ const config = loadConfig();
512
+ const client = new ApiClient(config.apiUrl, config.apiKey);
513
+ const data = await client.getExport(project);
514
+ if (opts.includeResults) {
515
+ try {
516
+ const runs = await client.listRuns(project);
517
+ if (runs.length > 0) {
518
+ const latestRun = await client.getRun(runs[runs.length - 1].id);
519
+ data.results = latestRun;
520
+ }
521
+ } catch {
522
+ }
523
+ }
524
+ console.log(stringify(data));
525
+ }
526
+
527
+ // src/commands/settings.ts
528
+ function getClient8() {
529
+ const config = loadConfig();
530
+ return new ApiClient(config.apiUrl, config.apiKey);
531
+ }
532
+ async function setProvider(name, opts) {
533
+ const client = getClient8();
534
+ const result = await client.updateProvider(name, opts);
535
+ console.log(`Provider ${result.name} updated successfully.`);
536
+ if (result.model) {
537
+ console.log(` Model: ${result.model}`);
538
+ }
539
+ }
540
+ async function showSettings() {
541
+ const client = getClient8();
542
+ const settings = await client.getSettings();
543
+ console.log("Provider settings:\n");
544
+ for (const provider of settings.providers) {
545
+ const status = provider.configured ? "configured" : "not configured";
546
+ console.log(` ${provider.name.padEnd(10)} ${status}`);
547
+ if (provider.configured) {
548
+ console.log(` Model: ${provider.model ?? "(default)"}`);
549
+ if (provider.quota) {
550
+ console.log(` Quota: ${provider.quota.maxConcurrency} concurrent \xB7 ${provider.quota.maxRequestsPerMinute}/min \xB7 ${provider.quota.maxRequestsPerDay}/day`);
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ // src/commands/schedule.ts
557
+ function getClient9() {
558
+ const config = loadConfig();
559
+ return new ApiClient(config.apiUrl, config.apiKey);
560
+ }
561
+ async function setSchedule(project, opts) {
562
+ const client = getClient9();
563
+ const body = {};
564
+ if (opts.preset) body.preset = opts.preset;
565
+ if (opts.cron) body.cron = opts.cron;
566
+ if (opts.timezone) body.timezone = opts.timezone;
567
+ if (opts.providers?.length) body.providers = opts.providers;
568
+ const result = await client.putSchedule(project, body);
569
+ console.log(`Schedule set for "${project}":`);
570
+ printSchedule(result);
571
+ }
572
+ async function showSchedule(project) {
573
+ const client = getClient9();
574
+ const result = await client.getSchedule(project);
575
+ printSchedule(result);
576
+ }
577
+ async function enableSchedule(project) {
578
+ const client = getClient9();
579
+ const current = await client.getSchedule(project);
580
+ const body = { timezone: current.timezone };
581
+ if (current.preset) body.preset = current.preset;
582
+ else body.cron = current.cronExpr;
583
+ if (current.providers.length) body.providers = current.providers;
584
+ await client.putSchedule(project, body);
585
+ console.log(`Schedule enabled for "${project}"`);
586
+ }
587
+ async function disableSchedule(project) {
588
+ const client = getClient9();
589
+ const current = await client.getSchedule(project);
590
+ const body = { timezone: current.timezone, enabled: false };
591
+ if (current.preset) body.preset = current.preset;
592
+ else body.cron = current.cronExpr;
593
+ if (current.providers.length) body.providers = current.providers;
594
+ await client.putSchedule(project, body);
595
+ console.log(`Schedule disabled for "${project}"`);
596
+ }
597
+ async function removeSchedule(project) {
598
+ const client = getClient9();
599
+ await client.deleteSchedule(project);
600
+ console.log(`Schedule removed for "${project}"`);
601
+ }
602
+ function printSchedule(s) {
603
+ const label = s.preset ?? s.cronExpr;
604
+ console.log(` Schedule: ${label}`);
605
+ console.log(` Cron: ${s.cronExpr}`);
606
+ console.log(` Timezone: ${s.timezone}`);
607
+ console.log(` Enabled: ${s.enabled ? "yes" : "no"}`);
608
+ if (s.providers.length) {
609
+ console.log(` Providers: ${s.providers.join(", ")}`);
610
+ }
611
+ if (s.lastRunAt) {
612
+ console.log(` Last run: ${s.lastRunAt}`);
613
+ }
614
+ if (s.nextRunAt) {
615
+ console.log(` Next run: ${s.nextRunAt}`);
616
+ }
617
+ }
618
+
619
+ // src/commands/notify.ts
620
+ function getClient10() {
621
+ const config = loadConfig();
622
+ return new ApiClient(config.apiUrl, config.apiKey);
623
+ }
624
+ async function addNotification(project, opts) {
625
+ const client = getClient10();
626
+ const result = await client.createNotification(project, {
627
+ channel: "webhook",
628
+ url: opts.webhook,
629
+ events: opts.events
630
+ });
631
+ console.log(`Notification created for "${project}":`);
632
+ printNotification(result);
633
+ }
634
+ async function listNotifications(project) {
635
+ const client = getClient10();
636
+ const results = await client.listNotifications(project);
637
+ if (results.length === 0) {
638
+ console.log(`No notifications configured for "${project}"`);
639
+ return;
640
+ }
641
+ console.log(`Notifications for "${project}":
642
+ `);
643
+ for (const n of results) {
644
+ printNotification(n);
645
+ console.log();
646
+ }
647
+ }
648
+ async function removeNotification(project, id) {
649
+ const client = getClient10();
650
+ await client.deleteNotification(project, id);
651
+ console.log(`Notification ${id} removed from "${project}"`);
652
+ }
653
+ async function testNotification(project, id) {
654
+ const client = getClient10();
655
+ const result = await client.testNotification(project, id);
656
+ if (result.ok) {
657
+ console.log(`Test webhook delivered successfully (HTTP ${result.status})`);
658
+ } else {
659
+ console.error(`Test webhook failed: HTTP ${result.status}`);
660
+ }
661
+ }
662
+ function printNotification(n) {
663
+ console.log(` ID: ${n.id}`);
664
+ console.log(` Channel: ${n.channel}`);
665
+ console.log(` URL: ${n.url}`);
666
+ console.log(` Events: ${n.events.join(", ")}`);
667
+ console.log(` Enabled: ${n.enabled ? "yes" : "no"}`);
668
+ }
669
+
670
+ // src/cli.ts
671
+ var USAGE = `
672
+ canonry \u2014 AEO monitoring CLI
673
+
674
+ Usage:
675
+ canonry init Initialize config and database
676
+ canonry serve Start the local server
677
+ canonry project create <name> Create a project
678
+ canonry project list List all projects
679
+ canonry project show <name> Show project details
680
+ canonry project delete <name> Delete a project
681
+ canonry keyword add <project> <kw> Add keywords to a project
682
+ canonry keyword list <project> List keywords for a project
683
+ canonry keyword import <project> <file> Import keywords from file
684
+ canonry competitor add <project> <domain> Add competitors
685
+ canonry competitor list <project> List competitors
686
+ canonry run <project> Trigger a run (all providers)
687
+ canonry run <project> --provider <name> Trigger a run for a specific provider
688
+ canonry runs <project> List runs for a project
689
+ canonry status <project> Show project summary
690
+ canonry evidence <project> Show keyword-level results
691
+ canonry history <project> Show audit trail
692
+ canonry export <project> Export project as YAML
693
+ canonry apply <file> Apply declarative config
694
+ canonry schedule set <project> Set schedule (--preset or --cron)
695
+ canonry schedule show <project> Show schedule
696
+ canonry schedule enable <project> Enable schedule
697
+ canonry schedule disable <project> Disable schedule
698
+ canonry schedule remove <project> Remove schedule
699
+ canonry notify add <project> Add webhook notification
700
+ canonry notify list <project> List notifications
701
+ canonry notify remove <project> <id> Remove notification
702
+ canonry notify test <project> <id> Send test webhook
703
+ canonry settings Show active provider and quota settings
704
+ canonry settings provider <name> Update a provider config (--api-key, --base-url, --model)
705
+ canonry --help Show this help
706
+ canonry --version Show version
707
+
708
+ Options:
709
+ --port <port> Server port (default: 4100)
710
+ --domain <domain> Canonical domain for project create
711
+ --country <code> Country code (default: US)
712
+ --language <lang> Language code (default: en)
713
+ --provider <name> Provider to use (gemini, openai, claude)
714
+ --include-results Include results in export
715
+ --preset <preset> Schedule preset (daily, weekly, twice-daily, daily@HH, weekly@DAY)
716
+ --cron <expr> Cron expression for schedule
717
+ --timezone <tz> IANA timezone for schedule (default: UTC)
718
+ --webhook <url> Webhook URL for notifications
719
+ --events <list> Comma-separated notification events
720
+ `.trim();
721
+ var VERSION = "0.1.0";
722
+ async function main() {
723
+ const args = process.argv.slice(2);
724
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
725
+ console.log(USAGE);
726
+ return;
727
+ }
728
+ if (args.includes("--version") || args.includes("-v")) {
729
+ console.log(VERSION);
730
+ return;
731
+ }
732
+ const command = args[0];
733
+ try {
734
+ switch (command) {
735
+ case "init":
736
+ await initCommand();
737
+ break;
738
+ case "serve": {
739
+ const { values } = parseArgs({
740
+ args: args.slice(1),
741
+ options: {
742
+ port: { type: "string", short: "p", default: "4100" }
743
+ },
744
+ allowPositionals: false
745
+ });
746
+ process.env.CANONRY_PORT = values.port;
747
+ await serveCommand();
748
+ break;
749
+ }
750
+ case "project": {
751
+ const subcommand = args[1];
752
+ switch (subcommand) {
753
+ case "create": {
754
+ const name = args[2];
755
+ if (!name) {
756
+ console.error("Error: project name is required");
757
+ process.exit(1);
758
+ }
759
+ const { values } = parseArgs({
760
+ args: args.slice(3),
761
+ options: {
762
+ domain: { type: "string", short: "d" },
763
+ country: { type: "string", default: "US" },
764
+ language: { type: "string", default: "en" },
765
+ "display-name": { type: "string" }
766
+ },
767
+ allowPositionals: false
768
+ });
769
+ await createProject(name, {
770
+ domain: values.domain ?? name,
771
+ country: values.country ?? "US",
772
+ language: values.language ?? "en",
773
+ displayName: values["display-name"] ?? name
774
+ });
775
+ break;
776
+ }
777
+ case "list":
778
+ await listProjects();
779
+ break;
780
+ case "show": {
781
+ const name = args[2];
782
+ if (!name) {
783
+ console.error("Error: project name is required");
784
+ process.exit(1);
785
+ }
786
+ await showProject(name);
787
+ break;
788
+ }
789
+ case "delete": {
790
+ const name = args[2];
791
+ if (!name) {
792
+ console.error("Error: project name is required");
793
+ process.exit(1);
794
+ }
795
+ await deleteProject(name);
796
+ break;
797
+ }
798
+ default:
799
+ console.error(`Unknown project subcommand: ${subcommand ?? "(none)"}`);
800
+ console.log("Available: create, list, show, delete");
801
+ process.exit(1);
802
+ }
803
+ break;
804
+ }
805
+ case "keyword": {
806
+ const subcommand = args[1];
807
+ switch (subcommand) {
808
+ case "add": {
809
+ const project = args[2];
810
+ const kws = args.slice(3);
811
+ if (!project || kws.length === 0) {
812
+ console.error("Error: project name and at least one keyword required");
813
+ process.exit(1);
814
+ }
815
+ await addKeywords(project, kws);
816
+ break;
817
+ }
818
+ case "list": {
819
+ const project = args[2];
820
+ if (!project) {
821
+ console.error("Error: project name is required");
822
+ process.exit(1);
823
+ }
824
+ await listKeywords(project);
825
+ break;
826
+ }
827
+ case "import": {
828
+ const project = args[2];
829
+ const filePath = args[3];
830
+ if (!project || !filePath) {
831
+ console.error("Error: project name and file path required");
832
+ process.exit(1);
833
+ }
834
+ await importKeywords(project, filePath);
835
+ break;
836
+ }
837
+ default:
838
+ console.error(`Unknown keyword subcommand: ${subcommand ?? "(none)"}`);
839
+ console.log("Available: add, list, import");
840
+ process.exit(1);
841
+ }
842
+ break;
843
+ }
844
+ case "competitor": {
845
+ const subcommand = args[1];
846
+ switch (subcommand) {
847
+ case "add": {
848
+ const project = args[2];
849
+ const domains = args.slice(3);
850
+ if (!project || domains.length === 0) {
851
+ console.error("Error: project name and at least one domain required");
852
+ process.exit(1);
853
+ }
854
+ await addCompetitors(project, domains);
855
+ break;
856
+ }
857
+ case "list": {
858
+ const project = args[2];
859
+ if (!project) {
860
+ console.error("Error: project name is required");
861
+ process.exit(1);
862
+ }
863
+ await listCompetitors(project);
864
+ break;
865
+ }
866
+ default:
867
+ console.error(`Unknown competitor subcommand: ${subcommand ?? "(none)"}`);
868
+ console.log("Available: add, list");
869
+ process.exit(1);
870
+ }
871
+ break;
872
+ }
873
+ case "run": {
874
+ const project = args[1];
875
+ if (!project) {
876
+ console.error("Error: project name is required");
877
+ process.exit(1);
878
+ }
879
+ const runParsed = parseArgs({
880
+ args: args.slice(2),
881
+ options: {
882
+ provider: { type: "string" }
883
+ },
884
+ allowPositionals: false
885
+ });
886
+ await triggerRun(project, { provider: runParsed.values.provider });
887
+ break;
888
+ }
889
+ case "runs": {
890
+ const project = args[1];
891
+ if (!project) {
892
+ console.error("Error: project name is required");
893
+ process.exit(1);
894
+ }
895
+ await listRuns(project);
896
+ break;
897
+ }
898
+ case "status": {
899
+ const project = args[1];
900
+ if (!project) {
901
+ console.error("Error: project name is required");
902
+ process.exit(1);
903
+ }
904
+ await showStatus(project);
905
+ break;
906
+ }
907
+ case "evidence": {
908
+ const project = args[1];
909
+ if (!project) {
910
+ console.error("Error: project name is required");
911
+ process.exit(1);
912
+ }
913
+ await showEvidence(project);
914
+ break;
915
+ }
916
+ case "history": {
917
+ const project = args[1];
918
+ if (!project) {
919
+ console.error("Error: project name is required");
920
+ process.exit(1);
921
+ }
922
+ await showHistory(project);
923
+ break;
924
+ }
925
+ case "export": {
926
+ const project = args[1];
927
+ if (!project) {
928
+ console.error("Error: project name is required");
929
+ process.exit(1);
930
+ }
931
+ const includeResults = args.includes("--include-results");
932
+ await exportProject(project, { includeResults });
933
+ break;
934
+ }
935
+ case "apply": {
936
+ const filePath = args[1];
937
+ if (!filePath) {
938
+ console.error("Error: file path is required");
939
+ process.exit(1);
940
+ }
941
+ await applyConfig(filePath);
942
+ break;
943
+ }
944
+ case "schedule": {
945
+ const subcommand = args[1];
946
+ const schedProject = args[2];
947
+ if (!schedProject && subcommand !== void 0) {
948
+ console.error("Error: project name is required");
949
+ process.exit(1);
950
+ }
951
+ switch (subcommand) {
952
+ case "set": {
953
+ const { values } = parseArgs({
954
+ args: args.slice(3),
955
+ options: {
956
+ preset: { type: "string" },
957
+ cron: { type: "string" },
958
+ timezone: { type: "string" },
959
+ provider: { type: "string", multiple: true }
960
+ },
961
+ allowPositionals: false
962
+ });
963
+ if (!values.preset && !values.cron) {
964
+ console.error("Error: --preset or --cron is required");
965
+ process.exit(1);
966
+ }
967
+ await setSchedule(schedProject, {
968
+ preset: values.preset,
969
+ cron: values.cron,
970
+ timezone: values.timezone,
971
+ providers: values.provider
972
+ });
973
+ break;
974
+ }
975
+ case "show":
976
+ await showSchedule(schedProject);
977
+ break;
978
+ case "enable":
979
+ await enableSchedule(schedProject);
980
+ break;
981
+ case "disable":
982
+ await disableSchedule(schedProject);
983
+ break;
984
+ case "remove":
985
+ await removeSchedule(schedProject);
986
+ break;
987
+ default:
988
+ console.error(`Unknown schedule subcommand: ${subcommand ?? "(none)"}`);
989
+ console.log("Available: set, show, enable, disable, remove");
990
+ process.exit(1);
991
+ }
992
+ break;
993
+ }
994
+ case "notify": {
995
+ const notifSubcommand = args[1];
996
+ const notifProject = args[2];
997
+ if (!notifProject && notifSubcommand !== void 0) {
998
+ console.error("Error: project name is required");
999
+ process.exit(1);
1000
+ }
1001
+ switch (notifSubcommand) {
1002
+ case "add": {
1003
+ const { values } = parseArgs({
1004
+ args: args.slice(3),
1005
+ options: {
1006
+ webhook: { type: "string" },
1007
+ events: { type: "string" }
1008
+ },
1009
+ allowPositionals: false
1010
+ });
1011
+ if (!values.webhook) {
1012
+ console.error("Error: --webhook is required");
1013
+ process.exit(1);
1014
+ }
1015
+ if (!values.events) {
1016
+ console.error("Error: --events is required (comma-separated)");
1017
+ process.exit(1);
1018
+ }
1019
+ await addNotification(notifProject, {
1020
+ webhook: values.webhook,
1021
+ events: values.events.split(",").map((e) => e.trim())
1022
+ });
1023
+ break;
1024
+ }
1025
+ case "list":
1026
+ await listNotifications(notifProject);
1027
+ break;
1028
+ case "remove": {
1029
+ const notifId = args[3];
1030
+ if (!notifId) {
1031
+ console.error("Error: notification ID is required");
1032
+ process.exit(1);
1033
+ }
1034
+ await removeNotification(notifProject, notifId);
1035
+ break;
1036
+ }
1037
+ case "test": {
1038
+ const testId = args[3];
1039
+ if (!testId) {
1040
+ console.error("Error: notification ID is required");
1041
+ process.exit(1);
1042
+ }
1043
+ await testNotification(notifProject, testId);
1044
+ break;
1045
+ }
1046
+ default:
1047
+ console.error(`Unknown notify subcommand: ${notifSubcommand ?? "(none)"}`);
1048
+ console.log("Available: add, list, remove, test");
1049
+ process.exit(1);
1050
+ }
1051
+ break;
1052
+ }
1053
+ case "settings": {
1054
+ const subcommand = args[1];
1055
+ if (subcommand === "provider") {
1056
+ const name = args[2];
1057
+ if (!name) {
1058
+ console.error("Error: provider name is required (gemini, openai, claude, local)");
1059
+ process.exit(1);
1060
+ }
1061
+ const { values } = parseArgs({
1062
+ args: args.slice(3),
1063
+ options: {
1064
+ "api-key": { type: "string" },
1065
+ "base-url": { type: "string" },
1066
+ model: { type: "string" }
1067
+ },
1068
+ allowPositionals: false
1069
+ });
1070
+ if (name === "local") {
1071
+ if (!values["base-url"]) {
1072
+ console.error("Error: --base-url is required for the local provider");
1073
+ process.exit(1);
1074
+ }
1075
+ } else {
1076
+ if (!values["api-key"]) {
1077
+ console.error("Error: --api-key is required");
1078
+ process.exit(1);
1079
+ }
1080
+ }
1081
+ await setProvider(name, { apiKey: values["api-key"], baseUrl: values["base-url"], model: values.model });
1082
+ } else {
1083
+ await showSettings();
1084
+ }
1085
+ break;
1086
+ }
1087
+ default:
1088
+ console.error(`Unknown command: ${command}`);
1089
+ console.log('Run "canonry --help" for usage.');
1090
+ process.exit(1);
1091
+ }
1092
+ } catch (err) {
1093
+ if (err instanceof Error) {
1094
+ console.error(`Error: ${err.message}`);
1095
+ } else {
1096
+ console.error("An unexpected error occurred");
1097
+ }
1098
+ process.exit(1);
1099
+ }
1100
+ }
1101
+ main();