@aeosome/cli 0.0.0-stage → 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/aeosome.js +830 -0
- package/package.json +30 -5
- package/README.md +0 -3
package/dist/aeosome.js
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/args.ts
|
|
4
|
+
var SWITCHES = new Set(["json", "help", "version"]);
|
|
5
|
+
var isFlag = (token) => token?.startsWith("--") ?? false;
|
|
6
|
+
function splitFlag(token) {
|
|
7
|
+
const body = token.slice(2);
|
|
8
|
+
const eq = body.indexOf("=");
|
|
9
|
+
return eq === -1 ? [body, undefined] : [body.slice(0, eq), body.slice(eq + 1)];
|
|
10
|
+
}
|
|
11
|
+
function takesValue(name, next) {
|
|
12
|
+
return !SWITCHES.has(name) && next !== undefined && !isFlag(next);
|
|
13
|
+
}
|
|
14
|
+
function flagValue(name, inline, next) {
|
|
15
|
+
if (inline !== undefined)
|
|
16
|
+
return { value: inline, consumed: 0 };
|
|
17
|
+
if (takesValue(name, next))
|
|
18
|
+
return { value: next, consumed: 1 };
|
|
19
|
+
return { value: true, consumed: 0 };
|
|
20
|
+
}
|
|
21
|
+
function consume(argv, i, flags, words) {
|
|
22
|
+
const token = argv[i] ?? "";
|
|
23
|
+
if (!isFlag(token))
|
|
24
|
+
return words.push(token) && 1;
|
|
25
|
+
const [name, inline] = splitFlag(token);
|
|
26
|
+
const { value, consumed } = flagValue(name, inline, argv[i + 1]);
|
|
27
|
+
flags.set(name, value);
|
|
28
|
+
return 1 + consumed;
|
|
29
|
+
}
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const flags = new Map;
|
|
32
|
+
const words = [];
|
|
33
|
+
let i = 0;
|
|
34
|
+
while (i < argv.length)
|
|
35
|
+
i += consume(argv, i, flags, words);
|
|
36
|
+
const [command, ...positionals] = words;
|
|
37
|
+
return { command, positionals, flags };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/client.ts
|
|
41
|
+
var VERSION = "0.1.0";
|
|
42
|
+
var EXIT = {
|
|
43
|
+
ok: 0,
|
|
44
|
+
usage: 1,
|
|
45
|
+
auth: 2,
|
|
46
|
+
plan: 3,
|
|
47
|
+
api: 4,
|
|
48
|
+
network: 5
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
class CliError extends Error {
|
|
52
|
+
code;
|
|
53
|
+
exitCode;
|
|
54
|
+
constructor(code, message, exitCode) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.exitCode = exitCode;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var TIMEOUT_MS = 30000;
|
|
61
|
+
async function parse(response) {
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text);
|
|
65
|
+
} catch {
|
|
66
|
+
return { message: text };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var messageOf = (status, body) => body.message ?? body.error ?? `HTTP ${status}`;
|
|
70
|
+
var UPGRADE_URL = "https://app.aeosome.com/dashboard/settings/billing";
|
|
71
|
+
function failureFor(status, body) {
|
|
72
|
+
const message = messageOf(status, body);
|
|
73
|
+
if (status === 401)
|
|
74
|
+
return new CliError("UNAUTHORIZED", "The API key is missing, invalid or revoked. Run `aeosome login`.", EXIT.auth);
|
|
75
|
+
if (body.code === "FEATURE_DISABLED")
|
|
76
|
+
return new CliError(body.code, `${message} Upgrade at ${UPGRADE_URL}`, EXIT.plan);
|
|
77
|
+
return new CliError(body.code ?? `HTTP_${status}`, message, EXIT.api);
|
|
78
|
+
}
|
|
79
|
+
async function send(url, init) {
|
|
80
|
+
try {
|
|
81
|
+
return await fetch(url, {
|
|
82
|
+
...init,
|
|
83
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
87
|
+
throw new CliError("NETWORK", `Could not reach ${url}: ${reason}`, EXIT.network);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function authHeaders(credentials) {
|
|
91
|
+
return {
|
|
92
|
+
authorization: `Bearer ${credentials.apiKey}`,
|
|
93
|
+
"user-agent": `aeosome-cli/${VERSION}`,
|
|
94
|
+
accept: "application/json"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function getJson(credentials, path) {
|
|
98
|
+
const response = await send(`${credentials.apiUrl}${path}`, {
|
|
99
|
+
headers: authHeaders(credentials)
|
|
100
|
+
});
|
|
101
|
+
const body = await parse(response);
|
|
102
|
+
if (!response.ok)
|
|
103
|
+
throw failureFor(response.status, body);
|
|
104
|
+
return body;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ../../packages/agent-tools/src/types.ts
|
|
108
|
+
var DAYS = {
|
|
109
|
+
name: "days",
|
|
110
|
+
description: "Look-back window in days (default 30). The plan's history limit caps it.",
|
|
111
|
+
type: "integer",
|
|
112
|
+
in: "query",
|
|
113
|
+
minimum: 1,
|
|
114
|
+
maximum: 365
|
|
115
|
+
};
|
|
116
|
+
var PAGE = {
|
|
117
|
+
name: "page",
|
|
118
|
+
description: "Page number, from 1.",
|
|
119
|
+
type: "integer",
|
|
120
|
+
in: "query",
|
|
121
|
+
minimum: 1
|
|
122
|
+
};
|
|
123
|
+
var LIMIT = {
|
|
124
|
+
name: "limit",
|
|
125
|
+
description: "Rows per page (default 20, max 100).",
|
|
126
|
+
type: "integer",
|
|
127
|
+
in: "query",
|
|
128
|
+
minimum: 1,
|
|
129
|
+
maximum: 100
|
|
130
|
+
};
|
|
131
|
+
var id = (description) => ({
|
|
132
|
+
name: "id",
|
|
133
|
+
description,
|
|
134
|
+
type: "string",
|
|
135
|
+
in: "path",
|
|
136
|
+
required: true
|
|
137
|
+
});
|
|
138
|
+
var PERIOD = ["period.days", "Period (days)"];
|
|
139
|
+
|
|
140
|
+
// ../../packages/agent-tools/src/tools-actions.ts
|
|
141
|
+
var ACTION_TOOLS = [
|
|
142
|
+
{
|
|
143
|
+
name: "get_competitor_leaderboard",
|
|
144
|
+
command: "competitors",
|
|
145
|
+
description: "Competitors ranked by share of voice in AI answers, with prompts won and citation share.",
|
|
146
|
+
path: "/api/competitor-analytics/leaderboard",
|
|
147
|
+
params: [DAYS],
|
|
148
|
+
view: {
|
|
149
|
+
headline: [PERIOD, ["dataSignals.totalRuns", "Answers analysed"]],
|
|
150
|
+
table: {
|
|
151
|
+
rows: "rows",
|
|
152
|
+
columns: [
|
|
153
|
+
["name", "Brand"],
|
|
154
|
+
["isOwn", "You"],
|
|
155
|
+
["shareOfVoice", "Share of voice (%)"],
|
|
156
|
+
["promptsWon", "Questions won"],
|
|
157
|
+
["citationShare", "Citation share (%)"]
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "get_citation_sources",
|
|
164
|
+
command: "citations",
|
|
165
|
+
description: "The domains AI engines cite most in answers about the store's products.",
|
|
166
|
+
path: "/api/citations/stats",
|
|
167
|
+
params: [],
|
|
168
|
+
view: {
|
|
169
|
+
headline: [
|
|
170
|
+
["totalCitations", "Citations"],
|
|
171
|
+
["uniqueDomains", "Domains"],
|
|
172
|
+
["ownDomainCount", "Citations of your domain"]
|
|
173
|
+
],
|
|
174
|
+
table: {
|
|
175
|
+
rows: "topDomains",
|
|
176
|
+
columns: [
|
|
177
|
+
["domain", "Domain"],
|
|
178
|
+
["count", "Citations"]
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "get_citation_opportunities",
|
|
185
|
+
command: "opportunities",
|
|
186
|
+
description: "Sites that AI engines cite for competitors but never for the store: the outreach list.",
|
|
187
|
+
path: "/api/citation-analytics/opportunities",
|
|
188
|
+
params: [DAYS],
|
|
189
|
+
view: {
|
|
190
|
+
headline: [PERIOD],
|
|
191
|
+
table: {
|
|
192
|
+
rows: "opportunities",
|
|
193
|
+
columns: [
|
|
194
|
+
["domain", "Domain"],
|
|
195
|
+
["domainType", "Type"],
|
|
196
|
+
["competitorMentions", "Competitor citations"],
|
|
197
|
+
["topCompetitor.name", "Top competitor"]
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "list_recommendations",
|
|
204
|
+
command: "fixes",
|
|
205
|
+
description: "Fixes to raise AI visibility, with priority, expected impact and step-by-step implementation guides.",
|
|
206
|
+
path: "/api/recommendations",
|
|
207
|
+
params: [
|
|
208
|
+
PAGE,
|
|
209
|
+
LIMIT,
|
|
210
|
+
{
|
|
211
|
+
name: "status",
|
|
212
|
+
description: "Filter by status.",
|
|
213
|
+
type: "string",
|
|
214
|
+
in: "query",
|
|
215
|
+
enum: ["pending", "applied", "dismissed"]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "priority",
|
|
219
|
+
description: "Filter by priority.",
|
|
220
|
+
type: "string",
|
|
221
|
+
in: "query",
|
|
222
|
+
enum: ["critical", "high", "medium", "low"]
|
|
223
|
+
}
|
|
224
|
+
],
|
|
225
|
+
view: {
|
|
226
|
+
headline: [
|
|
227
|
+
["total", "Fixes"],
|
|
228
|
+
["locked", "Fixes locked by plan"]
|
|
229
|
+
],
|
|
230
|
+
table: {
|
|
231
|
+
rows: "items",
|
|
232
|
+
columns: [
|
|
233
|
+
["priority", "Priority"],
|
|
234
|
+
["status", "Status"],
|
|
235
|
+
["recommendationText", "Fix"],
|
|
236
|
+
["expectedImpact", "Expected impact"]
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "get_improvement_summary",
|
|
243
|
+
command: "progress",
|
|
244
|
+
description: "Open issues by category, fixes applied in the last 7 and 30 days, and the measured visibility change after fixes.",
|
|
245
|
+
path: "/api/improve/summary",
|
|
246
|
+
params: [DAYS],
|
|
247
|
+
view: {
|
|
248
|
+
headline: [
|
|
249
|
+
PERIOD,
|
|
250
|
+
["applied.appliedLast7d", "Fixes applied (7 days)"],
|
|
251
|
+
["applied.appliedLast30d", "Fixes applied (30 days)"],
|
|
252
|
+
["verification.totalVerified", "Fixes verified"],
|
|
253
|
+
[
|
|
254
|
+
"verification.avgVisibilityDelta",
|
|
255
|
+
"Avg visibility change after a fix (points)"
|
|
256
|
+
]
|
|
257
|
+
],
|
|
258
|
+
table: {
|
|
259
|
+
rows: "issues",
|
|
260
|
+
columns: [
|
|
261
|
+
["category", "Category"],
|
|
262
|
+
["openCount", "Open issues"],
|
|
263
|
+
["productsAffected", "Products affected"]
|
|
264
|
+
]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "list_alerts",
|
|
270
|
+
command: "alerts",
|
|
271
|
+
description: "Recent alerts: visibility drops, new competitors, and findings.",
|
|
272
|
+
path: "/api/alerts",
|
|
273
|
+
params: [PAGE, LIMIT],
|
|
274
|
+
view: {
|
|
275
|
+
headline: [["total", "Unread alerts"]],
|
|
276
|
+
table: {
|
|
277
|
+
rows: "alerts",
|
|
278
|
+
columns: [
|
|
279
|
+
["triggeredAt", "When"],
|
|
280
|
+
["severity", "Severity"],
|
|
281
|
+
["title", "Alert"]
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: "get_usage",
|
|
288
|
+
command: "usage",
|
|
289
|
+
description: "Plan limits and current usage: products, answers this month, team seats and the billing period end.",
|
|
290
|
+
path: "/api/usage",
|
|
291
|
+
params: [],
|
|
292
|
+
view: {
|
|
293
|
+
headline: [
|
|
294
|
+
["planName", "Plan"],
|
|
295
|
+
["productsTracked.used", "Products tracked"],
|
|
296
|
+
["productsTracked.limit", "Product limit"],
|
|
297
|
+
["monthlyResponses.used", "Answers this month"],
|
|
298
|
+
["monthlyResponses.derivedCap", "Answer limit per month"],
|
|
299
|
+
["teamMembers.used", "Team members"],
|
|
300
|
+
["teamMembers.limit", "Team member limit"],
|
|
301
|
+
["periodEnd", "Period ends"]
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
// ../../packages/agent-tools/src/tools-visibility.ts
|
|
308
|
+
var VISIBILITY_TOOLS = [
|
|
309
|
+
{
|
|
310
|
+
name: "whoami",
|
|
311
|
+
command: "whoami",
|
|
312
|
+
description: "The workspace and plan this API key belongs to.",
|
|
313
|
+
path: "/api/api-keys/current",
|
|
314
|
+
params: [],
|
|
315
|
+
view: {
|
|
316
|
+
headline: [
|
|
317
|
+
["organization.name", "Workspace"],
|
|
318
|
+
["plan", "Plan"]
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "get_brief",
|
|
324
|
+
command: "brief",
|
|
325
|
+
description: "One-paragraph status of the store's AI visibility this week: what slipped, which rival is named instead, and what changed.",
|
|
326
|
+
path: "/api/insights/brief",
|
|
327
|
+
params: [],
|
|
328
|
+
view: {
|
|
329
|
+
headline: [
|
|
330
|
+
["status.sentence", "Status"],
|
|
331
|
+
["status.period", "Period"],
|
|
332
|
+
["questions.asked", "Shopper questions asked"],
|
|
333
|
+
["questions.shownIn", "Questions that showed the store"],
|
|
334
|
+
["rival.name", "Rival named most instead"]
|
|
335
|
+
],
|
|
336
|
+
table: { rows: "changes", columns: [["text", "Change"]] }
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "get_dashboard",
|
|
341
|
+
command: "dashboard",
|
|
342
|
+
description: "Headline AI visibility KPIs with trend against the prior period: visibility, top-3 presence, share of voice, citation health, shopping appearance.",
|
|
343
|
+
path: "/api/analytics/dashboard",
|
|
344
|
+
params: [DAYS],
|
|
345
|
+
view: {
|
|
346
|
+
headline: [
|
|
347
|
+
PERIOD,
|
|
348
|
+
["kpis.aiVisibility.value", "AI visibility (% of answers)"],
|
|
349
|
+
["kpis.aiVisibility.trend", "AI visibility change (points)"],
|
|
350
|
+
["kpis.top3Presence.value", "Top-3 presence (% of answers)"],
|
|
351
|
+
["kpis.shareOfVoice.value", "Share of voice (%)"],
|
|
352
|
+
["kpis.shoppingAppearance.value", "Shopping appearance (% of answers)"],
|
|
353
|
+
["dataSignals.totalRuns", "Answers analysed"],
|
|
354
|
+
["dataSignals.productsTracked", "Products tracked"]
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "get_share_of_voice",
|
|
360
|
+
command: "share-of-voice",
|
|
361
|
+
description: "Share of brand mentions in AI answers against competitors, optionally for one engine.",
|
|
362
|
+
path: "/api/analytics/share-of-voice",
|
|
363
|
+
params: [
|
|
364
|
+
DAYS,
|
|
365
|
+
{
|
|
366
|
+
name: "engine",
|
|
367
|
+
description: "One engine id: chatgpt or google_ai_mode.",
|
|
368
|
+
type: "string",
|
|
369
|
+
in: "query"
|
|
370
|
+
}
|
|
371
|
+
],
|
|
372
|
+
view: {
|
|
373
|
+
headline: [
|
|
374
|
+
["shareOfVoice", "Share of voice (%)"],
|
|
375
|
+
["brandMentions", "Brand mentions (answers)"],
|
|
376
|
+
["competitorMentions", "Competitor mentions (answers)"],
|
|
377
|
+
["totalResults", "Answers analysed"]
|
|
378
|
+
],
|
|
379
|
+
table: {
|
|
380
|
+
rows: "topCompetitors",
|
|
381
|
+
columns: [
|
|
382
|
+
["name", "Competitor"],
|
|
383
|
+
["mentions", "Mentions"],
|
|
384
|
+
["shareOfVoice", "Share (%)"]
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: "list_products",
|
|
391
|
+
command: "products",
|
|
392
|
+
description: "Tracked products with their store URLs.",
|
|
393
|
+
path: "/api/products",
|
|
394
|
+
params: [PAGE, LIMIT],
|
|
395
|
+
view: {
|
|
396
|
+
headline: [["total", "Products"]],
|
|
397
|
+
table: {
|
|
398
|
+
rows: "items",
|
|
399
|
+
columns: [
|
|
400
|
+
["id", "ID"],
|
|
401
|
+
["title", "Title"],
|
|
402
|
+
["url", "URL"]
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "list_prompts",
|
|
409
|
+
command: "prompts",
|
|
410
|
+
description: "Shopper questions sent to AI engines, whether the store was named, and which engines showed it. Use a question id with get_prompt_responses.",
|
|
411
|
+
path: "/api/prompts",
|
|
412
|
+
params: [
|
|
413
|
+
DAYS,
|
|
414
|
+
PAGE,
|
|
415
|
+
LIMIT,
|
|
416
|
+
{
|
|
417
|
+
name: "missingBrand",
|
|
418
|
+
description: '"true" to list only questions where the store was not named.',
|
|
419
|
+
type: "string",
|
|
420
|
+
in: "query",
|
|
421
|
+
enum: ["true", "false"]
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "productId",
|
|
425
|
+
description: "Only questions for this product id.",
|
|
426
|
+
type: "string",
|
|
427
|
+
in: "query"
|
|
428
|
+
}
|
|
429
|
+
],
|
|
430
|
+
view: {
|
|
431
|
+
headline: [["total", "Questions"]],
|
|
432
|
+
table: {
|
|
433
|
+
rows: "items",
|
|
434
|
+
columns: [
|
|
435
|
+
["generatedQueryIds.0", "Question ID"],
|
|
436
|
+
["queryText", "Question"],
|
|
437
|
+
["brandMentioned", "Named"],
|
|
438
|
+
["visibility", "Visibility (%)"],
|
|
439
|
+
["executionCount", "Answers"]
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "get_prompt_responses",
|
|
446
|
+
command: "responses",
|
|
447
|
+
description: "The full AI answers to one shopper question, per engine, with the products and sources each answer cited.",
|
|
448
|
+
path: "/api/prompts/{id}/responses",
|
|
449
|
+
params: [id("Question id from list_prompts (generatedQueryIds[0]).")],
|
|
450
|
+
view: {
|
|
451
|
+
headline: [
|
|
452
|
+
["queryText", "Question"],
|
|
453
|
+
["productTitle", "Product"]
|
|
454
|
+
],
|
|
455
|
+
table: {
|
|
456
|
+
rows: "responses",
|
|
457
|
+
columns: [
|
|
458
|
+
["platform", "Engine"],
|
|
459
|
+
["executedAt", "Asked at"],
|
|
460
|
+
["rawResponseText", "Answer"]
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
// ../../packages/agent-tools/src/catalog.ts
|
|
468
|
+
var AGENT_TOOLS = [
|
|
469
|
+
...VISIBILITY_TOOLS,
|
|
470
|
+
...ACTION_TOOLS
|
|
471
|
+
];
|
|
472
|
+
function findTool(nameOrCommand) {
|
|
473
|
+
return AGENT_TOOLS.find((t) => t.name === nameOrCommand || t.command === nameOrCommand);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ../../packages/agent-tools/src/request.ts
|
|
477
|
+
class ToolArgError extends Error {
|
|
478
|
+
}
|
|
479
|
+
function inRange(p, n) {
|
|
480
|
+
const min = p.minimum ?? Number.NEGATIVE_INFINITY;
|
|
481
|
+
const max = p.maximum ?? Number.POSITIVE_INFINITY;
|
|
482
|
+
return Number.isInteger(n) && n >= min && n <= max;
|
|
483
|
+
}
|
|
484
|
+
var isOutOfRange = (p, value) => p.type === "integer" && !inRange(p, Number(value));
|
|
485
|
+
var isOffEnum = (p, value) => p.enum !== undefined && !p.enum.includes(value);
|
|
486
|
+
function checkValue(p, value) {
|
|
487
|
+
if (isOutOfRange(p, value))
|
|
488
|
+
throw new ToolArgError(`${p.name} must be a whole number in range`);
|
|
489
|
+
if (isOffEnum(p, value))
|
|
490
|
+
throw new ToolArgError(`${p.name} must be one of ${p.enum?.join(", ")}`);
|
|
491
|
+
}
|
|
492
|
+
var isBlank = (raw) => raw === undefined || raw === null || raw === "";
|
|
493
|
+
function argValue(p, args) {
|
|
494
|
+
const raw = args[p.name];
|
|
495
|
+
if (isBlank(raw) && p.required)
|
|
496
|
+
throw new ToolArgError(`${p.name} is required`);
|
|
497
|
+
if (isBlank(raw))
|
|
498
|
+
return;
|
|
499
|
+
const value = String(raw);
|
|
500
|
+
checkValue(p, value);
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
function rejectUnknown(tool, args) {
|
|
504
|
+
const known = new Set(tool.params.map((p) => p.name));
|
|
505
|
+
const unknown = Object.keys(args).filter((k) => !known.has(k));
|
|
506
|
+
if (unknown.length > 0)
|
|
507
|
+
throw new ToolArgError(`Unknown argument: ${unknown.join(", ")}`);
|
|
508
|
+
}
|
|
509
|
+
function fillPath(tool, values) {
|
|
510
|
+
return tool.params.filter((p) => p.in === "path").reduce((path, p) => path.replace(`{${p.name}}`, encodeURIComponent(values.get(p.name) ?? "")), tool.path);
|
|
511
|
+
}
|
|
512
|
+
function collectValues(tool, args) {
|
|
513
|
+
const entries = tool.params.map((p) => [p.name, argValue(p, args)]).filter((e) => e[1] !== undefined);
|
|
514
|
+
return new Map(entries);
|
|
515
|
+
}
|
|
516
|
+
function buildToolPath(tool, args = {}) {
|
|
517
|
+
rejectUnknown(tool, args);
|
|
518
|
+
const values = collectValues(tool, args);
|
|
519
|
+
const query = new URLSearchParams(tool.params.filter((p) => p.in === "query" && values.has(p.name)).map((p) => [p.name, values.get(p.name) ?? ""])).toString();
|
|
520
|
+
const path = fillPath(tool, values);
|
|
521
|
+
return query ? `${path}?${query}` : path;
|
|
522
|
+
}
|
|
523
|
+
function readField(data, path) {
|
|
524
|
+
return path.split(".").reduce((current, key) => current !== null && typeof current === "object" ? current[key] : undefined, data);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/config.ts
|
|
528
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
529
|
+
import { homedir } from "node:os";
|
|
530
|
+
import { dirname, join } from "node:path";
|
|
531
|
+
var DEFAULT_API_URL = "https://api.aeosome.com";
|
|
532
|
+
function credentialsPath(env = process.env) {
|
|
533
|
+
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
534
|
+
return join(base, "aeosome", "credentials.json");
|
|
535
|
+
}
|
|
536
|
+
async function readSaved(path) {
|
|
537
|
+
try {
|
|
538
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
539
|
+
} catch {
|
|
540
|
+
return {};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function loadCredentials(env = process.env) {
|
|
544
|
+
const saved = await readSaved(credentialsPath(env));
|
|
545
|
+
return {
|
|
546
|
+
apiKey: env.AEOSOME_API_KEY || saved.apiKey,
|
|
547
|
+
apiUrl: env.AEOSOME_API_URL || saved.apiUrl || DEFAULT_API_URL
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function saveCredentials(credentials, env = process.env) {
|
|
551
|
+
const path = credentialsPath(env);
|
|
552
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
553
|
+
await writeFile(path, `${JSON.stringify(credentials, null, 2)}
|
|
554
|
+
`, {
|
|
555
|
+
mode: 384
|
|
556
|
+
});
|
|
557
|
+
return path;
|
|
558
|
+
}
|
|
559
|
+
async function clearCredentials(env = process.env) {
|
|
560
|
+
await rm(credentialsPath(env), { force: true });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/mcp-bridge.ts
|
|
564
|
+
import { createInterface } from "node:readline";
|
|
565
|
+
var idOf = (line) => {
|
|
566
|
+
try {
|
|
567
|
+
return JSON.parse(line).id;
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
function rpcError(id2, message) {
|
|
573
|
+
return JSON.stringify({
|
|
574
|
+
jsonrpc: "2.0",
|
|
575
|
+
id: id2 ?? null,
|
|
576
|
+
error: { code: -32000, message }
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async function errorText(response) {
|
|
580
|
+
const body = await response.json().catch(() => ({}));
|
|
581
|
+
return body.message ?? body.error ?? `HTTP ${response.status}`;
|
|
582
|
+
}
|
|
583
|
+
function post(credentials, line) {
|
|
584
|
+
return fetch(`${credentials.apiUrl}/mcp`, {
|
|
585
|
+
method: "POST",
|
|
586
|
+
headers: {
|
|
587
|
+
...authHeaders(credentials),
|
|
588
|
+
"content-type": "application/json"
|
|
589
|
+
},
|
|
590
|
+
body: line,
|
|
591
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
async function replyFor(response, id2) {
|
|
595
|
+
if (response.status === 202)
|
|
596
|
+
return null;
|
|
597
|
+
if (response.ok)
|
|
598
|
+
return response.text();
|
|
599
|
+
return id2 === undefined ? null : rpcError(id2, await errorText(response));
|
|
600
|
+
}
|
|
601
|
+
async function relay(credentials, line, write) {
|
|
602
|
+
const id2 = idOf(line);
|
|
603
|
+
const reply = await post(credentials, line).then((response) => replyFor(response, id2)).catch((error) => id2 === undefined ? null : rpcError(id2, String(error)));
|
|
604
|
+
if (reply)
|
|
605
|
+
write(reply);
|
|
606
|
+
}
|
|
607
|
+
async function runBridge(credentials) {
|
|
608
|
+
const write = (line) => process.stdout.write(`${line.trim()}
|
|
609
|
+
`);
|
|
610
|
+
const pending = [];
|
|
611
|
+
for await (const line of createInterface({ input: process.stdin })) {
|
|
612
|
+
if (line.trim())
|
|
613
|
+
pending.push(relay(credentials, line, write));
|
|
614
|
+
}
|
|
615
|
+
await Promise.all(pending);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/prompt.ts
|
|
619
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
620
|
+
import { Writable } from "node:stream";
|
|
621
|
+
var silent = new Writable({ write: (_chunk, _encoding, done) => done() });
|
|
622
|
+
async function readPiped() {
|
|
623
|
+
const chunks = [];
|
|
624
|
+
for await (const chunk of process.stdin)
|
|
625
|
+
chunks.push(Buffer.from(chunk));
|
|
626
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
627
|
+
}
|
|
628
|
+
async function readHidden() {
|
|
629
|
+
const rl = createInterface2({
|
|
630
|
+
input: process.stdin,
|
|
631
|
+
output: silent,
|
|
632
|
+
terminal: true
|
|
633
|
+
});
|
|
634
|
+
try {
|
|
635
|
+
return await new Promise((resolve) => rl.question("", resolve));
|
|
636
|
+
} finally {
|
|
637
|
+
rl.close();
|
|
638
|
+
process.stderr.write(`
|
|
639
|
+
`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function readSecret(question) {
|
|
643
|
+
process.stderr.write(question);
|
|
644
|
+
const text = await (process.stdin.isTTY ? readHidden() : readPiped());
|
|
645
|
+
return text.trim();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/render.ts
|
|
649
|
+
var MIN_COLUMN = 8;
|
|
650
|
+
var GAP = " ";
|
|
651
|
+
var formatText = (value) => String(value).replace(/\s+/g, " ").trim();
|
|
652
|
+
var FORMATTERS = {
|
|
653
|
+
boolean: (value) => value ? "yes" : "no",
|
|
654
|
+
object: (value) => JSON.stringify(value)
|
|
655
|
+
};
|
|
656
|
+
function formatValue(value) {
|
|
657
|
+
if (value === null || value === undefined)
|
|
658
|
+
return "-";
|
|
659
|
+
return (FORMATTERS[typeof value] ?? formatText)(value);
|
|
660
|
+
}
|
|
661
|
+
function clip(text, width) {
|
|
662
|
+
return text.length <= width ? text : `${text.slice(0, width - 1)}…`;
|
|
663
|
+
}
|
|
664
|
+
function renderHeadline(fields, data) {
|
|
665
|
+
const pad = Math.max(...fields.map(([, label]) => label.length));
|
|
666
|
+
return fields.map(([path, label]) => `${label.padEnd(pad)} ${formatValue(readField(data, path))}`);
|
|
667
|
+
}
|
|
668
|
+
function columnWidths(cells, width) {
|
|
669
|
+
const count = cells[0]?.length ?? 1;
|
|
670
|
+
const budget = Math.max(MIN_COLUMN, Math.floor((width - GAP.length * (count - 1)) / count));
|
|
671
|
+
return (cells[0] ?? []).map((_, c) => Math.min(budget, Math.max(...cells.map((row) => row[c]?.length ?? 0))));
|
|
672
|
+
}
|
|
673
|
+
function renderTable(columns, rows, width) {
|
|
674
|
+
const header = columns.map(([, label]) => label);
|
|
675
|
+
const body = rows.map((row) => columns.map(([path]) => formatValue(readField(row, path))));
|
|
676
|
+
const cells = [header, ...body];
|
|
677
|
+
const widths = columnWidths(cells, width);
|
|
678
|
+
return cells.map((row) => row.map((cell, c) => clip(cell, widths[c] ?? MIN_COLUMN).padEnd(widths[c] ?? 0)).join(GAP).trimEnd());
|
|
679
|
+
}
|
|
680
|
+
function tableLines(tool, data, width) {
|
|
681
|
+
const table = tool.view.table;
|
|
682
|
+
if (!table)
|
|
683
|
+
return [];
|
|
684
|
+
const rows = readField(data, table.rows);
|
|
685
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
686
|
+
return ["", "No rows."];
|
|
687
|
+
return ["", ...renderTable(table.columns, rows, width)];
|
|
688
|
+
}
|
|
689
|
+
function renderHuman(tool, data, width = 100) {
|
|
690
|
+
const headline = tool.view.headline ?? [];
|
|
691
|
+
return [...renderHeadline(headline, data), ...tableLines(tool, data, width)].join(`
|
|
692
|
+
`).trim();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/commands.ts
|
|
696
|
+
var flagName = (name) => name.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
697
|
+
function toolUsage(tool) {
|
|
698
|
+
const flags = tool.params.map((p) => p.in === "path" ? `<${p.name}>` : `[--${flagName(p.name)} <${p.type}>]`).join(" ");
|
|
699
|
+
return ` ${`${tool.command} ${flags}`.padEnd(58)} ${tool.description.split(". ")[0]}`;
|
|
700
|
+
}
|
|
701
|
+
function helpText() {
|
|
702
|
+
return [
|
|
703
|
+
`aeosome ${VERSION}: your AI visibility data from a terminal, a script or an agent.`,
|
|
704
|
+
"",
|
|
705
|
+
"Usage: aeosome <command> [flags] [--json]",
|
|
706
|
+
"",
|
|
707
|
+
"Account:",
|
|
708
|
+
" login Save an API key (Settings, Integrations, API access)",
|
|
709
|
+
" logout Forget the saved key",
|
|
710
|
+
" tools List data commands as JSON-ready metadata",
|
|
711
|
+
" mcp Run a stdio MCP server for Claude Desktop, Cursor and others",
|
|
712
|
+
"",
|
|
713
|
+
"Data (read-only):",
|
|
714
|
+
...AGENT_TOOLS.map(toolUsage),
|
|
715
|
+
"",
|
|
716
|
+
"Flags: --json prints the API response as JSON on stdout. Errors go to stderr.",
|
|
717
|
+
"Env: AEOSOME_API_KEY and AEOSOME_API_URL override the saved login.",
|
|
718
|
+
"Exit: 0 ok, 1 usage, 2 not signed in, 3 needs a paid plan, 4 API error, 5 network."
|
|
719
|
+
].join(`
|
|
720
|
+
`);
|
|
721
|
+
}
|
|
722
|
+
async function requireCredentials() {
|
|
723
|
+
const creds = await loadCredentials();
|
|
724
|
+
if (!creds.apiKey)
|
|
725
|
+
throw new CliError("UNAUTHORIZED", "Not signed in. Run `aeosome login` or set AEOSOME_API_KEY.", EXIT.auth);
|
|
726
|
+
return creds;
|
|
727
|
+
}
|
|
728
|
+
function emit(io, tool, data) {
|
|
729
|
+
io.out(io.json ? JSON.stringify(data, null, 2) : renderHuman(tool, data, io.width));
|
|
730
|
+
}
|
|
731
|
+
function flagArgs(tool, flags) {
|
|
732
|
+
const known = new Map(tool.params.map((p) => [flagName(p.name), p.name]));
|
|
733
|
+
const asArg = (value) => value === true ? "true" : value;
|
|
734
|
+
return Object.fromEntries([...flags].filter(([flag]) => flag !== "json").map(([flag, value]) => [known.get(flag) ?? flag, asArg(value)]));
|
|
735
|
+
}
|
|
736
|
+
function toolArgs(tool, args) {
|
|
737
|
+
const result = flagArgs(tool, args.flags);
|
|
738
|
+
const pathParam = tool.params.find((p) => p.in === "path");
|
|
739
|
+
const positional = args.positionals[0];
|
|
740
|
+
if (!pathParam || !positional)
|
|
741
|
+
return result;
|
|
742
|
+
return { ...result, [pathParam.name]: positional };
|
|
743
|
+
}
|
|
744
|
+
function pathFor(tool, args) {
|
|
745
|
+
try {
|
|
746
|
+
return buildToolPath(tool, toolArgs(tool, args));
|
|
747
|
+
} catch (error) {
|
|
748
|
+
if (error instanceof ToolArgError)
|
|
749
|
+
throw new CliError("USAGE", `${error.message}
|
|
750
|
+
${toolUsage(tool).trim()}`, EXIT.usage);
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function runTool(tool, args, io) {
|
|
755
|
+
const path = pathFor(tool, args);
|
|
756
|
+
emit(io, tool, await getJson(await requireCredentials(), path));
|
|
757
|
+
}
|
|
758
|
+
async function keyFromEnvOrPrompt() {
|
|
759
|
+
const apiKey = process.env.AEOSOME_API_KEY || await readSecret("API key (aeo_...): ");
|
|
760
|
+
if (!apiKey.startsWith("aeo_"))
|
|
761
|
+
throw new CliError("USAGE", "That is not an aeosome API key; it starts with aeo_.", EXIT.usage);
|
|
762
|
+
return apiKey;
|
|
763
|
+
}
|
|
764
|
+
async function login(io) {
|
|
765
|
+
const { apiUrl = "" } = await loadCredentials();
|
|
766
|
+
const creds = { apiKey: await keyFromEnvOrPrompt(), apiUrl };
|
|
767
|
+
const me = await getJson(creds, "/api/api-keys/current");
|
|
768
|
+
const savedTo = await saveCredentials(creds);
|
|
769
|
+
const sentence = `Signed in to ${me.organization.name} (${me.plan}). Key saved to ${savedTo}`;
|
|
770
|
+
io.out(io.json ? JSON.stringify({ ...me, savedTo }) : sentence);
|
|
771
|
+
}
|
|
772
|
+
async function logout(io) {
|
|
773
|
+
await clearCredentials();
|
|
774
|
+
io.out(io.json ? JSON.stringify({ signedOut: true }) : "Signed out.");
|
|
775
|
+
}
|
|
776
|
+
function tools(io) {
|
|
777
|
+
const list = AGENT_TOOLS.map(({ name, command, description, params }) => ({
|
|
778
|
+
name,
|
|
779
|
+
command,
|
|
780
|
+
description,
|
|
781
|
+
params
|
|
782
|
+
}));
|
|
783
|
+
io.out(io.json ? JSON.stringify(list, null, 2) : AGENT_TOOLS.map(toolUsage).join(`
|
|
784
|
+
`));
|
|
785
|
+
}
|
|
786
|
+
var ACCOUNT = {
|
|
787
|
+
login,
|
|
788
|
+
logout,
|
|
789
|
+
tools,
|
|
790
|
+
mcp: async () => runBridge(await requireCredentials()),
|
|
791
|
+
help: (io) => io.out(helpText()),
|
|
792
|
+
version: (io) => io.out(VERSION)
|
|
793
|
+
};
|
|
794
|
+
var commandName = (args) => args.flags.has("version") ? "version" : args.command ?? "help";
|
|
795
|
+
function resolveTool(command) {
|
|
796
|
+
const tool = findTool(command);
|
|
797
|
+
if (!tool)
|
|
798
|
+
throw new CliError("USAGE", `Unknown command: ${command}. Run \`aeosome help\`.`, EXIT.usage);
|
|
799
|
+
return tool;
|
|
800
|
+
}
|
|
801
|
+
async function run(args, io) {
|
|
802
|
+
const command = commandName(args);
|
|
803
|
+
const account = args.flags.has("help") ? ACCOUNT.help : ACCOUNT[command];
|
|
804
|
+
if (account)
|
|
805
|
+
return account(io);
|
|
806
|
+
return runTool(resolveTool(command), args, io);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/index.ts
|
|
810
|
+
var args = parseArgs(process.argv.slice(2));
|
|
811
|
+
var json = args.flags.has("json");
|
|
812
|
+
var codeOf = (error) => error instanceof CliError ? error.code : "INTERNAL";
|
|
813
|
+
var exitOf = (error) => error instanceof CliError ? error.exitCode : EXIT.api;
|
|
814
|
+
var messageOf2 = (error) => error instanceof Error ? error.message : String(error);
|
|
815
|
+
function fail(error) {
|
|
816
|
+
const payload = { error: { code: codeOf(error), message: messageOf2(error) } };
|
|
817
|
+
if (json)
|
|
818
|
+
process.stdout.write(`${JSON.stringify(payload)}
|
|
819
|
+
`);
|
|
820
|
+
else
|
|
821
|
+
process.stderr.write(`error: ${payload.error.message}
|
|
822
|
+
`);
|
|
823
|
+
process.exitCode = exitOf(error);
|
|
824
|
+
}
|
|
825
|
+
await run(args, {
|
|
826
|
+
out: (text) => process.stdout.write(`${text}
|
|
827
|
+
`),
|
|
828
|
+
json,
|
|
829
|
+
width: process.stdout.columns ?? 100
|
|
830
|
+
}).catch(fail);
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@aeosome/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read your aeosome AI visibility data from a terminal, a script or an AI agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"homepage": "https://aeosome.com",
|
|
8
|
+
"bin": {
|
|
9
|
+
"aeosome": "dist/aeosome.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@repo/agent-tools": "^0.0.1",
|
|
19
|
+
"@repo/biome-config": "^0.0.0",
|
|
20
|
+
"@repo/tier-config": "^0.0.1",
|
|
21
|
+
"@repo/typescript-config": "^0.0.0",
|
|
22
|
+
"@types/bun": "latest",
|
|
23
|
+
"typescript": "^5.7.3"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun build src/index.ts --target node --outfile dist/aeosome.js",
|
|
27
|
+
"check": "biome check .",
|
|
28
|
+
"check-types": "tsc --noEmit",
|
|
29
|
+
"test": "bun test --timeout 30000"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/README.md
DELETED