@letterstory/cli 0.103.0 → 0.104.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 +1 -0
- package/lib/cli.mjs +7 -4
- package/lib/commands/admin.mjs +21 -12
- package/lib/commands/repo.mjs +44 -3
- package/lib/commands/site.mjs +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -441,6 +441,7 @@ letterstory admin crm capture thread.png --context "this is for tryordinal.com"
|
|
|
441
441
|
# a screenshot → a lead: what it read, which account
|
|
442
442
|
letterstory admin crm capture thread.png --context "this is for tryordinal.com" # …and for real
|
|
443
443
|
letterstory admin crm capture --text-file notes.txt # pasted text or notes work the same way
|
|
444
|
+
letterstory admin crm capture thread.png --retry # sent before, some entries didn't save: write only what's missing
|
|
444
445
|
|
|
445
446
|
# One-click onboarding of a won client (key needs crm:write + fleet:onboard + org:create)
|
|
446
447
|
letterstory admin crm onboard <account-id> --domain acme.com \
|
package/lib/cli.mjs
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./commands.mjs";
|
|
61
61
|
|
|
62
62
|
// Keep in sync with cli/package.json.
|
|
63
|
-
export const VERSION = "0.
|
|
63
|
+
export const VERSION = "0.104.0";
|
|
64
64
|
|
|
65
65
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
66
66
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -773,7 +773,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
773
773
|
Needs spend:read (reads) / spend:write (writes).
|
|
774
774
|
admin crm import <file.csv> [--source <name>] [--tag "…"] [--dry-run]
|
|
775
775
|
| capture [shot.png …] [--text "…" | --text-file f] [--context "this is for acme.com"]
|
|
776
|
-
[--account <id>] [--dry-run] screenshots / a thread / a note → a lead
|
|
776
|
+
[--account <id>] [--dry-run] [--retry] screenshots / a thread / a note → a lead
|
|
777
777
|
| signals [--status new] [--intent qualified] [--source rb2b] [--account <id>]
|
|
778
778
|
| record --title "…" [--source bookface] [--kind other] [--intent qualified]
|
|
779
779
|
[--name …] [--email …] [--company …] [--domain …] [--linkedin …] [--detail …]
|
|
@@ -1172,9 +1172,12 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1172
1172
|
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1173
1173
|
repo suggestions Fixes found and not acted on yet, and how to start each
|
|
1174
1174
|
repo settings [--action build|build_pr|slack] [--branch <name>|none] [--slack-agent <name|U…>|none]
|
|
1175
|
+
[--slack-channel <C…>|none]
|
|
1175
1176
|
The org's defaults for every fix: build (preview first),
|
|
1176
|
-
build_pr, or slack (post it for your
|
|
1177
|
-
--slack-agent), and the branch changes go to
|
|
1177
|
+
build_pr, or slack (post it to --slack-channel for your
|
|
1178
|
+
own agent, tagging --slack-agent), and the branch changes go to
|
|
1179
|
+
repo slack-targets Slack channels and people Send to Slack can use, with ids
|
|
1180
|
+
repo slack-test Post a test into the Send to Slack channel
|
|
1178
1181
|
repo start --repo <owner/name> --instructions <text>|--instructions-file <path|->
|
|
1179
1182
|
[--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]...
|
|
1180
1183
|
[--action build|build_pr|slack] [--wait]
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -2777,7 +2777,7 @@ const CRM_CAPTURE_TYPES = {
|
|
|
2777
2777
|
async function cmdAdminCrmCapture(ctx, paths) {
|
|
2778
2778
|
const { flags, io } = ctx;
|
|
2779
2779
|
const usage =
|
|
2780
|
-
'Usage: admin crm capture [image.png …] [--text "…" | --text-file notes.txt] [--context "this is for acme.com"] [--account <id>] [--dry-run]';
|
|
2780
|
+
'Usage: admin crm capture [image.png …] [--text "…" | --text-file notes.txt] [--context "this is for acme.com"] [--account <id>] [--dry-run] [--retry]';
|
|
2781
2781
|
const images = paths.map((path) => {
|
|
2782
2782
|
const media_type = CRM_CAPTURE_TYPES[path.split(".").pop()?.toLowerCase() ?? ""];
|
|
2783
2783
|
if (!media_type) throw new CliError(`${path}: only png, jpg, gif and webp screenshots can be read`);
|
|
@@ -2807,6 +2807,8 @@ async function cmdAdminCrmCapture(ctx, paths) {
|
|
|
2807
2807
|
account_id: flagStr(flags.account),
|
|
2808
2808
|
source: flagStr(flags.source) ?? "agent",
|
|
2809
2809
|
dry_run: flagBool(flags["dry-run"]) || undefined,
|
|
2810
|
+
// Sent before but some entries didn't save: read it again, writing only what isn't in.
|
|
2811
|
+
retry: flagBool(flags.retry) || undefined,
|
|
2810
2812
|
})
|
|
2811
2813
|
);
|
|
2812
2814
|
if (flagBool(flags.json)) return printResult(io, flags, r);
|
|
@@ -2835,7 +2837,9 @@ export function captureSummary(r) {
|
|
|
2835
2837
|
if (r.batch) {
|
|
2836
2838
|
const width = Math.max(...r.batch.map((b) => (BATCH_STATUS[b.status] ?? b.status).length), 0);
|
|
2837
2839
|
return [
|
|
2838
|
-
|
|
2840
|
+
r.duplicate
|
|
2841
|
+
? `sent before: ${r.accountName ?? `${r.batch.length} already in`}`
|
|
2842
|
+
: `${r.dryRun ? "would save" : "saved"} ${r.accountName ?? `${r.batch.length} entries`}`,
|
|
2839
2843
|
...r.batch.map((b) => {
|
|
2840
2844
|
const status = (BATCH_STATUS[b.status] ?? b.status).padEnd(width);
|
|
2841
2845
|
const where = b.accountName ?? b.companyName ?? "?";
|
|
@@ -2847,12 +2851,13 @@ export function captureSummary(r) {
|
|
|
2847
2851
|
}),
|
|
2848
2852
|
];
|
|
2849
2853
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2854
|
+
// A preview says "sent before" too (the app checks before it reads), so that comes first.
|
|
2855
|
+
const verb = r.duplicate
|
|
2856
|
+
? "already captured →"
|
|
2857
|
+
: r.dryRun
|
|
2858
|
+
? r.accountId
|
|
2859
|
+
? "would add to"
|
|
2860
|
+
: "would create"
|
|
2856
2861
|
: r.accountCreated
|
|
2857
2862
|
? "created"
|
|
2858
2863
|
: "added to";
|
|
@@ -2876,7 +2881,7 @@ export function captureSummary(r) {
|
|
|
2876
2881
|
* outreach add "Jane Doe" [--company …] [--owner …] [--channel x] [--relationship …]
|
|
2877
2882
|
* [--ask …] [--linkedin …] [--email …] [--lookup]
|
|
2878
2883
|
* outreach lookup "Jane Doe from Acme" [shot.png …] prefilled from the web; writes nothing
|
|
2879
|
-
* outreach relookup [--missing linkedin]
|
|
2884
|
+
* outreach relookup [--missing linkedin|company|location|any] the web lookup again for whoever lacks it
|
|
2880
2885
|
* outreach drafts [--kind ask|thanks] [--owner …] drafts or thank-yous for everyone without one
|
|
2881
2886
|
* outreach engagement --outcome upvoted [shot.png] who upvoted, matched by exact name
|
|
2882
2887
|
* outreach customers [--dry-run] every customer's primary contact onto the list
|
|
@@ -2949,6 +2954,7 @@ async function cmdAdminCrmOutreach(ctx, args) {
|
|
|
2949
2954
|
for (const p of r.created) io.log(`added ${line(p)}`);
|
|
2950
2955
|
for (const d of r.duplicates)
|
|
2951
2956
|
io.log(`already on the list: ${d.existingName} (matched on ${d.matchedOn}) ${d.existingId}`);
|
|
2957
|
+
for (const x of r.skipped ?? []) io.log(`not added: ${x.name} (${x.reason})`);
|
|
2952
2958
|
return;
|
|
2953
2959
|
}
|
|
2954
2960
|
case "lookup": {
|
|
@@ -2999,7 +3005,7 @@ async function cmdAdminCrmOutreach(ctx, args) {
|
|
|
2999
3005
|
return json ? printResult(io, flags, r) : io.log(line(r));
|
|
3000
3006
|
}
|
|
3001
3007
|
case "relookup": {
|
|
3002
|
-
// admin crm outreach relookup [--missing linkedin|company|any] — the web lookup again,
|
|
3008
|
+
// admin crm outreach relookup [--missing linkedin|company|location|any] — the web lookup again,
|
|
3003
3009
|
// filling only blanks, calling until none remain (a dozen people per call).
|
|
3004
3010
|
const missing = flagStr(flags.missing) ?? "linkedin";
|
|
3005
3011
|
let total = 0;
|
|
@@ -3081,9 +3087,12 @@ async function cmdAdminCrmOutreach(ctx, args) {
|
|
|
3081
3087
|
);
|
|
3082
3088
|
if (json) return printResult(io, flags, r);
|
|
3083
3089
|
return io.log(
|
|
3084
|
-
r.dryRun
|
|
3090
|
+
(r.dryRun
|
|
3085
3091
|
? `would add up to ${r.candidates.length}: ${r.candidates.join(", ")}`
|
|
3086
|
-
: `added ${r.created.length}; already on the list: ${r.duplicates.length}`
|
|
3092
|
+
: `added ${r.created.length}; already on the list: ${r.duplicates.length}`) +
|
|
3093
|
+
(r.skipped?.length
|
|
3094
|
+
? `\nnot added (on the no-contact list): ${r.skipped.map((x) => x.name).join(", ")}`
|
|
3095
|
+
: "")
|
|
3087
3096
|
);
|
|
3088
3097
|
}
|
|
3089
3098
|
case "catch-up": {
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// repo install-letterprove → letterprove_open_install_pr
|
|
8
8
|
// repo suggestions → list_suggested_fixes
|
|
9
9
|
// repo settings → get_repo_change_settings, or set_repo_change_settings with --action / --branch
|
|
10
|
+
// repo slack-targets → list_slack_fix_targets repo slack-test → send_slack_test_message
|
|
10
11
|
|
|
11
12
|
import { readFileSync } from "node:fs";
|
|
12
13
|
import { basename } from "node:path";
|
|
@@ -81,12 +82,18 @@ const ARGS = {
|
|
|
81
82
|
},
|
|
82
83
|
get_repo_change_settings: {},
|
|
83
84
|
list_suggested_fixes: {},
|
|
85
|
+
list_slack_fix_targets: {},
|
|
86
|
+
send_slack_test_message: {},
|
|
84
87
|
set_repo_change_settings: {
|
|
85
88
|
default_action: { flag: "action", enum: ["build", "build_pr", "slack"] },
|
|
86
89
|
// --branch <name>, or --branch none for each repo's own default.
|
|
87
90
|
default_branch: { value: (flags) => (flags.branch === "none" ? null : flagStr(flags.branch)) },
|
|
88
91
|
// --slack-agent Devin (or a U… member id), or --slack-agent none to tag nobody.
|
|
89
92
|
slack_agent: { value: (flags) => (flags["slack-agent"] === "none" ? null : flagStr(flags["slack-agent"])) },
|
|
93
|
+
// --slack-channel C0123ABCD (from repo slack-targets), or none for the first linked channel.
|
|
94
|
+
slack_channel: {
|
|
95
|
+
value: (flags) => (flags["slack-channel"] === "none" ? null : flagStr(flags["slack-channel"])),
|
|
96
|
+
},
|
|
90
97
|
},
|
|
91
98
|
get_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
92
99
|
cancel_repo_change: { run_id: { positional: 0, required: true, label: "run-id" } },
|
|
@@ -151,9 +158,13 @@ export async function cmdRepo(ctx) {
|
|
|
151
158
|
return repoSettings(rest);
|
|
152
159
|
case "suggestions":
|
|
153
160
|
return repoSuggestions(rest);
|
|
161
|
+
case "slack-targets":
|
|
162
|
+
return repoSlackTargets(rest);
|
|
163
|
+
case "slack-test":
|
|
164
|
+
return repoSlackTest(rest);
|
|
154
165
|
default:
|
|
155
166
|
throw new CliError(
|
|
156
|
-
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, sites, set-site, reset-site, status, list, publish, cancel, repos, branches, settings, suggestions`
|
|
167
|
+
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, sites, set-site, reset-site, status, list, publish, cancel, repos, branches, settings, suggestions, slack-targets, slack-test`
|
|
157
168
|
);
|
|
158
169
|
}
|
|
159
170
|
}
|
|
@@ -407,8 +418,12 @@ async function repoBranches(ctx) {
|
|
|
407
418
|
*/
|
|
408
419
|
async function repoSettings(ctx) {
|
|
409
420
|
const { client, flags, io } = ctx;
|
|
410
|
-
refuseUnknownFlags(ctx, {}, ["action", "branch", "slack-agent"]);
|
|
411
|
-
const setting =
|
|
421
|
+
refuseUnknownFlags(ctx, {}, ["action", "branch", "slack-agent", "slack-channel"]);
|
|
422
|
+
const setting =
|
|
423
|
+
flags.action !== undefined ||
|
|
424
|
+
flags.branch !== undefined ||
|
|
425
|
+
flags["slack-agent"] !== undefined ||
|
|
426
|
+
flags["slack-channel"] !== undefined;
|
|
412
427
|
const result = setting
|
|
413
428
|
? await client.callTool("set_repo_change_settings", argsFromFlags(ARGS.set_repo_change_settings, flags))
|
|
414
429
|
: await client.callTool("get_repo_change_settings", {});
|
|
@@ -441,4 +456,30 @@ async function repoSuggestions(ctx) {
|
|
|
441
456
|
return 0;
|
|
442
457
|
}
|
|
443
458
|
|
|
459
|
+
/** `repo slack-targets`: the channels and people Send to Slack can use, with their ids. */
|
|
460
|
+
async function repoSlackTargets(ctx) {
|
|
461
|
+
const { client, flags, io } = ctx;
|
|
462
|
+
refuseUnknownFlags(ctx, {}, []);
|
|
463
|
+
const result = await client.callTool("list_slack_fix_targets", {});
|
|
464
|
+
printResult(io, flags, result, (r) =>
|
|
465
|
+
[
|
|
466
|
+
"Channels (repo settings --slack-channel <id>):",
|
|
467
|
+
...r.channels.map((c) => ` ${c.id} #${c.name}`),
|
|
468
|
+
"People (repo settings --slack-agent <id>):",
|
|
469
|
+
...r.people.map((p) => ` ${p.id} ${p.name}${p.is_bot ? " (bot)" : ""}`),
|
|
470
|
+
].join("\n")
|
|
471
|
+
);
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** `repo slack-test`: post a test into the Send to Slack channel. */
|
|
476
|
+
async function repoSlackTest(ctx) {
|
|
477
|
+
const { client, flags, io } = ctx;
|
|
478
|
+
refuseUnknownFlags(ctx, {}, []);
|
|
479
|
+
const result = await client.callTool("send_slack_test_message", {});
|
|
480
|
+
ok(ctx, result.message);
|
|
481
|
+
printResult(io, flags, result);
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
444
485
|
export { ARGS };
|
package/lib/commands/site.mjs
CHANGED