@ainyc/canonry 1.0.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts DELETED
@@ -1,470 +0,0 @@
1
- #!/usr/bin/env node --import tsx
2
- import { parseArgs } from 'node:util'
3
- import { initCommand } from './commands/init.js'
4
- import { serveCommand } from './commands/serve.js'
5
- import { createProject, listProjects, showProject, deleteProject } from './commands/project.js'
6
- import { addKeywords, listKeywords, importKeywords } from './commands/keyword.js'
7
- import { addCompetitors, listCompetitors } from './commands/competitor.js'
8
- import { triggerRun, listRuns } from './commands/run.js'
9
- import { showStatus } from './commands/status.js'
10
- import { showEvidence } from './commands/evidence.js'
11
- import { showHistory } from './commands/history.js'
12
- import { applyConfig } from './commands/apply.js'
13
- import { exportProject } from './commands/export-cmd.js'
14
- import { showSettings, setProvider } from './commands/settings.js'
15
- import { setSchedule, showSchedule, enableSchedule, disableSchedule, removeSchedule } from './commands/schedule.js'
16
- import { addNotification, listNotifications, removeNotification, testNotification } from './commands/notify.js'
17
-
18
- const USAGE = `
19
- canonry — AEO monitoring CLI
20
-
21
- Usage:
22
- canonry init Initialize config and database
23
- canonry serve Start the local server
24
- canonry project create <name> Create a project
25
- canonry project list List all projects
26
- canonry project show <name> Show project details
27
- canonry project delete <name> Delete a project
28
- canonry keyword add <project> <kw> Add keywords to a project
29
- canonry keyword list <project> List keywords for a project
30
- canonry keyword import <project> <file> Import keywords from file
31
- canonry competitor add <project> <domain> Add competitors
32
- canonry competitor list <project> List competitors
33
- canonry run <project> Trigger a run (all providers)
34
- canonry run <project> --provider <name> Trigger a run for a specific provider
35
- canonry runs <project> List runs for a project
36
- canonry status <project> Show project summary
37
- canonry evidence <project> Show keyword-level results
38
- canonry history <project> Show audit trail
39
- canonry export <project> Export project as YAML
40
- canonry apply <file> Apply declarative config
41
- canonry schedule set <project> Set schedule (--preset or --cron)
42
- canonry schedule show <project> Show schedule
43
- canonry schedule enable <project> Enable schedule
44
- canonry schedule disable <project> Disable schedule
45
- canonry schedule remove <project> Remove schedule
46
- canonry notify add <project> Add webhook notification
47
- canonry notify list <project> List notifications
48
- canonry notify remove <project> <id> Remove notification
49
- canonry notify test <project> <id> Send test webhook
50
- canonry settings Show active provider and quota settings
51
- canonry settings provider <name> Update a provider config (--api-key, --base-url, --model)
52
- canonry --help Show this help
53
- canonry --version Show version
54
-
55
- Options:
56
- --port <port> Server port (default: 4100)
57
- --domain <domain> Canonical domain for project create
58
- --country <code> Country code (default: US)
59
- --language <lang> Language code (default: en)
60
- --provider <name> Provider to use (gemini, openai, claude)
61
- --include-results Include results in export
62
- --preset <preset> Schedule preset (daily, weekly, twice-daily, daily@HH, weekly@DAY)
63
- --cron <expr> Cron expression for schedule
64
- --timezone <tz> IANA timezone for schedule (default: UTC)
65
- --webhook <url> Webhook URL for notifications
66
- --events <list> Comma-separated notification events
67
- `.trim()
68
-
69
- const VERSION = '0.1.0'
70
-
71
- async function main() {
72
- const args = process.argv.slice(2)
73
-
74
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
75
- console.log(USAGE)
76
- return
77
- }
78
-
79
- if (args.includes('--version') || args.includes('-v')) {
80
- console.log(VERSION)
81
- return
82
- }
83
-
84
- const command = args[0]!
85
-
86
- try {
87
- switch (command) {
88
- case 'init':
89
- await initCommand()
90
- break
91
-
92
- case 'serve': {
93
- const { values } = parseArgs({
94
- args: args.slice(1),
95
- options: {
96
- port: { type: 'string', short: 'p', default: '4100' },
97
- },
98
- allowPositionals: false,
99
- })
100
- process.env.CANONRY_PORT = values.port
101
- await serveCommand()
102
- break
103
- }
104
-
105
- case 'project': {
106
- const subcommand = args[1]
107
- switch (subcommand) {
108
- case 'create': {
109
- const name = args[2]
110
- if (!name) {
111
- console.error('Error: project name is required')
112
- process.exit(1)
113
- }
114
- const { values } = parseArgs({
115
- args: args.slice(3),
116
- options: {
117
- domain: { type: 'string', short: 'd' },
118
- country: { type: 'string', default: 'US' },
119
- language: { type: 'string', default: 'en' },
120
- 'display-name': { type: 'string' },
121
- },
122
- allowPositionals: false,
123
- })
124
- await createProject(name, {
125
- domain: values.domain ?? name,
126
- country: values.country ?? 'US',
127
- language: values.language ?? 'en',
128
- displayName: values['display-name'] ?? name,
129
- })
130
- break
131
- }
132
- case 'list':
133
- await listProjects()
134
- break
135
- case 'show': {
136
- const name = args[2]
137
- if (!name) {
138
- console.error('Error: project name is required')
139
- process.exit(1)
140
- }
141
- await showProject(name)
142
- break
143
- }
144
- case 'delete': {
145
- const name = args[2]
146
- if (!name) {
147
- console.error('Error: project name is required')
148
- process.exit(1)
149
- }
150
- await deleteProject(name)
151
- break
152
- }
153
- default:
154
- console.error(`Unknown project subcommand: ${subcommand ?? '(none)'}`)
155
- console.log('Available: create, list, show, delete')
156
- process.exit(1)
157
- }
158
- break
159
- }
160
-
161
- case 'keyword': {
162
- const subcommand = args[1]
163
- switch (subcommand) {
164
- case 'add': {
165
- const project = args[2]
166
- const kws = args.slice(3)
167
- if (!project || kws.length === 0) {
168
- console.error('Error: project name and at least one keyword required')
169
- process.exit(1)
170
- }
171
- await addKeywords(project, kws)
172
- break
173
- }
174
- case 'list': {
175
- const project = args[2]
176
- if (!project) {
177
- console.error('Error: project name is required')
178
- process.exit(1)
179
- }
180
- await listKeywords(project)
181
- break
182
- }
183
- case 'import': {
184
- const project = args[2]
185
- const filePath = args[3]
186
- if (!project || !filePath) {
187
- console.error('Error: project name and file path required')
188
- process.exit(1)
189
- }
190
- await importKeywords(project, filePath)
191
- break
192
- }
193
- default:
194
- console.error(`Unknown keyword subcommand: ${subcommand ?? '(none)'}`)
195
- console.log('Available: add, list, import')
196
- process.exit(1)
197
- }
198
- break
199
- }
200
-
201
- case 'competitor': {
202
- const subcommand = args[1]
203
- switch (subcommand) {
204
- case 'add': {
205
- const project = args[2]
206
- const domains = args.slice(3)
207
- if (!project || domains.length === 0) {
208
- console.error('Error: project name and at least one domain required')
209
- process.exit(1)
210
- }
211
- await addCompetitors(project, domains)
212
- break
213
- }
214
- case 'list': {
215
- const project = args[2]
216
- if (!project) {
217
- console.error('Error: project name is required')
218
- process.exit(1)
219
- }
220
- await listCompetitors(project)
221
- break
222
- }
223
- default:
224
- console.error(`Unknown competitor subcommand: ${subcommand ?? '(none)'}`)
225
- console.log('Available: add, list')
226
- process.exit(1)
227
- }
228
- break
229
- }
230
-
231
- case 'run': {
232
- const project = args[1]
233
- if (!project) {
234
- console.error('Error: project name is required')
235
- process.exit(1)
236
- }
237
- const runParsed = parseArgs({
238
- args: args.slice(2),
239
- options: {
240
- provider: { type: 'string' },
241
- },
242
- allowPositionals: false,
243
- })
244
- await triggerRun(project, { provider: runParsed.values.provider })
245
- break
246
- }
247
-
248
- case 'runs': {
249
- const project = args[1]
250
- if (!project) {
251
- console.error('Error: project name is required')
252
- process.exit(1)
253
- }
254
- await listRuns(project)
255
- break
256
- }
257
-
258
- case 'status': {
259
- const project = args[1]
260
- if (!project) {
261
- console.error('Error: project name is required')
262
- process.exit(1)
263
- }
264
- await showStatus(project)
265
- break
266
- }
267
-
268
- case 'evidence': {
269
- const project = args[1]
270
- if (!project) {
271
- console.error('Error: project name is required')
272
- process.exit(1)
273
- }
274
- await showEvidence(project)
275
- break
276
- }
277
-
278
- case 'history': {
279
- const project = args[1]
280
- if (!project) {
281
- console.error('Error: project name is required')
282
- process.exit(1)
283
- }
284
- await showHistory(project)
285
- break
286
- }
287
-
288
- case 'export': {
289
- const project = args[1]
290
- if (!project) {
291
- console.error('Error: project name is required')
292
- process.exit(1)
293
- }
294
- const includeResults = args.includes('--include-results')
295
- await exportProject(project, { includeResults })
296
- break
297
- }
298
-
299
- case 'apply': {
300
- const filePath = args[1]
301
- if (!filePath) {
302
- console.error('Error: file path is required')
303
- process.exit(1)
304
- }
305
- await applyConfig(filePath)
306
- break
307
- }
308
-
309
- case 'schedule': {
310
- const subcommand = args[1]
311
- const schedProject = args[2]
312
- if (!schedProject && subcommand !== undefined) {
313
- console.error('Error: project name is required')
314
- process.exit(1)
315
- }
316
- switch (subcommand) {
317
- case 'set': {
318
- const { values } = parseArgs({
319
- args: args.slice(3),
320
- options: {
321
- preset: { type: 'string' },
322
- cron: { type: 'string' },
323
- timezone: { type: 'string' },
324
- provider: { type: 'string', multiple: true },
325
- },
326
- allowPositionals: false,
327
- })
328
- if (!values.preset && !values.cron) {
329
- console.error('Error: --preset or --cron is required')
330
- process.exit(1)
331
- }
332
- await setSchedule(schedProject!, {
333
- preset: values.preset,
334
- cron: values.cron,
335
- timezone: values.timezone,
336
- providers: values.provider,
337
- })
338
- break
339
- }
340
- case 'show':
341
- await showSchedule(schedProject!)
342
- break
343
- case 'enable':
344
- await enableSchedule(schedProject!)
345
- break
346
- case 'disable':
347
- await disableSchedule(schedProject!)
348
- break
349
- case 'remove':
350
- await removeSchedule(schedProject!)
351
- break
352
- default:
353
- console.error(`Unknown schedule subcommand: ${subcommand ?? '(none)'}`)
354
- console.log('Available: set, show, enable, disable, remove')
355
- process.exit(1)
356
- }
357
- break
358
- }
359
-
360
- case 'notify': {
361
- const notifSubcommand = args[1]
362
- const notifProject = args[2]
363
- if (!notifProject && notifSubcommand !== undefined) {
364
- console.error('Error: project name is required')
365
- process.exit(1)
366
- }
367
- switch (notifSubcommand) {
368
- case 'add': {
369
- const { values } = parseArgs({
370
- args: args.slice(3),
371
- options: {
372
- webhook: { type: 'string' },
373
- events: { type: 'string' },
374
- },
375
- allowPositionals: false,
376
- })
377
- if (!values.webhook) {
378
- console.error('Error: --webhook is required')
379
- process.exit(1)
380
- }
381
- if (!values.events) {
382
- console.error('Error: --events is required (comma-separated)')
383
- process.exit(1)
384
- }
385
- await addNotification(notifProject!, {
386
- webhook: values.webhook,
387
- events: values.events.split(',').map(e => e.trim()),
388
- })
389
- break
390
- }
391
- case 'list':
392
- await listNotifications(notifProject!)
393
- break
394
- case 'remove': {
395
- const notifId = args[3]
396
- if (!notifId) {
397
- console.error('Error: notification ID is required')
398
- process.exit(1)
399
- }
400
- await removeNotification(notifProject!, notifId)
401
- break
402
- }
403
- case 'test': {
404
- const testId = args[3]
405
- if (!testId) {
406
- console.error('Error: notification ID is required')
407
- process.exit(1)
408
- }
409
- await testNotification(notifProject!, testId)
410
- break
411
- }
412
- default:
413
- console.error(`Unknown notify subcommand: ${notifSubcommand ?? '(none)'}`)
414
- console.log('Available: add, list, remove, test')
415
- process.exit(1)
416
- }
417
- break
418
- }
419
-
420
- case 'settings': {
421
- const subcommand = args[1]
422
- if (subcommand === 'provider') {
423
- const name = args[2]
424
- if (!name) {
425
- console.error('Error: provider name is required (gemini, openai, claude, local)')
426
- process.exit(1)
427
- }
428
- const { values } = parseArgs({
429
- args: args.slice(3),
430
- options: {
431
- 'api-key': { type: 'string' },
432
- 'base-url': { type: 'string' },
433
- model: { type: 'string' },
434
- },
435
- allowPositionals: false,
436
- })
437
- if (name === 'local') {
438
- if (!values['base-url']) {
439
- console.error('Error: --base-url is required for the local provider')
440
- process.exit(1)
441
- }
442
- } else {
443
- if (!values['api-key']) {
444
- console.error('Error: --api-key is required')
445
- process.exit(1)
446
- }
447
- }
448
- await setProvider(name, { apiKey: values['api-key'], baseUrl: values['base-url'], model: values.model })
449
- } else {
450
- await showSettings()
451
- }
452
- break
453
- }
454
-
455
- default:
456
- console.error(`Unknown command: ${command}`)
457
- console.log('Run "canonry --help" for usage.')
458
- process.exit(1)
459
- }
460
- } catch (err: unknown) {
461
- if (err instanceof Error) {
462
- console.error(`Error: ${err.message}`)
463
- } else {
464
- console.error('An unexpected error occurred')
465
- }
466
- process.exit(1)
467
- }
468
- }
469
-
470
- main()
package/src/client.ts DELETED
@@ -1,152 +0,0 @@
1
- export class ApiClient {
2
- private baseUrl: string
3
- private apiKey: string
4
-
5
- constructor(baseUrl: string, apiKey: string) {
6
- this.baseUrl = baseUrl.replace(/\/$/, '') + '/api/v1'
7
- this.apiKey = apiKey
8
- }
9
-
10
- private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
11
- const url = `${this.baseUrl}${path}`
12
- const headers: Record<string, string> = {
13
- 'Authorization': `Bearer ${this.apiKey}`,
14
- 'Content-Type': 'application/json',
15
- }
16
-
17
- const res = await fetch(url, {
18
- method,
19
- headers,
20
- body: body != null ? JSON.stringify(body) : undefined,
21
- })
22
-
23
- if (!res.ok) {
24
- let errorBody: unknown
25
- try {
26
- errorBody = await res.json()
27
- } catch {
28
- errorBody = { error: { code: 'UNKNOWN', message: res.statusText } }
29
- }
30
- const msg =
31
- errorBody &&
32
- typeof errorBody === 'object' &&
33
- 'error' in errorBody &&
34
- errorBody.error &&
35
- typeof errorBody.error === 'object' &&
36
- 'message' in errorBody.error
37
- ? String((errorBody.error as { message: string }).message)
38
- : `HTTP ${res.status}: ${res.statusText}`
39
- throw new Error(msg)
40
- }
41
-
42
- if (res.status === 204) {
43
- return undefined as T
44
- }
45
-
46
- return (await res.json()) as T
47
- }
48
-
49
- async putProject(name: string, body: object): Promise<object> {
50
- return this.request<object>('PUT', `/projects/${encodeURIComponent(name)}`, body)
51
- }
52
-
53
- async listProjects(): Promise<object[]> {
54
- return this.request<object[]>('GET', '/projects')
55
- }
56
-
57
- async getProject(name: string): Promise<object> {
58
- return this.request<object>('GET', `/projects/${encodeURIComponent(name)}`)
59
- }
60
-
61
- async deleteProject(name: string): Promise<void> {
62
- await this.request<void>('DELETE', `/projects/${encodeURIComponent(name)}`)
63
- }
64
-
65
- async putKeywords(project: string, keywords: string[]): Promise<void> {
66
- await this.request<unknown>('PUT', `/projects/${encodeURIComponent(project)}/keywords`, { keywords })
67
- }
68
-
69
- async listKeywords(project: string): Promise<object[]> {
70
- return this.request<object[]>('GET', `/projects/${encodeURIComponent(project)}/keywords`)
71
- }
72
-
73
- async appendKeywords(project: string, keywords: string[]): Promise<void> {
74
- await this.request<unknown>('POST', `/projects/${encodeURIComponent(project)}/keywords`, { keywords })
75
- }
76
-
77
- async putCompetitors(project: string, competitors: string[]): Promise<void> {
78
- await this.request<unknown>('PUT', `/projects/${encodeURIComponent(project)}/competitors`, { competitors })
79
- }
80
-
81
- async listCompetitors(project: string): Promise<object[]> {
82
- return this.request<object[]>('GET', `/projects/${encodeURIComponent(project)}/competitors`)
83
- }
84
-
85
- async triggerRun(project: string, body?: Record<string, unknown>): Promise<object> {
86
- return this.request<object>('POST', `/projects/${encodeURIComponent(project)}/runs`, body ?? {})
87
- }
88
-
89
- async listRuns(project: string): Promise<object[]> {
90
- return this.request<object[]>('GET', `/projects/${encodeURIComponent(project)}/runs`)
91
- }
92
-
93
- async getRun(id: string): Promise<object> {
94
- return this.request<object>('GET', `/runs/${encodeURIComponent(id)}`)
95
- }
96
-
97
- async getTimeline(project: string): Promise<object[]> {
98
- return this.request<object[]>('GET', `/projects/${encodeURIComponent(project)}/timeline`)
99
- }
100
-
101
- async getHistory(project: string): Promise<object[]> {
102
- return this.request<object[]>('GET', `/projects/${encodeURIComponent(project)}/history`)
103
- }
104
-
105
- async getExport(project: string): Promise<object> {
106
- return this.request<object>('GET', `/projects/${encodeURIComponent(project)}/export`)
107
- }
108
-
109
- async apply(config: object): Promise<object> {
110
- return this.request<object>('POST', '/apply', config)
111
- }
112
-
113
- async getStatus(project: string): Promise<object> {
114
- return this.request<object>('GET', `/projects/${encodeURIComponent(project)}`)
115
- }
116
-
117
- async getSettings(): Promise<object> {
118
- return this.request<object>('GET', '/settings')
119
- }
120
-
121
- async updateProvider(name: string, body: { apiKey?: string; baseUrl?: string; model?: string }): Promise<object> {
122
- return this.request<object>('PUT', `/settings/providers/${encodeURIComponent(name)}`, body)
123
- }
124
-
125
- async putSchedule(project: string, body: object): Promise<object> {
126
- return this.request<object>('PUT', `/projects/${encodeURIComponent(project)}/schedule`, body)
127
- }
128
-
129
- async getSchedule(project: string): Promise<object> {
130
- return this.request<object>('GET', `/projects/${encodeURIComponent(project)}/schedule`)
131
- }
132
-
133
- async deleteSchedule(project: string): Promise<void> {
134
- await this.request<void>('DELETE', `/projects/${encodeURIComponent(project)}/schedule`)
135
- }
136
-
137
- async createNotification(project: string, body: object): Promise<object> {
138
- return this.request<object>('POST', `/projects/${encodeURIComponent(project)}/notifications`, body)
139
- }
140
-
141
- async listNotifications(project: string): Promise<object[]> {
142
- return this.request<object[]>('GET', `/projects/${encodeURIComponent(project)}/notifications`)
143
- }
144
-
145
- async deleteNotification(project: string, id: string): Promise<void> {
146
- await this.request<void>('DELETE', `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}`)
147
- }
148
-
149
- async testNotification(project: string, id: string): Promise<object> {
150
- return this.request<object>('POST', `/projects/${encodeURIComponent(project)}/notifications/${encodeURIComponent(id)}/test`)
151
- }
152
- }
@@ -1,25 +0,0 @@
1
- import fs from 'node:fs'
2
- import { parse } from 'yaml'
3
- import { loadConfig } from '../config.js'
4
- import { ApiClient } from '../client.js'
5
-
6
- export async function applyConfig(filePath: string): Promise<void> {
7
- if (!fs.existsSync(filePath)) {
8
- throw new Error(`File not found: ${filePath}`)
9
- }
10
-
11
- const content = fs.readFileSync(filePath, 'utf-8')
12
- const config = parse(content) as object
13
-
14
- const clientConfig = loadConfig()
15
- const client = new ApiClient(clientConfig.apiUrl, clientConfig.apiKey)
16
-
17
- const result = await client.apply(config) as {
18
- id: string
19
- name: string
20
- displayName: string
21
- configRevision: number
22
- }
23
-
24
- console.log(`Applied config for "${result.name}" (revision ${result.configRevision})`)
25
- }
@@ -1,36 +0,0 @@
1
- import { loadConfig } from '../config.js'
2
- import { ApiClient } from '../client.js'
3
-
4
- function getClient(): ApiClient {
5
- const config = loadConfig()
6
- return new ApiClient(config.apiUrl, config.apiKey)
7
- }
8
-
9
- export async function addCompetitors(project: string, domains: string[]): Promise<void> {
10
- // First get existing competitors, then put the combined list
11
- const client = getClient()
12
- const existing = await client.listCompetitors(project) as Array<{ domain: string }>
13
- const existingDomains = existing.map(c => c.domain)
14
- const allDomains = [...new Set([...existingDomains, ...domains])]
15
- await client.putCompetitors(project, allDomains)
16
- console.log(`Added ${domains.length} competitor(s) to "${project}".`)
17
- }
18
-
19
- export async function listCompetitors(project: string): Promise<void> {
20
- const client = getClient()
21
- const comps = await client.listCompetitors(project) as Array<{
22
- id: string
23
- domain: string
24
- createdAt: string
25
- }>
26
-
27
- if (comps.length === 0) {
28
- console.log(`No competitors found for "${project}".`)
29
- return
30
- }
31
-
32
- console.log(`Competitors for "${project}" (${comps.length}):\n`)
33
- for (const c of comps) {
34
- console.log(` ${c.domain}`)
35
- }
36
- }