@ainyc/canonry 1.0.0 → 1.1.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 ADDED
@@ -0,0 +1,250 @@
1
+ # Canonry
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@ainyc/canonry)](https://www.npmjs.com/package/@ainyc/canonry) [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![Node.js >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
4
+
5
+ **Open-source AEO monitoring for your domain.** Canonry tracks how AI answer engines (ChatGPT, Gemini, Claude, and others) cite or omit your website for the keywords you care about.
6
+
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
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ npm install -g @ainyc/canonry
13
+ canonry init
14
+ canonry serve
15
+ ```
16
+
17
+ Open [http://localhost:4100](http://localhost:4100) to access the web dashboard.
18
+
19
+ ## Features
20
+
21
+ - **Multi-provider monitoring** -- query Gemini, OpenAI, Claude, and local LLMs (Ollama, LM Studio, or any OpenAI-compatible endpoint) from a single tool.
22
+ - **Three equal surfaces** -- CLI, REST API, and web dashboard all backed by the same API. No surface is privileged.
23
+ - **Config-as-code** -- manage projects with Kubernetes-style YAML files. Version control your monitoring setup.
24
+ - **Self-hosted** -- runs locally with SQLite. No cloud account, no external dependencies beyond the LLM API keys you choose to configure.
25
+ - **Scheduled monitoring** -- set up cron-based recurring runs to track citation changes over time.
26
+ - **Webhook notifications** -- get alerted when your citation status changes.
27
+ - **Audit logging** -- full history of every action taken through any surface.
28
+
29
+ ## CLI Reference
30
+
31
+ ### Setup
32
+
33
+ ```bash
34
+ canonry init # Initialize config and database
35
+ canonry serve # Start server (API + web dashboard)
36
+ canonry settings # View/edit configuration
37
+ ```
38
+
39
+ ### Projects
40
+
41
+ ```bash
42
+ canonry project create <name> --domain <domain> --country US --language en
43
+ canonry project list
44
+ canonry project show <name>
45
+ canonry project delete <name>
46
+ ```
47
+
48
+ ### Keywords and Competitors
49
+
50
+ ```bash
51
+ canonry keyword add <project> "keyword one" "keyword two"
52
+ canonry keyword list <project>
53
+ canonry keyword import <project> <file.csv>
54
+
55
+ canonry competitor add <project> competitor1.com competitor2.com
56
+ canonry competitor list <project>
57
+ ```
58
+
59
+ ### Visibility Runs
60
+
61
+ ```bash
62
+ canonry run <project> # Run all configured providers
63
+ canonry run <project> --provider gemini # Run a single provider
64
+ canonry runs <project> # List past runs
65
+ canonry status <project> # Current visibility summary
66
+ canonry evidence <project> # View citation evidence
67
+ canonry history <project> # Per-keyword citation timeline
68
+ canonry export <project> # Export project as YAML
69
+ ```
70
+
71
+ ### Config-as-Code
72
+
73
+ ```bash
74
+ canonry apply canonry.yaml # Declarative project apply
75
+ ```
76
+
77
+ ### Scheduling and Notifications
78
+
79
+ ```bash
80
+ canonry schedule set <project> --cron "0 8 * * *"
81
+ canonry schedule show <project>
82
+ canonry schedule enable <project>
83
+ canonry schedule disable <project>
84
+ canonry schedule remove <project>
85
+
86
+ canonry notify add <project> --url https://hooks.slack.com/...
87
+ canonry notify list <project>
88
+ canonry notify remove <project> <id>
89
+ canonry notify test <project> <id>
90
+ ```
91
+
92
+ ## Config-as-Code
93
+
94
+ Define your monitoring projects in version-controlled YAML files:
95
+
96
+ ```yaml
97
+ apiVersion: canonry/v1
98
+ kind: Project
99
+ metadata:
100
+ name: my-project
101
+ spec:
102
+ displayName: My Project
103
+ canonicalDomain: example.com
104
+ country: US
105
+ language: en
106
+ keywords:
107
+ - best dental implants near me
108
+ - emergency dentist open now
109
+ competitors:
110
+ - competitor.com
111
+ providers:
112
+ - gemini
113
+ - openai
114
+ - claude
115
+ - local
116
+ ```
117
+
118
+ Apply with the CLI or the API:
119
+
120
+ ```bash
121
+ canonry apply canonry.yaml
122
+ ```
123
+
124
+ ```bash
125
+ curl -X POST http://localhost:4100/api/v1/apply \
126
+ -H "Authorization: Bearer cnry_..." \
127
+ -H "Content-Type: application/yaml" \
128
+ --data-binary @canonry.yaml
129
+ ```
130
+
131
+ The database is authoritative. Config files are input, not state.
132
+
133
+ ## Provider Setup
134
+
135
+ Canonry queries multiple AI answer engines. Configure the providers you want during `canonry init`, or add them later via the settings page or API.
136
+
137
+ ### Gemini
138
+
139
+ Get an API key from [Google AI Studio](https://aistudio.google.com/apikey).
140
+
141
+ ### OpenAI
142
+
143
+ Get an API key from [platform.openai.com](https://platform.openai.com/api-keys).
144
+
145
+ ### Claude
146
+
147
+ Get an API key from [console.anthropic.com](https://console.anthropic.com/settings/keys).
148
+
149
+ ### Local LLMs
150
+
151
+ Any OpenAI-compatible endpoint works -- Ollama, LM Studio, llama.cpp, vLLM, and similar tools. Configure via CLI or API:
152
+
153
+ ```bash
154
+ canonry settings provider local --base-url http://localhost:11434/v1
155
+ ```
156
+
157
+ The base URL is the only required field. API key is optional (most local servers don't need one). You can also set a specific model:
158
+
159
+ ```bash
160
+ canonry settings provider local --base-url http://localhost:11434/v1 --model llama3
161
+ ```
162
+
163
+ > **Note:** Unless your local model has web search capabilities, responses will be based solely on its training data. Cloud providers (Gemini, OpenAI, Claude) use live web search to ground their answers, which produces more accurate citation results. Local LLMs are best used for comparing how different models perceive your brand without real-time search context.
164
+
165
+ ## API
166
+
167
+ All endpoints are served under `/api/v1/`. Authenticate with a bearer token:
168
+
169
+ ```
170
+ Authorization: Bearer cnry_...
171
+ ```
172
+
173
+ Key endpoints:
174
+
175
+ | Method | Path | Description |
176
+ |--------|------|-------------|
177
+ | `PUT` | `/api/v1/projects/{name}` | Create or update a project |
178
+ | `POST` | `/api/v1/projects/{name}/runs` | Trigger a visibility sweep |
179
+ | `GET` | `/api/v1/projects/{name}/timeline` | Per-keyword citation history |
180
+ | `GET` | `/api/v1/projects/{name}/snapshots/diff` | Compare two runs |
181
+ | `POST` | `/api/v1/apply` | Config-as-code apply |
182
+ | `GET` | `/api/v1/openapi.json` | OpenAPI spec (no auth required) |
183
+
184
+ ## Web Dashboard
185
+
186
+ The bundled web dashboard provides five views:
187
+
188
+ - **Overview** -- portfolio-level visibility scores across all projects with sparkline trends.
189
+ - **Project** -- command center with score gauges, keyword evidence tables, and competitor analysis.
190
+ - **Runs** -- history of all visibility sweeps with per-provider breakdowns.
191
+ - **Settings** -- provider configuration, scheduling, and notification management.
192
+ - **Setup** -- guided wizard for first-time onboarding.
193
+
194
+ Access it at [http://localhost:4100](http://localhost:4100) after running `canonry serve`.
195
+
196
+ ## Requirements
197
+
198
+ - Node.js >= 20
199
+ - At least one provider API key (or a local LLM endpoint)
200
+ - A C++ toolchain for building `better-sqlite3` native bindings (only needed if prebuilt binaries aren't available for your platform)
201
+
202
+ ### Native dependency setup
203
+
204
+ Canonry uses `better-sqlite3` for its embedded database. Prebuilt binaries are downloaded automatically for most platforms, but if `npm install` fails with a `node-gyp` error, you need to install build tools:
205
+
206
+ **macOS:**
207
+ ```bash
208
+ xcode-select --install
209
+ ```
210
+
211
+ **Debian / Ubuntu:**
212
+ ```bash
213
+ sudo apt-get install -y python3 make g++
214
+ ```
215
+
216
+ **Alpine Linux (Docker):**
217
+ ```bash
218
+ apk add --no-cache python3 make g++ gcc musl-dev
219
+ ```
220
+
221
+ **Windows:**
222
+ ```bash
223
+ npm install -g windows-build-tools
224
+ ```
225
+
226
+ If you're running in a minimal Docker image or CI environment without these tools, the install will fail. See the [better-sqlite3 troubleshooting guide](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) for additional help.
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ git clone https://github.com/ainyc/canonry.git
232
+ cd canonry
233
+ pnpm install
234
+ pnpm run typecheck
235
+ pnpm run test
236
+ pnpm run lint
237
+ pnpm run dev:web # Run SPA in dev mode
238
+ ```
239
+
240
+ ## Contributing
241
+
242
+ Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions.
243
+
244
+ ## License
245
+
246
+ [AGPL-3.0-only](./LICENSE)
247
+
248
+ ---
249
+
250
+ Built by [AI NYC](https://ainyc.ai)
@@ -60,6 +60,7 @@ function configExists() {
60
60
  }
61
61
 
62
62
  // src/server.ts
63
+ import { createRequire } from "module";
63
64
  import fs2 from "fs";
64
65
  import path2 from "path";
65
66
  import { fileURLToPath } from "url";
@@ -3507,6 +3508,8 @@ var Notifier = class {
3507
3508
  };
3508
3509
 
3509
3510
  // src/server.ts
3511
+ var _require = createRequire(import.meta.url);
3512
+ var { version: PKG_VERSION } = _require("../package.json");
3510
3513
  var DEFAULT_QUOTA = {
3511
3514
  maxConcurrency: 2,
3512
3515
  maxRequestsPerMinute: 10,
@@ -3684,7 +3687,7 @@ async function createServer(opts) {
3684
3687
  app.get("/health", async () => ({
3685
3688
  status: "ok",
3686
3689
  service: "canonry",
3687
- version: "0.1.0"
3690
+ version: PKG_VERSION
3688
3691
  }));
3689
3692
  scheduler.start();
3690
3693
  app.addHook("onClose", async () => {
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  loadConfig,
10
10
  migrate,
11
11
  saveConfig
12
- } from "./chunk-ONZDY6Q4.js";
12
+ } from "./chunk-W6AJ2472.js";
13
13
 
14
14
  // src/cli.ts
15
15
  import { parseArgs } from "util";
@@ -36,11 +36,11 @@ var DEFAULT_QUOTA = {
36
36
  maxRequestsPerMinute: 10,
37
37
  maxRequestsPerDay: 500
38
38
  };
39
- async function initCommand() {
39
+ async function initCommand(opts) {
40
40
  console.log("Initializing canonry...\n");
41
- if (configExists()) {
41
+ if (configExists() && !opts?.force) {
42
42
  console.log(`Config already exists at ${getConfigPath()}`);
43
- console.log("To reinitialize, delete the config file first.");
43
+ console.log('To reinitialize, run "canonry init --force".');
44
44
  return;
45
45
  }
46
46
  const configDir = getConfigDir();
@@ -109,13 +109,14 @@ Config saved to ${getConfigPath()}`);
109
109
  async function serveCommand() {
110
110
  const config = loadConfig();
111
111
  const port = parseInt(process.env.CANONRY_PORT ?? "4100", 10);
112
+ const host = process.env.CANONRY_HOST ?? "127.0.0.1";
112
113
  const db = createClient(config.database);
113
114
  migrate(db);
114
115
  const app = await createServer({ config, db });
115
116
  try {
116
- await app.listen({ host: "0.0.0.0", port });
117
+ await app.listen({ host, port });
117
118
  console.log(`
118
- Canonry server running at http://localhost:${port}`);
119
+ Canonry server running at http://${host === "0.0.0.0" ? "localhost" : host}:${port}`);
119
120
  console.log("Press Ctrl+C to stop.\n");
120
121
  } catch (err) {
121
122
  app.log.error(err);
@@ -668,11 +669,12 @@ function printNotification(n) {
668
669
  }
669
670
 
670
671
  // src/cli.ts
672
+ import { createRequire } from "module";
671
673
  var USAGE = `
672
674
  canonry \u2014 AEO monitoring CLI
673
675
 
674
676
  Usage:
675
- canonry init Initialize config and database
677
+ canonry init [--force] Initialize config and database
676
678
  canonry serve Start the local server
677
679
  canonry project create <name> Create a project
678
680
  canonry project list List all projects
@@ -707,6 +709,7 @@ Usage:
707
709
 
708
710
  Options:
709
711
  --port <port> Server port (default: 4100)
712
+ --host <host> Server bind address (default: 127.0.0.1)
710
713
  --domain <domain> Canonical domain for project create
711
714
  --country <code> Country code (default: US)
712
715
  --language <lang> Language code (default: en)
@@ -718,7 +721,8 @@ Options:
718
721
  --webhook <url> Webhook URL for notifications
719
722
  --events <list> Comma-separated notification events
720
723
  `.trim();
721
- var VERSION = "0.1.0";
724
+ var _require = createRequire(import.meta.url);
725
+ var { version: VERSION } = _require("../package.json");
722
726
  async function main() {
723
727
  const args = process.argv.slice(2);
724
728
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
@@ -732,18 +736,22 @@ async function main() {
732
736
  const command = args[0];
733
737
  try {
734
738
  switch (command) {
735
- case "init":
736
- await initCommand();
739
+ case "init": {
740
+ const initForce = args.includes("--force") || args.includes("-f");
741
+ await initCommand({ force: initForce });
737
742
  break;
743
+ }
738
744
  case "serve": {
739
745
  const { values } = parseArgs({
740
746
  args: args.slice(1),
741
747
  options: {
742
- port: { type: "string", short: "p", default: "4100" }
748
+ port: { type: "string", short: "p", default: "4100" },
749
+ host: { type: "string", short: "H" }
743
750
  },
744
751
  allowPositionals: false
745
752
  });
746
753
  process.env.CANONRY_PORT = values.port;
754
+ if (values.host) process.env.CANONRY_HOST = values.host;
747
755
  await serveCommand();
748
756
  break;
749
757
  }
@@ -0,0 +1,34 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import { DatabaseClient } from '@ainyc/canonry-db';
3
+ import { ProviderQuotaPolicy } from '@ainyc/canonry-contracts';
4
+
5
+ interface ProviderConfigEntry {
6
+ apiKey?: string;
7
+ baseUrl?: string;
8
+ model?: string;
9
+ quota?: ProviderQuotaPolicy;
10
+ }
11
+ interface CanonryConfig {
12
+ apiUrl: string;
13
+ database: string;
14
+ apiKey: string;
15
+ port?: number;
16
+ geminiApiKey?: string;
17
+ geminiModel?: string;
18
+ geminiQuota?: ProviderQuotaPolicy;
19
+ providers?: {
20
+ gemini?: ProviderConfigEntry;
21
+ openai?: ProviderConfigEntry;
22
+ claude?: ProviderConfigEntry;
23
+ local?: ProviderConfigEntry;
24
+ };
25
+ }
26
+ declare function loadConfig(): CanonryConfig;
27
+
28
+ declare function createServer(opts: {
29
+ config: CanonryConfig;
30
+ db: DatabaseClient;
31
+ open?: boolean;
32
+ }): Promise<FastifyInstance>;
33
+
34
+ export { type CanonryConfig, createServer, loadConfig };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-ONZDY6Q4.js";
4
+ } from "./chunk-W6AJ2472.js";
5
5
  export {
6
6
  createServer,
7
7
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "bin": {
@@ -8,16 +8,17 @@
8
8
  },
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
12
  "default": "./dist/index.js"
13
13
  }
14
14
  },
15
- "types": "./src/index.ts",
15
+ "types": "./dist/index.d.ts",
16
16
  "files": [
17
17
  "bin/",
18
18
  "dist/",
19
19
  "assets/",
20
- "src/"
20
+ "package.json",
21
+ "README.md"
21
22
  ],
22
23
  "engines": {
23
24
  "node": ">=20"
@@ -30,7 +31,7 @@
30
31
  "drizzle-orm": "^0.45.1",
31
32
  "fastify": "^5.4.0",
32
33
  "node-cron": "^4.2.1",
33
- "openai": "^4.85.0",
34
+ "openai": "^6.0.0",
34
35
  "pino-pretty": "^13.1.3",
35
36
  "yaml": "^2.7.1",
36
37
  "zod": "^4.1.12"
@@ -44,9 +45,9 @@
44
45
  "@ainyc/canonry-contracts": "0.0.0",
45
46
  "@ainyc/canonry-provider-claude": "0.0.0",
46
47
  "@ainyc/canonry-provider-gemini": "0.0.0",
47
- "@ainyc/canonry-db": "0.0.0",
48
48
  "@ainyc/canonry-provider-local": "0.0.0",
49
- "@ainyc/canonry-provider-openai": "0.0.0"
49
+ "@ainyc/canonry-provider-openai": "0.0.0",
50
+ "@ainyc/canonry-db": "0.0.0"
50
51
  },
51
52
  "scripts": {
52
53
  "build": "tsup && tsx build-web.ts",