@letterstory/cli 0.177.23 → 0.177.25
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/lib/cli.mjs +8 -1
- package/lib/commands/org.mjs +123 -1
- package/lib/commands/visits.mjs +31 -1
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
import { fetchParkedSections, withoutParked } from "./parked.mjs";
|
|
89
89
|
|
|
90
90
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
91
|
-
export const VERSION = "0.177.
|
|
91
|
+
export const VERSION = "0.177.25";
|
|
92
92
|
|
|
93
93
|
/** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
|
|
94
94
|
export const EXIT_PAYMENT_REQUIRED = 3;
|
|
@@ -97,6 +97,11 @@ export const EXIT_PAYMENT_REQUIRED = 3;
|
|
|
97
97
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
98
98
|
// `negate` spelling) in a command's ARGS map must be listed; boolean-flags.test.ts enforces it.
|
|
99
99
|
export const BOOLEAN_FLAGS = new Set([
|
|
100
|
+
// visits export: only the people or companies who fit the ICP, or only those who don't.
|
|
101
|
+
"icp",
|
|
102
|
+
"not-icp",
|
|
103
|
+
// org mcp add: the server signs in with OAuth instead of a header value.
|
|
104
|
+
"sign-in",
|
|
100
105
|
// collection onboard: connect without drafting or scheduling.
|
|
101
106
|
"connect-only",
|
|
102
107
|
// admin demo ideas: every live recipe, not only the Ideas that fit.
|
|
@@ -2003,6 +2008,8 @@ Visits (Visitors: AI agents reading your site, and AI answers sending people to
|
|
|
2003
2008
|
visits [--agent <a>] [--since 7d] [--path '/docs/*'] [--purpose live|index|training|automated_browser] [--kind page|agent_file|site_file|asset] [--paths <n>] [--site <id>]
|
|
2004
2009
|
Agent visits: per agent, why they came, most-read paths by kind
|
|
2005
2010
|
visits citations [--since 30d] [--site <id>] Visits that clicked through from ChatGPT, Claude, Perplexity…
|
|
2011
|
+
visits who [--icp|--not-icp] [--category <group>] [--path <p>] [--since 30d] [--limit <n>]
|
|
2012
|
+
Who was identified, and who fits your ICP
|
|
2006
2013
|
visits compare <agent-a> <agent-b> [--since 7d] [--path '/docs/*'] [--purpose <p>]
|
|
2007
2014
|
Pages one agent read that the other didn't
|
|
2008
2015
|
visits export [--since 30d] [--format json|csv] [--table agent_visits|citations]
|
package/lib/commands/org.mjs
CHANGED
|
@@ -58,6 +58,27 @@ export const ARGS = {
|
|
|
58
58
|
project_id: { value: (flags) => (flags.project === "none" ? null : flagStr(flags.project)) },
|
|
59
59
|
},
|
|
60
60
|
disconnect_linear: {},
|
|
61
|
+
list_mcp_connections: {},
|
|
62
|
+
connect_mcp: {
|
|
63
|
+
name: { positional: 0, label: "name", required: true },
|
|
64
|
+
url: { positional: 1, label: "url", required: true },
|
|
65
|
+
token: { flag: "token" },
|
|
66
|
+
header_name: { flag: "header" },
|
|
67
|
+
allow_tools: { flag: "allow", type: "list" },
|
|
68
|
+
sign_in: { flag: "sign-in", type: "bool" },
|
|
69
|
+
},
|
|
70
|
+
test_mcp_connection: { name: { positional: 0, label: "name", required: true } },
|
|
71
|
+
update_mcp_connection: {
|
|
72
|
+
name: { positional: 0, label: "name", required: true },
|
|
73
|
+
enabled: { value: (flags) => (flags.off ? false : flags.on ? true : undefined) },
|
|
74
|
+
allow_tools: {
|
|
75
|
+
value: (flags) => (flags.allow === "none" ? [] : flags.allow ? flagStr(flags.allow).split(",") : undefined),
|
|
76
|
+
},
|
|
77
|
+
token: { value: (flags) => (flags.clear ? null : flagStr(flags.token)) },
|
|
78
|
+
header_name: { flag: "header" },
|
|
79
|
+
sign_in: { flag: "sign-in", type: "bool" },
|
|
80
|
+
},
|
|
81
|
+
remove_mcp_connection: { name: { positional: 0, label: "name", required: true } },
|
|
61
82
|
connect_org_config_source: {
|
|
62
83
|
repo: { positional: 0, label: "owner/name" },
|
|
63
84
|
branch: { flag: "branch" },
|
|
@@ -91,11 +112,13 @@ export async function cmdOrg(ctx) {
|
|
|
91
112
|
return orgFields(rest);
|
|
92
113
|
case "linear":
|
|
93
114
|
return orgLinear(rest);
|
|
115
|
+
case "mcp":
|
|
116
|
+
return orgMcp(rest);
|
|
94
117
|
case "billing":
|
|
95
118
|
return orgBilling(rest);
|
|
96
119
|
default:
|
|
97
120
|
throw new CliError(
|
|
98
|
-
`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, fields, billing`
|
|
121
|
+
`Unknown org subcommand: ${sub ?? "(none)"}. Try: export, join, diff, apply, git, integrations, linear, mcp, fields, billing`
|
|
99
122
|
);
|
|
100
123
|
}
|
|
101
124
|
}
|
|
@@ -454,6 +477,105 @@ async function orgLinear(ctx) {
|
|
|
454
477
|
}
|
|
455
478
|
}
|
|
456
479
|
|
|
480
|
+
/**
|
|
481
|
+
* `org mcp` — other products' MCP servers Letter's agent can call (docs/architecture/mcp-connections.md):
|
|
482
|
+
* org mcp the connections (list_mcp_connections)
|
|
483
|
+
* org mcp add <name> <url> [--token <value> [--header <name>] | --sign-in] [--allow a,b] (connect_mcp)
|
|
484
|
+
* org mcp signin <name> sign in again with OAuth (update_mcp_connection)
|
|
485
|
+
* org mcp tools <name> read the server's tools again (test_mcp_connection)
|
|
486
|
+
* org mcp allow <name> <a,b|none> the tools that change things the agent may call (update_mcp_connection)
|
|
487
|
+
* org mcp on|off <name> (update_mcp_connection)
|
|
488
|
+
* org mcp token <name> <value>|--clear a new header value (update_mcp_connection)
|
|
489
|
+
* org mcp remove <name> (remove_mcp_connection)
|
|
490
|
+
*/
|
|
491
|
+
async function orgMcp(ctx) {
|
|
492
|
+
const { client, flags, io } = ctx;
|
|
493
|
+
const sub = ctx.positionals[0] ?? "list";
|
|
494
|
+
const rest = ctx.positionals.slice(1);
|
|
495
|
+
const call = async (tool, allowed, args) => {
|
|
496
|
+
refuseUnknownFlags(ctx, {}, allowed);
|
|
497
|
+
const result = await client.callTool(tool, args ?? argsFromFlags(ARGS[tool], flags, rest));
|
|
498
|
+
ok(ctx, result.message);
|
|
499
|
+
return result;
|
|
500
|
+
};
|
|
501
|
+
const tools = (r) =>
|
|
502
|
+
r.tools
|
|
503
|
+
.map((t) => `${t.allowed ? "●" : "○"} ${t.name}${t.read_only ? " (reads only)" : ""} ${t.description}`)
|
|
504
|
+
.join("\n");
|
|
505
|
+
switch (sub) {
|
|
506
|
+
case "list": {
|
|
507
|
+
const result = await call("list_mcp_connections", []);
|
|
508
|
+
printResult(
|
|
509
|
+
io,
|
|
510
|
+
flags,
|
|
511
|
+
result,
|
|
512
|
+
(r) =>
|
|
513
|
+
r.connections
|
|
514
|
+
.map(
|
|
515
|
+
(c) =>
|
|
516
|
+
`${c.enabled ? "●" : "○"} ${c.name} ${c.url} ${c.tool_count ?? "?"} tools${c.last_status === "failed" ? ` (failed: ${c.last_error})` : ""}`
|
|
517
|
+
)
|
|
518
|
+
.join("\n") || r.message
|
|
519
|
+
);
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
case "add": {
|
|
523
|
+
const result = await call("connect_mcp", ["token", "header", "allow", "sign-in"]);
|
|
524
|
+
printResult(io, flags, result, (r) =>
|
|
525
|
+
r.sign_in_url ? `${r.message}\n${r.sign_in_url}` : `${r.message}\n${tools(r)}`
|
|
526
|
+
);
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
case "signin": {
|
|
530
|
+
const result = await call("update_mcp_connection", [], {
|
|
531
|
+
name: requirePositional(rest, 0, "name"),
|
|
532
|
+
sign_in: true,
|
|
533
|
+
});
|
|
534
|
+
printResult(io, flags, result, (r) => `${r.message}\n${r.sign_in_url}`);
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
case "tools": {
|
|
538
|
+
const result = await call("test_mcp_connection", []);
|
|
539
|
+
printResult(io, flags, result, tools);
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
case "allow": {
|
|
543
|
+
const list = requirePositional(rest, 1, "a,b|none");
|
|
544
|
+
const result = await call("update_mcp_connection", [], {
|
|
545
|
+
name: requirePositional(rest, 0, "name"),
|
|
546
|
+
allow_tools: list === "none" ? [] : list.split(","),
|
|
547
|
+
});
|
|
548
|
+
printResult(io, flags, result, tools);
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
case "on":
|
|
552
|
+
case "off": {
|
|
553
|
+
const result = await call("update_mcp_connection", [], {
|
|
554
|
+
name: requirePositional(rest, 0, "name"),
|
|
555
|
+
enabled: sub === "on",
|
|
556
|
+
});
|
|
557
|
+
printResult(io, flags, result, (r) => r.message);
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
case "token": {
|
|
561
|
+
const name = requirePositional(rest, 0, "name");
|
|
562
|
+
const value = flags.clear ? null : requirePositional(rest, 1, "value");
|
|
563
|
+
const result = await call("update_mcp_connection", ["clear"], { name, token: value });
|
|
564
|
+
printResult(io, flags, result, (r) => r.message);
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
case "remove": {
|
|
568
|
+
const result = await call("remove_mcp_connection", []);
|
|
569
|
+
printResult(io, flags, result, (r) => r.message);
|
|
570
|
+
return 0;
|
|
571
|
+
}
|
|
572
|
+
default:
|
|
573
|
+
throw new CliError(
|
|
574
|
+
`Unknown org mcp subcommand: ${sub}. Try: add, signin, tools, allow, on, off, token, remove`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
457
579
|
const ROOT_FILE = "organization.json";
|
|
458
580
|
|
|
459
581
|
function slug(name) {
|
package/lib/commands/visits.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// letterstory visits compare chatgpt claude --path '/docs/*' --since 7d
|
|
10
10
|
// letterstory visits citations --since 30d
|
|
11
11
|
// letterstory visits people --since 30d [--path /pricing] people who read the site, and how many were identified
|
|
12
|
+
// letterstory visits who --icp [--category Investors] [--path /pricing] who was identified (--icp: who fits your ICP)
|
|
12
13
|
// letterstory visits export --since 30d --format csv
|
|
13
14
|
// letterstory visits sites
|
|
14
15
|
// letterstory visits domain composal.com [--site <id>] move the site to a new domain, keeping its key
|
|
@@ -43,6 +44,14 @@ export const ARGS = {
|
|
|
43
44
|
},
|
|
44
45
|
visitors_get_citations: { site_id: "site", days },
|
|
45
46
|
visitors_get_human_visits: { site_id: "site", days, path: "path" },
|
|
47
|
+
visitors_get_people: {
|
|
48
|
+
site_id: "site",
|
|
49
|
+
days,
|
|
50
|
+
path: "path",
|
|
51
|
+
category: "category",
|
|
52
|
+
limit: { flag: "limit", type: "num" },
|
|
53
|
+
icp: { flag: "icp", type: "bool", negate: "not-icp" },
|
|
54
|
+
},
|
|
46
55
|
visitors_admin_coverage: { site_id: "site" },
|
|
47
56
|
visitors_add_connector: {
|
|
48
57
|
site_id: "site",
|
|
@@ -99,6 +108,8 @@ export async function cmdVisits(ctx) {
|
|
|
99
108
|
return visitsCitations(rest);
|
|
100
109
|
case "people":
|
|
101
110
|
return visitsPeople(rest);
|
|
111
|
+
case "who":
|
|
112
|
+
return visitsWho(rest);
|
|
102
113
|
case "compare":
|
|
103
114
|
return visitsCompare(rest);
|
|
104
115
|
case "export":
|
|
@@ -119,7 +130,7 @@ export async function cmdVisits(ctx) {
|
|
|
119
130
|
return visitsDisconnect(rest);
|
|
120
131
|
default:
|
|
121
132
|
throw new CliError(
|
|
122
|
-
"Usage: visits [list|citations|people|compare|export|sites|domain|pages|health|connect|connectors|disconnect]"
|
|
133
|
+
"Usage: visits [list|citations|people|who|compare|export|sites|domain|pages|health|connect|connectors|disconnect]"
|
|
123
134
|
);
|
|
124
135
|
}
|
|
125
136
|
}
|
|
@@ -311,6 +322,25 @@ async function visitsPeople(ctx) {
|
|
|
311
322
|
return 0;
|
|
312
323
|
}
|
|
313
324
|
|
|
325
|
+
async function visitsWho(ctx) {
|
|
326
|
+
const { flags, io } = ctx;
|
|
327
|
+
const result = await call(ctx, "visitors_get_people");
|
|
328
|
+
printResult(io, flags, result, (r) => {
|
|
329
|
+
if (!r.enabled) return "Identification is off for this site.";
|
|
330
|
+
const lines = [];
|
|
331
|
+
if (r.icp) lines.push(`ICP: ${r.icp} (${r.icp_total} fit)`);
|
|
332
|
+
else if (r.icp_filter !== null) lines.push("No ICP set. Set it in Memory → General.");
|
|
333
|
+
lines.push(`${r.total} identified in the last ${r.window_days} days`);
|
|
334
|
+
for (const v of r.visitors) {
|
|
335
|
+
const fit = v.icp_fit === true ? ` ICP: ${v.icp_reason ?? "fits"}` : "";
|
|
336
|
+
const where = [v.title, v.company ?? v.company_domain].filter(Boolean).join(", ");
|
|
337
|
+
lines.push(` ${v.name ?? "Unknown"}${where ? ` (${where})` : ""} ${v.category ?? "Others"}${fit}`);
|
|
338
|
+
}
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
});
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
314
344
|
async function visitsCompare(ctx) {
|
|
315
345
|
const { flags, io } = ctx;
|
|
316
346
|
const result = await call(ctx, "visitors_compare_agents");
|