@reepl/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/README.md +104 -0
- package/dist/chunk-QSNFNE5Z.js +719 -0
- package/dist/chunk-QSNFNE5Z.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +982 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/index.d.ts +525 -0
- package/dist/core/index.js +57 -0
- package/dist/core/index.js.map +1 -0
- package/package.json +49 -0
- package/scripts/postinstall.mjs +56 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
MCP_CONNECT_PATH,
|
|
4
|
+
REEPL_WEB_APP_URL,
|
|
5
|
+
ReeplClient,
|
|
6
|
+
ReeplError,
|
|
7
|
+
TokenManager,
|
|
8
|
+
calendar_exports,
|
|
9
|
+
carousel_exports,
|
|
10
|
+
decodeJwt,
|
|
11
|
+
dms_exports,
|
|
12
|
+
jwtExpiryMs,
|
|
13
|
+
linkedin_exports,
|
|
14
|
+
profile_exports,
|
|
15
|
+
reddit_exports,
|
|
16
|
+
resolvePostRef,
|
|
17
|
+
signals_exports,
|
|
18
|
+
twitter_exports,
|
|
19
|
+
voice_exports,
|
|
20
|
+
workspaces_exports,
|
|
21
|
+
writingStyle_exports
|
|
22
|
+
} from "../chunk-QSNFNE5Z.js";
|
|
23
|
+
|
|
24
|
+
// src/cli/index.ts
|
|
25
|
+
import { Command } from "commander";
|
|
26
|
+
|
|
27
|
+
// src/cli/banner.ts
|
|
28
|
+
var BRAND = { r: 109, g: 66, b: 204 };
|
|
29
|
+
var WORDMARK = [
|
|
30
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 ",
|
|
31
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 ",
|
|
32
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 ",
|
|
33
|
+
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551 ",
|
|
34
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
35
|
+
"\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
36
|
+
];
|
|
37
|
+
var SPARKLE = " \u2726 \xB7 \u2726";
|
|
38
|
+
function supportsColor(stream) {
|
|
39
|
+
if (process.env.NO_COLOR) return false;
|
|
40
|
+
if (process.env.FORCE_COLOR) return true;
|
|
41
|
+
return Boolean(stream.isTTY) && process.env.TERM !== "dumb";
|
|
42
|
+
}
|
|
43
|
+
function purple(text, on) {
|
|
44
|
+
if (!on) return text;
|
|
45
|
+
return `\x1B[38;2;${BRAND.r};${BRAND.g};${BRAND.b}m${text}\x1B[0m`;
|
|
46
|
+
}
|
|
47
|
+
function dim(text, on) {
|
|
48
|
+
return on ? `\x1B[2m${text}\x1B[0m` : text;
|
|
49
|
+
}
|
|
50
|
+
function renderBanner(opts = {}, color = true) {
|
|
51
|
+
const v = opts.version ? ` v${opts.version}` : "";
|
|
52
|
+
const lines = [
|
|
53
|
+
"",
|
|
54
|
+
...WORDMARK.map((l) => purple(l, color)),
|
|
55
|
+
purple(SPARKLE, color),
|
|
56
|
+
"",
|
|
57
|
+
` ${purple("Reepl CLI", color)}${dim(v, color)} \u2014 your content workflow, in the terminal.`,
|
|
58
|
+
` ${dim("LinkedIn \xB7 X \xB7 Reddit \xB7 carousels \xB7 signals \xB7 writing styles", color)}`,
|
|
59
|
+
"",
|
|
60
|
+
` ${dim("Get started:", color)} reepl login ${dim("\xB7", color)} reepl --help ${dim("\xB7", color)} reepl schema`
|
|
61
|
+
];
|
|
62
|
+
if (opts.footer) lines.push("", ` ${opts.footer}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
return lines.join("\n");
|
|
65
|
+
}
|
|
66
|
+
function printBanner(opts = {}, stream = process.stdout) {
|
|
67
|
+
stream.write(renderBanner(opts, supportsColor(stream)) + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/cli/commands/auth.ts
|
|
71
|
+
import open from "open";
|
|
72
|
+
|
|
73
|
+
// src/cli/auth-flow.ts
|
|
74
|
+
import { randomBytes } from "crypto";
|
|
75
|
+
import { createServer } from "http";
|
|
76
|
+
var DONE_HTML = `<!doctype html><meta charset="utf-8"><title>Reepl CLI</title>
|
|
77
|
+
<body style="font-family:system-ui;background:#0b0b0f;color:#eaeaf0;display:grid;place-items:center;height:100vh;margin:0">
|
|
78
|
+
<div style="text-align:center"><h1 style="font-weight:600">You're signed in.</h1>
|
|
79
|
+
<p style="color:#9a9aa8">Return to your terminal \u2014 you can close this tab.</p></div></body>`;
|
|
80
|
+
async function loginViaBrowser(opts) {
|
|
81
|
+
const state = randomBytes(16).toString("hex");
|
|
82
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 60 * 1e3;
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const server = createServer((req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
87
|
+
if (url.pathname !== "/callback") {
|
|
88
|
+
res.writeHead(404).end("Not found");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const token = url.searchParams.get("token");
|
|
92
|
+
const refreshToken = url.searchParams.get("refresh_token");
|
|
93
|
+
const returnedState = url.searchParams.get("state");
|
|
94
|
+
if (returnedState !== state) {
|
|
95
|
+
res.writeHead(400).end("State mismatch");
|
|
96
|
+
cleanup();
|
|
97
|
+
reject(new ReeplError("UNAUTHENTICATED", "Login failed: state mismatch (possible CSRF)."));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!token || !refreshToken) {
|
|
101
|
+
res.writeHead(400).end("Missing token");
|
|
102
|
+
cleanup();
|
|
103
|
+
reject(
|
|
104
|
+
new ReeplError(
|
|
105
|
+
"UNAUTHENTICATED",
|
|
106
|
+
"Login did not return a refresh token. Ensure you are logged in to app.reepl.io and retry."
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(DONE_HTML);
|
|
112
|
+
cleanup();
|
|
113
|
+
const expiresAt = jwtExpiryMs(token) ?? Date.now() + 3600 * 1e3;
|
|
114
|
+
resolve({ tokens: { idToken: token, refreshToken, expiresAt } });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
cleanup();
|
|
117
|
+
reject(err);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
cleanup();
|
|
122
|
+
reject(new ReeplError("UNAUTHENTICATED", "Login timed out. Run `reepl login` again."));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
function cleanup() {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
server.close();
|
|
127
|
+
}
|
|
128
|
+
server.on("error", (err) => {
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(new ReeplError("NETWORK", `Could not start local login listener: ${err.message}`));
|
|
131
|
+
});
|
|
132
|
+
server.listen(0, "127.0.0.1", () => {
|
|
133
|
+
const { port } = server.address();
|
|
134
|
+
const callback = `http://127.0.0.1:${port}/callback`;
|
|
135
|
+
const handoff = new URL(REEPL_WEB_APP_URL + MCP_CONNECT_PATH);
|
|
136
|
+
handoff.searchParams.set("state", state);
|
|
137
|
+
handoff.searchParams.set("callback", callback);
|
|
138
|
+
handoff.searchParams.set("app", "cli");
|
|
139
|
+
const href = handoff.toString();
|
|
140
|
+
opts.onUrl?.(href);
|
|
141
|
+
opts.openBrowser(href).catch(() => {
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/cli/config.ts
|
|
148
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
149
|
+
import { homedir } from "os";
|
|
150
|
+
import { dirname, join } from "path";
|
|
151
|
+
function configDir() {
|
|
152
|
+
return process.env.REEPL_CONFIG_DIR || join(homedir(), ".reepl");
|
|
153
|
+
}
|
|
154
|
+
var credentialsPath = () => join(configDir(), "credentials.json");
|
|
155
|
+
var configPath = () => join(configDir(), "config.json");
|
|
156
|
+
async function readJson(path) {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err?.code === "ENOENT") return null;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function writeJson(path, value, mode) {
|
|
165
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
166
|
+
await writeFile(path, JSON.stringify(value, null, 2), { mode: mode ?? 384 });
|
|
167
|
+
if (mode) await chmod(path, mode).catch(() => {
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function readConfig() {
|
|
171
|
+
return await readJson(configPath()) ?? {};
|
|
172
|
+
}
|
|
173
|
+
async function writeConfig(patch) {
|
|
174
|
+
const next = { ...await readConfig(), ...patch };
|
|
175
|
+
await writeJson(configPath(), next, 384);
|
|
176
|
+
return next;
|
|
177
|
+
}
|
|
178
|
+
var FileTokenStore = class {
|
|
179
|
+
async load() {
|
|
180
|
+
return readJson(credentialsPath());
|
|
181
|
+
}
|
|
182
|
+
async save(tokens) {
|
|
183
|
+
await writeJson(credentialsPath(), tokens, 384);
|
|
184
|
+
}
|
|
185
|
+
async clear() {
|
|
186
|
+
await rm(credentialsPath(), { force: true });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/cli/context.ts
|
|
191
|
+
async function getContext(overrideWorkspaceId) {
|
|
192
|
+
const store = new FileTokenStore();
|
|
193
|
+
const tokens = new TokenManager(store, {
|
|
194
|
+
cognitoDomain: process.env.REEPL_COGNITO_DOMAIN || void 0,
|
|
195
|
+
clientId: process.env.REEPL_COGNITO_CLIENT_ID || void 0
|
|
196
|
+
});
|
|
197
|
+
const config = await readConfig();
|
|
198
|
+
const workspaceId = overrideWorkspaceId ?? config.currentWorkspaceId;
|
|
199
|
+
const client = new ReeplClient({
|
|
200
|
+
getToken: () => tokens.getIdToken(),
|
|
201
|
+
workspaceId: workspaceId ?? null,
|
|
202
|
+
apiBaseUrl: process.env.REEPL_API_BASE_URL || void 0,
|
|
203
|
+
publicBaseUrl: process.env.REEPL_PUBLIC_API_BASE_URL || void 0
|
|
204
|
+
});
|
|
205
|
+
return { client, tokens, store, workspaceId };
|
|
206
|
+
}
|
|
207
|
+
async function currentUserId(ctx) {
|
|
208
|
+
const token = await ctx.tokens.getIdToken();
|
|
209
|
+
const sub = decodeJwt(token)?.sub;
|
|
210
|
+
if (!sub) throw new ReeplError("UNAUTHENTICATED", "Could not read user id from token. Run `reepl login`.");
|
|
211
|
+
return String(sub);
|
|
212
|
+
}
|
|
213
|
+
async function ensureWorkspaceId(ctx) {
|
|
214
|
+
if (ctx.workspaceId) return ctx.workspaceId;
|
|
215
|
+
const def = await workspaces_exports.getDefaultWorkspaceId(ctx.client);
|
|
216
|
+
if (!def) {
|
|
217
|
+
throw new ReeplError("CONFIG", "No workspace selected and no default found. Run `reepl workspace list`.");
|
|
218
|
+
}
|
|
219
|
+
return def;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/cli/output.ts
|
|
223
|
+
var forceJson;
|
|
224
|
+
function setJsonMode(value) {
|
|
225
|
+
forceJson = value;
|
|
226
|
+
}
|
|
227
|
+
function isJsonMode() {
|
|
228
|
+
if (forceJson !== void 0) return forceJson;
|
|
229
|
+
return !process.stdout.isTTY;
|
|
230
|
+
}
|
|
231
|
+
function success(data, render) {
|
|
232
|
+
if (isJsonMode()) {
|
|
233
|
+
process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (render) {
|
|
237
|
+
process.stdout.write(render(data) + "\n");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
process.stdout.write(prettyValue(data) + "\n");
|
|
241
|
+
}
|
|
242
|
+
function fail(err) {
|
|
243
|
+
const reepl = err instanceof ReeplError ? err : new ReeplError("API_ERROR", err instanceof Error ? err.message : String(err));
|
|
244
|
+
if (isJsonMode()) {
|
|
245
|
+
process.stdout.write(
|
|
246
|
+
JSON.stringify({ ok: false, error: reepl.toJSON() }, null, 2) + "\n"
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
process.stderr.write(`Error [${reepl.code}]: ${reepl.message}
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
process.exit(reepl.exitCode);
|
|
253
|
+
}
|
|
254
|
+
function prettyValue(data) {
|
|
255
|
+
if (typeof data === "string") return data;
|
|
256
|
+
return JSON.stringify(data, null, 2);
|
|
257
|
+
}
|
|
258
|
+
function table(rows, columns) {
|
|
259
|
+
if (rows.length === 0) return "(none)";
|
|
260
|
+
const widths = columns.map(
|
|
261
|
+
(c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length))
|
|
262
|
+
);
|
|
263
|
+
const header = columns.map((c, i) => c.padEnd(widths[i])).join(" ");
|
|
264
|
+
const sep = widths.map((w) => "-".repeat(w)).join(" ");
|
|
265
|
+
const body = rows.map((r) => columns.map((c, i) => String(r[c] ?? "").padEnd(widths[i])).join(" ")).join("\n");
|
|
266
|
+
return `${header}
|
|
267
|
+
${sep}
|
|
268
|
+
${body}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/cli/commands/auth.ts
|
|
272
|
+
function registerAuthCommands(program2) {
|
|
273
|
+
program2.command("login").description("Authenticate with Reepl via your browser (or --token/--refresh for headless)").option("--token <idToken>", "Provide a Cognito ID token directly (skips the browser)").option("--refresh <refreshToken>", "Provide a Cognito refresh token directly").action(async (opts) => {
|
|
274
|
+
try {
|
|
275
|
+
const store = new FileTokenStore();
|
|
276
|
+
let tokens;
|
|
277
|
+
if (opts.token && opts.refresh) {
|
|
278
|
+
tokens = {
|
|
279
|
+
idToken: opts.token,
|
|
280
|
+
refreshToken: opts.refresh,
|
|
281
|
+
expiresAt: jwtExpiryMs(opts.token) ?? Date.now() + 3600 * 1e3
|
|
282
|
+
};
|
|
283
|
+
} else if (opts.token || opts.refresh) {
|
|
284
|
+
throw new Error("--token and --refresh must be provided together.");
|
|
285
|
+
} else {
|
|
286
|
+
const result = await loginViaBrowser({
|
|
287
|
+
openBrowser: (url) => open(url),
|
|
288
|
+
onUrl: (url) => {
|
|
289
|
+
if (!isJsonMode()) {
|
|
290
|
+
process.stderr.write(
|
|
291
|
+
`Opening your browser to sign in...
|
|
292
|
+
If it doesn't open, visit:
|
|
293
|
+
${url}
|
|
294
|
+
|
|
295
|
+
`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
tokens = result.tokens;
|
|
301
|
+
}
|
|
302
|
+
await store.save(tokens);
|
|
303
|
+
const { client } = await getContext();
|
|
304
|
+
const me = await profile_exports.getProfile(client);
|
|
305
|
+
await writeConfig({ email: me.email });
|
|
306
|
+
success(
|
|
307
|
+
{ email: me.email, name: me.name, plan: me.subscriptionType },
|
|
308
|
+
(d) => `Logged in as ${d.email ?? "unknown"}${d.plan ? ` (${d.plan})` : ""}.`
|
|
309
|
+
);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
fail(err);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
program2.command("logout").description("Remove stored Reepl credentials from this machine").action(async () => {
|
|
315
|
+
try {
|
|
316
|
+
await new FileTokenStore().clear();
|
|
317
|
+
await writeConfig({ email: void 0 });
|
|
318
|
+
success({ loggedOut: true }, () => "Logged out. Credentials removed.");
|
|
319
|
+
} catch (err) {
|
|
320
|
+
fail(err);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
program2.command("whoami").description("Show the currently authenticated account and plan").action(async () => {
|
|
324
|
+
try {
|
|
325
|
+
const { client } = await getContext();
|
|
326
|
+
const me = await profile_exports.getProfile(client);
|
|
327
|
+
const paid = profile_exports.hasActivePlan(me);
|
|
328
|
+
success(
|
|
329
|
+
{
|
|
330
|
+
userID: me.userID,
|
|
331
|
+
email: me.email,
|
|
332
|
+
name: me.name,
|
|
333
|
+
plan: me.subscriptionType,
|
|
334
|
+
hasActivePlan: paid,
|
|
335
|
+
creditsAvailable: me.credits_available
|
|
336
|
+
},
|
|
337
|
+
(d) => `${d.email ?? "unknown"}${d.name ? ` (${d.name})` : ""}
|
|
338
|
+
Plan: ${d.plan ?? "free"}${d.hasActivePlan ? "" : " (no active purchase)"}
|
|
339
|
+
Credits: ${d.creditsAvailable ?? "n/a"}`
|
|
340
|
+
);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
fail(err);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/cli/commands/calendar.ts
|
|
348
|
+
function registerCalendarCommands(program2) {
|
|
349
|
+
program2.command("calendar").alias("schedule").description("Show the content calendar (scheduled posts across LinkedIn and X)").option("-p, --platform <platform>", "linkedin | x | all", "all").option("--limit <n>", "Max results per platform", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
350
|
+
try {
|
|
351
|
+
const platform = (opts.platform ?? "all").toLowerCase();
|
|
352
|
+
const { client } = await getContext();
|
|
353
|
+
const items = await calendar_exports.getSchedule(client, {
|
|
354
|
+
platform: platform === "twitter" ? "x" : platform,
|
|
355
|
+
limit: opts.limit
|
|
356
|
+
});
|
|
357
|
+
success(
|
|
358
|
+
items,
|
|
359
|
+
(rows) => table(
|
|
360
|
+
rows.map((i) => ({
|
|
361
|
+
platform: i.platform,
|
|
362
|
+
id: i.id,
|
|
363
|
+
scheduledFor: i.scheduledFor ?? "",
|
|
364
|
+
content: i.content
|
|
365
|
+
})),
|
|
366
|
+
["platform", "id", "scheduledFor", "content"]
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
fail(err);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/cli/commands/carousel.ts
|
|
376
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
377
|
+
async function loadSlides(opts) {
|
|
378
|
+
const raw = opts.slidesFile ? await readFile2(opts.slidesFile, "utf8") : opts.slides;
|
|
379
|
+
if (!raw) throw new Error("Provide slides via --slides <json> or --slides-file <path>.");
|
|
380
|
+
let parsed;
|
|
381
|
+
try {
|
|
382
|
+
parsed = JSON.parse(raw);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new Error("--slides must be a JSON array of { headline, body, layout? } objects.");
|
|
385
|
+
}
|
|
386
|
+
if (!Array.isArray(parsed)) throw new Error("Slides JSON must be an array.");
|
|
387
|
+
return parsed;
|
|
388
|
+
}
|
|
389
|
+
function registerCarouselCommands(program2) {
|
|
390
|
+
const cmd = program2.command("carousel").description("Manage carousel drafts");
|
|
391
|
+
cmd.command("list").description("List carousel drafts").option("--limit <n>", "Max results", (v) => parseInt(v, 10)).option("--search <q>", "Search query").action(async (opts) => {
|
|
392
|
+
try {
|
|
393
|
+
const { client } = await getContext();
|
|
394
|
+
const result = await carousel_exports.listDrafts(client, { limit: opts.limit, search: opts.search });
|
|
395
|
+
success(result, (d) => {
|
|
396
|
+
const drafts = Array.isArray(d) ? d : d?.drafts ?? [];
|
|
397
|
+
if (!drafts.length) return "(none)";
|
|
398
|
+
return table(
|
|
399
|
+
drafts.map((x) => ({
|
|
400
|
+
id: x.draft_id ?? x.id ?? "",
|
|
401
|
+
slides: x.slide_count ?? x.carousels?.length ?? "",
|
|
402
|
+
title: String(x.title ?? "").slice(0, 50)
|
|
403
|
+
})),
|
|
404
|
+
["id", "slides", "title"]
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
} catch (err) {
|
|
408
|
+
fail(err);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
cmd.command("get <draftId>").description("Get a carousel draft").action(async (draftId) => {
|
|
412
|
+
try {
|
|
413
|
+
const { client } = await getContext();
|
|
414
|
+
success(await carousel_exports.getDraft(client, draftId));
|
|
415
|
+
} catch (err) {
|
|
416
|
+
fail(err);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
cmd.command("create").description("Create a carousel draft from slides JSON").requiredOption("--title <title>", "Draft title").option("--slides <json>", "Slides as a JSON array").option("--slides-file <path>", "Read slides JSON from a file").option("--theme <theme>", "Theme id").action(async (opts) => {
|
|
420
|
+
try {
|
|
421
|
+
const slides = await loadSlides(opts);
|
|
422
|
+
const { client } = await getContext();
|
|
423
|
+
const result = await carousel_exports.createDraft(client, { title: opts.title, slides, theme: opts.theme });
|
|
424
|
+
success(result, (d) => `Created carousel draft ${d?.draft?.draft_id ?? d?.draft_id ?? ""}.`);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
fail(err);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
cmd.command("update <draftId>").description("Update a carousel draft (title, slides, theme, fonts)").option("--title <title>", "New title").option("--slides <json>", "Replacement slides as a JSON array").option("--slides-file <path>", "Read replacement slides JSON from a file").option("--theme <theme>", "Theme id").option("--title-font <size>", "Title font size").option("--body-font <size>", "Body font size").action(async (draftId, opts) => {
|
|
430
|
+
try {
|
|
431
|
+
const slides = opts.slides || opts.slidesFile ? await loadSlides(opts) : void 0;
|
|
432
|
+
const { client } = await getContext();
|
|
433
|
+
const result = await carousel_exports.updateDraft(client, draftId, {
|
|
434
|
+
title: opts.title,
|
|
435
|
+
slides,
|
|
436
|
+
theme: opts.theme,
|
|
437
|
+
titleFontSize: opts.titleFont,
|
|
438
|
+
bodyFontSize: opts.bodyFont
|
|
439
|
+
});
|
|
440
|
+
success(result, () => `Updated carousel draft ${draftId}.`);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
fail(err);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
cmd.command("delete <draftId>").description("Delete a carousel draft").action(async (draftId) => {
|
|
446
|
+
try {
|
|
447
|
+
const { client } = await getContext();
|
|
448
|
+
await carousel_exports.deleteDraft(client, draftId);
|
|
449
|
+
success({ draftId, deleted: true }, () => `Deleted carousel draft ${draftId}.`);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
fail(err);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
cmd.command("generate").description("AI-generate carousel slides from a topic or URL").option("--topic <topic>", "Topic to generate from").option("--url <url>", "Article/YouTube URL to generate from").option("--slides <n>", "Number of slides (1-10)", (v) => parseInt(v, 10)).option("--instructions <text>", "Extra AI instructions").action(async (opts) => {
|
|
455
|
+
try {
|
|
456
|
+
const ctx = await getContext();
|
|
457
|
+
const userID = await currentUserId(ctx);
|
|
458
|
+
const result = await carousel_exports.generate(ctx.client, userID, {
|
|
459
|
+
topic: opts.topic,
|
|
460
|
+
url: opts.url,
|
|
461
|
+
numberOfSlides: opts.slides,
|
|
462
|
+
aiInstructions: opts.instructions
|
|
463
|
+
});
|
|
464
|
+
success(result);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
fail(err);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/cli/input.ts
|
|
472
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
473
|
+
async function readStdin() {
|
|
474
|
+
const chunks = [];
|
|
475
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
476
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
477
|
+
}
|
|
478
|
+
async function resolveContent(opts) {
|
|
479
|
+
if (opts.file) return (await readFile3(opts.file, "utf8")).trim();
|
|
480
|
+
if (opts.content === "-") return (await readStdin()).trim();
|
|
481
|
+
if (opts.content !== void 0) return opts.content;
|
|
482
|
+
if (!process.stdin.isTTY) {
|
|
483
|
+
const piped = (await readStdin()).trim();
|
|
484
|
+
if (piped) return piped;
|
|
485
|
+
}
|
|
486
|
+
throw new Error("No content provided. Use -c/--content, --file <path>, or pipe via stdin.");
|
|
487
|
+
}
|
|
488
|
+
function parseList(value) {
|
|
489
|
+
if (!value) return void 0;
|
|
490
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/cli/commands/comment.ts
|
|
494
|
+
function coercePlatform(p) {
|
|
495
|
+
if (!p) return void 0;
|
|
496
|
+
const v = p.toLowerCase();
|
|
497
|
+
if (v === "x" || v === "twitter") return "x";
|
|
498
|
+
if (v === "linkedin" || v === "li") return "linkedin";
|
|
499
|
+
if (v === "reddit") return "reddit";
|
|
500
|
+
throw new Error(`Unknown platform "${p}". Use linkedin, x, or reddit.`);
|
|
501
|
+
}
|
|
502
|
+
function registerCommentCommands(program2) {
|
|
503
|
+
program2.command("comment").description("Comment on / reply to a post by URL or id (LinkedIn, X, Reddit)").argument("[urlOrId]", "Post URL or id (alternatively use --url)").option("--url <url>", "Post URL or id").option("-t, --text <text>", "Comment text (use '-' to read stdin)").option("--file <path>", "Read comment text from a file").option("-p, --platform <platform>", "Force platform: linkedin | x | reddit").action(async (urlOrIdArg, opts) => {
|
|
504
|
+
try {
|
|
505
|
+
const ref = opts.url ?? urlOrIdArg;
|
|
506
|
+
if (!ref) throw new Error("Provide a post URL or id (positional arg or --url).");
|
|
507
|
+
const text = await resolveContent({ content: opts.text, file: opts.file });
|
|
508
|
+
const target = resolvePostRef(ref, coercePlatform(opts.platform));
|
|
509
|
+
const { client } = await getContext();
|
|
510
|
+
let result;
|
|
511
|
+
if (target.platform === "linkedin") {
|
|
512
|
+
result = await linkedin_exports.addComment(client, target.id, text);
|
|
513
|
+
} else if (target.platform === "reddit") {
|
|
514
|
+
result = await reddit_exports.comment(client, target.id, text);
|
|
515
|
+
} else {
|
|
516
|
+
result = await twitter_exports.reply(client, target.id, text);
|
|
517
|
+
}
|
|
518
|
+
success(
|
|
519
|
+
{ platform: target.platform, id: target.id, result },
|
|
520
|
+
(d) => `Comment posted on ${d.platform} (${d.id}).`
|
|
521
|
+
);
|
|
522
|
+
} catch (err) {
|
|
523
|
+
fail(err);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/cli/commands/dm.ts
|
|
529
|
+
function registerDmCommands(program2) {
|
|
530
|
+
const cmd = program2.command("dm").description("X (Twitter) direct messages \u2014 Premium plan required");
|
|
531
|
+
cmd.command("list").description("List DM conversations").option("--limit <n>", "Max results", (v) => parseInt(v, 10)).option("--offset <n>", "Offset", (v) => parseInt(v, 10)).action(async (opts) => {
|
|
532
|
+
try {
|
|
533
|
+
const { client } = await getContext();
|
|
534
|
+
const result = await dms_exports.listConversations(client, { limit: opts.limit, offset: opts.offset });
|
|
535
|
+
success(result, (d) => {
|
|
536
|
+
const convs = Array.isArray(d) ? d : d?.conversations ?? [];
|
|
537
|
+
if (!convs.length) return "(no conversations)";
|
|
538
|
+
return table(
|
|
539
|
+
convs.map((c) => ({
|
|
540
|
+
id: c.participantId ?? c.conversationId ?? c.id ?? "",
|
|
541
|
+
with: c.username ?? c.name ?? c.handle ?? "",
|
|
542
|
+
last: String(c.lastMessage?.text ?? c.lastMessageText ?? "").slice(0, 50)
|
|
543
|
+
})),
|
|
544
|
+
["id", "with", "last"]
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
} catch (err) {
|
|
548
|
+
fail(err);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
cmd.command("messages <conversationId>").description("Show messages in a conversation").action(async (conversationId) => {
|
|
552
|
+
try {
|
|
553
|
+
const { client } = await getContext();
|
|
554
|
+
success(await dms_exports.getMessages(client, conversationId));
|
|
555
|
+
} catch (err) {
|
|
556
|
+
fail(err);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
cmd.command("refresh").description("Pull the latest DMs from X (Premium)").action(async () => {
|
|
560
|
+
try {
|
|
561
|
+
const { client } = await getContext();
|
|
562
|
+
success(await dms_exports.refresh(client), () => "DM inbox refreshed.");
|
|
563
|
+
} catch (err) {
|
|
564
|
+
fail(err);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
cmd.command("reply <conversationId>").description("Send a reply into a DM conversation (Premium)").option("-t, --text <text>", "Message text (use '-' to read stdin)").option("--file <path>", "Read message text from a file").action(async (conversationId, opts) => {
|
|
568
|
+
try {
|
|
569
|
+
const text = await resolveContent({ content: opts.text, file: opts.file });
|
|
570
|
+
const { client } = await getContext();
|
|
571
|
+
const result = await dms_exports.sendMessage(client, conversationId, text);
|
|
572
|
+
success({ conversationId, result }, () => `Sent DM to ${conversationId}.`);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
fail(err);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/cli/commands/post.ts
|
|
580
|
+
function platformOf(opt) {
|
|
581
|
+
const p = (opt ?? "linkedin").toLowerCase();
|
|
582
|
+
if (p === "x" || p === "twitter") return "x";
|
|
583
|
+
if (p === "linkedin" || p === "li") return "linkedin";
|
|
584
|
+
throw new Error(`Unknown platform "${opt}". Use linkedin or x.`);
|
|
585
|
+
}
|
|
586
|
+
function registerPostCommands(program2) {
|
|
587
|
+
const post = program2.command("post").description("Create, list, update, and delete posts");
|
|
588
|
+
post.command("create").description("Create a post as a draft, scheduled, or published now").option("-p, --platform <platform>", "linkedin or x", "linkedin").option("-c, --content <text>", "Post text (use '-' to read stdin)").option("--file <path>", "Read post text from a file").option("--title <title>", "Draft title (LinkedIn)").option("--media <urls>", "Comma-separated media URLs").option("--schedule <iso>", "Schedule for an ISO-8601 timestamp").option("--publish", "Publish immediately instead of saving a draft").option("--draft", "Save as a draft (default)").action(async (opts) => {
|
|
589
|
+
try {
|
|
590
|
+
const platform = platformOf(opts.platform);
|
|
591
|
+
const content = await resolveContent(opts);
|
|
592
|
+
const media = parseList(opts.media);
|
|
593
|
+
const { client } = await getContext();
|
|
594
|
+
let result;
|
|
595
|
+
if (platform === "linkedin") {
|
|
596
|
+
if (opts.schedule) {
|
|
597
|
+
result = await linkedin_exports.schedulePost(client, {
|
|
598
|
+
content,
|
|
599
|
+
scheduledFor: opts.schedule,
|
|
600
|
+
title: opts.title,
|
|
601
|
+
mediaUrls: media
|
|
602
|
+
});
|
|
603
|
+
} else if (opts.publish) {
|
|
604
|
+
result = await linkedin_exports.publishNow(client, { content, title: opts.title, mediaUrls: media });
|
|
605
|
+
} else {
|
|
606
|
+
result = await linkedin_exports.createDraft(client, { content, title: opts.title, mediaUrls: media });
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
const threadTweets = [{ text: content, mediaUrls: media }];
|
|
610
|
+
if (opts.schedule) {
|
|
611
|
+
result = await twitter_exports.createPost(client, { threadTweets, scheduledFor: opts.schedule });
|
|
612
|
+
} else if (opts.publish) {
|
|
613
|
+
result = await twitter_exports.createPost(client, { threadTweets });
|
|
614
|
+
} else {
|
|
615
|
+
result = await twitter_exports.createDraft(client, { threadTweets, title: opts.title });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const mode = opts.schedule ? "scheduled" : opts.publish ? "published" : "draft";
|
|
619
|
+
success({ platform, mode, result }, (d) => `Created ${d.platform} ${d.mode}.`);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
fail(err);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
post.command("list").description("List posts or drafts").option("-p, --platform <platform>", "linkedin or x", "linkedin").option("-s, --status <status>", "scheduled | published | failed | drafts", "scheduled").option("--limit <n>", "Max results", (v) => parseInt(v, 10)).option("--search <q>", "Search query").action(async (opts) => {
|
|
625
|
+
try {
|
|
626
|
+
const platform = platformOf(opts.platform);
|
|
627
|
+
const { client } = await getContext();
|
|
628
|
+
const isDrafts = opts.status === "drafts";
|
|
629
|
+
let result;
|
|
630
|
+
if (platform === "linkedin") {
|
|
631
|
+
result = isDrafts ? await linkedin_exports.listDrafts(client, { limit: opts.limit, search: opts.search }) : await linkedin_exports.listPosts(client, { status: opts.status, limit: opts.limit, search: opts.search });
|
|
632
|
+
} else {
|
|
633
|
+
result = isDrafts ? await twitter_exports.listDrafts(client, { limit: opts.limit, search: opts.search }) : await twitter_exports.listPosts(client, { status: opts.status, limit: opts.limit, search: opts.search });
|
|
634
|
+
}
|
|
635
|
+
success(result, (d) => renderPostList(d));
|
|
636
|
+
} catch (err) {
|
|
637
|
+
fail(err);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
post.command("update <id>").description("Update a scheduled post or draft").option("-p, --platform <platform>", "linkedin or x", "linkedin").option("-c, --content <text>", "New text (use '-' to read stdin)").option("--file <path>", "Read new text from a file").option("--schedule <iso>", "Reschedule to an ISO-8601 timestamp").option("--draft", "Target a draft instead of a scheduled post (LinkedIn)").action(async (id, opts) => {
|
|
641
|
+
try {
|
|
642
|
+
const platform = platformOf(opts.platform);
|
|
643
|
+
const content = opts.content !== void 0 || opts.file ? await resolveContent(opts) : void 0;
|
|
644
|
+
const { client } = await getContext();
|
|
645
|
+
let result;
|
|
646
|
+
if (platform === "linkedin") {
|
|
647
|
+
result = opts.draft ? await linkedin_exports.updateDraft(client, id, {
|
|
648
|
+
...content !== void 0 ? { content } : {}
|
|
649
|
+
}) : await linkedin_exports.updateScheduledPost(client, id, {
|
|
650
|
+
...content !== void 0 ? { content } : {},
|
|
651
|
+
...opts.schedule ? { scheduledFor: opts.schedule } : {}
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
result = await twitter_exports.updatePost(client, id, {
|
|
655
|
+
...content !== void 0 ? { threadTweets: [{ text: content }] } : {},
|
|
656
|
+
...opts.schedule ? { scheduledFor: opts.schedule } : {}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
success({ id, result }, () => `Updated ${platform} post ${id}.`);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
fail(err);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
post.command("delete <id>").description("Delete a scheduled post or draft").option("-p, --platform <platform>", "linkedin or x", "linkedin").option("--draft", "Delete a draft instead of a scheduled post").action(async (id, opts) => {
|
|
665
|
+
try {
|
|
666
|
+
const platform = platformOf(opts.platform);
|
|
667
|
+
const { client } = await getContext();
|
|
668
|
+
if (platform === "linkedin") {
|
|
669
|
+
await (opts.draft ? linkedin_exports.deleteDraft(client, id) : linkedin_exports.deletePost(client, id));
|
|
670
|
+
} else {
|
|
671
|
+
await (opts.draft ? twitter_exports.deleteDraft(client, id) : twitter_exports.deletePost(client, id));
|
|
672
|
+
}
|
|
673
|
+
success({ id, deleted: true }, () => `Deleted ${platform} ${opts.draft ? "draft" : "post"} ${id}.`);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
fail(err);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
function renderPostList(raw) {
|
|
680
|
+
const arr = Array.isArray(raw) ? raw : raw?.posts ?? raw?.drafts ?? raw?.items ?? [];
|
|
681
|
+
if (!arr.length) return "(none)";
|
|
682
|
+
return table(
|
|
683
|
+
arr.map((p) => ({
|
|
684
|
+
id: p.postId ?? p.post_id ?? p.draft_id ?? p.id ?? "",
|
|
685
|
+
status: p.status ?? (p.draft_id ? "draft" : ""),
|
|
686
|
+
when: p.scheduledFor ?? p.publishedAt ?? "",
|
|
687
|
+
text: String(p.content ?? p.title ?? "").replace(/\s+/g, " ").slice(0, 60)
|
|
688
|
+
})),
|
|
689
|
+
["id", "status", "when", "text"]
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/cli/commands/reddit.ts
|
|
694
|
+
function registerRedditCommands(program2) {
|
|
695
|
+
const cmd = program2.command("reddit").description("Search Reddit and reply to submissions");
|
|
696
|
+
cmd.command("search <keyword>").description("Search Reddit for signals by keyword (Pro plan required)").action(async (keyword) => {
|
|
697
|
+
try {
|
|
698
|
+
const { client } = await getContext();
|
|
699
|
+
const result = await reddit_exports.search(client, keyword);
|
|
700
|
+
success(result, (d) => {
|
|
701
|
+
const posts = Array.isArray(d) ? d : d?.results ?? d?.posts ?? [];
|
|
702
|
+
if (!posts.length) return "(no results)";
|
|
703
|
+
return table(
|
|
704
|
+
posts.map((p) => ({
|
|
705
|
+
subreddit: p.subreddit ?? "",
|
|
706
|
+
score: p.score ?? p.opportunityScore ?? "",
|
|
707
|
+
title: String(p.title ?? "").slice(0, 70),
|
|
708
|
+
url: p.url ?? p.permalink ?? ""
|
|
709
|
+
})),
|
|
710
|
+
["subreddit", "score", "title", "url"]
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
} catch (err) {
|
|
714
|
+
fail(err);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
cmd.command("status").description("Show remaining Reddit search quota").action(async () => {
|
|
718
|
+
try {
|
|
719
|
+
const { client } = await getContext();
|
|
720
|
+
success(await reddit_exports.searchStatus(client));
|
|
721
|
+
} catch (err) {
|
|
722
|
+
fail(err);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
cmd.command("reply <postUrlOrId>").description("Reply to a Reddit submission").option("-t, --text <text>", "Reply text (use '-' to read stdin)").option("--file <path>", "Read reply text from a file").action(async (postUrlOrId, opts) => {
|
|
726
|
+
try {
|
|
727
|
+
const text = await resolveContent({ content: opts.text, file: opts.file });
|
|
728
|
+
const target = resolvePostRef(postUrlOrId, "reddit");
|
|
729
|
+
const { client } = await getContext();
|
|
730
|
+
const result = await reddit_exports.comment(client, target.id, text);
|
|
731
|
+
success({ id: target.id, result }, (d) => `Replied to Reddit submission ${d.id}.`);
|
|
732
|
+
} catch (err) {
|
|
733
|
+
fail(err);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/cli/commands/schema.ts
|
|
739
|
+
function serialize(cmd) {
|
|
740
|
+
const args = (cmd.registeredArguments ?? []).map((a) => ({
|
|
741
|
+
name: a.name(),
|
|
742
|
+
required: Boolean(a.required),
|
|
743
|
+
description: a.description || void 0
|
|
744
|
+
}));
|
|
745
|
+
return {
|
|
746
|
+
name: cmd.name(),
|
|
747
|
+
description: cmd.description(),
|
|
748
|
+
arguments: args,
|
|
749
|
+
options: cmd.options.map((o) => ({
|
|
750
|
+
flags: o.flags,
|
|
751
|
+
description: o.description,
|
|
752
|
+
...o.defaultValue !== void 0 ? { defaultValue: o.defaultValue } : {}
|
|
753
|
+
})),
|
|
754
|
+
subcommands: cmd.commands.filter((c) => c.name() !== "help").map((c) => serialize(c))
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function registerSchemaCommand(program2) {
|
|
758
|
+
program2.command("schema").description("Print the full command/flag tree as JSON (for agents and tooling)").action(() => {
|
|
759
|
+
success(serialize(program2));
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/cli/commands/signals.ts
|
|
764
|
+
function registerSignalsCommands(program2) {
|
|
765
|
+
const cmd = program2.command("signals").description("Your signals inbox (Reddit / YouTube / RSS / trends)");
|
|
766
|
+
cmd.command("list").description("List signals in your inbox").action(async () => {
|
|
767
|
+
try {
|
|
768
|
+
const { client } = await getContext();
|
|
769
|
+
const result = await signals_exports.getInbox(client);
|
|
770
|
+
success(result, (d) => {
|
|
771
|
+
const items = Array.isArray(d) ? d : d?.signals ?? d?.items ?? [];
|
|
772
|
+
if (!items.length) return "(no signals)";
|
|
773
|
+
return table(
|
|
774
|
+
items.map((s) => ({
|
|
775
|
+
source: s.source ?? s.type ?? "",
|
|
776
|
+
title: String(s.title ?? s.text ?? "").slice(0, 70),
|
|
777
|
+
url: s.url ?? s.link ?? ""
|
|
778
|
+
})),
|
|
779
|
+
["source", "title", "url"]
|
|
780
|
+
);
|
|
781
|
+
});
|
|
782
|
+
} catch (err) {
|
|
783
|
+
fail(err);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/cli/commands/style.ts
|
|
789
|
+
async function styleContext() {
|
|
790
|
+
const base = await getContext();
|
|
791
|
+
if (base.workspaceId) return base;
|
|
792
|
+
const workspaceId = await ensureWorkspaceId(base);
|
|
793
|
+
return getContext(workspaceId);
|
|
794
|
+
}
|
|
795
|
+
function registerStyleCommands(program2) {
|
|
796
|
+
const cmd = program2.command("style").description("View and train your writing styles");
|
|
797
|
+
cmd.command("get").description("Show writing-style summary for all facets").action(async () => {
|
|
798
|
+
try {
|
|
799
|
+
const { client } = await styleContext();
|
|
800
|
+
success(await writingStyle_exports.get(client));
|
|
801
|
+
} catch (err) {
|
|
802
|
+
fail(err);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
cmd.command("set").description("Save override instructions for a facet").requiredOption("--facet <facet>", "posts | comments | messages").option("-i, --instructions <text>", "Override instructions (use '-' for stdin)").option("--file <path>", "Read instructions from a file").action(async (opts) => {
|
|
806
|
+
try {
|
|
807
|
+
const instructions = await resolveContent({ content: opts.instructions, file: opts.file });
|
|
808
|
+
const { client } = await styleContext();
|
|
809
|
+
const result = await writingStyle_exports.override(client, opts.facet, instructions);
|
|
810
|
+
success(result, () => `Saved ${opts.facet} style override.`);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
fail(err);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
cmd.command("train").description("(Re)synthesize a facet's writing style (Pro+; posts/comments only)").requiredOption("--facet <facet>", "posts | comments").action(async (opts) => {
|
|
816
|
+
try {
|
|
817
|
+
const { client } = await styleContext();
|
|
818
|
+
const result = await writingStyle_exports.train(client, opts.facet);
|
|
819
|
+
success(result, () => `Training started for ${opts.facet}.`);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
fail(err);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
cmd.command("test").description("Playground: compare baseline vs styled generation for a facet").requiredOption("--facet <facet>", "posts | comments | messages").option("-i, --input <text>", "Prompt input (use '-' for stdin)").option("--file <path>", "Read input from a file").action(async (opts) => {
|
|
825
|
+
try {
|
|
826
|
+
const input = await resolveContent({ content: opts.input, file: opts.file });
|
|
827
|
+
const { client } = await styleContext();
|
|
828
|
+
success(await writingStyle_exports.testVoiceStyle(client, opts.facet, input));
|
|
829
|
+
} catch (err) {
|
|
830
|
+
fail(err);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/cli/commands/voice.ts
|
|
836
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
837
|
+
function registerVoiceCommands(program2) {
|
|
838
|
+
const cmd = program2.command("voice").description("View and update your voice profile");
|
|
839
|
+
cmd.command("get").description("Show the current voice profile").action(async () => {
|
|
840
|
+
try {
|
|
841
|
+
const { client } = await getContext();
|
|
842
|
+
success(await voice_exports.get(client));
|
|
843
|
+
} catch (err) {
|
|
844
|
+
fail(err);
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
cmd.command("update").description("Update the voice profile from a JSON file or flags").option("--file <path>", "JSON body to deep-merge (allowAutoUpdate, userInstructions, ...)").option("--active", "Set isActive true").option("--inactive", "Set isActive false").option("--allow-auto-update", "Allow the generated profile to auto-update").action(async (opts) => {
|
|
848
|
+
try {
|
|
849
|
+
let body = {};
|
|
850
|
+
if (opts.file) {
|
|
851
|
+
const parsed = JSON.parse(await readFile4(opts.file, "utf8"));
|
|
852
|
+
if (parsed && typeof parsed === "object") body = parsed;
|
|
853
|
+
}
|
|
854
|
+
if (opts.active) body.isActive = true;
|
|
855
|
+
if (opts.inactive) body.isActive = false;
|
|
856
|
+
if (opts.allowAutoUpdate) body.allowAutoUpdate = true;
|
|
857
|
+
if (Object.keys(body).length === 0) {
|
|
858
|
+
throw new Error("Nothing to update. Pass --file <json> or a flag (--active/--inactive/--allow-auto-update).");
|
|
859
|
+
}
|
|
860
|
+
const { client } = await getContext();
|
|
861
|
+
success(await voice_exports.update(client, body), () => "Voice profile updated.");
|
|
862
|
+
} catch (err) {
|
|
863
|
+
fail(err);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/cli/commands/workspace.ts
|
|
869
|
+
function registerWorkspaceCommands(program2) {
|
|
870
|
+
const workspace = program2.command("workspace").alias("ws").description("List and switch the active workspace (organization)");
|
|
871
|
+
workspace.command("list").description("List your workspaces").action(async () => {
|
|
872
|
+
try {
|
|
873
|
+
const { client } = await getContext();
|
|
874
|
+
const list = await workspaces_exports.listWorkspaces(client);
|
|
875
|
+
const current = (await readConfig()).currentWorkspaceId;
|
|
876
|
+
success(
|
|
877
|
+
list.map((w) => ({ ...w, isCurrent: w.id === current })),
|
|
878
|
+
(rows) => table(
|
|
879
|
+
rows.map((w) => ({
|
|
880
|
+
current: w.isCurrent ? "*" : w.isDefault ? "d" : "",
|
|
881
|
+
id: w.id,
|
|
882
|
+
name: w.name ?? ""
|
|
883
|
+
})),
|
|
884
|
+
["current", "id", "name"]
|
|
885
|
+
)
|
|
886
|
+
);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
fail(err);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
workspace.command("switch <workspaceId>").description("Set the active workspace used by subsequent commands").action(async (workspaceId) => {
|
|
892
|
+
try {
|
|
893
|
+
const { client } = await getContext();
|
|
894
|
+
const list = await workspaces_exports.listWorkspaces(client);
|
|
895
|
+
const match = list.find((w) => w.id === workspaceId);
|
|
896
|
+
if (!match) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Workspace "${workspaceId}" not found. Run \`reepl workspace list\` to see valid ids.`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
await writeConfig({ currentWorkspaceId: workspaceId });
|
|
902
|
+
success(
|
|
903
|
+
{ currentWorkspaceId: workspaceId, name: match.name },
|
|
904
|
+
(d) => `Switched to workspace ${d.currentWorkspaceId}${d.name ? ` (${d.name})` : ""}.`
|
|
905
|
+
);
|
|
906
|
+
} catch (err) {
|
|
907
|
+
fail(err);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
workspace.command("current").description("Show the active workspace").action(async () => {
|
|
911
|
+
try {
|
|
912
|
+
const config = await readConfig();
|
|
913
|
+
if (!config.currentWorkspaceId) {
|
|
914
|
+
success({ currentWorkspaceId: null }, () => "No workspace selected (using account default).");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
success(
|
|
918
|
+
{ currentWorkspaceId: config.currentWorkspaceId },
|
|
919
|
+
(d) => `Current workspace: ${d.currentWorkspaceId}`
|
|
920
|
+
);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
fail(err);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/cli/commands/x.ts
|
|
928
|
+
function registerXCommands(program2) {
|
|
929
|
+
const cmd = program2.command("x").description("X (Twitter) engagement actions");
|
|
930
|
+
cmd.command("reply <tweetUrlOrId>").description("Publish a reply to a tweet (Premium)").option("-t, --text <text>", "Reply text (use '-' to read stdin)").option("--file <path>", "Read reply text from a file").action(async (tweetUrlOrId, opts) => {
|
|
931
|
+
try {
|
|
932
|
+
const text = await resolveContent({ content: opts.text, file: opts.file });
|
|
933
|
+
const { id } = resolvePostRef(tweetUrlOrId, "x");
|
|
934
|
+
const { client } = await getContext();
|
|
935
|
+
const result = await twitter_exports.reply(client, id, text);
|
|
936
|
+
success({ tweetId: id, result }, (d) => `Replied to tweet ${d.tweetId}.`);
|
|
937
|
+
} catch (err) {
|
|
938
|
+
fail(err);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/cli/index.ts
|
|
944
|
+
var program = new Command();
|
|
945
|
+
program.name("reepl").description("Reepl CLI \u2014 manage LinkedIn / X / Reddit content, carousels, signals, and more.").version("0.1.0").option("--json", "Output machine-readable JSON (auto-enabled when piped)").option("--no-json", "Force human-readable output even when piped").hook("preAction", (thisCommand) => {
|
|
946
|
+
const opts = thisCommand.opts();
|
|
947
|
+
setJsonMode(opts.json === void 0 ? void 0 : Boolean(opts.json));
|
|
948
|
+
});
|
|
949
|
+
program.exitOverride();
|
|
950
|
+
program.configureOutput({
|
|
951
|
+
writeOut: (str) => process.stdout.write(str),
|
|
952
|
+
writeErr: () => {
|
|
953
|
+
}
|
|
954
|
+
// suppress commander's default error text; we emit the envelope
|
|
955
|
+
});
|
|
956
|
+
registerAuthCommands(program);
|
|
957
|
+
registerWorkspaceCommands(program);
|
|
958
|
+
registerPostCommands(program);
|
|
959
|
+
registerCalendarCommands(program);
|
|
960
|
+
registerCommentCommands(program);
|
|
961
|
+
registerRedditCommands(program);
|
|
962
|
+
registerSignalsCommands(program);
|
|
963
|
+
registerCarouselCommands(program);
|
|
964
|
+
registerStyleCommands(program);
|
|
965
|
+
registerVoiceCommands(program);
|
|
966
|
+
registerDmCommands(program);
|
|
967
|
+
registerXCommands(program);
|
|
968
|
+
registerSchemaCommand(program);
|
|
969
|
+
if (process.argv.length <= 2) {
|
|
970
|
+
printBanner({ version: program.version() });
|
|
971
|
+
process.stdout.write("Run `reepl --help` to see all commands.\n");
|
|
972
|
+
process.exit(0);
|
|
973
|
+
}
|
|
974
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
975
|
+
const code = err?.code;
|
|
976
|
+
if (code === "commander.helpDisplayed" || code === "commander.help" || code === "commander.version") {
|
|
977
|
+
process.exit(0);
|
|
978
|
+
}
|
|
979
|
+
const message = String(err?.message ?? "Invalid command usage.").replace(/^error:\s*/i, "");
|
|
980
|
+
fail(new ReeplError("VALIDATION", message));
|
|
981
|
+
});
|
|
982
|
+
//# sourceMappingURL=index.js.map
|