@ainyc/canonry 1.1.2 → 1.3.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/README.md +70 -0
- package/dist/{chunk-W6AJ2472.js → chunk-GUHHQZQM.js} +8 -3
- package/dist/cli.js +167 -9
- package/dist/index.js +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
AEO (Answer Engine Optimization) is the practice of ensuring your content is accurately represented in AI-generated answers. As search shifts from links to synthesized responses, monitoring your visibility across answer engines is essential.
|
|
8
8
|
|
|
9
|
+

|
|
10
|
+
|
|
9
11
|
## Quick Start
|
|
10
12
|
|
|
11
13
|
```bash
|
|
@@ -32,6 +34,7 @@ Open [http://localhost:4100](http://localhost:4100) to access the web dashboard.
|
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
36
|
canonry init # Initialize config and database
|
|
37
|
+
canonry bootstrap # Bootstrap hosted config/database from env vars
|
|
35
38
|
canonry serve # Start server (API + web dashboard)
|
|
36
39
|
canonry settings # View/edit configuration
|
|
37
40
|
```
|
|
@@ -237,6 +240,73 @@ pnpm run lint
|
|
|
237
240
|
pnpm run dev:web # Run SPA in dev mode
|
|
238
241
|
```
|
|
239
242
|
|
|
243
|
+
## Docker Deployment
|
|
244
|
+
|
|
245
|
+
Canonry currently deploys as a **single Node.js service with a SQLite file on persistent disk**.
|
|
246
|
+
|
|
247
|
+
The repo includes a production `Dockerfile` and entry script. The default container entrypoint runs `canonry bootstrap` and then `canonry serve`.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
docker build -t canonry .
|
|
251
|
+
|
|
252
|
+
docker run --rm \
|
|
253
|
+
-p 4100:4100 \
|
|
254
|
+
-e PORT=4100 \
|
|
255
|
+
-e CANONRY_CONFIG_DIR=/data/canonry \
|
|
256
|
+
-e GEMINI_API_KEY=your-key \
|
|
257
|
+
-v canonry-data:/data \
|
|
258
|
+
canonry
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Keep the container to a single replica and mount persistent storage at `/data` so SQLite and `config.yaml` survive restarts.
|
|
262
|
+
|
|
263
|
+
No CORS configuration is required for this Docker setup. The dashboard and API are served by the same Canonry process on the same origin. CORS only becomes relevant if you split the frontend and API onto different domains.
|
|
264
|
+
|
|
265
|
+
## Deploy on Railway or Render
|
|
266
|
+
|
|
267
|
+
Use the **repo root** as the service root. `@ainyc/canonry` depends on shared workspace packages under `packages/*`, so deploying from a subdirectory will break the build.
|
|
268
|
+
|
|
269
|
+
### Hosted environment variables
|
|
270
|
+
|
|
271
|
+
Set at least one provider:
|
|
272
|
+
|
|
273
|
+
- `GEMINI_API_KEY`
|
|
274
|
+
- `OPENAI_API_KEY`
|
|
275
|
+
- `ANTHROPIC_API_KEY`
|
|
276
|
+
- `LOCAL_BASE_URL` (plus optional `LOCAL_API_KEY` and `LOCAL_MODEL`)
|
|
277
|
+
|
|
278
|
+
Set these for hosted persistence/bootstrap:
|
|
279
|
+
|
|
280
|
+
- `CANONRY_CONFIG_DIR=/data/canonry`
|
|
281
|
+
- Optional `CANONRY_API_KEY=cnry_...` to pin the generated API key instead of letting bootstrap create one
|
|
282
|
+
- Optional `CANONRY_DATABASE_PATH=/data/canonry/data.db`
|
|
283
|
+
|
|
284
|
+
The hosted bootstrap command is idempotent. It creates `config.yaml`, creates or migrates the SQLite database, and inserts the API key row the server expects.
|
|
285
|
+
|
|
286
|
+
### Railway
|
|
287
|
+
|
|
288
|
+
Create one service from this repo using the checked-in `Dockerfile`, then attach a persistent volume mounted at `/data`.
|
|
289
|
+
|
|
290
|
+
- Add the provider and Canonry env vars in the service's **Variables** tab. Railway can also bulk import them from `.env` files or the Raw Editor.
|
|
291
|
+
- Leave the start command unset so Railway uses the image `ENTRYPOINT`.
|
|
292
|
+
- Health check: `/health`
|
|
293
|
+
- Recommended env: `CANONRY_CONFIG_DIR=/data/canonry`
|
|
294
|
+
|
|
295
|
+
SQLite should live on the mounted volume, so keep the service to a single instance.
|
|
296
|
+
|
|
297
|
+
### Render
|
|
298
|
+
|
|
299
|
+
Create one **Web Service** from this repo with runtime **Docker**, then attach a persistent disk mounted at `/data`.
|
|
300
|
+
|
|
301
|
+
- Add the provider and Canonry env vars in the service's **Environment** settings or an Environment Group.
|
|
302
|
+
- Leave the start command unset so Render uses the image `ENTRYPOINT`.
|
|
303
|
+
- Health check path: `/health`
|
|
304
|
+
- Recommended env: `CANONRY_CONFIG_DIR=/data/canonry`
|
|
305
|
+
|
|
306
|
+
Render makes Docker service env vars available at runtime and also exposes them to Docker builds as build args. This image does not use `ARG` for provider secrets, so those values are only consumed at runtime by the entry script and Canonry process.
|
|
307
|
+
|
|
308
|
+
SQLite should live on the persistent disk, so keep the service to a single instance.
|
|
309
|
+
|
|
240
310
|
## Contributing
|
|
241
311
|
|
|
242
312
|
Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions.
|
|
@@ -10,6 +10,10 @@ import path from "path";
|
|
|
10
10
|
import os from "os";
|
|
11
11
|
import { parse, stringify } from "yaml";
|
|
12
12
|
function getConfigDir() {
|
|
13
|
+
const override = process.env.CANONRY_CONFIG_DIR?.trim();
|
|
14
|
+
if (override) {
|
|
15
|
+
return override;
|
|
16
|
+
}
|
|
13
17
|
return path.join(os.homedir(), ".canonry");
|
|
14
18
|
}
|
|
15
19
|
function getConfigPath() {
|
|
@@ -3697,13 +3701,14 @@ async function createServer(opts) {
|
|
|
3697
3701
|
}
|
|
3698
3702
|
|
|
3699
3703
|
export {
|
|
3704
|
+
providerQuotaPolicySchema,
|
|
3705
|
+
apiKeys,
|
|
3706
|
+
createClient,
|
|
3707
|
+
migrate,
|
|
3700
3708
|
getConfigDir,
|
|
3701
3709
|
getConfigPath,
|
|
3702
3710
|
loadConfig,
|
|
3703
3711
|
saveConfig,
|
|
3704
3712
|
configExists,
|
|
3705
|
-
apiKeys,
|
|
3706
|
-
createClient,
|
|
3707
|
-
migrate,
|
|
3708
3713
|
createServer
|
|
3709
3714
|
};
|
package/dist/cli.js
CHANGED
|
@@ -8,17 +8,169 @@ import {
|
|
|
8
8
|
getConfigPath,
|
|
9
9
|
loadConfig,
|
|
10
10
|
migrate,
|
|
11
|
+
providerQuotaPolicySchema,
|
|
11
12
|
saveConfig
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GUHHQZQM.js";
|
|
13
14
|
|
|
14
15
|
// src/cli.ts
|
|
15
16
|
import { parseArgs } from "util";
|
|
16
17
|
|
|
17
|
-
// src/commands/
|
|
18
|
+
// src/commands/bootstrap.ts
|
|
18
19
|
import crypto from "crypto";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import { eq } from "drizzle-orm";
|
|
22
|
+
|
|
23
|
+
// ../config/src/index.ts
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
var envSchema = z.object({
|
|
26
|
+
DATABASE_URL: z.string().default("postgresql://aeo:aeo@postgres:5432/aeo_platform"),
|
|
27
|
+
API_PORT: z.coerce.number().int().positive().default(3e3),
|
|
28
|
+
WORKER_PORT: z.coerce.number().int().positive().default(3001),
|
|
29
|
+
WEB_PORT: z.coerce.number().int().positive().default(4173),
|
|
30
|
+
BOOTSTRAP_SECRET: z.string().default("change-me"),
|
|
31
|
+
// Gemini
|
|
32
|
+
GEMINI_API_KEY: z.string().optional(),
|
|
33
|
+
GEMINI_MODEL: z.string().optional(),
|
|
34
|
+
GEMINI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
|
|
35
|
+
GEMINI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
|
|
36
|
+
GEMINI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
|
|
37
|
+
// OpenAI
|
|
38
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
39
|
+
OPENAI_MODEL: z.string().optional(),
|
|
40
|
+
OPENAI_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
|
|
41
|
+
OPENAI_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
|
|
42
|
+
OPENAI_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3),
|
|
43
|
+
// Anthropic / Claude
|
|
44
|
+
ANTHROPIC_API_KEY: z.string().optional(),
|
|
45
|
+
ANTHROPIC_MODEL: z.string().optional(),
|
|
46
|
+
ANTHROPIC_MAX_CONCURRENCY: z.coerce.number().int().positive().default(2),
|
|
47
|
+
ANTHROPIC_MAX_REQUESTS_PER_MINUTE: z.coerce.number().int().positive().default(10),
|
|
48
|
+
ANTHROPIC_MAX_REQUESTS_PER_DAY: z.coerce.number().int().positive().default(1e3)
|
|
49
|
+
});
|
|
50
|
+
var bootstrapEnvSchema = z.object({
|
|
51
|
+
CANONRY_API_KEY: z.string().optional(),
|
|
52
|
+
CANONRY_API_URL: z.string().optional(),
|
|
53
|
+
CANONRY_DATABASE_PATH: z.string().optional(),
|
|
54
|
+
GEMINI_API_KEY: z.string().optional(),
|
|
55
|
+
GEMINI_MODEL: z.string().optional(),
|
|
56
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
57
|
+
OPENAI_MODEL: z.string().optional(),
|
|
58
|
+
ANTHROPIC_API_KEY: z.string().optional(),
|
|
59
|
+
ANTHROPIC_MODEL: z.string().optional(),
|
|
60
|
+
LOCAL_BASE_URL: z.string().optional(),
|
|
61
|
+
LOCAL_API_KEY: z.string().optional(),
|
|
62
|
+
LOCAL_MODEL: z.string().optional()
|
|
63
|
+
});
|
|
64
|
+
function getBootstrapEnv(source) {
|
|
65
|
+
const parsed = bootstrapEnvSchema.parse(source);
|
|
66
|
+
const providers = {};
|
|
67
|
+
if (parsed.GEMINI_API_KEY) {
|
|
68
|
+
providers.gemini = {
|
|
69
|
+
apiKey: parsed.GEMINI_API_KEY,
|
|
70
|
+
model: parsed.GEMINI_MODEL || "gemini-2.5-flash",
|
|
71
|
+
quota: providerQuotaPolicySchema.parse({
|
|
72
|
+
maxConcurrency: 2,
|
|
73
|
+
maxRequestsPerMinute: 10,
|
|
74
|
+
maxRequestsPerDay: 500
|
|
75
|
+
})
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (parsed.OPENAI_API_KEY) {
|
|
79
|
+
providers.openai = {
|
|
80
|
+
apiKey: parsed.OPENAI_API_KEY,
|
|
81
|
+
model: parsed.OPENAI_MODEL || "gpt-4o",
|
|
82
|
+
quota: providerQuotaPolicySchema.parse({
|
|
83
|
+
maxConcurrency: 2,
|
|
84
|
+
maxRequestsPerMinute: 10,
|
|
85
|
+
maxRequestsPerDay: 500
|
|
86
|
+
})
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (parsed.ANTHROPIC_API_KEY) {
|
|
90
|
+
providers.claude = {
|
|
91
|
+
apiKey: parsed.ANTHROPIC_API_KEY,
|
|
92
|
+
model: parsed.ANTHROPIC_MODEL || "claude-sonnet-4-6",
|
|
93
|
+
quota: providerQuotaPolicySchema.parse({
|
|
94
|
+
maxConcurrency: 2,
|
|
95
|
+
maxRequestsPerMinute: 10,
|
|
96
|
+
maxRequestsPerDay: 500
|
|
97
|
+
})
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (parsed.LOCAL_BASE_URL) {
|
|
101
|
+
providers.local = {
|
|
102
|
+
baseUrl: parsed.LOCAL_BASE_URL,
|
|
103
|
+
apiKey: parsed.LOCAL_API_KEY,
|
|
104
|
+
model: parsed.LOCAL_MODEL || "llama3",
|
|
105
|
+
quota: providerQuotaPolicySchema.parse({
|
|
106
|
+
maxConcurrency: 2,
|
|
107
|
+
maxRequestsPerMinute: 10,
|
|
108
|
+
maxRequestsPerDay: 500
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
apiKey: parsed.CANONRY_API_KEY,
|
|
114
|
+
apiUrl: parsed.CANONRY_API_URL,
|
|
115
|
+
databasePath: parsed.CANONRY_DATABASE_PATH,
|
|
116
|
+
providers
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/commands/bootstrap.ts
|
|
121
|
+
async function bootstrapCommand(_opts) {
|
|
122
|
+
const env = getBootstrapEnv(process.env);
|
|
123
|
+
const providers = env.providers;
|
|
124
|
+
const hasProvider = providers?.gemini || providers?.openai || providers?.claude || providers?.local;
|
|
125
|
+
if (!hasProvider) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"Bootstrap requires at least one provider env var. Set GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, or LOCAL_BASE_URL."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const configDir = getConfigDir();
|
|
131
|
+
const databasePath = env.databasePath || path.join(configDir, "data.db");
|
|
132
|
+
const existing = configExists();
|
|
133
|
+
let rawApiKey;
|
|
134
|
+
let generatedApiKey;
|
|
135
|
+
if (env.apiKey) {
|
|
136
|
+
rawApiKey = env.apiKey;
|
|
137
|
+
} else if (existing) {
|
|
138
|
+
rawApiKey = loadConfig().apiKey;
|
|
139
|
+
} else {
|
|
140
|
+
generatedApiKey = `cnry_${crypto.randomBytes(16).toString("hex")}`;
|
|
141
|
+
rawApiKey = generatedApiKey;
|
|
142
|
+
}
|
|
143
|
+
const keyHash = crypto.createHash("sha256").update(rawApiKey).digest("hex");
|
|
144
|
+
const keyPrefix = rawApiKey.slice(0, 9);
|
|
145
|
+
const db = createClient(databasePath);
|
|
146
|
+
migrate(db);
|
|
147
|
+
db.delete(apiKeys).where(eq(apiKeys.name, "default")).run();
|
|
148
|
+
db.insert(apiKeys).values({
|
|
149
|
+
id: crypto.randomUUID(),
|
|
150
|
+
name: "default",
|
|
151
|
+
keyHash,
|
|
152
|
+
keyPrefix,
|
|
153
|
+
scopes: '["*"]',
|
|
154
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
155
|
+
}).run();
|
|
156
|
+
saveConfig({
|
|
157
|
+
apiUrl: env.apiUrl || `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
|
|
158
|
+
database: databasePath,
|
|
159
|
+
apiKey: rawApiKey,
|
|
160
|
+
providers
|
|
161
|
+
});
|
|
162
|
+
console.log(`Bootstrap complete. Config saved to ${getConfigPath()}`);
|
|
163
|
+
console.log(`SQLite database path: ${databasePath}`);
|
|
164
|
+
if (generatedApiKey) {
|
|
165
|
+
console.log(`API key: ${generatedApiKey}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/commands/init.ts
|
|
170
|
+
import crypto2 from "crypto";
|
|
19
171
|
import fs from "fs";
|
|
20
172
|
import readline from "readline";
|
|
21
|
-
import
|
|
173
|
+
import path2 from "path";
|
|
22
174
|
function prompt(question) {
|
|
23
175
|
const rl = readline.createInterface({
|
|
24
176
|
input: process.stdin,
|
|
@@ -76,14 +228,14 @@ async function initCommand(opts) {
|
|
|
76
228
|
console.error("\nAt least one provider is required.");
|
|
77
229
|
process.exit(1);
|
|
78
230
|
}
|
|
79
|
-
const rawApiKey = `cnry_${
|
|
80
|
-
const keyHash =
|
|
231
|
+
const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
|
|
232
|
+
const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
|
|
81
233
|
const keyPrefix = rawApiKey.slice(0, 9);
|
|
82
|
-
const databasePath =
|
|
234
|
+
const databasePath = path2.join(configDir, "data.db");
|
|
83
235
|
const db = createClient(databasePath);
|
|
84
236
|
migrate(db);
|
|
85
237
|
db.insert(apiKeys).values({
|
|
86
|
-
id:
|
|
238
|
+
id: crypto2.randomUUID(),
|
|
87
239
|
name: "default",
|
|
88
240
|
keyHash,
|
|
89
241
|
keyPrefix,
|
|
@@ -132,8 +284,8 @@ var ApiClient = class {
|
|
|
132
284
|
this.baseUrl = baseUrl.replace(/\/$/, "") + "/api/v1";
|
|
133
285
|
this.apiKey = apiKey;
|
|
134
286
|
}
|
|
135
|
-
async request(method,
|
|
136
|
-
const url = `${this.baseUrl}${
|
|
287
|
+
async request(method, path3, body) {
|
|
288
|
+
const url = `${this.baseUrl}${path3}`;
|
|
137
289
|
const headers = {
|
|
138
290
|
"Authorization": `Bearer ${this.apiKey}`,
|
|
139
291
|
"Content-Type": "application/json"
|
|
@@ -675,6 +827,7 @@ canonry \u2014 AEO monitoring CLI
|
|
|
675
827
|
|
|
676
828
|
Usage:
|
|
677
829
|
canonry init [--force] Initialize config and database
|
|
830
|
+
canonry bootstrap [--force] Bootstrap config/database from env vars
|
|
678
831
|
canonry serve Start the local server
|
|
679
832
|
canonry project create <name> Create a project
|
|
680
833
|
canonry project list List all projects
|
|
@@ -741,6 +894,11 @@ async function main() {
|
|
|
741
894
|
await initCommand({ force: initForce });
|
|
742
895
|
break;
|
|
743
896
|
}
|
|
897
|
+
case "bootstrap": {
|
|
898
|
+
const bootstrapForce = args.includes("--force") || args.includes("-f");
|
|
899
|
+
await bootstrapCommand({ force: bootstrapForce });
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
744
902
|
case "serve": {
|
|
745
903
|
const { values } = parseArgs({
|
|
746
904
|
args: args.slice(1),
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -51,13 +51,14 @@
|
|
|
51
51
|
"@types/node-cron": "^3.0.11",
|
|
52
52
|
"tsup": "^8.5.1",
|
|
53
53
|
"tsx": "^4.19.0",
|
|
54
|
-
"@ainyc/canonry-contracts": "0.0.0",
|
|
55
|
-
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
56
|
-
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
57
54
|
"@ainyc/canonry-api-routes": "0.0.0",
|
|
55
|
+
"@ainyc/canonry-config": "0.0.0",
|
|
56
|
+
"@ainyc/canonry-contracts": "0.0.0",
|
|
58
57
|
"@ainyc/canonry-db": "0.0.0",
|
|
59
58
|
"@ainyc/canonry-provider-local": "0.0.0",
|
|
60
|
-
"@ainyc/canonry-provider-
|
|
59
|
+
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
60
|
+
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
61
|
+
"@ainyc/canonry-provider-openai": "0.0.0"
|
|
61
62
|
},
|
|
62
63
|
"scripts": {
|
|
63
64
|
"build": "tsup && tsx build-web.ts",
|