@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/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ainyc/canonry",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"bin": {
|
|
7
|
+
"canonry": "./bin/canonry.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"types": "./src/index.ts",
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"dist/",
|
|
19
|
+
"assets/",
|
|
20
|
+
"src/"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@anthropic-ai/sdk": "^0.78.0",
|
|
27
|
+
"@fastify/static": "^8.1.0",
|
|
28
|
+
"@google/generative-ai": "^0.24.1",
|
|
29
|
+
"better-sqlite3": "^12.6.2",
|
|
30
|
+
"drizzle-orm": "^0.45.1",
|
|
31
|
+
"fastify": "^5.4.0",
|
|
32
|
+
"node-cron": "^4.2.1",
|
|
33
|
+
"openai": "^4.85.0",
|
|
34
|
+
"pino-pretty": "^13.1.3",
|
|
35
|
+
"yaml": "^2.7.1",
|
|
36
|
+
"zod": "^4.1.12"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
40
|
+
"@types/node-cron": "^3.0.11",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
42
|
+
"tsx": "^4.19.0",
|
|
43
|
+
"@ainyc/canonry-api-routes": "0.0.0",
|
|
44
|
+
"@ainyc/canonry-contracts": "0.0.0",
|
|
45
|
+
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
46
|
+
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
47
|
+
"@ainyc/canonry-db": "0.0.0",
|
|
48
|
+
"@ainyc/canonry-provider-local": "0.0.0",
|
|
49
|
+
"@ainyc/canonry-provider-openai": "0.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup && tsx build-web.ts",
|
|
53
|
+
"build:web": "tsx build-web.ts",
|
|
54
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
55
|
+
"test": "tsx --test test/*.test.ts",
|
|
56
|
+
"lint": "eslint src/ test/"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
}
|