radar-cli 0.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/dist/index.js +331 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/convex-client.ts
|
|
7
|
+
import { ConvexHttpClient } from "convex/browser";
|
|
8
|
+
|
|
9
|
+
// ../../convex/_generated/api.js
|
|
10
|
+
import { anyApi, componentsGeneric } from "convex/server";
|
|
11
|
+
var api = anyApi;
|
|
12
|
+
var components = componentsGeneric();
|
|
13
|
+
|
|
14
|
+
// src/convex-client.ts
|
|
15
|
+
var DEFAULT_CONVEX_URL = "https://tough-bird-920.convex.cloud";
|
|
16
|
+
var cachedClient = null;
|
|
17
|
+
function getConvexClient() {
|
|
18
|
+
if (cachedClient) return cachedClient;
|
|
19
|
+
const url = process.env.CONVEX_URL ?? DEFAULT_CONVEX_URL;
|
|
20
|
+
cachedClient = new ConvexHttpClient(url);
|
|
21
|
+
return cachedClient;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/commands/context.ts
|
|
25
|
+
function registerContext(program2) {
|
|
26
|
+
program2.command("context").description(
|
|
27
|
+
"Get an overview of a site's knowledge \u2014 metadata and file summaries without full body content"
|
|
28
|
+
).argument("<domain>", "Website domain, e.g. github.com").action(async (domain) => {
|
|
29
|
+
const client = getConvexClient();
|
|
30
|
+
const site = await client.query(api.sites.getByDomain, { domain });
|
|
31
|
+
if (!site) {
|
|
32
|
+
console.error(
|
|
33
|
+
`No knowledge found for "${domain}". Try "radar search" or "radar explore" to discover or create knowledge for this site.`
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const files = await client.query(api.files.listBySite, {
|
|
38
|
+
siteId: site._id
|
|
39
|
+
});
|
|
40
|
+
const fileList = files.map(
|
|
41
|
+
(f) => `- **${f.path}** \u2014 ${f.title} [${f.confidence}]
|
|
42
|
+
${f.summary}`
|
|
43
|
+
).join("\n");
|
|
44
|
+
const lines = [
|
|
45
|
+
`# ${site.name} (${site.domain})`,
|
|
46
|
+
"",
|
|
47
|
+
`> ${site.description}`,
|
|
48
|
+
"",
|
|
49
|
+
`Tags: ${site.tags.join(", ")}`,
|
|
50
|
+
`Files: ${site.fileCount}`,
|
|
51
|
+
site.complexity ? `Complexity: ${site.complexity}` : null,
|
|
52
|
+
site.authRequired ? `Auth required: yes` : null,
|
|
53
|
+
"",
|
|
54
|
+
"## Available Knowledge Files",
|
|
55
|
+
"",
|
|
56
|
+
fileList || "_No files yet._"
|
|
57
|
+
].filter((line) => line !== null);
|
|
58
|
+
console.log(lines.join("\n"));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/commands/read.ts
|
|
63
|
+
function reconstructMarkdown(file) {
|
|
64
|
+
const frontmatter = [
|
|
65
|
+
"---",
|
|
66
|
+
`title: "${file.title}"`,
|
|
67
|
+
`domain: "${file.domain}"`,
|
|
68
|
+
`path: "${file.path}"`,
|
|
69
|
+
`summary: "${file.summary}"`,
|
|
70
|
+
`tags: [${file.tags.map((t) => `"${t}"`).join(", ")}]`,
|
|
71
|
+
`entities:`,
|
|
72
|
+
` primary: "${file.entities.primary}"`,
|
|
73
|
+
` disambiguation: "${file.entities.disambiguation}"`,
|
|
74
|
+
` related_concepts: [${file.entities.relatedConcepts.map((c) => `"${c}"`).join(", ")}]`,
|
|
75
|
+
`intent:`,
|
|
76
|
+
` core_question: "${file.intent.coreQuestion}"`,
|
|
77
|
+
` audience: "${file.intent.audience}"`,
|
|
78
|
+
`confidence: "${file.confidence}"`,
|
|
79
|
+
`requires_auth: ${file.requiresAuth}`,
|
|
80
|
+
file.selectorsCount !== void 0 ? `selectors_count: ${file.selectorsCount}` : null,
|
|
81
|
+
`related_files: [${file.relatedFiles.map((f) => `"${f}"`).join(", ")}]`,
|
|
82
|
+
`version: ${file.version}`,
|
|
83
|
+
`last_updated: "${new Date(file.lastUpdated).toISOString()}"`,
|
|
84
|
+
`last_contributor: "${file.lastContributor}"`,
|
|
85
|
+
`last_change_reason: "${file.lastChangeReason}"`,
|
|
86
|
+
"---"
|
|
87
|
+
].filter((line) => line !== null).join("\n");
|
|
88
|
+
return `${frontmatter}
|
|
89
|
+
|
|
90
|
+
${file.content}`;
|
|
91
|
+
}
|
|
92
|
+
function registerRead(program2) {
|
|
93
|
+
program2.command("read").description(
|
|
94
|
+
"Get the full content of a specific knowledge file, including YAML frontmatter and markdown body"
|
|
95
|
+
).argument("<domain>", "Website domain, e.g. github.com").argument("<path>", 'File path within the site, e.g. "flows/checkout"').action(async (domain, path) => {
|
|
96
|
+
const client = getConvexClient();
|
|
97
|
+
const file = await client.query(api.files.getByDomainPath, {
|
|
98
|
+
domain,
|
|
99
|
+
path
|
|
100
|
+
});
|
|
101
|
+
if (!file) {
|
|
102
|
+
console.error(
|
|
103
|
+
`No file found at "${domain}/${path}". Use "radar list ${domain}" to see available files.`
|
|
104
|
+
);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
console.log(reconstructMarkdown(file));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/commands/list.ts
|
|
112
|
+
import { minimatch } from "minimatch";
|
|
113
|
+
function registerList(program2) {
|
|
114
|
+
program2.command("list").description(
|
|
115
|
+
"List knowledge files for a domain with frontmatter summaries (no body content)"
|
|
116
|
+
).argument("<domain>", "Website domain, e.g. github.com").option(
|
|
117
|
+
"--glob <pattern>",
|
|
118
|
+
'Glob pattern to filter files, e.g. "flows/*"'
|
|
119
|
+
).action(async (domain, opts) => {
|
|
120
|
+
const client = getConvexClient();
|
|
121
|
+
const site = await client.query(api.sites.getByDomain, { domain });
|
|
122
|
+
if (!site) {
|
|
123
|
+
console.error(`No site found for "${domain}".`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
let files = await client.query(api.files.listBySite, {
|
|
127
|
+
siteId: site._id
|
|
128
|
+
});
|
|
129
|
+
if (opts.glob) {
|
|
130
|
+
files = files.filter((f) => minimatch(f.path, opts.glob));
|
|
131
|
+
}
|
|
132
|
+
if (files.length === 0) {
|
|
133
|
+
console.log(
|
|
134
|
+
`No files found for "${domain}"${opts.glob ? ` matching "${opts.glob}"` : ""}.`
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const formatted = files.map(
|
|
139
|
+
(f) => `- **${f.path}** \u2014 ${f.title} [${f.confidence}]
|
|
140
|
+
${f.summary}
|
|
141
|
+
Tags: ${f.tags.join(", ")}`
|
|
142
|
+
).join("\n");
|
|
143
|
+
console.log(
|
|
144
|
+
`${files.length} file(s) for ${domain}${opts.glob ? ` (matching "${opts.glob}")` : ""}:
|
|
145
|
+
|
|
146
|
+
${formatted}`
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/commands/search.ts
|
|
152
|
+
function registerSearch(program2) {
|
|
153
|
+
program2.command("search").description(
|
|
154
|
+
"Full-text search across all knowledge file content. Returns matching files with frontmatter."
|
|
155
|
+
).argument("<query>", "Search query text").option("--domain <domain>", "Filter results to a specific domain").action(async (query, opts) => {
|
|
156
|
+
const client = getConvexClient();
|
|
157
|
+
const results = await client.query(api.files.search, {
|
|
158
|
+
query,
|
|
159
|
+
domain: opts.domain
|
|
160
|
+
});
|
|
161
|
+
if (results.length === 0) {
|
|
162
|
+
console.log(
|
|
163
|
+
`No results found for "${query}"${opts.domain ? ` in ${opts.domain}` : ""}. Try broader search terms or check if the site has been explored.`
|
|
164
|
+
);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const formatted = results.map(
|
|
168
|
+
(f) => `### ${f.domain}/${f.path}
|
|
169
|
+
**${f.title}** [${f.confidence}]
|
|
170
|
+
${f.summary}
|
|
171
|
+
Tags: ${f.tags.join(", ")}`
|
|
172
|
+
).join("\n\n");
|
|
173
|
+
console.log(`Found ${results.length} result(s):
|
|
174
|
+
|
|
175
|
+
${formatted}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/commands/submit.ts
|
|
180
|
+
import { readFileSync } from "fs";
|
|
181
|
+
import { parse } from "zod-matter";
|
|
182
|
+
|
|
183
|
+
// src/schemas/frontmatter.ts
|
|
184
|
+
import { z } from "zod";
|
|
185
|
+
var knowledgeFrontmatterSchema = z.object({
|
|
186
|
+
title: z.string(),
|
|
187
|
+
domain: z.string(),
|
|
188
|
+
path: z.string(),
|
|
189
|
+
summary: z.string().max(300),
|
|
190
|
+
tags: z.array(z.string()),
|
|
191
|
+
entities: z.object({
|
|
192
|
+
primary: z.string(),
|
|
193
|
+
disambiguation: z.string(),
|
|
194
|
+
related_concepts: z.array(z.string())
|
|
195
|
+
}),
|
|
196
|
+
intent: z.object({
|
|
197
|
+
core_question: z.string(),
|
|
198
|
+
audience: z.enum(["browser-agent", "coding-agent", "human", "any"])
|
|
199
|
+
}),
|
|
200
|
+
confidence: z.enum(["low", "medium", "high"]),
|
|
201
|
+
requires_auth: z.boolean(),
|
|
202
|
+
selectors_count: z.number().int().min(0).optional(),
|
|
203
|
+
related_files: z.array(z.string()).default([]),
|
|
204
|
+
version: z.number().int().positive(),
|
|
205
|
+
last_updated: z.string().datetime(),
|
|
206
|
+
last_contributor: z.string(),
|
|
207
|
+
last_change_reason: z.string()
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// src/commands/submit.ts
|
|
211
|
+
function registerSubmit(program2) {
|
|
212
|
+
program2.command("submit").description(
|
|
213
|
+
"Submit a knowledge file to Radar. The file must be markdown with valid YAML frontmatter. Auto-approved, versioned, and attributed."
|
|
214
|
+
).argument("<file>", "Path to a local markdown file with YAML frontmatter").requiredOption(
|
|
215
|
+
"--contributor <name>",
|
|
216
|
+
'Your name/identifier, e.g. "my-agent"'
|
|
217
|
+
).requiredOption(
|
|
218
|
+
"--reason <reason>",
|
|
219
|
+
'Why this change was made, e.g. "Added checkout flow tips"'
|
|
220
|
+
).option(
|
|
221
|
+
"--agent-type <type>",
|
|
222
|
+
'Type of agent, e.g. "claude-code", "cursor"'
|
|
223
|
+
).action(
|
|
224
|
+
async (filePath, opts) => {
|
|
225
|
+
let content;
|
|
226
|
+
try {
|
|
227
|
+
content = readFileSync(filePath, "utf-8");
|
|
228
|
+
} catch {
|
|
229
|
+
console.error(`Could not read file: ${filePath}`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
let parsed;
|
|
233
|
+
try {
|
|
234
|
+
parsed = parse(content, knowledgeFrontmatterSchema);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
console.error(
|
|
238
|
+
`Invalid knowledge file format:
|
|
239
|
+
|
|
240
|
+
${message}
|
|
241
|
+
|
|
242
|
+
Ensure the markdown has valid YAML frontmatter matching the knowledge file schema.`
|
|
243
|
+
);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
const { data: fm, content: body } = parsed;
|
|
247
|
+
const client = getConvexClient();
|
|
248
|
+
const result = await client.mutation(api.files.submit, {
|
|
249
|
+
domain: fm.domain,
|
|
250
|
+
path: fm.path,
|
|
251
|
+
title: fm.title,
|
|
252
|
+
summary: fm.summary,
|
|
253
|
+
tags: fm.tags,
|
|
254
|
+
entities: {
|
|
255
|
+
primary: fm.entities.primary,
|
|
256
|
+
disambiguation: fm.entities.disambiguation,
|
|
257
|
+
relatedConcepts: fm.entities.related_concepts
|
|
258
|
+
},
|
|
259
|
+
intent: {
|
|
260
|
+
coreQuestion: fm.intent.core_question,
|
|
261
|
+
audience: fm.intent.audience
|
|
262
|
+
},
|
|
263
|
+
confidence: fm.confidence,
|
|
264
|
+
requiresAuth: fm.requires_auth,
|
|
265
|
+
selectorsCount: fm.selectors_count,
|
|
266
|
+
relatedFiles: fm.related_files,
|
|
267
|
+
content: body.trim(),
|
|
268
|
+
contributorName: opts.contributor,
|
|
269
|
+
changeReason: opts.reason,
|
|
270
|
+
agentType: opts.agentType
|
|
271
|
+
});
|
|
272
|
+
console.log(
|
|
273
|
+
`Submitted successfully!
|
|
274
|
+
|
|
275
|
+
File: ${fm.domain}/${fm.path}
|
|
276
|
+
Version: ${result.version}
|
|
277
|
+
Points awarded: ${result.pointsAwarded}`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/urls.ts
|
|
284
|
+
function normalizeUrl(rawUrl) {
|
|
285
|
+
if (!rawUrl.includes("://")) rawUrl = `https://${rawUrl}`;
|
|
286
|
+
const parsed = new URL(rawUrl);
|
|
287
|
+
let domain = parsed.hostname.toLowerCase();
|
|
288
|
+
if (domain.startsWith("www.")) domain = domain.slice(4);
|
|
289
|
+
let path = parsed.pathname.toLowerCase();
|
|
290
|
+
if (path.endsWith("/") && path.length > 1) path = path.slice(0, -1);
|
|
291
|
+
if (path === "") path = "/";
|
|
292
|
+
return { domain, path, url: `https://${domain}${path}` };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/commands/explore.ts
|
|
296
|
+
function registerExplore(program2) {
|
|
297
|
+
program2.command("explore").description(
|
|
298
|
+
"Trigger a Browser Use exploration of a website. Queues an automated agent to navigate the site and generate knowledge files."
|
|
299
|
+
).argument(
|
|
300
|
+
"<url>",
|
|
301
|
+
'URL of the site to explore, e.g. "https://amazon.com" or "github.com"'
|
|
302
|
+
).action(async (url) => {
|
|
303
|
+
const { domain, url: normalizedUrl } = normalizeUrl(url);
|
|
304
|
+
const client = getConvexClient();
|
|
305
|
+
const explorationId = await client.mutation(api.explorations.create, {
|
|
306
|
+
domain,
|
|
307
|
+
url: normalizedUrl
|
|
308
|
+
});
|
|
309
|
+
console.log(
|
|
310
|
+
`Exploration queued for ${domain}.
|
|
311
|
+
|
|
312
|
+
Exploration ID: ${explorationId}
|
|
313
|
+
URL: ${normalizedUrl}
|
|
314
|
+
|
|
315
|
+
The Browser Use agent will explore the site and generate knowledge files. Check the Radar web UI for real-time progress.`
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/index.ts
|
|
321
|
+
var program = new Command();
|
|
322
|
+
program.name("radar").description(
|
|
323
|
+
"Radar CLI \u2014 query and contribute to the shared knowledge base for web agents"
|
|
324
|
+
).version("0.1.0");
|
|
325
|
+
registerContext(program);
|
|
326
|
+
registerRead(program);
|
|
327
|
+
registerList(program);
|
|
328
|
+
registerSearch(program);
|
|
329
|
+
registerSubmit(program);
|
|
330
|
+
registerExplore(program);
|
|
331
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "radar-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Radar — the shared knowledge base for web agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"radar": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsc -b --watch",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"check-types": "tsc --noEmit",
|
|
17
|
+
"prepublishOnly": "pnpm run build",
|
|
18
|
+
"publish:npm": "npm publish --access public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"radar",
|
|
22
|
+
"web-agents",
|
|
23
|
+
"knowledge-base",
|
|
24
|
+
"mcp",
|
|
25
|
+
"browser-automation"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"commander": "^13.1.0",
|
|
30
|
+
"convex": "^1.21.0",
|
|
31
|
+
"gray-matter": "^4.0.3",
|
|
32
|
+
"minimatch": "^10.2.4",
|
|
33
|
+
"zod": "^3.25.76",
|
|
34
|
+
"zod-matter": "^0.1.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@radar/typescript-config": "workspace:*",
|
|
38
|
+
"@types/node": "^25.3.3",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "5.9.2"
|
|
41
|
+
}
|
|
42
|
+
}
|