@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/LICENSE +661 -0
- package/assets/assets/index-CkNSldWM.css +1 -0
- package/assets/assets/index-DHoyZdlF.js +63 -0
- package/assets/index.html +17 -0
- package/bin/canonry.mjs +2 -0
- package/dist/chunk-ONZDY6Q4.js +3706 -0
- package/dist/cli.js +1101 -0
- package/dist/index.js +8 -0
- package/package.json +58 -0
- package/src/cli.ts +470 -0
- package/src/client.ts +152 -0
- package/src/commands/apply.ts +25 -0
- package/src/commands/competitor.ts +36 -0
- package/src/commands/evidence.ts +41 -0
- package/src/commands/export-cmd.ts +40 -0
- package/src/commands/history.ts +41 -0
- package/src/commands/init.ts +122 -0
- package/src/commands/keyword.ts +54 -0
- package/src/commands/notify.ts +70 -0
- package/src/commands/project.ts +89 -0
- package/src/commands/run.ts +54 -0
- package/src/commands/schedule.ts +90 -0
- package/src/commands/serve.ts +24 -0
- package/src/commands/settings.ts +45 -0
- package/src/commands/status.ts +52 -0
- package/src/config.ts +90 -0
- package/src/index.ts +2 -0
- package/src/job-runner.ts +368 -0
- package/src/notifier.ts +227 -0
- package/src/provider-registry.ts +55 -0
- package/src/scheduler.ts +161 -0
- package/src/server.ts +249 -0
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();
|