innernote 0.3.4 → 0.5.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 +13 -5
- package/dist/index.js +1179 -72
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -500,6 +500,57 @@ function resolveRef(ref) {
|
|
|
500
500
|
return /^[a-z0-9]{20,}$/.test(ref) ? { id: ref, title: ref } : null;
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// src/posts.ts
|
|
504
|
+
async function listPosts(status) {
|
|
505
|
+
const data = await request(`/api/posts${status ? `?status=${encodeURIComponent(status)}` : ""}`);
|
|
506
|
+
return data.posts;
|
|
507
|
+
}
|
|
508
|
+
async function fetchPost(id) {
|
|
509
|
+
return (await listPosts()).find((p) => p._id === id) ?? null;
|
|
510
|
+
}
|
|
511
|
+
async function listSeries() {
|
|
512
|
+
const data = await request("/api/campaigns");
|
|
513
|
+
return data.campaigns;
|
|
514
|
+
}
|
|
515
|
+
function matchSeries(all, name) {
|
|
516
|
+
const want = name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
517
|
+
return all.find((s) => s.name.trim().toLowerCase().replace(/\s+/g, " ") === want) ?? null;
|
|
518
|
+
}
|
|
519
|
+
function linkedinUrl(post) {
|
|
520
|
+
return post.linkedinPostUrn ? `https://www.linkedin.com/feed/update/${post.linkedinPostUrn}/` : null;
|
|
521
|
+
}
|
|
522
|
+
function describeMedia(post) {
|
|
523
|
+
const m = post.media;
|
|
524
|
+
if (!m || m.kind === "text")
|
|
525
|
+
return null;
|
|
526
|
+
const title = typeof m.title === "string" && m.title ? `: ${m.title}` : "";
|
|
527
|
+
const assets = Array.isArray(m.assets) ? m.assets.length : 0;
|
|
528
|
+
switch (m.kind) {
|
|
529
|
+
case "image":
|
|
530
|
+
return "image";
|
|
531
|
+
case "multiImage":
|
|
532
|
+
return `${assets} images`;
|
|
533
|
+
case "video":
|
|
534
|
+
return `video${title}`;
|
|
535
|
+
case "document":
|
|
536
|
+
return `PDF${title}`;
|
|
537
|
+
case "poll":
|
|
538
|
+
return "poll";
|
|
539
|
+
case "link":
|
|
540
|
+
return "link";
|
|
541
|
+
default:
|
|
542
|
+
return m.kind;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function postForJson(post, series) {
|
|
546
|
+
const inSeries = post.campaignId ? series.find((s) => s._id === post.campaignId) : undefined;
|
|
547
|
+
return {
|
|
548
|
+
...post,
|
|
549
|
+
seriesName: inSeries?.name ?? null,
|
|
550
|
+
linkedinUrl: linkedinUrl(post)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
503
554
|
// src/flags.ts
|
|
504
555
|
function parseFlags(args, valueFlags = []) {
|
|
505
556
|
const values = new Map;
|
|
@@ -600,29 +651,42 @@ ${data.ideas.length} ${data.ideas.length === 1 ? "idea" : "ideas"}`));
|
|
|
600
651
|
return 0;
|
|
601
652
|
}
|
|
602
653
|
async function drafts(args) {
|
|
603
|
-
const { json, rest } = parseFlags(args);
|
|
654
|
+
const { json, rest, value } = parseFlags(args, ["series"]);
|
|
604
655
|
const status = rest[0];
|
|
605
|
-
const
|
|
656
|
+
const seriesName = value("series");
|
|
657
|
+
const [all, series] = await Promise.all([listPosts(status), listSeries()]);
|
|
658
|
+
let posts = all;
|
|
659
|
+
if (seriesName) {
|
|
660
|
+
const target = matchSeries(series, seriesName);
|
|
661
|
+
if (!target) {
|
|
662
|
+
note(`No series called "${seriesName}".`);
|
|
663
|
+
if (series.length)
|
|
664
|
+
note(dim2(`Yours: ${series.map((s) => s.name).join(", ")}`));
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
posts = all.filter((p) => p.campaignId === target._id).sort((a, b) => (a.campaignOrder ?? "").localeCompare(b.campaignOrder ?? ""));
|
|
668
|
+
}
|
|
606
669
|
if (json) {
|
|
607
|
-
out(JSON.stringify(
|
|
670
|
+
out(JSON.stringify(posts.map((p) => postForJson(p, series)), null, 2));
|
|
608
671
|
return 0;
|
|
609
672
|
}
|
|
610
|
-
if (
|
|
611
|
-
note("No posts yet. Write one with: write");
|
|
673
|
+
if (posts.length === 0) {
|
|
674
|
+
note(seriesName ? "Nothing in that series yet." : "No posts yet. Write one with: write");
|
|
612
675
|
return 0;
|
|
613
676
|
}
|
|
614
|
-
const shown =
|
|
677
|
+
const shown = posts.slice(0, 30);
|
|
615
678
|
rememberList(shown.map((p) => ({ id: p._id, title: (p.title || p.content).replace(/\s+/g, " ") })));
|
|
616
679
|
for (const [i, post] of shown.entries()) {
|
|
617
680
|
const n = String(i + 1).padStart(2);
|
|
618
|
-
|
|
681
|
+
const likes = post.status === "published" && post.linkedinLikes !== undefined ? dim2(` ${post.linkedinLikes} likes`) : "";
|
|
682
|
+
out(`${caramel(n)} ${dim2(post.status.padEnd(9))} ${oneLine(post.title || post.content, 52)}${likes}`);
|
|
619
683
|
}
|
|
620
|
-
if (
|
|
684
|
+
if (posts.length > shown.length) {
|
|
621
685
|
note(dim2(`
|
|
622
|
-
...and ${
|
|
686
|
+
...and ${posts.length - shown.length} more`));
|
|
623
687
|
}
|
|
624
688
|
note(dim2(`
|
|
625
|
-
open <number> to work on one`));
|
|
689
|
+
open <number> to work on one show <id> for everything about one`));
|
|
626
690
|
return 0;
|
|
627
691
|
}
|
|
628
692
|
async function week(args) {
|
|
@@ -650,22 +714,34 @@ async function week(args) {
|
|
|
650
714
|
}
|
|
651
715
|
async function context(args) {
|
|
652
716
|
const { json } = parseFlags(args);
|
|
653
|
-
const [pillars, series] = await Promise.all([
|
|
717
|
+
const [pillars, series, notebook, products] = await Promise.all([
|
|
654
718
|
request("/api/pillars"),
|
|
655
|
-
request("/api/campaigns")
|
|
719
|
+
request("/api/campaigns"),
|
|
720
|
+
request("/api/notebook/brief").catch(() => ({ brief: null })),
|
|
721
|
+
request("/api/products").catch(() => ({ products: [] }))
|
|
656
722
|
]);
|
|
723
|
+
const productRows = products.products.map((p) => ({ name: p.name, oneLiner: p.oneLiner, postCount: p.postCount }));
|
|
657
724
|
if (json) {
|
|
658
|
-
out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns }, null, 2));
|
|
725
|
+
out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns, products: productRows, brief: notebook.brief }, null, 2));
|
|
659
726
|
return 0;
|
|
660
727
|
}
|
|
661
728
|
out();
|
|
729
|
+
if (notebook.brief) {
|
|
730
|
+
out(` ${bold2("Who you are, as innernote knows it")}`);
|
|
731
|
+
for (const line of notebook.brief.split(`
|
|
732
|
+
`)) {
|
|
733
|
+
if (line.trim())
|
|
734
|
+
out(` ${dim2(line.replace(/^#+\s*/, ""))}`);
|
|
735
|
+
}
|
|
736
|
+
out();
|
|
737
|
+
}
|
|
662
738
|
if (pillars.pillars.length) {
|
|
663
739
|
out(` ${bold2("What you post about")}`);
|
|
664
740
|
for (const p of pillars.pillars) {
|
|
665
741
|
out(` ${caramel(p.name)}${typeof p.weight === "number" ? dim2(` ${p.weight}%`) : ""}`);
|
|
666
742
|
}
|
|
667
743
|
} else {
|
|
668
|
-
out(` ${dim2("No
|
|
744
|
+
out(` ${dim2("No topics yet. Set them in Settings.")}`);
|
|
669
745
|
}
|
|
670
746
|
out();
|
|
671
747
|
if (series.campaigns.length) {
|
|
@@ -681,6 +757,15 @@ async function context(args) {
|
|
|
681
757
|
} else {
|
|
682
758
|
out(` ${dim2("No series running.")}`);
|
|
683
759
|
}
|
|
760
|
+
if (productRows.length) {
|
|
761
|
+
out();
|
|
762
|
+
out(` ${bold2("Products")}${dim2(" work one into a post with")} ${caramel('ask --post <id> --product "<name>"')}`);
|
|
763
|
+
for (const p of productRows) {
|
|
764
|
+
out(` ${caramel(p.name)}${p.postCount ? dim2(` ${p.postCount} ${p.postCount === 1 ? "post" : "posts"}`) : ""}`);
|
|
765
|
+
if (p.oneLiner)
|
|
766
|
+
out(` ${dim2(oneLine(p.oneLiner, 64))}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
684
769
|
out();
|
|
685
770
|
return 0;
|
|
686
771
|
}
|
|
@@ -853,6 +938,7 @@ async function whileThinking(phases, run) {
|
|
|
853
938
|
}
|
|
854
939
|
|
|
855
940
|
// src/commands/write.ts
|
|
941
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
856
942
|
async function capture(args) {
|
|
857
943
|
const { json, rest } = parseFlags(args);
|
|
858
944
|
const typed = rest.join(" ").trim();
|
|
@@ -876,14 +962,26 @@ async function capture(args) {
|
|
|
876
962
|
note(dim2("`ideas` to see them, `write` to turn one into a post"));
|
|
877
963
|
return 0;
|
|
878
964
|
}
|
|
965
|
+
async function saveNew(content, series, seriesSummary, title) {
|
|
966
|
+
return request("/api/posts", {
|
|
967
|
+
method: "POST",
|
|
968
|
+
body: {
|
|
969
|
+
content,
|
|
970
|
+
source: "cli",
|
|
971
|
+
...title ? { title } : {},
|
|
972
|
+
...series ? { series, seriesSummary: seriesSummary ?? undefined } : {}
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
}
|
|
879
976
|
async function write(args) {
|
|
880
|
-
const { json, rest, value, has } = parseFlags(args, ["thought", "format", "series"]);
|
|
977
|
+
const { json, rest, value, has } = parseFlags(args, ["thought", "format", "series", "title"]);
|
|
881
978
|
const topic = rest.join(" ").trim();
|
|
882
979
|
const thoughtFlag = value("thought");
|
|
883
980
|
const piped = thoughtFlag || topic ? null : await readStdin();
|
|
884
981
|
const thought = thoughtFlag ?? (piped ?? undefined);
|
|
885
982
|
const format = value("format");
|
|
886
983
|
const series = value("series");
|
|
984
|
+
const title = value("title");
|
|
887
985
|
const save = has("save");
|
|
888
986
|
const call = () => request("/api/content/generate", {
|
|
889
987
|
method: "POST",
|
|
@@ -902,8 +1000,14 @@ async function write(args) {
|
|
|
902
1000
|
seriesSummary: post.seriesSummary ?? undefined,
|
|
903
1001
|
quality: post.qualityScore
|
|
904
1002
|
});
|
|
1003
|
+
const saved = save ? await saveNew(post.content, series, post.seriesSummary, title) : null;
|
|
1004
|
+
if (saved)
|
|
1005
|
+
updateDraft({ id: saved.id });
|
|
1006
|
+
if (saved && !title) {
|
|
1007
|
+
await request(`/api/posts/${encodeURIComponent(saved.id)}/title`, { method: "POST", timeoutMs: 30000 }).catch(() => {});
|
|
1008
|
+
}
|
|
905
1009
|
if (json) {
|
|
906
|
-
out(JSON.stringify(post, null, 2));
|
|
1010
|
+
out(JSON.stringify(saved ? { ...post, id: saved.id } : post, null, 2));
|
|
907
1011
|
return 0;
|
|
908
1012
|
}
|
|
909
1013
|
out();
|
|
@@ -920,16 +1024,7 @@ async function write(args) {
|
|
|
920
1024
|
else if (post.similarity?.band === "adjacent")
|
|
921
1025
|
notes.push("close to something you already published");
|
|
922
1026
|
note(dim2(notes.join(", ")));
|
|
923
|
-
if (
|
|
924
|
-
const saved = await request("/api/posts", {
|
|
925
|
-
method: "POST",
|
|
926
|
-
body: {
|
|
927
|
-
content: post.content,
|
|
928
|
-
source: "cli",
|
|
929
|
-
...series ? { series, seriesSummary: post.seriesSummary } : {}
|
|
930
|
-
}
|
|
931
|
-
});
|
|
932
|
-
updateDraft({ id: saved.id });
|
|
1027
|
+
if (saved) {
|
|
933
1028
|
ok(series ? `Saved into "${series}". ${dim2(saved.id)}` : `Saved as a draft. ${dim2(saved.id)}`);
|
|
934
1029
|
} else {
|
|
935
1030
|
note(dim2("Not saved yet. `save` to keep it, `shape shorter` to change it."));
|
|
@@ -993,8 +1088,17 @@ async function shape(args) {
|
|
|
993
1088
|
note(dim2(held?.id ? "`save` to update it" : "`save` to keep it"));
|
|
994
1089
|
return 0;
|
|
995
1090
|
}
|
|
1091
|
+
function parseWhen(input) {
|
|
1092
|
+
const local = input.trim().match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{1,2}):(\d{2})$/);
|
|
1093
|
+
if (local) {
|
|
1094
|
+
const [, y, mo, d, h, mi] = local.map(Number);
|
|
1095
|
+
return new Date(y, mo - 1, d, h, mi).getTime();
|
|
1096
|
+
}
|
|
1097
|
+
const t = Date.parse(input);
|
|
1098
|
+
return Number.isFinite(t) ? t : null;
|
|
1099
|
+
}
|
|
996
1100
|
async function queue(args) {
|
|
997
|
-
const { json, rest } = parseFlags(args);
|
|
1101
|
+
const { json, rest, value } = parseFlags(args, ["at"]);
|
|
998
1102
|
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
999
1103
|
if (!ref) {
|
|
1000
1104
|
fail("Which post?");
|
|
@@ -1002,7 +1106,20 @@ async function queue(args) {
|
|
|
1002
1106
|
note(dim2(`Or pass the id: ${CMD} queue <post-id>`));
|
|
1003
1107
|
return 1;
|
|
1004
1108
|
}
|
|
1005
|
-
const
|
|
1109
|
+
const at = value("at");
|
|
1110
|
+
let result;
|
|
1111
|
+
if (at !== undefined) {
|
|
1112
|
+
const when2 = parseWhen(at);
|
|
1113
|
+
if (when2 == null) {
|
|
1114
|
+
fail(`"${at}" is not a time.`);
|
|
1115
|
+
note(dim2(`${CMD} queue <post-id> --at "2026-10-12 12:31"`));
|
|
1116
|
+
return 1;
|
|
1117
|
+
}
|
|
1118
|
+
await request(`/api/linkedin/schedule`, { method: "POST", body: { postId: ref.id, scheduledFor: when2 }, timeoutMs: 60000 });
|
|
1119
|
+
result = { success: true, scheduledFor: when2 };
|
|
1120
|
+
} else {
|
|
1121
|
+
result = await request(`/api/posts/${encodeURIComponent(ref.id)}/queue`, { method: "POST", timeoutMs: 60000 });
|
|
1122
|
+
}
|
|
1006
1123
|
if (json) {
|
|
1007
1124
|
out(JSON.stringify(result, null, 2));
|
|
1008
1125
|
return 0;
|
|
@@ -1076,9 +1193,21 @@ async function open(args) {
|
|
|
1076
1193
|
return 0;
|
|
1077
1194
|
}
|
|
1078
1195
|
async function ask(args) {
|
|
1079
|
-
const { json, rest } = parseFlags(args);
|
|
1196
|
+
const { json, rest, value, has } = parseFlags(args, ["post", "passage", "preset", "shape", "product"]);
|
|
1080
1197
|
const message = rest.join(" ").trim();
|
|
1081
1198
|
const held = getDraft();
|
|
1199
|
+
const postRef = value("post");
|
|
1200
|
+
if (postRef) {
|
|
1201
|
+
const target = {
|
|
1202
|
+
passage: value("passage"),
|
|
1203
|
+
preset: value("preset"),
|
|
1204
|
+
shape: value("shape"),
|
|
1205
|
+
region: has("hook") ? "hook" : has("ending") ? "ending" : undefined,
|
|
1206
|
+
product: value("product"),
|
|
1207
|
+
productMode: has("rewrite") ? "rewrite" : "weave"
|
|
1208
|
+
};
|
|
1209
|
+
return askAboutPost(postRef, message, target, json);
|
|
1210
|
+
}
|
|
1082
1211
|
if (!message) {
|
|
1083
1212
|
fail("Say what you want changed.");
|
|
1084
1213
|
note(dim2("ask cut the second half and land it harder"));
|
|
@@ -1104,7 +1233,7 @@ async function ask(args) {
|
|
|
1104
1233
|
id = saved.id;
|
|
1105
1234
|
updateDraft({ id });
|
|
1106
1235
|
}
|
|
1107
|
-
const call = () => request(`/api/posts/${encodeURIComponent(id)}/edit`, { method: "POST", body: { text: message, scope: "whole" }, timeoutMs: 150000 });
|
|
1236
|
+
const call = () => request(`/api/posts/${encodeURIComponent(id)}/edit`, { method: "POST", body: { text: held.content, instruction: message, scope: "whole" }, timeoutMs: 150000 });
|
|
1108
1237
|
const result = json ? await call() : await whileThinking([
|
|
1109
1238
|
{ at: 0, text: "reading the post..." },
|
|
1110
1239
|
{ at: 3, text: "working on it..." },
|
|
@@ -1130,7 +1259,9 @@ async function ask(args) {
|
|
|
1130
1259
|
return 0;
|
|
1131
1260
|
}
|
|
1132
1261
|
async function show(args) {
|
|
1133
|
-
const { json } = parseFlags(args);
|
|
1262
|
+
const { json, rest } = parseFlags(args);
|
|
1263
|
+
if (rest[0])
|
|
1264
|
+
return showPost(rest[0], json);
|
|
1134
1265
|
const held = getDraft();
|
|
1135
1266
|
if (!held?.content) {
|
|
1136
1267
|
note("Nothing in hand.");
|
|
@@ -1184,6 +1315,280 @@ async function unqueue(args) {
|
|
|
1184
1315
|
note(dim2("`drafts` to find it, `queue` to send it back"));
|
|
1185
1316
|
return 0;
|
|
1186
1317
|
}
|
|
1318
|
+
async function postFromRef(refArg, usage) {
|
|
1319
|
+
const ref = resolveRef(refArg);
|
|
1320
|
+
if (!ref) {
|
|
1321
|
+
fail(`No post ${refArg}.`);
|
|
1322
|
+
note(dim2(usage));
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
const post = await fetchPost(ref.id);
|
|
1326
|
+
if (!post) {
|
|
1327
|
+
fail("That post could not be found.");
|
|
1328
|
+
note(dim2(`\`${CMD} drafts\` lists them with their ids`));
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
return post;
|
|
1332
|
+
}
|
|
1333
|
+
var PRESETS = {
|
|
1334
|
+
selection: ["sharpen", "shorter", "punchier", "simpler", "example", "deslop", "concrete", "warmer", "list"],
|
|
1335
|
+
whole: ["tighten_all", "as_list", "as_story", "fix_spacing"],
|
|
1336
|
+
region: ["rewrite_hook", "rewrite_ending", "ending_question"]
|
|
1337
|
+
};
|
|
1338
|
+
var SHAPE_NAMES = [
|
|
1339
|
+
"story",
|
|
1340
|
+
"opinion",
|
|
1341
|
+
"listicle",
|
|
1342
|
+
"contrarian",
|
|
1343
|
+
"experience",
|
|
1344
|
+
"howto",
|
|
1345
|
+
"lessons",
|
|
1346
|
+
"casestudy",
|
|
1347
|
+
"framework",
|
|
1348
|
+
"question",
|
|
1349
|
+
"announcement",
|
|
1350
|
+
"prediction",
|
|
1351
|
+
"beliefbreak"
|
|
1352
|
+
];
|
|
1353
|
+
function askBody(content, message, t) {
|
|
1354
|
+
const preset = t.preset;
|
|
1355
|
+
if (preset && ![...PRESETS.selection, ...PRESETS.whole, ...PRESETS.region].includes(preset)) {
|
|
1356
|
+
return `There is no preset called "${preset}". Presets: ${[...PRESETS.selection, ...PRESETS.whole, ...PRESETS.region].join(", ")}`;
|
|
1357
|
+
}
|
|
1358
|
+
if (t.productId) {
|
|
1359
|
+
return { text: content, productId: t.productId, productMode: t.productMode ?? "weave" };
|
|
1360
|
+
}
|
|
1361
|
+
if (t.shape) {
|
|
1362
|
+
if (!SHAPE_NAMES.includes(t.shape))
|
|
1363
|
+
return `There is no shape called "${t.shape}". Shapes: ${SHAPE_NAMES.join(", ")}`;
|
|
1364
|
+
return { text: content, format: t.shape };
|
|
1365
|
+
}
|
|
1366
|
+
if (t.passage !== undefined) {
|
|
1367
|
+
const at = content.indexOf(t.passage);
|
|
1368
|
+
if (!t.passage.trim() || at < 0)
|
|
1369
|
+
return "That passage is not in the post. Copy it exactly, from `show`.";
|
|
1370
|
+
if (preset && !PRESETS.selection.includes(preset)) {
|
|
1371
|
+
return `"${preset}" works on the whole post or its hook or ending, not on a passage. On a passage: ${PRESETS.selection.join(", ")}`;
|
|
1372
|
+
}
|
|
1373
|
+
if (!preset && !message)
|
|
1374
|
+
return "Say what to change in that passage, or name a preset.";
|
|
1375
|
+
return {
|
|
1376
|
+
text: content,
|
|
1377
|
+
scope: "selection",
|
|
1378
|
+
fragment: t.passage,
|
|
1379
|
+
selStart: at,
|
|
1380
|
+
selEnd: at + t.passage.length,
|
|
1381
|
+
...preset ? { preset } : { instruction: message }
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
if (t.region) {
|
|
1385
|
+
if (preset)
|
|
1386
|
+
return "Name a hook or ending preset on its own: --preset rewrite_hook, rewrite_ending or ending_question.";
|
|
1387
|
+
if (!message)
|
|
1388
|
+
return `Say what to change in the ${t.region}.`;
|
|
1389
|
+
return { text: content, scope: "region", region: t.region, instruction: message };
|
|
1390
|
+
}
|
|
1391
|
+
if (preset) {
|
|
1392
|
+
if (PRESETS.selection.includes(preset))
|
|
1393
|
+
return `"${preset}" works on a passage: add --passage "the exact words".`;
|
|
1394
|
+
return { text: content, preset };
|
|
1395
|
+
}
|
|
1396
|
+
if (!message)
|
|
1397
|
+
return "Say what you want changed.";
|
|
1398
|
+
return { text: content, instruction: message, scope: "whole" };
|
|
1399
|
+
}
|
|
1400
|
+
async function askAboutPost(refArg, message, target, json) {
|
|
1401
|
+
const post = await postFromRef(refArg, `${CMD} ask --post <post-id> "what to change"`);
|
|
1402
|
+
if (!post)
|
|
1403
|
+
return 1;
|
|
1404
|
+
if (post.status === "published") {
|
|
1405
|
+
fail("That post is already live on LinkedIn, so it cannot be changed here.");
|
|
1406
|
+
return 1;
|
|
1407
|
+
}
|
|
1408
|
+
if (target.product) {
|
|
1409
|
+
const { products } = await request("/api/products");
|
|
1410
|
+
const want = target.product.trim().toLowerCase();
|
|
1411
|
+
const hit = products.find((p) => p.name.trim().toLowerCase() === want) ?? products.find((p) => p.name.toLowerCase().includes(want));
|
|
1412
|
+
if (!hit) {
|
|
1413
|
+
fail(products.length ? `There is no product called "${target.product}". Products: ${products.map((p) => p.name).join(", ")}` : "There are no products yet. Add one in Settings, under Products.");
|
|
1414
|
+
return 1;
|
|
1415
|
+
}
|
|
1416
|
+
target = { ...target, productId: hit._id };
|
|
1417
|
+
}
|
|
1418
|
+
const body = askBody(post.content, message, target);
|
|
1419
|
+
if (typeof body === "string") {
|
|
1420
|
+
fail(body);
|
|
1421
|
+
return 1;
|
|
1422
|
+
}
|
|
1423
|
+
const call = () => request(`/api/posts/${encodeURIComponent(post._id)}/edit`, { method: "POST", body, timeoutMs: 150000 });
|
|
1424
|
+
const result = json ? await call() : await whileThinking([
|
|
1425
|
+
{ at: 0, text: "reading the post..." },
|
|
1426
|
+
{ at: 3, text: "working on it..." },
|
|
1427
|
+
{ at: 14, text: "still going..." }
|
|
1428
|
+
], call);
|
|
1429
|
+
let stored = post.content;
|
|
1430
|
+
if (result.changed && result.post) {
|
|
1431
|
+
const after = await fetchPost(post._id);
|
|
1432
|
+
stored = after?.content ?? stored;
|
|
1433
|
+
if (stored !== result.post) {
|
|
1434
|
+
await request(`/api/posts/${encodeURIComponent(post._id)}/draft`, {
|
|
1435
|
+
method: "POST",
|
|
1436
|
+
body: { text: result.post, previous: stored, origin: "suggestion" }
|
|
1437
|
+
});
|
|
1438
|
+
stored = result.post;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (json) {
|
|
1442
|
+
out(JSON.stringify({ id: post._id, changed: !!result.changed, post: stored, passage: result.fragment ?? null, reply: result.reply ?? null }, null, 2));
|
|
1443
|
+
return 0;
|
|
1444
|
+
}
|
|
1445
|
+
out();
|
|
1446
|
+
out(stored);
|
|
1447
|
+
out();
|
|
1448
|
+
if (result.changed && result.fragment && target.passage !== undefined) {
|
|
1449
|
+
note(dim2(`was: ${target.passage}`));
|
|
1450
|
+
note(dim2(`now: ${result.fragment}`));
|
|
1451
|
+
}
|
|
1452
|
+
if (result.reply)
|
|
1453
|
+
note(dim2(result.reply));
|
|
1454
|
+
note(dim2(result.changed ? `Saved. ${post._id}` : `No change. ${post._id}`));
|
|
1455
|
+
return 0;
|
|
1456
|
+
}
|
|
1457
|
+
async function showPost(refArg, json) {
|
|
1458
|
+
const post = await postFromRef(refArg, `${CMD} show <post-id>`);
|
|
1459
|
+
if (!post)
|
|
1460
|
+
return 1;
|
|
1461
|
+
const series = await listSeries();
|
|
1462
|
+
if (json) {
|
|
1463
|
+
out(JSON.stringify(postForJson(post, series), null, 2));
|
|
1464
|
+
return 0;
|
|
1465
|
+
}
|
|
1466
|
+
const when = (ms) => new Date(ms).toLocaleString(undefined, { weekday: "short", day: "numeric", month: "short", hour: "numeric", minute: "2-digit" });
|
|
1467
|
+
const seriesName = post.campaignId ? series.find((s) => s._id === post.campaignId)?.name : undefined;
|
|
1468
|
+
const media = describeMedia(post);
|
|
1469
|
+
const url = linkedinUrl(post);
|
|
1470
|
+
out();
|
|
1471
|
+
out(post.content);
|
|
1472
|
+
out();
|
|
1473
|
+
const facts = [
|
|
1474
|
+
["title", post.title || "untitled"],
|
|
1475
|
+
["status", post.status],
|
|
1476
|
+
["series", seriesName],
|
|
1477
|
+
["attached", media ?? undefined],
|
|
1478
|
+
["first comment", post.firstComment ? post.firstComment.replace(/\s+/g, " ").slice(0, 80) : undefined],
|
|
1479
|
+
["scheduled", post.scheduledFor && post.status !== "published" ? when(post.scheduledFor) : undefined],
|
|
1480
|
+
["published", post.publishedAt ? when(post.publishedAt) : undefined],
|
|
1481
|
+
["on linkedin", url ?? undefined],
|
|
1482
|
+
[
|
|
1483
|
+
"reactions",
|
|
1484
|
+
post.linkedinLikes !== undefined || post.linkedinComments !== undefined ? `${post.linkedinLikes ?? 0} likes, ${post.linkedinComments ?? 0} comments` : undefined
|
|
1485
|
+
],
|
|
1486
|
+
["id", post._id]
|
|
1487
|
+
];
|
|
1488
|
+
for (const [label, valueText] of facts) {
|
|
1489
|
+
if (valueText)
|
|
1490
|
+
note(`${dim2(label.padEnd(14))}${valueText}`);
|
|
1491
|
+
}
|
|
1492
|
+
return 0;
|
|
1493
|
+
}
|
|
1494
|
+
async function edit(args) {
|
|
1495
|
+
const { json, rest, value } = parseFlags(args, ["text", "file", "title", "first-comment", "series", "status"]);
|
|
1496
|
+
const refArg = rest[0];
|
|
1497
|
+
if (!refArg) {
|
|
1498
|
+
fail("Edit which post?");
|
|
1499
|
+
note(dim2(`${CMD} edit <post-id> --file post.txt`));
|
|
1500
|
+
note(dim2(`${CMD} edit <post-id> --title "A title"`));
|
|
1501
|
+
return 1;
|
|
1502
|
+
}
|
|
1503
|
+
const title = value("title");
|
|
1504
|
+
const firstComment = value("first-comment");
|
|
1505
|
+
const seriesName = value("series");
|
|
1506
|
+
const status = value("status");
|
|
1507
|
+
const file = value("file");
|
|
1508
|
+
let text = value("text");
|
|
1509
|
+
if (file)
|
|
1510
|
+
text = await readFile2(file, "utf8");
|
|
1511
|
+
const fieldsGiven = title !== undefined || firstComment !== undefined || seriesName !== undefined || status !== undefined;
|
|
1512
|
+
if (text === undefined && !fieldsGiven) {
|
|
1513
|
+
text = await readStdin() ?? undefined;
|
|
1514
|
+
}
|
|
1515
|
+
if (text === undefined && !fieldsGiven) {
|
|
1516
|
+
fail("Nothing to change.");
|
|
1517
|
+
note(dim2("--file, --text or piped text for the post, --title, --first-comment, --series, --status"));
|
|
1518
|
+
return 1;
|
|
1519
|
+
}
|
|
1520
|
+
if (status !== undefined && status !== "ready" && status !== "draft") {
|
|
1521
|
+
fail("--status is ready or draft.");
|
|
1522
|
+
note(dim2("Scheduling is `queue`, and taking it out of the queue is `unqueue`."));
|
|
1523
|
+
return 1;
|
|
1524
|
+
}
|
|
1525
|
+
const post = await postFromRef(refArg, `${CMD} edit <post-id> --file post.txt`);
|
|
1526
|
+
if (!post)
|
|
1527
|
+
return 1;
|
|
1528
|
+
const changed = [];
|
|
1529
|
+
let content = post.content;
|
|
1530
|
+
if (text !== undefined) {
|
|
1531
|
+
const next = text.replace(/\s+$/, "");
|
|
1532
|
+
if (!next.trim()) {
|
|
1533
|
+
fail("That text is empty.");
|
|
1534
|
+
return 1;
|
|
1535
|
+
}
|
|
1536
|
+
if (post.status === "published") {
|
|
1537
|
+
fail("That post is already live on LinkedIn, so its text cannot be changed here.");
|
|
1538
|
+
return 1;
|
|
1539
|
+
}
|
|
1540
|
+
if (next !== post.content) {
|
|
1541
|
+
await request(`/api/posts/${encodeURIComponent(post._id)}/draft`, {
|
|
1542
|
+
method: "POST",
|
|
1543
|
+
body: { text: next, previous: post.content, origin: "stream" }
|
|
1544
|
+
});
|
|
1545
|
+
content = next;
|
|
1546
|
+
changed.push("text");
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
if (title !== undefined || firstComment !== undefined) {
|
|
1550
|
+
await request("/api/posts", {
|
|
1551
|
+
method: "POST",
|
|
1552
|
+
body: {
|
|
1553
|
+
id: post._id,
|
|
1554
|
+
content,
|
|
1555
|
+
...title !== undefined ? { title } : {},
|
|
1556
|
+
...firstComment !== undefined ? { firstComment } : {}
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
if (title !== undefined)
|
|
1560
|
+
changed.push("title");
|
|
1561
|
+
if (firstComment !== undefined)
|
|
1562
|
+
changed.push("first comment");
|
|
1563
|
+
}
|
|
1564
|
+
if (seriesName !== undefined) {
|
|
1565
|
+
let toCampaignId = null;
|
|
1566
|
+
if (seriesName.trim().toLowerCase() !== "none" && seriesName.trim()) {
|
|
1567
|
+
const all = await listSeries();
|
|
1568
|
+
const target = matchSeries(all, seriesName);
|
|
1569
|
+
if (!target) {
|
|
1570
|
+
fail(`No series called "${seriesName}".`);
|
|
1571
|
+
if (all.length)
|
|
1572
|
+
note(dim2(`Yours: ${all.map((x) => x.name).join(", ")}`));
|
|
1573
|
+
return 1;
|
|
1574
|
+
}
|
|
1575
|
+
toCampaignId = target._id;
|
|
1576
|
+
}
|
|
1577
|
+
await request("/api/campaigns/move", { method: "POST", body: { postId: post._id, toCampaignId } });
|
|
1578
|
+
changed.push(toCampaignId ? "series" : "series (taken out)");
|
|
1579
|
+
}
|
|
1580
|
+
if (status !== undefined) {
|
|
1581
|
+
await request(`/api/posts/${encodeURIComponent(post._id)}/ready`, { method: status === "ready" ? "POST" : "DELETE" });
|
|
1582
|
+
changed.push(`status (${status})`);
|
|
1583
|
+
}
|
|
1584
|
+
if (json) {
|
|
1585
|
+
out(JSON.stringify({ id: post._id, changed }, null, 2));
|
|
1586
|
+
return 0;
|
|
1587
|
+
}
|
|
1588
|
+
ok(changed.length ? `Updated the ${changed.join(", ")}. ${dim2(post._id)}` : `Nothing was different. ${dim2(post._id)}`);
|
|
1589
|
+
note(dim2(`\`${CMD} show ${post._id}\` to read it back`));
|
|
1590
|
+
return 0;
|
|
1591
|
+
}
|
|
1187
1592
|
|
|
1188
1593
|
// src/ui.ts
|
|
1189
1594
|
var useColor2 = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
@@ -1340,12 +1745,13 @@ var COMMANDS = {
|
|
|
1340
1745
|
write: {
|
|
1341
1746
|
summary: "draft a post in your voice",
|
|
1342
1747
|
usage: "innernote write [topic] [options]",
|
|
1343
|
-
detail: "Not a prompt box. innernote already holds your voice, your
|
|
1748
|
+
detail: "Not a prompt box. innernote already holds your voice, your topics and what you have published, so give it the subject and let it write. Run it bare and it picks the topic you have covered least.",
|
|
1344
1749
|
options: [
|
|
1345
1750
|
["--thought <text>", "turn a half-formed note into a post, keeping your phrasing"],
|
|
1346
1751
|
["--series <name>", "write into one of your series, so it follows on"],
|
|
1347
1752
|
["--format <name>", "story, listicle, contrarian, ..."],
|
|
1348
|
-
["--save", "keep it as a draft in the app"]
|
|
1753
|
+
["--save", "keep it as a draft in the app"],
|
|
1754
|
+
["--title <text>", "the title it is saved under (with --save); without it, one is written for you"]
|
|
1349
1755
|
],
|
|
1350
1756
|
examples: [
|
|
1351
1757
|
"innernote write",
|
|
@@ -1373,15 +1779,127 @@ var COMMANDS = {
|
|
|
1373
1779
|
]
|
|
1374
1780
|
},
|
|
1375
1781
|
ask: {
|
|
1376
|
-
summary: "talk to the post in your own words",
|
|
1377
|
-
usage: "innernote ask <what you want
|
|
1378
|
-
detail: "The Writer, here. The app's composer is a conversation with the post in front of you, and this is the same assistant: it can change the post, answer a question about it, or say it would rather not.
|
|
1782
|
+
summary: "talk to the post in your own words, or edit one passage of it",
|
|
1783
|
+
usage: "innernote ask [--post <id>] [--passage <text> | --hook | --ending] [--preset <name> | --shape <name> | --product <name>] [what you want]",
|
|
1784
|
+
detail: "The Writer, here. The app's composer is a conversation with the post in front of you, and this is the same assistant: it can change the post, answer a question about it, or say it would rather not. In a session it works on the post in hand. With --post it works on a saved post in one command, and the change is kept in the post's history. --passage is the editor's selection: the exact words to change, and only those change. --hook and --ending are the opening and closing block. --preset is a toolbar edit by name, --shape the format picker.",
|
|
1785
|
+
options: [
|
|
1786
|
+
["--post <id>", "a saved post, by id, instead of the one in hand"],
|
|
1787
|
+
["--passage <text>", "change only these exact words of the post"],
|
|
1788
|
+
["--hook, --ending", "change only the opening, or the closing"],
|
|
1789
|
+
["--preset <name>", "on a passage: sharpen shorter punchier simpler example deslop concrete warmer list. On the post: tighten_all as_list as_story fix_spacing rewrite_hook rewrite_ending ending_question"],
|
|
1790
|
+
["--shape <name>", "story opinion listicle contrarian experience howto lessons casestudy framework question announcement prediction beliefbreak"],
|
|
1791
|
+
["--product <name>", "work one of your products into the post, where it honestly belongs (`context` lists them)"],
|
|
1792
|
+
["--rewrite", "with --product: rewrite the post around it instead"]
|
|
1793
|
+
],
|
|
1379
1794
|
examples: [
|
|
1380
1795
|
"ask cut the second half and land it harder",
|
|
1381
1796
|
"ask this ending is soft, what would you do",
|
|
1382
|
-
|
|
1797
|
+
'innernote ask --post p17abc... "make the opening shorter"',
|
|
1798
|
+
'innernote ask --post p17abc... --passage "we got lucky" "say what the luck actually was"',
|
|
1799
|
+
'innernote ask --post p17abc... --passage "we got lucky" --preset concrete',
|
|
1800
|
+
"innernote ask --post p17abc... --preset ending_question",
|
|
1801
|
+
"innernote ask --post p17abc... --shape story",
|
|
1802
|
+
'innernote ask --post p17abc... --product "innernote"'
|
|
1383
1803
|
]
|
|
1384
1804
|
},
|
|
1805
|
+
edit: {
|
|
1806
|
+
summary: "change a saved post: its text, title, first comment, series or status",
|
|
1807
|
+
usage: "innernote edit <id> [--file <path> | --text <text>] [--title <text>] [--first-comment <text>] [--series <name>] [--status ready|draft]",
|
|
1808
|
+
detail: "Puts your own words into a saved post. New text comes from --file, --text or a pipe, and is saved the way the app saves a draft, so the old text is one Undo away in the post's history. A post already live on LinkedIn keeps its text. The id comes from `drafts --json` or `show`.",
|
|
1809
|
+
options: [
|
|
1810
|
+
["--file <path>", "the new text of the post, from a file"],
|
|
1811
|
+
["--text <text>", "the new text of the post, inline"],
|
|
1812
|
+
["--title <text>", "the post's title in the app"],
|
|
1813
|
+
["--first-comment <text>", "the comment posted under it when it goes live"],
|
|
1814
|
+
["--series <name>", "file it into a series, or `none` to take it out"],
|
|
1815
|
+
["--status <ready|draft>", "mark it ready, or send it back to draft"]
|
|
1816
|
+
],
|
|
1817
|
+
examples: [
|
|
1818
|
+
"innernote edit p17abc... --file post.txt",
|
|
1819
|
+
"pbpaste | innernote edit p17abc...",
|
|
1820
|
+
'innernote edit p17abc... --title "The sales call" --first-comment "link: innernote.space"',
|
|
1821
|
+
'innernote edit p17abc... --series "Build in Public" --status ready'
|
|
1822
|
+
]
|
|
1823
|
+
},
|
|
1824
|
+
captures: {
|
|
1825
|
+
summary: "your Capture page: what you keep to write from later",
|
|
1826
|
+
usage: "innernote captures [<id>] | captures add <link or text> [--note <why>] | captures delete <id>",
|
|
1827
|
+
detail: "The same list as Capture in the app. With no argument it lists what you kept, newest first, with ids. With an id it prints that capture in full. `add` keeps a link (a LinkedIn post, an article, a page) or a thought in your own words: a link is read once, through innernote's cache, and kept with its text. `repurpose --capture <id>` writes a post from one.",
|
|
1828
|
+
options: [
|
|
1829
|
+
["--note <why>", "with add: why you kept it, in a line"],
|
|
1830
|
+
["--limit <n>", "how many to list (default 30)"]
|
|
1831
|
+
],
|
|
1832
|
+
examples: [
|
|
1833
|
+
"innernote captures",
|
|
1834
|
+
'innernote captures add https://www.linkedin.com/posts/... --note "the pricing argument"',
|
|
1835
|
+
"innernote captures j57abc..."
|
|
1836
|
+
]
|
|
1837
|
+
},
|
|
1838
|
+
repurpose: {
|
|
1839
|
+
summary: "write your own post from a link, a capture or some text",
|
|
1840
|
+
usage: "innernote repurpose <link or text> | repurpose --capture <id> [--borrow <what>] [--instruction <text>] [--shape <name>] [--save]",
|
|
1841
|
+
detail: "Turn into a post, from here. The source is read, and the post is written in your voice from what you choose to borrow, then checked so it reads as yours and not theirs. --borrow idea takes their point and uses your proof, structure takes only how it is built, spark only reacts to it, and faithful relays what it reports in your words, with credit. A profile link yields its recent posts: the first is used, --pick chooses another.",
|
|
1842
|
+
options: [
|
|
1843
|
+
["--capture <id>", "a capture from `captures`, instead of a link"],
|
|
1844
|
+
["--borrow <what>", "idea, structure, spark or faithful"],
|
|
1845
|
+
["--instruction <text>", "anything else it should do, in your words"],
|
|
1846
|
+
["--shape <name>", "story, listicle, howto, lessons and the rest, as in `ask --shape`"],
|
|
1847
|
+
["--pick <n>", "which of a profile's posts to write from"],
|
|
1848
|
+
["--save", "keep it as a draft"],
|
|
1849
|
+
["--series <name>", "with --save: file it into a series"],
|
|
1850
|
+
["--title <text>", "with --save: its title (default: one is written for you)"]
|
|
1851
|
+
],
|
|
1852
|
+
examples: [
|
|
1853
|
+
"innernote repurpose https://www.linkedin.com/posts/... --borrow structure",
|
|
1854
|
+
"innernote repurpose --capture j57abc... --borrow idea --save",
|
|
1855
|
+
"pbpaste | innernote repurpose --borrow spark"
|
|
1856
|
+
]
|
|
1857
|
+
},
|
|
1858
|
+
radar: {
|
|
1859
|
+
summary: "recent posts from the creators you watch",
|
|
1860
|
+
usage: "innernote radar [--limit <n>] [--full]",
|
|
1861
|
+
detail: "The same feed as Radar in the app, newest first, with each post's link. Who you watch is set on the Radar page. `repurpose <link>` writes your own post from one.",
|
|
1862
|
+
options: [
|
|
1863
|
+
["--limit <n>", "how many posts (default 15)"],
|
|
1864
|
+
["--full", "print each post in full, not one line"]
|
|
1865
|
+
],
|
|
1866
|
+
examples: ["innernote radar", "innernote radar --full --limit 5", "innernote radar --json"]
|
|
1867
|
+
},
|
|
1868
|
+
history: {
|
|
1869
|
+
summary: "every earlier version of a post, and putting one back",
|
|
1870
|
+
usage: "innernote history <id> [--show <n>] [--restore <n>]",
|
|
1871
|
+
detail: "Every change to a post's words is kept as a version first: yours, the assistant's, an edit from here. This lists them, newest first. --restore puts one back, and the text it replaces is kept as the newest version, so a restore is itself one step from undone.",
|
|
1872
|
+
options: [
|
|
1873
|
+
["--show <n>", "print version n in full"],
|
|
1874
|
+
["--restore <n>", "put version n back"]
|
|
1875
|
+
],
|
|
1876
|
+
examples: ["innernote history p17abc...", "innernote history p17abc... --show 3", "innernote history p17abc... --restore 3"]
|
|
1877
|
+
},
|
|
1878
|
+
attach: {
|
|
1879
|
+
summary: "put an image, images, a PDF or a video on a saved post",
|
|
1880
|
+
usage: "innernote attach <id> <file...> [--title <text>] | innernote attach <id> --remove",
|
|
1881
|
+
detail: "Uploads straight to innernote's storage on a signed link, the way the app does, and attaches it to the post. Several images become a grid. A PDF shows on LinkedIn as a carousel people swipe through, under its title. A post carries one kind of attachment, so attaching replaces what was there.",
|
|
1882
|
+
options: [
|
|
1883
|
+
["--title <text>", "the title of a PDF or a video (default: the file name)"],
|
|
1884
|
+
["--remove", "take the attachment off"]
|
|
1885
|
+
],
|
|
1886
|
+
examples: [
|
|
1887
|
+
"innernote attach p17abc... picture.png",
|
|
1888
|
+
'innernote attach p17abc... slides.pdf --title "How we price"',
|
|
1889
|
+
"innernote attach p17abc... --remove"
|
|
1890
|
+
]
|
|
1891
|
+
},
|
|
1892
|
+
delete: {
|
|
1893
|
+
summary: "move a post to the trash",
|
|
1894
|
+
usage: "innernote delete <id>",
|
|
1895
|
+
detail: "The same as Delete in the Posts list: the post is kept, and `restore` brings it back. It does not take anything down from LinkedIn.",
|
|
1896
|
+
examples: ["innernote delete p17abc...", "innernote restore p17abc..."]
|
|
1897
|
+
},
|
|
1898
|
+
restore: {
|
|
1899
|
+
summary: "bring a post back out of the trash",
|
|
1900
|
+
usage: "innernote restore <id>",
|
|
1901
|
+
examples: ["innernote restore p17abc..."]
|
|
1902
|
+
},
|
|
1385
1903
|
save: {
|
|
1386
1904
|
summary: "keep the draft you are looking at",
|
|
1387
1905
|
usage: "innernote save",
|
|
@@ -1389,10 +1907,10 @@ var COMMANDS = {
|
|
|
1389
1907
|
examples: ['write "hiring your first engineer"', "shape shorter", "save"]
|
|
1390
1908
|
},
|
|
1391
1909
|
show: {
|
|
1392
|
-
summary: "see
|
|
1393
|
-
usage: "innernote show",
|
|
1394
|
-
detail: "
|
|
1395
|
-
examples: ["show"]
|
|
1910
|
+
summary: "see a post, with everything the app knows about it",
|
|
1911
|
+
usage: "innernote show [id]",
|
|
1912
|
+
detail: "With an id: the post's text on stdout, and its title, status, series, attachment, first comment, schedule, LinkedIn link and reactions on stderr, all of it in --json. Without one, in a session: the post you are holding, and whether it is saved.",
|
|
1913
|
+
examples: ["innernote show p17abc...", "innernote show p17abc... --json", "show"]
|
|
1396
1914
|
},
|
|
1397
1915
|
drop: {
|
|
1398
1916
|
summary: "put down whatever you are holding",
|
|
@@ -1407,9 +1925,10 @@ var COMMANDS = {
|
|
|
1407
1925
|
},
|
|
1408
1926
|
drafts: {
|
|
1409
1927
|
summary: "your posts, numbered",
|
|
1410
|
-
usage: "innernote drafts [status]",
|
|
1411
|
-
detail: "Numbered so you can pick one with `open 3` instead of copying a thirty-two character id. The numbers refer to the list you are looking at and are replaced by the next one.",
|
|
1412
|
-
|
|
1928
|
+
usage: "innernote drafts [status] [--series <name>]",
|
|
1929
|
+
detail: "Numbered so you can pick one with `open 3` instead of copying a thirty-two character id. The numbers refer to the list you are looking at and are replaced by the next one. --series lists one series in its own order. Each --json row also carries the series name and, once published, the LinkedIn link.",
|
|
1930
|
+
options: [["--series <name>", "only the posts in this series, in its order"]],
|
|
1931
|
+
examples: ["drafts", "drafts ready", 'drafts --series "Growing innernote"', "drafts --json | jq -r '.[]._id'"]
|
|
1413
1932
|
},
|
|
1414
1933
|
open: {
|
|
1415
1934
|
summary: "pull a post out of the list and work on it",
|
|
@@ -1435,15 +1954,16 @@ var COMMANDS = {
|
|
|
1435
1954
|
examples: ["innernote skill", "innernote skill --install"]
|
|
1436
1955
|
},
|
|
1437
1956
|
context: {
|
|
1438
|
-
summary: "what you post about: your
|
|
1957
|
+
summary: "what you post about: your topics, your series and your products",
|
|
1439
1958
|
usage: "innernote context",
|
|
1440
|
-
detail: "Your standing context:
|
|
1959
|
+
detail: "Your standing context: your topics with their weights, every running series with what it is about and what it is for, and your products. This is the thing to read before deciding what to write, and the thing an agent should read first for the same reason. --json prints the raw payload for scripts and models.",
|
|
1441
1960
|
examples: ["innernote context", "innernote context --json"]
|
|
1442
1961
|
},
|
|
1443
1962
|
queue: {
|
|
1444
1963
|
summary: "schedule a post into your next open slot",
|
|
1445
|
-
usage: "innernote queue <post-id>",
|
|
1964
|
+
usage: "innernote queue <post-id> [--at <time>]",
|
|
1446
1965
|
detail: "A queued post GOES LIVE on LinkedIn on its own at your next cadence slot. Nobody presses anything again. Needs a connection you allowed to publish: tick that box in Settings when you generate the code. Takes a number from the last list, or a full id. It never assumes the draft in hand: this is the one command that makes you say which post.",
|
|
1966
|
+
options: [["--at <time>", `a time you pick, like "2026-10-12 12:31" (this machine's timezone), instead of the next open slot`]],
|
|
1447
1967
|
examples: ["innernote drafts", "innernote queue 3", "innernote queue p17a1yk936ywnan10643efjtc58dhkah"]
|
|
1448
1968
|
}
|
|
1449
1969
|
};
|
|
@@ -1557,7 +2077,7 @@ function suggest(state) {
|
|
|
1557
2077
|
// package.json
|
|
1558
2078
|
var package_default = {
|
|
1559
2079
|
name: "innernote",
|
|
1560
|
-
version: "0.
|
|
2080
|
+
version: "0.5.0",
|
|
1561
2081
|
description: "Write LinkedIn posts in your voice, from the terminal.",
|
|
1562
2082
|
keywords: [
|
|
1563
2083
|
"linkedin",
|
|
@@ -1662,7 +2182,7 @@ async function home(inSession = false) {
|
|
|
1662
2182
|
}
|
|
1663
2183
|
|
|
1664
2184
|
// src/commands/skill.ts
|
|
1665
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile as
|
|
2185
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1666
2186
|
import { homedir as homedir2 } from "node:os";
|
|
1667
2187
|
import { dirname, join as join2 } from "node:path";
|
|
1668
2188
|
import { fileURLToPath } from "node:url";
|
|
@@ -1670,7 +2190,7 @@ async function readSkill() {
|
|
|
1670
2190
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1671
2191
|
for (const rel of ["../../skills/innernote-cli/SKILL.md", "../skills/innernote-cli/SKILL.md"]) {
|
|
1672
2192
|
try {
|
|
1673
|
-
return await
|
|
2193
|
+
return await readFile3(join2(here, rel), "utf8");
|
|
1674
2194
|
} catch {}
|
|
1675
2195
|
}
|
|
1676
2196
|
return null;
|
|
@@ -1697,16 +2217,474 @@ async function skill(args) {
|
|
|
1697
2217
|
return 0;
|
|
1698
2218
|
}
|
|
1699
2219
|
|
|
2220
|
+
// src/commands/post.ts
|
|
2221
|
+
import { readFile as readFile4, stat } from "node:fs/promises";
|
|
2222
|
+
import { basename, extname } from "node:path";
|
|
2223
|
+
var TYPES = {
|
|
2224
|
+
".jpg": "image/jpeg",
|
|
2225
|
+
".jpeg": "image/jpeg",
|
|
2226
|
+
".png": "image/png",
|
|
2227
|
+
".gif": "image/gif",
|
|
2228
|
+
".webp": "image/webp",
|
|
2229
|
+
".pdf": "application/pdf",
|
|
2230
|
+
".mp4": "video/mp4",
|
|
2231
|
+
".mov": "video/quicktime"
|
|
2232
|
+
};
|
|
2233
|
+
function imageSize(buf) {
|
|
2234
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2235
|
+
if (buf.length > 24 && buf[0] === 137 && buf[1] === 80) {
|
|
2236
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
2237
|
+
}
|
|
2238
|
+
if (buf.length > 10 && buf[0] === 71 && buf[1] === 73 && buf[2] === 70) {
|
|
2239
|
+
return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
|
|
2240
|
+
}
|
|
2241
|
+
if (buf.length > 30 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
|
|
2242
|
+
const kind = String.fromCharCode(buf[12], buf[13], buf[14], buf[15]);
|
|
2243
|
+
if (kind === "VP8X") {
|
|
2244
|
+
return { width: 1 + (buf[24] | buf[25] << 8 | buf[26] << 16), height: 1 + (buf[27] | buf[28] << 8 | buf[29] << 16) };
|
|
2245
|
+
}
|
|
2246
|
+
if (kind === "VP8 ")
|
|
2247
|
+
return { width: view.getUint16(26, true) & 16383, height: view.getUint16(28, true) & 16383 };
|
|
2248
|
+
if (kind === "VP8L") {
|
|
2249
|
+
const b = view.getUint32(21, true);
|
|
2250
|
+
return { width: (b & 16383) + 1, height: (b >> 14 & 16383) + 1 };
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (buf.length > 4 && buf[0] === 255 && buf[1] === 216) {
|
|
2254
|
+
let i = 2;
|
|
2255
|
+
while (i + 9 < buf.length) {
|
|
2256
|
+
if (buf[i] !== 255)
|
|
2257
|
+
return;
|
|
2258
|
+
const marker = buf[i + 1];
|
|
2259
|
+
const len = view.getUint16(i + 2);
|
|
2260
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
2261
|
+
return { width: view.getUint16(i + 7), height: view.getUint16(i + 5) };
|
|
2262
|
+
}
|
|
2263
|
+
i += 2 + len;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
async function upload(path) {
|
|
2269
|
+
const contentType = TYPES[extname(path).toLowerCase()];
|
|
2270
|
+
if (!contentType) {
|
|
2271
|
+
throw new Error(`Can't attach ${basename(path)}. Images (jpg, png, gif, webp), a PDF, or a video (mp4, mov).`);
|
|
2272
|
+
}
|
|
2273
|
+
const size = (await stat(path)).size;
|
|
2274
|
+
const bytes = await readFile4(path);
|
|
2275
|
+
const signed = await request("/api/uploads/presign", {
|
|
2276
|
+
method: "POST",
|
|
2277
|
+
body: { filename: basename(path), contentType, size }
|
|
2278
|
+
});
|
|
2279
|
+
const put = await fetch(signed.uploadUrl, { method: "PUT", headers: { "Content-Type": contentType }, body: bytes });
|
|
2280
|
+
if (!put.ok)
|
|
2281
|
+
throw new Error(`The upload of ${basename(path)} failed (${put.status}).`);
|
|
2282
|
+
const dims = contentType.startsWith("image/") ? imageSize(new Uint8Array(bytes)) : undefined;
|
|
2283
|
+
return { asset: { url: signed.publicUrl, name: basename(path), ...dims ?? {} }, contentType };
|
|
2284
|
+
}
|
|
2285
|
+
async function attach(args) {
|
|
2286
|
+
const { json, rest, value, has } = parseFlags(args, ["title"]);
|
|
2287
|
+
const [refArg, ...files] = rest;
|
|
2288
|
+
if (!refArg || !files.length && !has("remove")) {
|
|
2289
|
+
fail("Attach what to which post?");
|
|
2290
|
+
note(dim2(`${CMD} attach <post-id> picture.png`));
|
|
2291
|
+
note(dim2(`${CMD} attach <post-id> slides.pdf --title "How we price"`));
|
|
2292
|
+
return 1;
|
|
2293
|
+
}
|
|
2294
|
+
const ref = resolveRef(refArg);
|
|
2295
|
+
const post = ref ? await fetchPost(ref.id) : null;
|
|
2296
|
+
if (!post) {
|
|
2297
|
+
fail("That post could not be found.");
|
|
2298
|
+
note(dim2(`\`${CMD} drafts\` lists them with their ids`));
|
|
2299
|
+
return 1;
|
|
2300
|
+
}
|
|
2301
|
+
if (post.status === "published") {
|
|
2302
|
+
fail("That post is already live on LinkedIn, so its attachment cannot change here.");
|
|
2303
|
+
return 1;
|
|
2304
|
+
}
|
|
2305
|
+
let media;
|
|
2306
|
+
if (has("remove")) {
|
|
2307
|
+
media = { kind: "text" };
|
|
2308
|
+
} else {
|
|
2309
|
+
const uploaded = [];
|
|
2310
|
+
for (const f of files)
|
|
2311
|
+
uploaded.push(await upload(f));
|
|
2312
|
+
const kinds = new Set(uploaded.map((u) => u.contentType.split("/")[0]));
|
|
2313
|
+
const title = value("title") ?? basename(files[0], extname(files[0]));
|
|
2314
|
+
if (uploaded.every((u) => u.contentType.startsWith("image/"))) {
|
|
2315
|
+
if (uploaded.length > 20) {
|
|
2316
|
+
fail("A post takes at most 20 images.");
|
|
2317
|
+
return 1;
|
|
2318
|
+
}
|
|
2319
|
+
media = uploaded.length === 1 ? { kind: "image", asset: uploaded[0].asset } : { kind: "multiImage", assets: uploaded.map((u) => u.asset) };
|
|
2320
|
+
} else if (uploaded.length === 1 && uploaded[0].contentType === "application/pdf") {
|
|
2321
|
+
media = { kind: "document", asset: uploaded[0].asset, title };
|
|
2322
|
+
} else if (uploaded.length === 1 && kinds.has("video")) {
|
|
2323
|
+
media = { kind: "video", asset: uploaded[0].asset, title };
|
|
2324
|
+
} else {
|
|
2325
|
+
fail("A post carries one kind of attachment: images, or one PDF, or one video.");
|
|
2326
|
+
return 1;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
await request("/api/posts", { method: "POST", body: { id: post._id, content: post.content, media } });
|
|
2330
|
+
if (json) {
|
|
2331
|
+
out(JSON.stringify({ id: post._id, media }, null, 2));
|
|
2332
|
+
return 0;
|
|
2333
|
+
}
|
|
2334
|
+
ok(media.kind === "text" ? `Took the attachment off. ${dim2(post._id)}` : `Attached. ${dim2(post._id)}`);
|
|
2335
|
+
note(dim2(`\`${CMD} show ${post._id}\` to check it`));
|
|
2336
|
+
return 0;
|
|
2337
|
+
}
|
|
2338
|
+
async function deletePost(args) {
|
|
2339
|
+
const { json, rest } = parseFlags(args);
|
|
2340
|
+
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
2341
|
+
if (!ref) {
|
|
2342
|
+
fail("Delete which post?");
|
|
2343
|
+
note(dim2(`${CMD} delete <post-id>`));
|
|
2344
|
+
return 1;
|
|
2345
|
+
}
|
|
2346
|
+
await request(`/api/posts/${encodeURIComponent(ref.id)}`, { method: "DELETE" });
|
|
2347
|
+
if (json) {
|
|
2348
|
+
out(JSON.stringify({ id: ref.id, deleted: true }, null, 2));
|
|
2349
|
+
return 0;
|
|
2350
|
+
}
|
|
2351
|
+
ok(`Moved to the trash. ${dim2(ref.id)}`);
|
|
2352
|
+
note(dim2(`\`${CMD} restore ${ref.id}\` brings it back`));
|
|
2353
|
+
return 0;
|
|
2354
|
+
}
|
|
2355
|
+
async function restorePost(args) {
|
|
2356
|
+
const { json, rest } = parseFlags(args);
|
|
2357
|
+
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
2358
|
+
if (!ref) {
|
|
2359
|
+
fail("Restore which post?");
|
|
2360
|
+
note(dim2(`${CMD} restore <post-id>`));
|
|
2361
|
+
return 1;
|
|
2362
|
+
}
|
|
2363
|
+
await request(`/api/posts/${encodeURIComponent(ref.id)}/restore`, { method: "POST" });
|
|
2364
|
+
if (json) {
|
|
2365
|
+
out(JSON.stringify({ id: ref.id, restored: true }, null, 2));
|
|
2366
|
+
return 0;
|
|
2367
|
+
}
|
|
2368
|
+
ok(`Restored. It is back in your posts. ${dim2(ref.id)}`);
|
|
2369
|
+
return 0;
|
|
2370
|
+
}
|
|
2371
|
+
async function history(args) {
|
|
2372
|
+
const { json, rest, value } = parseFlags(args, ["show", "restore"]);
|
|
2373
|
+
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
2374
|
+
if (!ref) {
|
|
2375
|
+
fail("Whose history?");
|
|
2376
|
+
note(dim2(`${CMD} history <post-id>`));
|
|
2377
|
+
return 1;
|
|
2378
|
+
}
|
|
2379
|
+
const post = await fetchPost(ref.id);
|
|
2380
|
+
if (!post) {
|
|
2381
|
+
fail("That post could not be found.");
|
|
2382
|
+
note(dim2(`\`${CMD} drafts\` lists them with their ids`));
|
|
2383
|
+
return 1;
|
|
2384
|
+
}
|
|
2385
|
+
const { versions } = await request(`/api/posts/${encodeURIComponent(post._id)}/versions`);
|
|
2386
|
+
const pick = (n) => versions.find((v) => String(v.versionNumber) === n || v.id === n);
|
|
2387
|
+
const restore = value("restore");
|
|
2388
|
+
if (restore) {
|
|
2389
|
+
const v = pick(restore);
|
|
2390
|
+
if (!v) {
|
|
2391
|
+
fail(`There is no version ${restore}. \`${CMD} history ${post._id}\` lists them.`);
|
|
2392
|
+
return 1;
|
|
2393
|
+
}
|
|
2394
|
+
if (post.status === "published") {
|
|
2395
|
+
fail("That post is live on LinkedIn, so its text cannot go back to an older version here.");
|
|
2396
|
+
return 1;
|
|
2397
|
+
}
|
|
2398
|
+
const res = await request(`/api/posts/${encodeURIComponent(post._id)}/versions`, {
|
|
2399
|
+
method: "POST",
|
|
2400
|
+
body: { versionId: v.id }
|
|
2401
|
+
});
|
|
2402
|
+
if (json) {
|
|
2403
|
+
out(JSON.stringify({ id: post._id, restored: v.versionNumber, content: res.content }, null, 2));
|
|
2404
|
+
return 0;
|
|
2405
|
+
}
|
|
2406
|
+
out(res.content);
|
|
2407
|
+
note();
|
|
2408
|
+
ok(`Back to version ${v.versionNumber}. What it replaced is kept as the newest version. ${dim2(post._id)}`);
|
|
2409
|
+
return 0;
|
|
2410
|
+
}
|
|
2411
|
+
const show2 = value("show");
|
|
2412
|
+
if (show2) {
|
|
2413
|
+
const v = pick(show2);
|
|
2414
|
+
if (!v) {
|
|
2415
|
+
fail(`There is no version ${show2}.`);
|
|
2416
|
+
return 1;
|
|
2417
|
+
}
|
|
2418
|
+
if (json) {
|
|
2419
|
+
out(JSON.stringify(v, null, 2));
|
|
2420
|
+
return 0;
|
|
2421
|
+
}
|
|
2422
|
+
out(v.content);
|
|
2423
|
+
return 0;
|
|
2424
|
+
}
|
|
2425
|
+
if (json) {
|
|
2426
|
+
out(JSON.stringify(versions, null, 2));
|
|
2427
|
+
return 0;
|
|
2428
|
+
}
|
|
2429
|
+
if (!versions.length) {
|
|
2430
|
+
out(dim2("No earlier versions. Every change from now on is kept."));
|
|
2431
|
+
return 0;
|
|
2432
|
+
}
|
|
2433
|
+
out();
|
|
2434
|
+
for (const v of versions) {
|
|
2435
|
+
const why = v.instruction ? oneLine(v.instruction, 50) : v.source;
|
|
2436
|
+
out(` ${`v${v.versionNumber}`.padEnd(5)}${dim2(`${ago(v.createdAt).padEnd(5)} ${why}`)}`);
|
|
2437
|
+
out(` ${oneLine(v.content, 66)}`);
|
|
2438
|
+
}
|
|
2439
|
+
out();
|
|
2440
|
+
note(dim2(`\`${CMD} history ${post._id} --show <n>\` reads one, \`--restore <n>\` puts it back`));
|
|
2441
|
+
return 0;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// src/commands/write.ts
|
|
2445
|
+
async function saveNew2(content, series, seriesSummary, title) {
|
|
2446
|
+
return request("/api/posts", {
|
|
2447
|
+
method: "POST",
|
|
2448
|
+
body: {
|
|
2449
|
+
content,
|
|
2450
|
+
source: "cli",
|
|
2451
|
+
...title ? { title } : {},
|
|
2452
|
+
...series ? { series, seriesSummary: seriesSummary ?? undefined } : {}
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/commands/sources.ts
|
|
2458
|
+
function looksLikeUrl(input) {
|
|
2459
|
+
const t = input.trim();
|
|
2460
|
+
return !/\s/.test(t) && (/^https?:\/\//i.test(t) || /^[\w-]+\.[\w.-]+\/\S/.test(t));
|
|
2461
|
+
}
|
|
2462
|
+
function capturedAt(c) {
|
|
2463
|
+
return c.createdAt ?? c._creationTime;
|
|
2464
|
+
}
|
|
2465
|
+
async function captures(args) {
|
|
2466
|
+
const { json, rest, value } = parseFlags(args, ["note", "limit"]);
|
|
2467
|
+
const [sub, ...more] = rest;
|
|
2468
|
+
if (sub === "add") {
|
|
2469
|
+
const piped = more.length ? null : await readStdin();
|
|
2470
|
+
const input = (more.join(" ") || piped || "").trim();
|
|
2471
|
+
if (!input) {
|
|
2472
|
+
fail("Add what? A link, or a thought in your own words.");
|
|
2473
|
+
note(dim2(`${CMD} captures add https://www.linkedin.com/posts/...`));
|
|
2474
|
+
return 1;
|
|
2475
|
+
}
|
|
2476
|
+
const noteText = value("note");
|
|
2477
|
+
const body = looksLikeUrl(input) ? { kind: "link", input, ...noteText ? { note: noteText } : {} } : { kind: "thought", content: input, ...noteText ? { note: noteText } : {} };
|
|
2478
|
+
const call = () => request("/api/captures", { method: "POST", body, timeoutMs: 120000 });
|
|
2479
|
+
const { capture: capture2 } = json ? await call() : await whileThinking([{ at: 0, text: "reading it..." }, { at: 8, text: "still reading..." }], call);
|
|
2480
|
+
if (json) {
|
|
2481
|
+
out(JSON.stringify(capture2, null, 2));
|
|
2482
|
+
return 0;
|
|
2483
|
+
}
|
|
2484
|
+
ok(`Kept on your Capture page. ${dim2(capture2._id)}`);
|
|
2485
|
+
note(dim2(capture2.title ? oneLine(capture2.title, 72) : oneLine(capture2.content, 72)));
|
|
2486
|
+
note(dim2(`\`${CMD} repurpose --capture ${capture2._id}\` writes a post from it`));
|
|
2487
|
+
return 0;
|
|
2488
|
+
}
|
|
2489
|
+
if (sub === "delete") {
|
|
2490
|
+
const id = more[0];
|
|
2491
|
+
if (!id) {
|
|
2492
|
+
fail("Delete which capture?");
|
|
2493
|
+
note(dim2(`${CMD} captures delete <capture-id>`));
|
|
2494
|
+
return 1;
|
|
2495
|
+
}
|
|
2496
|
+
await request(`/api/captures/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
2497
|
+
if (json) {
|
|
2498
|
+
out(JSON.stringify({ id, deleted: true }, null, 2));
|
|
2499
|
+
return 0;
|
|
2500
|
+
}
|
|
2501
|
+
ok(`Deleted. ${dim2(id)}`);
|
|
2502
|
+
return 0;
|
|
2503
|
+
}
|
|
2504
|
+
if (sub) {
|
|
2505
|
+
const { capture: capture2 } = await request(`/api/captures/${encodeURIComponent(sub)}`);
|
|
2506
|
+
if (json) {
|
|
2507
|
+
out(JSON.stringify(capture2, null, 2));
|
|
2508
|
+
return 0;
|
|
2509
|
+
}
|
|
2510
|
+
out(capture2.content);
|
|
2511
|
+
note();
|
|
2512
|
+
const facts = [
|
|
2513
|
+
["title", capture2.title],
|
|
2514
|
+
["kind", [capture2.kind, capture2.format].filter(Boolean).join(", ")],
|
|
2515
|
+
["by", [capture2.author, capture2.publication].filter(Boolean).join(", ") || undefined],
|
|
2516
|
+
["link", capture2.sourceUrl],
|
|
2517
|
+
["your note", capture2.note],
|
|
2518
|
+
["topics", capture2.topics?.length ? capture2.topics.join(", ") : undefined],
|
|
2519
|
+
["saved", capturedAt(capture2) ? `${ago(capturedAt(capture2))} ago` : undefined],
|
|
2520
|
+
["id", capture2._id]
|
|
2521
|
+
];
|
|
2522
|
+
for (const [k, v] of facts)
|
|
2523
|
+
if (v)
|
|
2524
|
+
note(`${dim2(k.padEnd(10))} ${v}`);
|
|
2525
|
+
return 0;
|
|
2526
|
+
}
|
|
2527
|
+
const { captures: rows } = await request("/api/captures");
|
|
2528
|
+
const limit = Number(value("limit") ?? 30) || 30;
|
|
2529
|
+
if (json) {
|
|
2530
|
+
out(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
2531
|
+
return 0;
|
|
2532
|
+
}
|
|
2533
|
+
if (!rows.length) {
|
|
2534
|
+
out(dim2("Nothing kept yet."));
|
|
2535
|
+
note(dim2(`${CMD} captures add <link>`));
|
|
2536
|
+
return 0;
|
|
2537
|
+
}
|
|
2538
|
+
out();
|
|
2539
|
+
for (const c of rows.slice(0, limit)) {
|
|
2540
|
+
const when = capturedAt(c) ? ago(capturedAt(c)) : "";
|
|
2541
|
+
const what = c.title || c.content;
|
|
2542
|
+
out(` ${caramel(oneLine(what, 66))}`);
|
|
2543
|
+
out(` ${dim2([c.format ?? c.kind, c.author, when, c._id].filter(Boolean).join(" "))}`);
|
|
2544
|
+
out();
|
|
2545
|
+
}
|
|
2546
|
+
if (rows.length > limit)
|
|
2547
|
+
note(dim2(`${rows.length - limit} more. --limit ${rows.length} to see them all`));
|
|
2548
|
+
note(dim2(`\`${CMD} captures <id>\` reads one in full`));
|
|
2549
|
+
return 0;
|
|
2550
|
+
}
|
|
2551
|
+
var BORROW = ["idea", "structure", "spark", "faithful"];
|
|
2552
|
+
async function repurpose(args) {
|
|
2553
|
+
const { json, rest, value, has } = parseFlags(args, ["capture", "borrow", "instruction", "shape", "pick", "series", "title"]);
|
|
2554
|
+
const borrow = value("borrow");
|
|
2555
|
+
if (borrow && !BORROW.includes(borrow)) {
|
|
2556
|
+
fail(`There is no --borrow "${borrow}". One of: ${BORROW.join(", ")}`);
|
|
2557
|
+
return 1;
|
|
2558
|
+
}
|
|
2559
|
+
let sourceText = "";
|
|
2560
|
+
let captureId;
|
|
2561
|
+
let from = "";
|
|
2562
|
+
const capture2 = value("capture");
|
|
2563
|
+
if (capture2) {
|
|
2564
|
+
const { capture: c } = await request(`/api/captures/${encodeURIComponent(capture2)}`);
|
|
2565
|
+
sourceText = c.content;
|
|
2566
|
+
captureId = c._id;
|
|
2567
|
+
from = c.title ?? oneLine(c.content, 60);
|
|
2568
|
+
} else {
|
|
2569
|
+
const piped = rest.length ? null : await readStdin();
|
|
2570
|
+
const input = (rest.join(" ") || piped || "").trim();
|
|
2571
|
+
if (!input) {
|
|
2572
|
+
fail("Repurpose what? A link, some text, or --capture <id>.");
|
|
2573
|
+
note(dim2(`${CMD} repurpose https://www.linkedin.com/posts/...`));
|
|
2574
|
+
note(dim2(`${CMD} repurpose --capture <capture-id> --borrow structure`));
|
|
2575
|
+
return 1;
|
|
2576
|
+
}
|
|
2577
|
+
const read = () => request("/api/reframe/extract", { method: "POST", body: { input }, timeoutMs: 120000 });
|
|
2578
|
+
const src = json ? await read() : await whileThinking([{ at: 0, text: "reading it..." }, { at: 8, text: "still reading..." }], read);
|
|
2579
|
+
if (src.rejected) {
|
|
2580
|
+
fail(src.message ?? "That could not be read. Paste the text instead.");
|
|
2581
|
+
return 1;
|
|
2582
|
+
}
|
|
2583
|
+
sourceText = src.text ?? "";
|
|
2584
|
+
if (src.candidates && src.candidates.length > 1) {
|
|
2585
|
+
const pick = Number(value("pick") ?? 1);
|
|
2586
|
+
const chosen = src.candidates[pick - 1];
|
|
2587
|
+
if (!chosen) {
|
|
2588
|
+
fail(`There are ${src.candidates.length} posts there. --pick 1 to ${src.candidates.length}.`);
|
|
2589
|
+
return 1;
|
|
2590
|
+
}
|
|
2591
|
+
sourceText = chosen.text;
|
|
2592
|
+
if (!json) {
|
|
2593
|
+
note(dim2(`${src.candidates.length} posts found there, writing from number ${pick}:`));
|
|
2594
|
+
src.candidates.forEach((c, i) => note(dim2(` ${i + 1}. ${oneLine(c.title ?? c.text, 70)}`)));
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
from = src.title ?? src.author ?? oneLine(sourceText, 60);
|
|
2598
|
+
}
|
|
2599
|
+
const shape2 = value("shape");
|
|
2600
|
+
const call = () => request("/api/reframe/rewrite", {
|
|
2601
|
+
method: "POST",
|
|
2602
|
+
body: {
|
|
2603
|
+
sourceText,
|
|
2604
|
+
origin: "found",
|
|
2605
|
+
...captureId ? { captureId } : {},
|
|
2606
|
+
...borrow ? { borrowMode: borrow } : {},
|
|
2607
|
+
...value("instruction") ? { instruction: value("instruction") } : {},
|
|
2608
|
+
...shape2 ? { postFormat: shape2 } : {}
|
|
2609
|
+
},
|
|
2610
|
+
timeoutMs: 200000
|
|
2611
|
+
});
|
|
2612
|
+
const result = json ? await call() : await whileThinking([
|
|
2613
|
+
{ at: 0, text: "reading what it argues..." },
|
|
2614
|
+
{ at: 6, text: "writing it in your voice..." },
|
|
2615
|
+
{ at: 30, text: "checking it is yours, not theirs..." }
|
|
2616
|
+
], call);
|
|
2617
|
+
const post = result.variations[0];
|
|
2618
|
+
if (!post?.content) {
|
|
2619
|
+
fail("Nothing came back. Try again, or try another --borrow.");
|
|
2620
|
+
return 1;
|
|
2621
|
+
}
|
|
2622
|
+
const series = value("series");
|
|
2623
|
+
const title = value("title");
|
|
2624
|
+
setDraft({ content: post.content, series });
|
|
2625
|
+
const saved = has("save") ? await saveNew2(post.content, series, undefined, title) : null;
|
|
2626
|
+
if (saved)
|
|
2627
|
+
updateDraft({ id: saved.id });
|
|
2628
|
+
if (saved && !title) {
|
|
2629
|
+
await request(`/api/posts/${encodeURIComponent(saved.id)}/title`, { method: "POST", timeoutMs: 30000 }).catch(() => {});
|
|
2630
|
+
}
|
|
2631
|
+
if (json) {
|
|
2632
|
+
out(JSON.stringify({ ...post, ...saved ? { id: saved.id } : {} }, null, 2));
|
|
2633
|
+
return 0;
|
|
2634
|
+
}
|
|
2635
|
+
out();
|
|
2636
|
+
await reveal(post.content, (chunk) => process.stdout.write(chunk));
|
|
2637
|
+
out();
|
|
2638
|
+
note(dim2(`${bar(post.qualityScore)} ${post.qualityScore} from: ${oneLine(from, 60)}`));
|
|
2639
|
+
if (saved)
|
|
2640
|
+
ok(series ? `Saved into "${series}". ${dim2(saved.id)}` : `Saved as a draft. ${dim2(saved.id)}`);
|
|
2641
|
+
else
|
|
2642
|
+
note(dim2("Not saved yet. `save` to keep it, or run again with --save."));
|
|
2643
|
+
return 0;
|
|
2644
|
+
}
|
|
2645
|
+
async function radar(args) {
|
|
2646
|
+
const { json, value, has } = parseFlags(args, ["limit"]);
|
|
2647
|
+
const feed = await request("/api/trend/feed");
|
|
2648
|
+
const limit = Number(value("limit") ?? 15) || 15;
|
|
2649
|
+
const cards = feed.cards.filter((c) => !c.pending);
|
|
2650
|
+
if (json) {
|
|
2651
|
+
out(JSON.stringify({ creators: feed.creators, cards: cards.slice(0, limit) }, null, 2));
|
|
2652
|
+
return 0;
|
|
2653
|
+
}
|
|
2654
|
+
if (!feed.creators.length) {
|
|
2655
|
+
out(dim2("You are not watching anyone yet. Add creators on the Radar page, under Who you watch."));
|
|
2656
|
+
return 0;
|
|
2657
|
+
}
|
|
2658
|
+
out();
|
|
2659
|
+
for (const c of cards.slice(0, limit)) {
|
|
2660
|
+
const m = c.metrics;
|
|
2661
|
+
const stats = m ? [m.likes ? `${m.likes} likes` : "", m.comments ? `${m.comments} comments` : ""].filter(Boolean).join(", ") : "";
|
|
2662
|
+
out(` ${bold2(c.authorName)}${dim2(` ${[c.postedAt ? ago(c.postedAt) : "", stats].filter(Boolean).join(" ")}`)}`);
|
|
2663
|
+
if (c.locked)
|
|
2664
|
+
out(` ${dim2("held back on the trial")}`);
|
|
2665
|
+
else if (has("full") && c.text)
|
|
2666
|
+
out(c.text.split(`
|
|
2667
|
+
`).map((l) => ` ${l}`).join(`
|
|
2668
|
+
`));
|
|
2669
|
+
else if (c.text)
|
|
2670
|
+
out(` ${oneLine(c.text, 72)}`);
|
|
2671
|
+
out(` ${dim2(c.postUrl)}`);
|
|
2672
|
+
out();
|
|
2673
|
+
}
|
|
2674
|
+
note(dim2(`\`${CMD} repurpose <link>\` writes your own post from one`));
|
|
2675
|
+
return 0;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
1700
2678
|
// src/session.ts
|
|
1701
2679
|
import { createInterface } from "node:readline";
|
|
1702
2680
|
|
|
1703
2681
|
// src/ghost.ts
|
|
1704
|
-
function ghostFor(line2, cursor,
|
|
2682
|
+
function ghostFor(line2, cursor, history2, names) {
|
|
1705
2683
|
if (!line2 || cursor !== line2.length)
|
|
1706
2684
|
return null;
|
|
1707
2685
|
if (line2.startsWith(" "))
|
|
1708
2686
|
return null;
|
|
1709
|
-
const fromHistory =
|
|
2687
|
+
const fromHistory = history2.find((h) => h.startsWith(line2) && h.length > line2.length);
|
|
1710
2688
|
if (fromHistory)
|
|
1711
2689
|
return fromHistory.slice(line2.length);
|
|
1712
2690
|
if (!line2.includes(" ")) {
|
|
@@ -1754,12 +2732,13 @@ var COMMANDS2 = {
|
|
|
1754
2732
|
write: {
|
|
1755
2733
|
summary: "draft a post in your voice",
|
|
1756
2734
|
usage: "innernote write [topic] [options]",
|
|
1757
|
-
detail: "Not a prompt box. innernote already holds your voice, your
|
|
2735
|
+
detail: "Not a prompt box. innernote already holds your voice, your topics and what you have published, so give it the subject and let it write. Run it bare and it picks the topic you have covered least.",
|
|
1758
2736
|
options: [
|
|
1759
2737
|
["--thought <text>", "turn a half-formed note into a post, keeping your phrasing"],
|
|
1760
2738
|
["--series <name>", "write into one of your series, so it follows on"],
|
|
1761
2739
|
["--format <name>", "story, listicle, contrarian, ..."],
|
|
1762
|
-
["--save", "keep it as a draft in the app"]
|
|
2740
|
+
["--save", "keep it as a draft in the app"],
|
|
2741
|
+
["--title <text>", "the title it is saved under (with --save); without it, one is written for you"]
|
|
1763
2742
|
],
|
|
1764
2743
|
examples: [
|
|
1765
2744
|
"innernote write",
|
|
@@ -1787,15 +2766,127 @@ var COMMANDS2 = {
|
|
|
1787
2766
|
]
|
|
1788
2767
|
},
|
|
1789
2768
|
ask: {
|
|
1790
|
-
summary: "talk to the post in your own words",
|
|
1791
|
-
usage: "innernote ask <what you want
|
|
1792
|
-
detail: "The Writer, here. The app's composer is a conversation with the post in front of you, and this is the same assistant: it can change the post, answer a question about it, or say it would rather not.
|
|
2769
|
+
summary: "talk to the post in your own words, or edit one passage of it",
|
|
2770
|
+
usage: "innernote ask [--post <id>] [--passage <text> | --hook | --ending] [--preset <name> | --shape <name> | --product <name>] [what you want]",
|
|
2771
|
+
detail: "The Writer, here. The app's composer is a conversation with the post in front of you, and this is the same assistant: it can change the post, answer a question about it, or say it would rather not. In a session it works on the post in hand. With --post it works on a saved post in one command, and the change is kept in the post's history. --passage is the editor's selection: the exact words to change, and only those change. --hook and --ending are the opening and closing block. --preset is a toolbar edit by name, --shape the format picker.",
|
|
2772
|
+
options: [
|
|
2773
|
+
["--post <id>", "a saved post, by id, instead of the one in hand"],
|
|
2774
|
+
["--passage <text>", "change only these exact words of the post"],
|
|
2775
|
+
["--hook, --ending", "change only the opening, or the closing"],
|
|
2776
|
+
["--preset <name>", "on a passage: sharpen shorter punchier simpler example deslop concrete warmer list. On the post: tighten_all as_list as_story fix_spacing rewrite_hook rewrite_ending ending_question"],
|
|
2777
|
+
["--shape <name>", "story opinion listicle contrarian experience howto lessons casestudy framework question announcement prediction beliefbreak"],
|
|
2778
|
+
["--product <name>", "work one of your products into the post, where it honestly belongs (`context` lists them)"],
|
|
2779
|
+
["--rewrite", "with --product: rewrite the post around it instead"]
|
|
2780
|
+
],
|
|
1793
2781
|
examples: [
|
|
1794
2782
|
"ask cut the second half and land it harder",
|
|
1795
2783
|
"ask this ending is soft, what would you do",
|
|
1796
|
-
|
|
2784
|
+
'innernote ask --post p17abc... "make the opening shorter"',
|
|
2785
|
+
'innernote ask --post p17abc... --passage "we got lucky" "say what the luck actually was"',
|
|
2786
|
+
'innernote ask --post p17abc... --passage "we got lucky" --preset concrete',
|
|
2787
|
+
"innernote ask --post p17abc... --preset ending_question",
|
|
2788
|
+
"innernote ask --post p17abc... --shape story",
|
|
2789
|
+
'innernote ask --post p17abc... --product "innernote"'
|
|
2790
|
+
]
|
|
2791
|
+
},
|
|
2792
|
+
edit: {
|
|
2793
|
+
summary: "change a saved post: its text, title, first comment, series or status",
|
|
2794
|
+
usage: "innernote edit <id> [--file <path> | --text <text>] [--title <text>] [--first-comment <text>] [--series <name>] [--status ready|draft]",
|
|
2795
|
+
detail: "Puts your own words into a saved post. New text comes from --file, --text or a pipe, and is saved the way the app saves a draft, so the old text is one Undo away in the post's history. A post already live on LinkedIn keeps its text. The id comes from `drafts --json` or `show`.",
|
|
2796
|
+
options: [
|
|
2797
|
+
["--file <path>", "the new text of the post, from a file"],
|
|
2798
|
+
["--text <text>", "the new text of the post, inline"],
|
|
2799
|
+
["--title <text>", "the post's title in the app"],
|
|
2800
|
+
["--first-comment <text>", "the comment posted under it when it goes live"],
|
|
2801
|
+
["--series <name>", "file it into a series, or `none` to take it out"],
|
|
2802
|
+
["--status <ready|draft>", "mark it ready, or send it back to draft"]
|
|
2803
|
+
],
|
|
2804
|
+
examples: [
|
|
2805
|
+
"innernote edit p17abc... --file post.txt",
|
|
2806
|
+
"pbpaste | innernote edit p17abc...",
|
|
2807
|
+
'innernote edit p17abc... --title "The sales call" --first-comment "link: innernote.space"',
|
|
2808
|
+
'innernote edit p17abc... --series "Build in Public" --status ready'
|
|
2809
|
+
]
|
|
2810
|
+
},
|
|
2811
|
+
captures: {
|
|
2812
|
+
summary: "your Capture page: what you keep to write from later",
|
|
2813
|
+
usage: "innernote captures [<id>] | captures add <link or text> [--note <why>] | captures delete <id>",
|
|
2814
|
+
detail: "The same list as Capture in the app. With no argument it lists what you kept, newest first, with ids. With an id it prints that capture in full. `add` keeps a link (a LinkedIn post, an article, a page) or a thought in your own words: a link is read once, through innernote's cache, and kept with its text. `repurpose --capture <id>` writes a post from one.",
|
|
2815
|
+
options: [
|
|
2816
|
+
["--note <why>", "with add: why you kept it, in a line"],
|
|
2817
|
+
["--limit <n>", "how many to list (default 30)"]
|
|
2818
|
+
],
|
|
2819
|
+
examples: [
|
|
2820
|
+
"innernote captures",
|
|
2821
|
+
'innernote captures add https://www.linkedin.com/posts/... --note "the pricing argument"',
|
|
2822
|
+
"innernote captures j57abc..."
|
|
1797
2823
|
]
|
|
1798
2824
|
},
|
|
2825
|
+
repurpose: {
|
|
2826
|
+
summary: "write your own post from a link, a capture or some text",
|
|
2827
|
+
usage: "innernote repurpose <link or text> | repurpose --capture <id> [--borrow <what>] [--instruction <text>] [--shape <name>] [--save]",
|
|
2828
|
+
detail: "Turn into a post, from here. The source is read, and the post is written in your voice from what you choose to borrow, then checked so it reads as yours and not theirs. --borrow idea takes their point and uses your proof, structure takes only how it is built, spark only reacts to it, and faithful relays what it reports in your words, with credit. A profile link yields its recent posts: the first is used, --pick chooses another.",
|
|
2829
|
+
options: [
|
|
2830
|
+
["--capture <id>", "a capture from `captures`, instead of a link"],
|
|
2831
|
+
["--borrow <what>", "idea, structure, spark or faithful"],
|
|
2832
|
+
["--instruction <text>", "anything else it should do, in your words"],
|
|
2833
|
+
["--shape <name>", "story, listicle, howto, lessons and the rest, as in `ask --shape`"],
|
|
2834
|
+
["--pick <n>", "which of a profile's posts to write from"],
|
|
2835
|
+
["--save", "keep it as a draft"],
|
|
2836
|
+
["--series <name>", "with --save: file it into a series"],
|
|
2837
|
+
["--title <text>", "with --save: its title (default: one is written for you)"]
|
|
2838
|
+
],
|
|
2839
|
+
examples: [
|
|
2840
|
+
"innernote repurpose https://www.linkedin.com/posts/... --borrow structure",
|
|
2841
|
+
"innernote repurpose --capture j57abc... --borrow idea --save",
|
|
2842
|
+
"pbpaste | innernote repurpose --borrow spark"
|
|
2843
|
+
]
|
|
2844
|
+
},
|
|
2845
|
+
radar: {
|
|
2846
|
+
summary: "recent posts from the creators you watch",
|
|
2847
|
+
usage: "innernote radar [--limit <n>] [--full]",
|
|
2848
|
+
detail: "The same feed as Radar in the app, newest first, with each post's link. Who you watch is set on the Radar page. `repurpose <link>` writes your own post from one.",
|
|
2849
|
+
options: [
|
|
2850
|
+
["--limit <n>", "how many posts (default 15)"],
|
|
2851
|
+
["--full", "print each post in full, not one line"]
|
|
2852
|
+
],
|
|
2853
|
+
examples: ["innernote radar", "innernote radar --full --limit 5", "innernote radar --json"]
|
|
2854
|
+
},
|
|
2855
|
+
history: {
|
|
2856
|
+
summary: "every earlier version of a post, and putting one back",
|
|
2857
|
+
usage: "innernote history <id> [--show <n>] [--restore <n>]",
|
|
2858
|
+
detail: "Every change to a post's words is kept as a version first: yours, the assistant's, an edit from here. This lists them, newest first. --restore puts one back, and the text it replaces is kept as the newest version, so a restore is itself one step from undone.",
|
|
2859
|
+
options: [
|
|
2860
|
+
["--show <n>", "print version n in full"],
|
|
2861
|
+
["--restore <n>", "put version n back"]
|
|
2862
|
+
],
|
|
2863
|
+
examples: ["innernote history p17abc...", "innernote history p17abc... --show 3", "innernote history p17abc... --restore 3"]
|
|
2864
|
+
},
|
|
2865
|
+
attach: {
|
|
2866
|
+
summary: "put an image, images, a PDF or a video on a saved post",
|
|
2867
|
+
usage: "innernote attach <id> <file...> [--title <text>] | innernote attach <id> --remove",
|
|
2868
|
+
detail: "Uploads straight to innernote's storage on a signed link, the way the app does, and attaches it to the post. Several images become a grid. A PDF shows on LinkedIn as a carousel people swipe through, under its title. A post carries one kind of attachment, so attaching replaces what was there.",
|
|
2869
|
+
options: [
|
|
2870
|
+
["--title <text>", "the title of a PDF or a video (default: the file name)"],
|
|
2871
|
+
["--remove", "take the attachment off"]
|
|
2872
|
+
],
|
|
2873
|
+
examples: [
|
|
2874
|
+
"innernote attach p17abc... picture.png",
|
|
2875
|
+
'innernote attach p17abc... slides.pdf --title "How we price"',
|
|
2876
|
+
"innernote attach p17abc... --remove"
|
|
2877
|
+
]
|
|
2878
|
+
},
|
|
2879
|
+
delete: {
|
|
2880
|
+
summary: "move a post to the trash",
|
|
2881
|
+
usage: "innernote delete <id>",
|
|
2882
|
+
detail: "The same as Delete in the Posts list: the post is kept, and `restore` brings it back. It does not take anything down from LinkedIn.",
|
|
2883
|
+
examples: ["innernote delete p17abc...", "innernote restore p17abc..."]
|
|
2884
|
+
},
|
|
2885
|
+
restore: {
|
|
2886
|
+
summary: "bring a post back out of the trash",
|
|
2887
|
+
usage: "innernote restore <id>",
|
|
2888
|
+
examples: ["innernote restore p17abc..."]
|
|
2889
|
+
},
|
|
1799
2890
|
save: {
|
|
1800
2891
|
summary: "keep the draft you are looking at",
|
|
1801
2892
|
usage: "innernote save",
|
|
@@ -1803,10 +2894,10 @@ var COMMANDS2 = {
|
|
|
1803
2894
|
examples: ['write "hiring your first engineer"', "shape shorter", "save"]
|
|
1804
2895
|
},
|
|
1805
2896
|
show: {
|
|
1806
|
-
summary: "see
|
|
1807
|
-
usage: "innernote show",
|
|
1808
|
-
detail: "
|
|
1809
|
-
examples: ["show"]
|
|
2897
|
+
summary: "see a post, with everything the app knows about it",
|
|
2898
|
+
usage: "innernote show [id]",
|
|
2899
|
+
detail: "With an id: the post's text on stdout, and its title, status, series, attachment, first comment, schedule, LinkedIn link and reactions on stderr, all of it in --json. Without one, in a session: the post you are holding, and whether it is saved.",
|
|
2900
|
+
examples: ["innernote show p17abc...", "innernote show p17abc... --json", "show"]
|
|
1810
2901
|
},
|
|
1811
2902
|
drop: {
|
|
1812
2903
|
summary: "put down whatever you are holding",
|
|
@@ -1821,9 +2912,10 @@ var COMMANDS2 = {
|
|
|
1821
2912
|
},
|
|
1822
2913
|
drafts: {
|
|
1823
2914
|
summary: "your posts, numbered",
|
|
1824
|
-
usage: "innernote drafts [status]",
|
|
1825
|
-
detail: "Numbered so you can pick one with `open 3` instead of copying a thirty-two character id. The numbers refer to the list you are looking at and are replaced by the next one.",
|
|
1826
|
-
|
|
2915
|
+
usage: "innernote drafts [status] [--series <name>]",
|
|
2916
|
+
detail: "Numbered so you can pick one with `open 3` instead of copying a thirty-two character id. The numbers refer to the list you are looking at and are replaced by the next one. --series lists one series in its own order. Each --json row also carries the series name and, once published, the LinkedIn link.",
|
|
2917
|
+
options: [["--series <name>", "only the posts in this series, in its order"]],
|
|
2918
|
+
examples: ["drafts", "drafts ready", 'drafts --series "Growing innernote"', "drafts --json | jq -r '.[]._id'"]
|
|
1827
2919
|
},
|
|
1828
2920
|
open: {
|
|
1829
2921
|
summary: "pull a post out of the list and work on it",
|
|
@@ -1849,15 +2941,16 @@ var COMMANDS2 = {
|
|
|
1849
2941
|
examples: ["innernote skill", "innernote skill --install"]
|
|
1850
2942
|
},
|
|
1851
2943
|
context: {
|
|
1852
|
-
summary: "what you post about: your
|
|
2944
|
+
summary: "what you post about: your topics, your series and your products",
|
|
1853
2945
|
usage: "innernote context",
|
|
1854
|
-
detail: "Your standing context:
|
|
2946
|
+
detail: "Your standing context: your topics with their weights, every running series with what it is about and what it is for, and your products. This is the thing to read before deciding what to write, and the thing an agent should read first for the same reason. --json prints the raw payload for scripts and models.",
|
|
1855
2947
|
examples: ["innernote context", "innernote context --json"]
|
|
1856
2948
|
},
|
|
1857
2949
|
queue: {
|
|
1858
2950
|
summary: "schedule a post into your next open slot",
|
|
1859
|
-
usage: "innernote queue <post-id>",
|
|
2951
|
+
usage: "innernote queue <post-id> [--at <time>]",
|
|
1860
2952
|
detail: "A queued post GOES LIVE on LinkedIn on its own at your next cadence slot. Nobody presses anything again. Needs a connection you allowed to publish: tick that box in Settings when you generate the code. Takes a number from the last list, or a full id. It never assumes the draft in hand: this is the one command that makes you say which post.",
|
|
2953
|
+
options: [["--at <time>", `a time you pick, like "2026-10-12 12:31" (this machine's timezone), instead of the next open slot`]],
|
|
1861
2954
|
examples: ["innernote drafts", "innernote queue 3", "innernote queue p17a1yk936ywnan10643efjtc58dhkah"]
|
|
1862
2955
|
}
|
|
1863
2956
|
};
|
|
@@ -1914,7 +3007,7 @@ function tokenize(line2) {
|
|
|
1914
3007
|
quote = null;
|
|
1915
3008
|
else
|
|
1916
3009
|
cur += ch;
|
|
1917
|
-
} else if (ch === '"' || ch === "'") {
|
|
3010
|
+
} else if ((ch === '"' || ch === "'") && cur === "") {
|
|
1918
3011
|
quote = ch;
|
|
1919
3012
|
} else if (/\s/.test(ch)) {
|
|
1920
3013
|
if (cur)
|
|
@@ -2143,9 +3236,11 @@ var VERSION2 = package_default.version;
|
|
|
2143
3236
|
function commandList() {
|
|
2144
3237
|
const groups = [
|
|
2145
3238
|
["Getting connected", ["login", "logout", "whoami", "skill"]],
|
|
2146
|
-
["Writing", ["write", "shape", "ask", "save", "capture"]],
|
|
2147
|
-
["
|
|
2148
|
-
["
|
|
3239
|
+
["Writing", ["write", "repurpose", "shape", "ask", "edit", "attach", "save", "capture"]],
|
|
3240
|
+
["Sources", ["captures", "radar"]],
|
|
3241
|
+
["Looking", ["show", "history", "ideas", "drafts", "open", "week", "context"]],
|
|
3242
|
+
["Shipping", ["queue", "unqueue"]],
|
|
3243
|
+
["Tidying", ["delete", "restore"]]
|
|
2149
3244
|
];
|
|
2150
3245
|
const lines = [];
|
|
2151
3246
|
for (const [title, names] of groups) {
|
|
@@ -2179,6 +3274,14 @@ var COMMANDS3 = {
|
|
|
2179
3274
|
write,
|
|
2180
3275
|
shape,
|
|
2181
3276
|
ask,
|
|
3277
|
+
edit,
|
|
3278
|
+
attach,
|
|
3279
|
+
delete: deletePost,
|
|
3280
|
+
restore: restorePost,
|
|
3281
|
+
history,
|
|
3282
|
+
captures,
|
|
3283
|
+
repurpose,
|
|
3284
|
+
radar,
|
|
2182
3285
|
save,
|
|
2183
3286
|
show,
|
|
2184
3287
|
drop,
|
|
@@ -2205,6 +3308,10 @@ async function main() {
|
|
|
2205
3308
|
return d.id ? "saved" : "unsaved";
|
|
2206
3309
|
},
|
|
2207
3310
|
run: async (name, rest) => {
|
|
3311
|
+
if (rest.includes("--help")) {
|
|
3312
|
+
out2(commandHelp(name) ?? HELP());
|
|
3313
|
+
return 0;
|
|
3314
|
+
}
|
|
2208
3315
|
const h = COMMANDS3[name];
|
|
2209
3316
|
return h ? h(rest) : 1;
|
|
2210
3317
|
}
|