innernote 0.3.3 → 0.4.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 +8 -4
- package/dist/index.js +807 -112
- package/package.json +22 -1
package/README.md
CHANGED
|
@@ -90,16 +90,20 @@ from it fails the suite, so what you read here is what the binary has.
|
|
|
90
90
|
| `capture <text>` | save an idea to your inbox |
|
|
91
91
|
| `write [topic]` | draft a post in your voice |
|
|
92
92
|
| `shape <shape...>` | push a post shorter, punchier, warmer |
|
|
93
|
-
| `ask <anything>` | talk to the post in your own words |
|
|
93
|
+
| `ask [--post <id>] <anything>` | talk to the post in your own words; with `--post`, a saved post in one command. `--passage`, `--hook`, `--ending`, `--preset` and `--shape` edit one part of it, as the editor's selection, toolbar and format picker do |
|
|
94
|
+
| `edit <id>` | put new text, a title, a first comment, a series or a ready state on a saved post |
|
|
95
|
+
| `attach <id> <file...>` | put an image, images, a PDF or a video on a saved post |
|
|
94
96
|
| `save` | keep the draft you are holding |
|
|
95
|
-
| `show` |
|
|
97
|
+
| `show [id]` | a post with everything the app knows about it, or the one you are holding |
|
|
96
98
|
| `drop` | put down whatever you are holding |
|
|
97
99
|
| `open <n or id>` | pull a post out of the last list and work on it |
|
|
98
100
|
| `ideas [status]` | list what you have captured |
|
|
99
|
-
| `drafts [status]` | list your posts, numbered |
|
|
101
|
+
| `drafts [status] [--series <name>]` | list your posts, numbered, or one series in its order |
|
|
100
102
|
| `week` | what is scheduled for the next seven days |
|
|
101
|
-
| `queue <post-id
|
|
103
|
+
| `queue <post-id> [--at <time>]` | schedule a post into your next open slot, or at a time you pick |
|
|
102
104
|
| `unqueue <post-id>` | pull it back out before it goes live |
|
|
105
|
+
| `delete <id>` | move a post to the trash |
|
|
106
|
+
| `restore <id>` | bring it back out of the trash |
|
|
103
107
|
| `skill [--install]` | print the agent skill, or register it for Claude Code |
|
|
104
108
|
|
|
105
109
|
### write
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,9 @@ class ApiError extends Error {
|
|
|
83
83
|
get isRefusal() {
|
|
84
84
|
return this.status === 402 || this.status === 429;
|
|
85
85
|
}
|
|
86
|
+
get isNotConnected() {
|
|
87
|
+
return this.status === 401;
|
|
88
|
+
}
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
class NotLoggedIn extends Error {
|
|
@@ -497,13 +500,92 @@ function resolveRef(ref) {
|
|
|
497
500
|
return /^[a-z0-9]{20,}$/.test(ref) ? { id: ref, title: ref } : null;
|
|
498
501
|
}
|
|
499
502
|
|
|
500
|
-
// src/
|
|
501
|
-
function
|
|
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;
|
|
502
547
|
return {
|
|
503
|
-
|
|
504
|
-
|
|
548
|
+
...post,
|
|
549
|
+
seriesName: inSeries?.name ?? null,
|
|
550
|
+
linkedinUrl: linkedinUrl(post)
|
|
505
551
|
};
|
|
506
552
|
}
|
|
553
|
+
|
|
554
|
+
// src/flags.ts
|
|
555
|
+
function parseFlags(args, valueFlags = []) {
|
|
556
|
+
const values = new Map;
|
|
557
|
+
const present = new Set;
|
|
558
|
+
const rest = [];
|
|
559
|
+
for (let i = 0;i < args.length; i++) {
|
|
560
|
+
const arg = args[i];
|
|
561
|
+
if (!arg.startsWith("--")) {
|
|
562
|
+
rest.push(arg);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const eq = arg.indexOf("=");
|
|
566
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
567
|
+
present.add(name);
|
|
568
|
+
if (!valueFlags.includes(name))
|
|
569
|
+
continue;
|
|
570
|
+
if (eq !== -1) {
|
|
571
|
+
values.set(name, arg.slice(eq + 1));
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const next = args[i + 1];
|
|
575
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
576
|
+
values.set(name, next);
|
|
577
|
+
i++;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
json: present.has("json"),
|
|
582
|
+
rest,
|
|
583
|
+
value: (name) => values.get(name),
|
|
584
|
+
has: (name) => present.has(name)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/commands/read.ts
|
|
507
589
|
function powers(scopes) {
|
|
508
590
|
if (!scopes || scopes.length === 0)
|
|
509
591
|
return "full access";
|
|
@@ -569,29 +651,42 @@ ${data.ideas.length} ${data.ideas.length === 1 ? "idea" : "ideas"}`));
|
|
|
569
651
|
return 0;
|
|
570
652
|
}
|
|
571
653
|
async function drafts(args) {
|
|
572
|
-
const { json, rest } = parseFlags(args);
|
|
654
|
+
const { json, rest, value } = parseFlags(args, ["series"]);
|
|
573
655
|
const status = rest[0];
|
|
574
|
-
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
|
+
}
|
|
575
669
|
if (json) {
|
|
576
|
-
out(JSON.stringify(
|
|
670
|
+
out(JSON.stringify(posts.map((p) => postForJson(p, series)), null, 2));
|
|
577
671
|
return 0;
|
|
578
672
|
}
|
|
579
|
-
if (
|
|
580
|
-
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");
|
|
581
675
|
return 0;
|
|
582
676
|
}
|
|
583
|
-
const shown =
|
|
677
|
+
const shown = posts.slice(0, 30);
|
|
584
678
|
rememberList(shown.map((p) => ({ id: p._id, title: (p.title || p.content).replace(/\s+/g, " ") })));
|
|
585
679
|
for (const [i, post] of shown.entries()) {
|
|
586
680
|
const n = String(i + 1).padStart(2);
|
|
587
|
-
|
|
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}`);
|
|
588
683
|
}
|
|
589
|
-
if (
|
|
684
|
+
if (posts.length > shown.length) {
|
|
590
685
|
note(dim2(`
|
|
591
|
-
...and ${
|
|
686
|
+
...and ${posts.length - shown.length} more`));
|
|
592
687
|
}
|
|
593
688
|
note(dim2(`
|
|
594
|
-
open <number> to work on one`));
|
|
689
|
+
open <number> to work on one show <id> for everything about one`));
|
|
595
690
|
return 0;
|
|
596
691
|
}
|
|
597
692
|
async function week(args) {
|
|
@@ -619,22 +714,32 @@ async function week(args) {
|
|
|
619
714
|
}
|
|
620
715
|
async function context(args) {
|
|
621
716
|
const { json } = parseFlags(args);
|
|
622
|
-
const [pillars, series] = await Promise.all([
|
|
717
|
+
const [pillars, series, notebook] = await Promise.all([
|
|
623
718
|
request("/api/pillars"),
|
|
624
|
-
request("/api/campaigns")
|
|
719
|
+
request("/api/campaigns"),
|
|
720
|
+
request("/api/notebook/brief").catch(() => ({ brief: null }))
|
|
625
721
|
]);
|
|
626
722
|
if (json) {
|
|
627
|
-
out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns }, null, 2));
|
|
723
|
+
out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns, brief: notebook.brief }, null, 2));
|
|
628
724
|
return 0;
|
|
629
725
|
}
|
|
630
726
|
out();
|
|
727
|
+
if (notebook.brief) {
|
|
728
|
+
out(` ${bold2("Who you are, as innernote knows it")}`);
|
|
729
|
+
for (const line of notebook.brief.split(`
|
|
730
|
+
`)) {
|
|
731
|
+
if (line.trim())
|
|
732
|
+
out(` ${dim2(line.replace(/^#+\s*/, ""))}`);
|
|
733
|
+
}
|
|
734
|
+
out();
|
|
735
|
+
}
|
|
631
736
|
if (pillars.pillars.length) {
|
|
632
737
|
out(` ${bold2("What you post about")}`);
|
|
633
738
|
for (const p of pillars.pillars) {
|
|
634
739
|
out(` ${caramel(p.name)}${typeof p.weight === "number" ? dim2(` ${p.weight}%`) : ""}`);
|
|
635
740
|
}
|
|
636
741
|
} else {
|
|
637
|
-
out(` ${dim2("No
|
|
742
|
+
out(` ${dim2("No topics yet. Set them in Settings.")}`);
|
|
638
743
|
}
|
|
639
744
|
out();
|
|
640
745
|
if (series.campaigns.length) {
|
|
@@ -654,14 +759,6 @@ async function context(args) {
|
|
|
654
759
|
return 0;
|
|
655
760
|
}
|
|
656
761
|
|
|
657
|
-
// src/commands/read.ts
|
|
658
|
-
function parseFlags2(args) {
|
|
659
|
-
return {
|
|
660
|
-
json: args.includes("--json"),
|
|
661
|
-
rest: args.filter((a) => !a.startsWith("--"))
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
|
|
665
762
|
// src/progress.ts
|
|
666
763
|
var DRAFTING = [
|
|
667
764
|
{ at: 0, text: "reading your voice..." },
|
|
@@ -830,10 +927,12 @@ async function whileThinking(phases, run) {
|
|
|
830
927
|
}
|
|
831
928
|
|
|
832
929
|
// src/commands/write.ts
|
|
930
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
833
931
|
async function capture(args) {
|
|
834
|
-
const { json, rest } =
|
|
835
|
-
const
|
|
836
|
-
const
|
|
932
|
+
const { json, rest } = parseFlags(args);
|
|
933
|
+
const typed = rest.join(" ").trim();
|
|
934
|
+
const piped = typed ? null : await readStdin();
|
|
935
|
+
const content = (typed || piped || "").trim();
|
|
837
936
|
if (!content) {
|
|
838
937
|
fail("Nothing to capture.");
|
|
839
938
|
note(dim2('Try: innernote capture "the thing you just thought of"'));
|
|
@@ -852,22 +951,27 @@ async function capture(args) {
|
|
|
852
951
|
note(dim2("`ideas` to see them, `write` to turn one into a post"));
|
|
853
952
|
return 0;
|
|
854
953
|
}
|
|
855
|
-
async function
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
954
|
+
async function saveNew(content, series, seriesSummary, title) {
|
|
955
|
+
return request("/api/posts", {
|
|
956
|
+
method: "POST",
|
|
957
|
+
body: {
|
|
958
|
+
content,
|
|
959
|
+
source: "cli",
|
|
960
|
+
...title ? { title } : {},
|
|
961
|
+
...series ? { series, seriesSummary: seriesSummary ?? undefined } : {}
|
|
861
962
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
const
|
|
870
|
-
const
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
async function write(args) {
|
|
966
|
+
const { json, rest, value, has } = parseFlags(args, ["thought", "format", "series", "title"]);
|
|
967
|
+
const topic = rest.join(" ").trim();
|
|
968
|
+
const thoughtFlag = value("thought");
|
|
969
|
+
const piped = thoughtFlag || topic ? null : await readStdin();
|
|
970
|
+
const thought = thoughtFlag ?? (piped ?? undefined);
|
|
971
|
+
const format = value("format");
|
|
972
|
+
const series = value("series");
|
|
973
|
+
const title = value("title");
|
|
974
|
+
const save = has("save");
|
|
871
975
|
const call = () => request("/api/content/generate", {
|
|
872
976
|
method: "POST",
|
|
873
977
|
body: {
|
|
@@ -885,8 +989,14 @@ async function write(args) {
|
|
|
885
989
|
seriesSummary: post.seriesSummary ?? undefined,
|
|
886
990
|
quality: post.qualityScore
|
|
887
991
|
});
|
|
992
|
+
const saved = save ? await saveNew(post.content, series, post.seriesSummary, title) : null;
|
|
993
|
+
if (saved)
|
|
994
|
+
updateDraft({ id: saved.id });
|
|
995
|
+
if (saved && !title) {
|
|
996
|
+
await request(`/api/posts/${encodeURIComponent(saved.id)}/title`, { method: "POST", timeoutMs: 30000 }).catch(() => {});
|
|
997
|
+
}
|
|
888
998
|
if (json) {
|
|
889
|
-
out(JSON.stringify(post, null, 2));
|
|
999
|
+
out(JSON.stringify(saved ? { ...post, id: saved.id } : post, null, 2));
|
|
890
1000
|
return 0;
|
|
891
1001
|
}
|
|
892
1002
|
out();
|
|
@@ -903,16 +1013,7 @@ async function write(args) {
|
|
|
903
1013
|
else if (post.similarity?.band === "adjacent")
|
|
904
1014
|
notes.push("close to something you already published");
|
|
905
1015
|
note(dim2(notes.join(", ")));
|
|
906
|
-
if (
|
|
907
|
-
const saved = await request("/api/posts", {
|
|
908
|
-
method: "POST",
|
|
909
|
-
body: {
|
|
910
|
-
content: post.content,
|
|
911
|
-
source: "cli",
|
|
912
|
-
...series ? { series, seriesSummary: post.seriesSummary } : {}
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
updateDraft({ id: saved.id });
|
|
1016
|
+
if (saved) {
|
|
916
1017
|
ok(series ? `Saved into "${series}". ${dim2(saved.id)}` : `Saved as a draft. ${dim2(saved.id)}`);
|
|
917
1018
|
} else {
|
|
918
1019
|
note(dim2("Not saved yet. `save` to keep it, `shape shorter` to change it."));
|
|
@@ -934,11 +1035,11 @@ var SHAPES = [
|
|
|
934
1035
|
"jargon"
|
|
935
1036
|
];
|
|
936
1037
|
async function shape(args) {
|
|
937
|
-
const { json, rest } =
|
|
938
|
-
const
|
|
1038
|
+
const { json, rest } = parseFlags(args);
|
|
1039
|
+
const held = getDraft();
|
|
1040
|
+
const piped = held ? null : await readStdin();
|
|
939
1041
|
const chosen = rest.filter((a) => SHAPES.includes(a));
|
|
940
1042
|
const freeText = rest.filter((a) => !SHAPES.includes(a)).join(" ").trim();
|
|
941
|
-
const held = getDraft();
|
|
942
1043
|
const content = (piped ?? held?.content ?? "").trim();
|
|
943
1044
|
if (!content) {
|
|
944
1045
|
fail("Nothing to reshape.");
|
|
@@ -976,15 +1077,38 @@ async function shape(args) {
|
|
|
976
1077
|
note(dim2(held?.id ? "`save` to update it" : "`save` to keep it"));
|
|
977
1078
|
return 0;
|
|
978
1079
|
}
|
|
1080
|
+
function parseWhen(input) {
|
|
1081
|
+
const local = input.trim().match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{1,2}):(\d{2})$/);
|
|
1082
|
+
if (local) {
|
|
1083
|
+
const [, y, mo, d, h, mi] = local.map(Number);
|
|
1084
|
+
return new Date(y, mo - 1, d, h, mi).getTime();
|
|
1085
|
+
}
|
|
1086
|
+
const t = Date.parse(input);
|
|
1087
|
+
return Number.isFinite(t) ? t : null;
|
|
1088
|
+
}
|
|
979
1089
|
async function queue(args) {
|
|
980
|
-
const { json, rest } =
|
|
981
|
-
const
|
|
982
|
-
if (!
|
|
1090
|
+
const { json, rest, value } = parseFlags(args, ["at"]);
|
|
1091
|
+
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
1092
|
+
if (!ref) {
|
|
983
1093
|
fail("Which post?");
|
|
984
|
-
note(dim2(`${CMD}
|
|
1094
|
+
note(dim2(`${CMD} drafts then ${CMD} queue 3`));
|
|
1095
|
+
note(dim2(`Or pass the id: ${CMD} queue <post-id>`));
|
|
985
1096
|
return 1;
|
|
986
1097
|
}
|
|
987
|
-
const
|
|
1098
|
+
const at = value("at");
|
|
1099
|
+
let result;
|
|
1100
|
+
if (at !== undefined) {
|
|
1101
|
+
const when2 = parseWhen(at);
|
|
1102
|
+
if (when2 == null) {
|
|
1103
|
+
fail(`"${at}" is not a time.`);
|
|
1104
|
+
note(dim2(`${CMD} queue <post-id> --at "2026-10-12 12:31"`));
|
|
1105
|
+
return 1;
|
|
1106
|
+
}
|
|
1107
|
+
await request(`/api/linkedin/schedule`, { method: "POST", body: { postId: ref.id, scheduledFor: when2 }, timeoutMs: 60000 });
|
|
1108
|
+
result = { success: true, scheduledFor: when2 };
|
|
1109
|
+
} else {
|
|
1110
|
+
result = await request(`/api/posts/${encodeURIComponent(ref.id)}/queue`, { method: "POST", timeoutMs: 60000 });
|
|
1111
|
+
}
|
|
988
1112
|
if (json) {
|
|
989
1113
|
out(JSON.stringify(result, null, 2));
|
|
990
1114
|
return 0;
|
|
@@ -999,7 +1123,7 @@ async function queue(args) {
|
|
|
999
1123
|
return 0;
|
|
1000
1124
|
}
|
|
1001
1125
|
async function save(args) {
|
|
1002
|
-
const { json } =
|
|
1126
|
+
const { json } = parseFlags(args);
|
|
1003
1127
|
const held = getDraft();
|
|
1004
1128
|
if (!held?.content) {
|
|
1005
1129
|
fail("Nothing to save.");
|
|
@@ -1026,7 +1150,7 @@ async function save(args) {
|
|
|
1026
1150
|
return 0;
|
|
1027
1151
|
}
|
|
1028
1152
|
async function open(args) {
|
|
1029
|
-
const { json, rest } =
|
|
1153
|
+
const { json, rest } = parseFlags(args);
|
|
1030
1154
|
const ref = rest[0];
|
|
1031
1155
|
if (!ref) {
|
|
1032
1156
|
fail("Open which one?");
|
|
@@ -1058,9 +1182,19 @@ async function open(args) {
|
|
|
1058
1182
|
return 0;
|
|
1059
1183
|
}
|
|
1060
1184
|
async function ask(args) {
|
|
1061
|
-
const { json, rest } =
|
|
1185
|
+
const { json, rest, value, has } = parseFlags(args, ["post", "passage", "preset", "shape"]);
|
|
1062
1186
|
const message = rest.join(" ").trim();
|
|
1063
1187
|
const held = getDraft();
|
|
1188
|
+
const postRef = value("post");
|
|
1189
|
+
if (postRef) {
|
|
1190
|
+
const target = {
|
|
1191
|
+
passage: value("passage"),
|
|
1192
|
+
preset: value("preset"),
|
|
1193
|
+
shape: value("shape"),
|
|
1194
|
+
region: has("hook") ? "hook" : has("ending") ? "ending" : undefined
|
|
1195
|
+
};
|
|
1196
|
+
return askAboutPost(postRef, message, target, json);
|
|
1197
|
+
}
|
|
1064
1198
|
if (!message) {
|
|
1065
1199
|
fail("Say what you want changed.");
|
|
1066
1200
|
note(dim2("ask cut the second half and land it harder"));
|
|
@@ -1086,7 +1220,7 @@ async function ask(args) {
|
|
|
1086
1220
|
id = saved.id;
|
|
1087
1221
|
updateDraft({ id });
|
|
1088
1222
|
}
|
|
1089
|
-
const call = () => request(`/api/posts/${encodeURIComponent(id)}/edit`, { method: "POST", body: { text: message, scope: "whole" }, timeoutMs: 150000 });
|
|
1223
|
+
const call = () => request(`/api/posts/${encodeURIComponent(id)}/edit`, { method: "POST", body: { text: held.content, instruction: message, scope: "whole" }, timeoutMs: 150000 });
|
|
1090
1224
|
const result = json ? await call() : await whileThinking([
|
|
1091
1225
|
{ at: 0, text: "reading the post..." },
|
|
1092
1226
|
{ at: 3, text: "working on it..." },
|
|
@@ -1112,7 +1246,9 @@ async function ask(args) {
|
|
|
1112
1246
|
return 0;
|
|
1113
1247
|
}
|
|
1114
1248
|
async function show(args) {
|
|
1115
|
-
const { json } =
|
|
1249
|
+
const { json, rest } = parseFlags(args);
|
|
1250
|
+
if (rest[0])
|
|
1251
|
+
return showPost(rest[0], json);
|
|
1116
1252
|
const held = getDraft();
|
|
1117
1253
|
if (!held?.content) {
|
|
1118
1254
|
note("Nothing in hand.");
|
|
@@ -1150,7 +1286,7 @@ async function drop() {
|
|
|
1150
1286
|
return 0;
|
|
1151
1287
|
}
|
|
1152
1288
|
async function unqueue(args) {
|
|
1153
|
-
const { json, rest } =
|
|
1289
|
+
const { json, rest } = parseFlags(args);
|
|
1154
1290
|
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
1155
1291
|
if (!ref) {
|
|
1156
1292
|
fail("Which post?");
|
|
@@ -1166,6 +1302,267 @@ async function unqueue(args) {
|
|
|
1166
1302
|
note(dim2("`drafts` to find it, `queue` to send it back"));
|
|
1167
1303
|
return 0;
|
|
1168
1304
|
}
|
|
1305
|
+
async function postFromRef(refArg, usage) {
|
|
1306
|
+
const ref = resolveRef(refArg);
|
|
1307
|
+
if (!ref) {
|
|
1308
|
+
fail(`No post ${refArg}.`);
|
|
1309
|
+
note(dim2(usage));
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
const post = await fetchPost(ref.id);
|
|
1313
|
+
if (!post) {
|
|
1314
|
+
fail("That post could not be found.");
|
|
1315
|
+
note(dim2(`\`${CMD} drafts\` lists them with their ids`));
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
return post;
|
|
1319
|
+
}
|
|
1320
|
+
var PRESETS = {
|
|
1321
|
+
selection: ["sharpen", "shorter", "punchier", "simpler", "example", "deslop", "concrete", "warmer", "list"],
|
|
1322
|
+
whole: ["tighten_all", "as_list", "as_story", "fix_spacing"],
|
|
1323
|
+
region: ["rewrite_hook", "rewrite_ending", "ending_question"]
|
|
1324
|
+
};
|
|
1325
|
+
var SHAPE_NAMES = [
|
|
1326
|
+
"story",
|
|
1327
|
+
"opinion",
|
|
1328
|
+
"listicle",
|
|
1329
|
+
"contrarian",
|
|
1330
|
+
"experience",
|
|
1331
|
+
"howto",
|
|
1332
|
+
"lessons",
|
|
1333
|
+
"casestudy",
|
|
1334
|
+
"framework",
|
|
1335
|
+
"question",
|
|
1336
|
+
"announcement",
|
|
1337
|
+
"prediction",
|
|
1338
|
+
"beliefbreak"
|
|
1339
|
+
];
|
|
1340
|
+
function askBody(content, message, t) {
|
|
1341
|
+
const preset = t.preset;
|
|
1342
|
+
if (preset && ![...PRESETS.selection, ...PRESETS.whole, ...PRESETS.region].includes(preset)) {
|
|
1343
|
+
return `There is no preset called "${preset}". Presets: ${[...PRESETS.selection, ...PRESETS.whole, ...PRESETS.region].join(", ")}`;
|
|
1344
|
+
}
|
|
1345
|
+
if (t.shape) {
|
|
1346
|
+
if (!SHAPE_NAMES.includes(t.shape))
|
|
1347
|
+
return `There is no shape called "${t.shape}". Shapes: ${SHAPE_NAMES.join(", ")}`;
|
|
1348
|
+
return { text: content, format: t.shape };
|
|
1349
|
+
}
|
|
1350
|
+
if (t.passage !== undefined) {
|
|
1351
|
+
const at = content.indexOf(t.passage);
|
|
1352
|
+
if (!t.passage.trim() || at < 0)
|
|
1353
|
+
return "That passage is not in the post. Copy it exactly, from `show`.";
|
|
1354
|
+
if (preset && !PRESETS.selection.includes(preset)) {
|
|
1355
|
+
return `"${preset}" works on the whole post or its hook or ending, not on a passage. On a passage: ${PRESETS.selection.join(", ")}`;
|
|
1356
|
+
}
|
|
1357
|
+
if (!preset && !message)
|
|
1358
|
+
return "Say what to change in that passage, or name a preset.";
|
|
1359
|
+
return {
|
|
1360
|
+
text: content,
|
|
1361
|
+
scope: "selection",
|
|
1362
|
+
fragment: t.passage,
|
|
1363
|
+
selStart: at,
|
|
1364
|
+
selEnd: at + t.passage.length,
|
|
1365
|
+
...preset ? { preset } : { instruction: message }
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
if (t.region) {
|
|
1369
|
+
if (preset)
|
|
1370
|
+
return "Name a hook or ending preset on its own: --preset rewrite_hook, rewrite_ending or ending_question.";
|
|
1371
|
+
if (!message)
|
|
1372
|
+
return `Say what to change in the ${t.region}.`;
|
|
1373
|
+
return { text: content, scope: "region", region: t.region, instruction: message };
|
|
1374
|
+
}
|
|
1375
|
+
if (preset) {
|
|
1376
|
+
if (PRESETS.selection.includes(preset))
|
|
1377
|
+
return `"${preset}" works on a passage: add --passage "the exact words".`;
|
|
1378
|
+
return { text: content, preset };
|
|
1379
|
+
}
|
|
1380
|
+
if (!message)
|
|
1381
|
+
return "Say what you want changed.";
|
|
1382
|
+
return { text: content, instruction: message, scope: "whole" };
|
|
1383
|
+
}
|
|
1384
|
+
async function askAboutPost(refArg, message, target, json) {
|
|
1385
|
+
const post = await postFromRef(refArg, `${CMD} ask --post <post-id> "what to change"`);
|
|
1386
|
+
if (!post)
|
|
1387
|
+
return 1;
|
|
1388
|
+
if (post.status === "published") {
|
|
1389
|
+
fail("That post is already live on LinkedIn, so it cannot be changed here.");
|
|
1390
|
+
return 1;
|
|
1391
|
+
}
|
|
1392
|
+
const body = askBody(post.content, message, target);
|
|
1393
|
+
if (typeof body === "string") {
|
|
1394
|
+
fail(body);
|
|
1395
|
+
return 1;
|
|
1396
|
+
}
|
|
1397
|
+
const call = () => request(`/api/posts/${encodeURIComponent(post._id)}/edit`, { method: "POST", body, timeoutMs: 150000 });
|
|
1398
|
+
const result = json ? await call() : await whileThinking([
|
|
1399
|
+
{ at: 0, text: "reading the post..." },
|
|
1400
|
+
{ at: 3, text: "working on it..." },
|
|
1401
|
+
{ at: 14, text: "still going..." }
|
|
1402
|
+
], call);
|
|
1403
|
+
let stored = post.content;
|
|
1404
|
+
if (result.changed && result.post) {
|
|
1405
|
+
const after = await fetchPost(post._id);
|
|
1406
|
+
stored = after?.content ?? stored;
|
|
1407
|
+
if (stored !== result.post) {
|
|
1408
|
+
await request(`/api/posts/${encodeURIComponent(post._id)}/draft`, {
|
|
1409
|
+
method: "POST",
|
|
1410
|
+
body: { text: result.post, previous: stored, origin: "suggestion" }
|
|
1411
|
+
});
|
|
1412
|
+
stored = result.post;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
if (json) {
|
|
1416
|
+
out(JSON.stringify({ id: post._id, changed: !!result.changed, post: stored, passage: result.fragment ?? null, reply: result.reply ?? null }, null, 2));
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
out();
|
|
1420
|
+
out(stored);
|
|
1421
|
+
out();
|
|
1422
|
+
if (result.changed && result.fragment && target.passage !== undefined) {
|
|
1423
|
+
note(dim2(`was: ${target.passage}`));
|
|
1424
|
+
note(dim2(`now: ${result.fragment}`));
|
|
1425
|
+
}
|
|
1426
|
+
if (result.reply)
|
|
1427
|
+
note(dim2(result.reply));
|
|
1428
|
+
note(dim2(result.changed ? `Saved. ${post._id}` : `No change. ${post._id}`));
|
|
1429
|
+
return 0;
|
|
1430
|
+
}
|
|
1431
|
+
async function showPost(refArg, json) {
|
|
1432
|
+
const post = await postFromRef(refArg, `${CMD} show <post-id>`);
|
|
1433
|
+
if (!post)
|
|
1434
|
+
return 1;
|
|
1435
|
+
const series = await listSeries();
|
|
1436
|
+
if (json) {
|
|
1437
|
+
out(JSON.stringify(postForJson(post, series), null, 2));
|
|
1438
|
+
return 0;
|
|
1439
|
+
}
|
|
1440
|
+
const when = (ms) => new Date(ms).toLocaleString(undefined, { weekday: "short", day: "numeric", month: "short", hour: "numeric", minute: "2-digit" });
|
|
1441
|
+
const seriesName = post.campaignId ? series.find((s) => s._id === post.campaignId)?.name : undefined;
|
|
1442
|
+
const media = describeMedia(post);
|
|
1443
|
+
const url = linkedinUrl(post);
|
|
1444
|
+
out();
|
|
1445
|
+
out(post.content);
|
|
1446
|
+
out();
|
|
1447
|
+
const facts = [
|
|
1448
|
+
["title", post.title || "untitled"],
|
|
1449
|
+
["status", post.status],
|
|
1450
|
+
["series", seriesName],
|
|
1451
|
+
["attached", media ?? undefined],
|
|
1452
|
+
["first comment", post.firstComment ? post.firstComment.replace(/\s+/g, " ").slice(0, 80) : undefined],
|
|
1453
|
+
["scheduled", post.scheduledFor && post.status !== "published" ? when(post.scheduledFor) : undefined],
|
|
1454
|
+
["published", post.publishedAt ? when(post.publishedAt) : undefined],
|
|
1455
|
+
["on linkedin", url ?? undefined],
|
|
1456
|
+
[
|
|
1457
|
+
"reactions",
|
|
1458
|
+
post.linkedinLikes !== undefined || post.linkedinComments !== undefined ? `${post.linkedinLikes ?? 0} likes, ${post.linkedinComments ?? 0} comments` : undefined
|
|
1459
|
+
],
|
|
1460
|
+
["id", post._id]
|
|
1461
|
+
];
|
|
1462
|
+
for (const [label, valueText] of facts) {
|
|
1463
|
+
if (valueText)
|
|
1464
|
+
note(`${dim2(label.padEnd(14))}${valueText}`);
|
|
1465
|
+
}
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
async function edit(args) {
|
|
1469
|
+
const { json, rest, value } = parseFlags(args, ["text", "file", "title", "first-comment", "series", "status"]);
|
|
1470
|
+
const refArg = rest[0];
|
|
1471
|
+
if (!refArg) {
|
|
1472
|
+
fail("Edit which post?");
|
|
1473
|
+
note(dim2(`${CMD} edit <post-id> --file post.txt`));
|
|
1474
|
+
note(dim2(`${CMD} edit <post-id> --title "A title"`));
|
|
1475
|
+
return 1;
|
|
1476
|
+
}
|
|
1477
|
+
const title = value("title");
|
|
1478
|
+
const firstComment = value("first-comment");
|
|
1479
|
+
const seriesName = value("series");
|
|
1480
|
+
const status = value("status");
|
|
1481
|
+
const file = value("file");
|
|
1482
|
+
let text = value("text");
|
|
1483
|
+
if (file)
|
|
1484
|
+
text = await readFile2(file, "utf8");
|
|
1485
|
+
const fieldsGiven = title !== undefined || firstComment !== undefined || seriesName !== undefined || status !== undefined;
|
|
1486
|
+
if (text === undefined && !fieldsGiven) {
|
|
1487
|
+
text = await readStdin() ?? undefined;
|
|
1488
|
+
}
|
|
1489
|
+
if (text === undefined && !fieldsGiven) {
|
|
1490
|
+
fail("Nothing to change.");
|
|
1491
|
+
note(dim2("--file, --text or piped text for the post, --title, --first-comment, --series, --status"));
|
|
1492
|
+
return 1;
|
|
1493
|
+
}
|
|
1494
|
+
if (status !== undefined && status !== "ready" && status !== "draft") {
|
|
1495
|
+
fail("--status is ready or draft.");
|
|
1496
|
+
note(dim2("Scheduling is `queue`, and taking it out of the queue is `unqueue`."));
|
|
1497
|
+
return 1;
|
|
1498
|
+
}
|
|
1499
|
+
const post = await postFromRef(refArg, `${CMD} edit <post-id> --file post.txt`);
|
|
1500
|
+
if (!post)
|
|
1501
|
+
return 1;
|
|
1502
|
+
const changed = [];
|
|
1503
|
+
let content = post.content;
|
|
1504
|
+
if (text !== undefined) {
|
|
1505
|
+
const next = text.replace(/\s+$/, "");
|
|
1506
|
+
if (!next.trim()) {
|
|
1507
|
+
fail("That text is empty.");
|
|
1508
|
+
return 1;
|
|
1509
|
+
}
|
|
1510
|
+
if (post.status === "published") {
|
|
1511
|
+
fail("That post is already live on LinkedIn, so its text cannot be changed here.");
|
|
1512
|
+
return 1;
|
|
1513
|
+
}
|
|
1514
|
+
if (next !== post.content) {
|
|
1515
|
+
await request(`/api/posts/${encodeURIComponent(post._id)}/draft`, {
|
|
1516
|
+
method: "POST",
|
|
1517
|
+
body: { text: next, previous: post.content, origin: "stream" }
|
|
1518
|
+
});
|
|
1519
|
+
content = next;
|
|
1520
|
+
changed.push("text");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
if (title !== undefined || firstComment !== undefined) {
|
|
1524
|
+
await request("/api/posts", {
|
|
1525
|
+
method: "POST",
|
|
1526
|
+
body: {
|
|
1527
|
+
id: post._id,
|
|
1528
|
+
content,
|
|
1529
|
+
...title !== undefined ? { title } : {},
|
|
1530
|
+
...firstComment !== undefined ? { firstComment } : {}
|
|
1531
|
+
}
|
|
1532
|
+
});
|
|
1533
|
+
if (title !== undefined)
|
|
1534
|
+
changed.push("title");
|
|
1535
|
+
if (firstComment !== undefined)
|
|
1536
|
+
changed.push("first comment");
|
|
1537
|
+
}
|
|
1538
|
+
if (seriesName !== undefined) {
|
|
1539
|
+
let toCampaignId = null;
|
|
1540
|
+
if (seriesName.trim().toLowerCase() !== "none" && seriesName.trim()) {
|
|
1541
|
+
const all = await listSeries();
|
|
1542
|
+
const target = matchSeries(all, seriesName);
|
|
1543
|
+
if (!target) {
|
|
1544
|
+
fail(`No series called "${seriesName}".`);
|
|
1545
|
+
if (all.length)
|
|
1546
|
+
note(dim2(`Yours: ${all.map((x) => x.name).join(", ")}`));
|
|
1547
|
+
return 1;
|
|
1548
|
+
}
|
|
1549
|
+
toCampaignId = target._id;
|
|
1550
|
+
}
|
|
1551
|
+
await request("/api/campaigns/move", { method: "POST", body: { postId: post._id, toCampaignId } });
|
|
1552
|
+
changed.push(toCampaignId ? "series" : "series (taken out)");
|
|
1553
|
+
}
|
|
1554
|
+
if (status !== undefined) {
|
|
1555
|
+
await request(`/api/posts/${encodeURIComponent(post._id)}/ready`, { method: status === "ready" ? "POST" : "DELETE" });
|
|
1556
|
+
changed.push(`status (${status})`);
|
|
1557
|
+
}
|
|
1558
|
+
if (json) {
|
|
1559
|
+
out(JSON.stringify({ id: post._id, changed }, null, 2));
|
|
1560
|
+
return 0;
|
|
1561
|
+
}
|
|
1562
|
+
ok(changed.length ? `Updated the ${changed.join(", ")}. ${dim2(post._id)}` : `Nothing was different. ${dim2(post._id)}`);
|
|
1563
|
+
note(dim2(`\`${CMD} show ${post._id}\` to read it back`));
|
|
1564
|
+
return 0;
|
|
1565
|
+
}
|
|
1169
1566
|
|
|
1170
1567
|
// src/ui.ts
|
|
1171
1568
|
var useColor2 = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
@@ -1322,12 +1719,13 @@ var COMMANDS = {
|
|
|
1322
1719
|
write: {
|
|
1323
1720
|
summary: "draft a post in your voice",
|
|
1324
1721
|
usage: "innernote write [topic] [options]",
|
|
1325
|
-
detail: "Not a prompt box. innernote already holds your voice, your
|
|
1722
|
+
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.",
|
|
1326
1723
|
options: [
|
|
1327
1724
|
["--thought <text>", "turn a half-formed note into a post, keeping your phrasing"],
|
|
1328
1725
|
["--series <name>", "write into one of your series, so it follows on"],
|
|
1329
1726
|
["--format <name>", "story, listicle, contrarian, ..."],
|
|
1330
|
-
["--save", "keep it as a draft in the app"]
|
|
1727
|
+
["--save", "keep it as a draft in the app"],
|
|
1728
|
+
["--title <text>", "the title it is saved under (with --save); without it, one is written for you"]
|
|
1331
1729
|
],
|
|
1332
1730
|
examples: [
|
|
1333
1731
|
"innernote write",
|
|
@@ -1355,15 +1753,70 @@ var COMMANDS = {
|
|
|
1355
1753
|
]
|
|
1356
1754
|
},
|
|
1357
1755
|
ask: {
|
|
1358
|
-
summary: "talk to the post in your own words",
|
|
1359
|
-
usage: "innernote ask <what you want
|
|
1360
|
-
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.
|
|
1756
|
+
summary: "talk to the post in your own words, or edit one passage of it",
|
|
1757
|
+
usage: "innernote ask [--post <id>] [--passage <text> | --hook | --ending] [--preset <name> | --shape <name>] [what you want]",
|
|
1758
|
+
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.",
|
|
1759
|
+
options: [
|
|
1760
|
+
["--post <id>", "a saved post, by id, instead of the one in hand"],
|
|
1761
|
+
["--passage <text>", "change only these exact words of the post"],
|
|
1762
|
+
["--hook, --ending", "change only the opening, or the closing"],
|
|
1763
|
+
["--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"],
|
|
1764
|
+
["--shape <name>", "story opinion listicle contrarian experience howto lessons casestudy framework question announcement prediction beliefbreak"]
|
|
1765
|
+
],
|
|
1361
1766
|
examples: [
|
|
1362
1767
|
"ask cut the second half and land it harder",
|
|
1363
1768
|
"ask this ending is soft, what would you do",
|
|
1364
|
-
|
|
1769
|
+
'innernote ask --post p17abc... "make the opening shorter"',
|
|
1770
|
+
'innernote ask --post p17abc... --passage "we got lucky" "say what the luck actually was"',
|
|
1771
|
+
'innernote ask --post p17abc... --passage "we got lucky" --preset concrete',
|
|
1772
|
+
"innernote ask --post p17abc... --preset ending_question",
|
|
1773
|
+
"innernote ask --post p17abc... --shape story"
|
|
1365
1774
|
]
|
|
1366
1775
|
},
|
|
1776
|
+
edit: {
|
|
1777
|
+
summary: "change a saved post: its text, title, first comment, series or status",
|
|
1778
|
+
usage: "innernote edit <id> [--file <path> | --text <text>] [--title <text>] [--first-comment <text>] [--series <name>] [--status ready|draft]",
|
|
1779
|
+
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`.",
|
|
1780
|
+
options: [
|
|
1781
|
+
["--file <path>", "the new text of the post, from a file"],
|
|
1782
|
+
["--text <text>", "the new text of the post, inline"],
|
|
1783
|
+
["--title <text>", "the post's title in the app"],
|
|
1784
|
+
["--first-comment <text>", "the comment posted under it when it goes live"],
|
|
1785
|
+
["--series <name>", "file it into a series, or `none` to take it out"],
|
|
1786
|
+
["--status <ready|draft>", "mark it ready, or send it back to draft"]
|
|
1787
|
+
],
|
|
1788
|
+
examples: [
|
|
1789
|
+
"innernote edit p17abc... --file post.txt",
|
|
1790
|
+
"pbpaste | innernote edit p17abc...",
|
|
1791
|
+
'innernote edit p17abc... --title "The sales call" --first-comment "link: innernote.space"',
|
|
1792
|
+
'innernote edit p17abc... --series "Build in Public" --status ready'
|
|
1793
|
+
]
|
|
1794
|
+
},
|
|
1795
|
+
attach: {
|
|
1796
|
+
summary: "put an image, images, a PDF or a video on a saved post",
|
|
1797
|
+
usage: "innernote attach <id> <file...> [--title <text>] | innernote attach <id> --remove",
|
|
1798
|
+
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.",
|
|
1799
|
+
options: [
|
|
1800
|
+
["--title <text>", "the title of a PDF or a video (default: the file name)"],
|
|
1801
|
+
["--remove", "take the attachment off"]
|
|
1802
|
+
],
|
|
1803
|
+
examples: [
|
|
1804
|
+
"innernote attach p17abc... picture.png",
|
|
1805
|
+
'innernote attach p17abc... slides.pdf --title "How we price"',
|
|
1806
|
+
"innernote attach p17abc... --remove"
|
|
1807
|
+
]
|
|
1808
|
+
},
|
|
1809
|
+
delete: {
|
|
1810
|
+
summary: "move a post to the trash",
|
|
1811
|
+
usage: "innernote delete <id>",
|
|
1812
|
+
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.",
|
|
1813
|
+
examples: ["innernote delete p17abc...", "innernote restore p17abc..."]
|
|
1814
|
+
},
|
|
1815
|
+
restore: {
|
|
1816
|
+
summary: "bring a post back out of the trash",
|
|
1817
|
+
usage: "innernote restore <id>",
|
|
1818
|
+
examples: ["innernote restore p17abc..."]
|
|
1819
|
+
},
|
|
1367
1820
|
save: {
|
|
1368
1821
|
summary: "keep the draft you are looking at",
|
|
1369
1822
|
usage: "innernote save",
|
|
@@ -1371,10 +1824,10 @@ var COMMANDS = {
|
|
|
1371
1824
|
examples: ['write "hiring your first engineer"', "shape shorter", "save"]
|
|
1372
1825
|
},
|
|
1373
1826
|
show: {
|
|
1374
|
-
summary: "see
|
|
1375
|
-
usage: "innernote show",
|
|
1376
|
-
detail: "
|
|
1377
|
-
examples: ["show"]
|
|
1827
|
+
summary: "see a post, with everything the app knows about it",
|
|
1828
|
+
usage: "innernote show [id]",
|
|
1829
|
+
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.",
|
|
1830
|
+
examples: ["innernote show p17abc...", "innernote show p17abc... --json", "show"]
|
|
1378
1831
|
},
|
|
1379
1832
|
drop: {
|
|
1380
1833
|
summary: "put down whatever you are holding",
|
|
@@ -1389,9 +1842,10 @@ var COMMANDS = {
|
|
|
1389
1842
|
},
|
|
1390
1843
|
drafts: {
|
|
1391
1844
|
summary: "your posts, numbered",
|
|
1392
|
-
usage: "innernote drafts [status]",
|
|
1393
|
-
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.",
|
|
1394
|
-
|
|
1845
|
+
usage: "innernote drafts [status] [--series <name>]",
|
|
1846
|
+
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.",
|
|
1847
|
+
options: [["--series <name>", "only the posts in this series, in its order"]],
|
|
1848
|
+
examples: ["drafts", "drafts ready", 'drafts --series "Growing innernote"', "drafts --json | jq -r '.[]._id'"]
|
|
1395
1849
|
},
|
|
1396
1850
|
open: {
|
|
1397
1851
|
summary: "pull a post out of the list and work on it",
|
|
@@ -1417,16 +1871,17 @@ var COMMANDS = {
|
|
|
1417
1871
|
examples: ["innernote skill", "innernote skill --install"]
|
|
1418
1872
|
},
|
|
1419
1873
|
context: {
|
|
1420
|
-
summary: "what you post about: your
|
|
1874
|
+
summary: "what you post about: your topics and your series",
|
|
1421
1875
|
usage: "innernote context",
|
|
1422
|
-
detail: "Your standing context:
|
|
1876
|
+
detail: "Your standing context: your topics with their weights, and every running series with what it is about and what it is for. 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.",
|
|
1423
1877
|
examples: ["innernote context", "innernote context --json"]
|
|
1424
1878
|
},
|
|
1425
1879
|
queue: {
|
|
1426
1880
|
summary: "schedule a post into your next open slot",
|
|
1427
|
-
usage: "innernote queue <post-id>",
|
|
1428
|
-
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.",
|
|
1429
|
-
|
|
1881
|
+
usage: "innernote queue <post-id> [--at <time>]",
|
|
1882
|
+
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.",
|
|
1883
|
+
options: [["--at <time>", `a time you pick, like "2026-10-12 12:31" (this machine's timezone), instead of the next open slot`]],
|
|
1884
|
+
examples: ["innernote drafts", "innernote queue 3", "innernote queue p17a1yk936ywnan10643efjtc58dhkah"]
|
|
1430
1885
|
}
|
|
1431
1886
|
};
|
|
1432
1887
|
function commandHelp(name) {
|
|
@@ -1539,8 +1994,29 @@ function suggest(state) {
|
|
|
1539
1994
|
// package.json
|
|
1540
1995
|
var package_default = {
|
|
1541
1996
|
name: "innernote",
|
|
1542
|
-
version: "0.
|
|
1997
|
+
version: "0.4.0",
|
|
1543
1998
|
description: "Write LinkedIn posts in your voice, from the terminal.",
|
|
1999
|
+
keywords: [
|
|
2000
|
+
"linkedin",
|
|
2001
|
+
"mcp",
|
|
2002
|
+
"mcp-server",
|
|
2003
|
+
"model-context-protocol",
|
|
2004
|
+
"ai-agent",
|
|
2005
|
+
"claude",
|
|
2006
|
+
"claude-code",
|
|
2007
|
+
"cursor",
|
|
2008
|
+
"agent-skill",
|
|
2009
|
+
"writing",
|
|
2010
|
+
"content",
|
|
2011
|
+
"personal-branding",
|
|
2012
|
+
"ghostwriting",
|
|
2013
|
+
"cli"
|
|
2014
|
+
],
|
|
2015
|
+
homepage: "https://www.innernote.space/cli",
|
|
2016
|
+
license: "UNLICENSED",
|
|
2017
|
+
bugs: {
|
|
2018
|
+
url: "https://www.innernote.space/cli"
|
|
2019
|
+
},
|
|
1544
2020
|
type: "module",
|
|
1545
2021
|
bin: {
|
|
1546
2022
|
innernote: "dist/index.js"
|
|
@@ -1623,7 +2099,7 @@ async function home(inSession = false) {
|
|
|
1623
2099
|
}
|
|
1624
2100
|
|
|
1625
2101
|
// src/commands/skill.ts
|
|
1626
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile as
|
|
2102
|
+
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1627
2103
|
import { homedir as homedir2 } from "node:os";
|
|
1628
2104
|
import { dirname, join as join2 } from "node:path";
|
|
1629
2105
|
import { fileURLToPath } from "node:url";
|
|
@@ -1631,7 +2107,7 @@ async function readSkill() {
|
|
|
1631
2107
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1632
2108
|
for (const rel of ["../../skills/innernote-cli/SKILL.md", "../skills/innernote-cli/SKILL.md"]) {
|
|
1633
2109
|
try {
|
|
1634
|
-
return await
|
|
2110
|
+
return await readFile3(join2(here, rel), "utf8");
|
|
1635
2111
|
} catch {}
|
|
1636
2112
|
}
|
|
1637
2113
|
return null;
|
|
@@ -1658,6 +2134,158 @@ async function skill(args) {
|
|
|
1658
2134
|
return 0;
|
|
1659
2135
|
}
|
|
1660
2136
|
|
|
2137
|
+
// src/commands/post.ts
|
|
2138
|
+
import { readFile as readFile4, stat } from "node:fs/promises";
|
|
2139
|
+
import { basename, extname } from "node:path";
|
|
2140
|
+
var TYPES = {
|
|
2141
|
+
".jpg": "image/jpeg",
|
|
2142
|
+
".jpeg": "image/jpeg",
|
|
2143
|
+
".png": "image/png",
|
|
2144
|
+
".gif": "image/gif",
|
|
2145
|
+
".webp": "image/webp",
|
|
2146
|
+
".pdf": "application/pdf",
|
|
2147
|
+
".mp4": "video/mp4",
|
|
2148
|
+
".mov": "video/quicktime"
|
|
2149
|
+
};
|
|
2150
|
+
function imageSize(buf) {
|
|
2151
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2152
|
+
if (buf.length > 24 && buf[0] === 137 && buf[1] === 80) {
|
|
2153
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
2154
|
+
}
|
|
2155
|
+
if (buf.length > 10 && buf[0] === 71 && buf[1] === 73 && buf[2] === 70) {
|
|
2156
|
+
return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
|
|
2157
|
+
}
|
|
2158
|
+
if (buf.length > 30 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
|
|
2159
|
+
const kind = String.fromCharCode(buf[12], buf[13], buf[14], buf[15]);
|
|
2160
|
+
if (kind === "VP8X") {
|
|
2161
|
+
return { width: 1 + (buf[24] | buf[25] << 8 | buf[26] << 16), height: 1 + (buf[27] | buf[28] << 8 | buf[29] << 16) };
|
|
2162
|
+
}
|
|
2163
|
+
if (kind === "VP8 ")
|
|
2164
|
+
return { width: view.getUint16(26, true) & 16383, height: view.getUint16(28, true) & 16383 };
|
|
2165
|
+
if (kind === "VP8L") {
|
|
2166
|
+
const b = view.getUint32(21, true);
|
|
2167
|
+
return { width: (b & 16383) + 1, height: (b >> 14 & 16383) + 1 };
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (buf.length > 4 && buf[0] === 255 && buf[1] === 216) {
|
|
2171
|
+
let i = 2;
|
|
2172
|
+
while (i + 9 < buf.length) {
|
|
2173
|
+
if (buf[i] !== 255)
|
|
2174
|
+
return;
|
|
2175
|
+
const marker = buf[i + 1];
|
|
2176
|
+
const len = view.getUint16(i + 2);
|
|
2177
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
2178
|
+
return { width: view.getUint16(i + 7), height: view.getUint16(i + 5) };
|
|
2179
|
+
}
|
|
2180
|
+
i += 2 + len;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
async function upload(path) {
|
|
2186
|
+
const contentType = TYPES[extname(path).toLowerCase()];
|
|
2187
|
+
if (!contentType) {
|
|
2188
|
+
throw new Error(`Can't attach ${basename(path)}. Images (jpg, png, gif, webp), a PDF, or a video (mp4, mov).`);
|
|
2189
|
+
}
|
|
2190
|
+
const size = (await stat(path)).size;
|
|
2191
|
+
const bytes = await readFile4(path);
|
|
2192
|
+
const signed = await request("/api/uploads/presign", {
|
|
2193
|
+
method: "POST",
|
|
2194
|
+
body: { filename: basename(path), contentType, size }
|
|
2195
|
+
});
|
|
2196
|
+
const put = await fetch(signed.uploadUrl, { method: "PUT", headers: { "Content-Type": contentType }, body: bytes });
|
|
2197
|
+
if (!put.ok)
|
|
2198
|
+
throw new Error(`The upload of ${basename(path)} failed (${put.status}).`);
|
|
2199
|
+
const dims = contentType.startsWith("image/") ? imageSize(new Uint8Array(bytes)) : undefined;
|
|
2200
|
+
return { asset: { url: signed.publicUrl, name: basename(path), ...dims ?? {} }, contentType };
|
|
2201
|
+
}
|
|
2202
|
+
async function attach(args) {
|
|
2203
|
+
const { json, rest, value, has } = parseFlags(args, ["title"]);
|
|
2204
|
+
const [refArg, ...files] = rest;
|
|
2205
|
+
if (!refArg || !files.length && !has("remove")) {
|
|
2206
|
+
fail("Attach what to which post?");
|
|
2207
|
+
note(dim2(`${CMD} attach <post-id> picture.png`));
|
|
2208
|
+
note(dim2(`${CMD} attach <post-id> slides.pdf --title "How we price"`));
|
|
2209
|
+
return 1;
|
|
2210
|
+
}
|
|
2211
|
+
const ref = resolveRef(refArg);
|
|
2212
|
+
const post = ref ? await fetchPost(ref.id) : null;
|
|
2213
|
+
if (!post) {
|
|
2214
|
+
fail("That post could not be found.");
|
|
2215
|
+
note(dim2(`\`${CMD} drafts\` lists them with their ids`));
|
|
2216
|
+
return 1;
|
|
2217
|
+
}
|
|
2218
|
+
if (post.status === "published") {
|
|
2219
|
+
fail("That post is already live on LinkedIn, so its attachment cannot change here.");
|
|
2220
|
+
return 1;
|
|
2221
|
+
}
|
|
2222
|
+
let media;
|
|
2223
|
+
if (has("remove")) {
|
|
2224
|
+
media = { kind: "text" };
|
|
2225
|
+
} else {
|
|
2226
|
+
const uploaded = [];
|
|
2227
|
+
for (const f of files)
|
|
2228
|
+
uploaded.push(await upload(f));
|
|
2229
|
+
const kinds = new Set(uploaded.map((u) => u.contentType.split("/")[0]));
|
|
2230
|
+
const title = value("title") ?? basename(files[0], extname(files[0]));
|
|
2231
|
+
if (uploaded.every((u) => u.contentType.startsWith("image/"))) {
|
|
2232
|
+
if (uploaded.length > 20) {
|
|
2233
|
+
fail("A post takes at most 20 images.");
|
|
2234
|
+
return 1;
|
|
2235
|
+
}
|
|
2236
|
+
media = uploaded.length === 1 ? { kind: "image", asset: uploaded[0].asset } : { kind: "multiImage", assets: uploaded.map((u) => u.asset) };
|
|
2237
|
+
} else if (uploaded.length === 1 && uploaded[0].contentType === "application/pdf") {
|
|
2238
|
+
media = { kind: "document", asset: uploaded[0].asset, title };
|
|
2239
|
+
} else if (uploaded.length === 1 && kinds.has("video")) {
|
|
2240
|
+
media = { kind: "video", asset: uploaded[0].asset, title };
|
|
2241
|
+
} else {
|
|
2242
|
+
fail("A post carries one kind of attachment: images, or one PDF, or one video.");
|
|
2243
|
+
return 1;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
await request("/api/posts", { method: "POST", body: { id: post._id, content: post.content, media } });
|
|
2247
|
+
if (json) {
|
|
2248
|
+
out(JSON.stringify({ id: post._id, media }, null, 2));
|
|
2249
|
+
return 0;
|
|
2250
|
+
}
|
|
2251
|
+
ok(media.kind === "text" ? `Took the attachment off. ${dim2(post._id)}` : `Attached. ${dim2(post._id)}`);
|
|
2252
|
+
note(dim2(`\`${CMD} show ${post._id}\` to check it`));
|
|
2253
|
+
return 0;
|
|
2254
|
+
}
|
|
2255
|
+
async function deletePost(args) {
|
|
2256
|
+
const { json, rest } = parseFlags(args);
|
|
2257
|
+
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
2258
|
+
if (!ref) {
|
|
2259
|
+
fail("Delete which post?");
|
|
2260
|
+
note(dim2(`${CMD} delete <post-id>`));
|
|
2261
|
+
return 1;
|
|
2262
|
+
}
|
|
2263
|
+
await request(`/api/posts/${encodeURIComponent(ref.id)}`, { method: "DELETE" });
|
|
2264
|
+
if (json) {
|
|
2265
|
+
out(JSON.stringify({ id: ref.id, deleted: true }, null, 2));
|
|
2266
|
+
return 0;
|
|
2267
|
+
}
|
|
2268
|
+
ok(`Moved to the trash. ${dim2(ref.id)}`);
|
|
2269
|
+
note(dim2(`\`${CMD} restore ${ref.id}\` brings it back`));
|
|
2270
|
+
return 0;
|
|
2271
|
+
}
|
|
2272
|
+
async function restorePost(args) {
|
|
2273
|
+
const { json, rest } = parseFlags(args);
|
|
2274
|
+
const ref = rest[0] ? resolveRef(rest[0]) : null;
|
|
2275
|
+
if (!ref) {
|
|
2276
|
+
fail("Restore which post?");
|
|
2277
|
+
note(dim2(`${CMD} restore <post-id>`));
|
|
2278
|
+
return 1;
|
|
2279
|
+
}
|
|
2280
|
+
await request(`/api/posts/${encodeURIComponent(ref.id)}/restore`, { method: "POST" });
|
|
2281
|
+
if (json) {
|
|
2282
|
+
out(JSON.stringify({ id: ref.id, restored: true }, null, 2));
|
|
2283
|
+
return 0;
|
|
2284
|
+
}
|
|
2285
|
+
ok(`Restored. It is back in your posts. ${dim2(ref.id)}`);
|
|
2286
|
+
return 0;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
1661
2289
|
// src/session.ts
|
|
1662
2290
|
import { createInterface } from "node:readline";
|
|
1663
2291
|
|
|
@@ -1715,12 +2343,13 @@ var COMMANDS2 = {
|
|
|
1715
2343
|
write: {
|
|
1716
2344
|
summary: "draft a post in your voice",
|
|
1717
2345
|
usage: "innernote write [topic] [options]",
|
|
1718
|
-
detail: "Not a prompt box. innernote already holds your voice, your
|
|
2346
|
+
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.",
|
|
1719
2347
|
options: [
|
|
1720
2348
|
["--thought <text>", "turn a half-formed note into a post, keeping your phrasing"],
|
|
1721
2349
|
["--series <name>", "write into one of your series, so it follows on"],
|
|
1722
2350
|
["--format <name>", "story, listicle, contrarian, ..."],
|
|
1723
|
-
["--save", "keep it as a draft in the app"]
|
|
2351
|
+
["--save", "keep it as a draft in the app"],
|
|
2352
|
+
["--title <text>", "the title it is saved under (with --save); without it, one is written for you"]
|
|
1724
2353
|
],
|
|
1725
2354
|
examples: [
|
|
1726
2355
|
"innernote write",
|
|
@@ -1748,15 +2377,70 @@ var COMMANDS2 = {
|
|
|
1748
2377
|
]
|
|
1749
2378
|
},
|
|
1750
2379
|
ask: {
|
|
1751
|
-
summary: "talk to the post in your own words",
|
|
1752
|
-
usage: "innernote ask <what you want
|
|
1753
|
-
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.
|
|
2380
|
+
summary: "talk to the post in your own words, or edit one passage of it",
|
|
2381
|
+
usage: "innernote ask [--post <id>] [--passage <text> | --hook | --ending] [--preset <name> | --shape <name>] [what you want]",
|
|
2382
|
+
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.",
|
|
2383
|
+
options: [
|
|
2384
|
+
["--post <id>", "a saved post, by id, instead of the one in hand"],
|
|
2385
|
+
["--passage <text>", "change only these exact words of the post"],
|
|
2386
|
+
["--hook, --ending", "change only the opening, or the closing"],
|
|
2387
|
+
["--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"],
|
|
2388
|
+
["--shape <name>", "story opinion listicle contrarian experience howto lessons casestudy framework question announcement prediction beliefbreak"]
|
|
2389
|
+
],
|
|
1754
2390
|
examples: [
|
|
1755
2391
|
"ask cut the second half and land it harder",
|
|
1756
2392
|
"ask this ending is soft, what would you do",
|
|
1757
|
-
|
|
2393
|
+
'innernote ask --post p17abc... "make the opening shorter"',
|
|
2394
|
+
'innernote ask --post p17abc... --passage "we got lucky" "say what the luck actually was"',
|
|
2395
|
+
'innernote ask --post p17abc... --passage "we got lucky" --preset concrete',
|
|
2396
|
+
"innernote ask --post p17abc... --preset ending_question",
|
|
2397
|
+
"innernote ask --post p17abc... --shape story"
|
|
2398
|
+
]
|
|
2399
|
+
},
|
|
2400
|
+
edit: {
|
|
2401
|
+
summary: "change a saved post: its text, title, first comment, series or status",
|
|
2402
|
+
usage: "innernote edit <id> [--file <path> | --text <text>] [--title <text>] [--first-comment <text>] [--series <name>] [--status ready|draft]",
|
|
2403
|
+
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`.",
|
|
2404
|
+
options: [
|
|
2405
|
+
["--file <path>", "the new text of the post, from a file"],
|
|
2406
|
+
["--text <text>", "the new text of the post, inline"],
|
|
2407
|
+
["--title <text>", "the post's title in the app"],
|
|
2408
|
+
["--first-comment <text>", "the comment posted under it when it goes live"],
|
|
2409
|
+
["--series <name>", "file it into a series, or `none` to take it out"],
|
|
2410
|
+
["--status <ready|draft>", "mark it ready, or send it back to draft"]
|
|
2411
|
+
],
|
|
2412
|
+
examples: [
|
|
2413
|
+
"innernote edit p17abc... --file post.txt",
|
|
2414
|
+
"pbpaste | innernote edit p17abc...",
|
|
2415
|
+
'innernote edit p17abc... --title "The sales call" --first-comment "link: innernote.space"',
|
|
2416
|
+
'innernote edit p17abc... --series "Build in Public" --status ready'
|
|
2417
|
+
]
|
|
2418
|
+
},
|
|
2419
|
+
attach: {
|
|
2420
|
+
summary: "put an image, images, a PDF or a video on a saved post",
|
|
2421
|
+
usage: "innernote attach <id> <file...> [--title <text>] | innernote attach <id> --remove",
|
|
2422
|
+
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.",
|
|
2423
|
+
options: [
|
|
2424
|
+
["--title <text>", "the title of a PDF or a video (default: the file name)"],
|
|
2425
|
+
["--remove", "take the attachment off"]
|
|
2426
|
+
],
|
|
2427
|
+
examples: [
|
|
2428
|
+
"innernote attach p17abc... picture.png",
|
|
2429
|
+
'innernote attach p17abc... slides.pdf --title "How we price"',
|
|
2430
|
+
"innernote attach p17abc... --remove"
|
|
1758
2431
|
]
|
|
1759
2432
|
},
|
|
2433
|
+
delete: {
|
|
2434
|
+
summary: "move a post to the trash",
|
|
2435
|
+
usage: "innernote delete <id>",
|
|
2436
|
+
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.",
|
|
2437
|
+
examples: ["innernote delete p17abc...", "innernote restore p17abc..."]
|
|
2438
|
+
},
|
|
2439
|
+
restore: {
|
|
2440
|
+
summary: "bring a post back out of the trash",
|
|
2441
|
+
usage: "innernote restore <id>",
|
|
2442
|
+
examples: ["innernote restore p17abc..."]
|
|
2443
|
+
},
|
|
1760
2444
|
save: {
|
|
1761
2445
|
summary: "keep the draft you are looking at",
|
|
1762
2446
|
usage: "innernote save",
|
|
@@ -1764,10 +2448,10 @@ var COMMANDS2 = {
|
|
|
1764
2448
|
examples: ['write "hiring your first engineer"', "shape shorter", "save"]
|
|
1765
2449
|
},
|
|
1766
2450
|
show: {
|
|
1767
|
-
summary: "see
|
|
1768
|
-
usage: "innernote show",
|
|
1769
|
-
detail: "
|
|
1770
|
-
examples: ["show"]
|
|
2451
|
+
summary: "see a post, with everything the app knows about it",
|
|
2452
|
+
usage: "innernote show [id]",
|
|
2453
|
+
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.",
|
|
2454
|
+
examples: ["innernote show p17abc...", "innernote show p17abc... --json", "show"]
|
|
1771
2455
|
},
|
|
1772
2456
|
drop: {
|
|
1773
2457
|
summary: "put down whatever you are holding",
|
|
@@ -1782,9 +2466,10 @@ var COMMANDS2 = {
|
|
|
1782
2466
|
},
|
|
1783
2467
|
drafts: {
|
|
1784
2468
|
summary: "your posts, numbered",
|
|
1785
|
-
usage: "innernote drafts [status]",
|
|
1786
|
-
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.",
|
|
1787
|
-
|
|
2469
|
+
usage: "innernote drafts [status] [--series <name>]",
|
|
2470
|
+
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.",
|
|
2471
|
+
options: [["--series <name>", "only the posts in this series, in its order"]],
|
|
2472
|
+
examples: ["drafts", "drafts ready", 'drafts --series "Growing innernote"', "drafts --json | jq -r '.[]._id'"]
|
|
1788
2473
|
},
|
|
1789
2474
|
open: {
|
|
1790
2475
|
summary: "pull a post out of the list and work on it",
|
|
@@ -1810,16 +2495,17 @@ var COMMANDS2 = {
|
|
|
1810
2495
|
examples: ["innernote skill", "innernote skill --install"]
|
|
1811
2496
|
},
|
|
1812
2497
|
context: {
|
|
1813
|
-
summary: "what you post about: your
|
|
2498
|
+
summary: "what you post about: your topics and your series",
|
|
1814
2499
|
usage: "innernote context",
|
|
1815
|
-
detail: "Your standing context:
|
|
2500
|
+
detail: "Your standing context: your topics with their weights, and every running series with what it is about and what it is for. 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.",
|
|
1816
2501
|
examples: ["innernote context", "innernote context --json"]
|
|
1817
2502
|
},
|
|
1818
2503
|
queue: {
|
|
1819
2504
|
summary: "schedule a post into your next open slot",
|
|
1820
|
-
usage: "innernote queue <post-id>",
|
|
1821
|
-
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.",
|
|
1822
|
-
|
|
2505
|
+
usage: "innernote queue <post-id> [--at <time>]",
|
|
2506
|
+
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.",
|
|
2507
|
+
options: [["--at <time>", `a time you pick, like "2026-10-12 12:31" (this machine's timezone), instead of the next open slot`]],
|
|
2508
|
+
examples: ["innernote drafts", "innernote queue 3", "innernote queue p17a1yk936ywnan10643efjtc58dhkah"]
|
|
1823
2509
|
}
|
|
1824
2510
|
};
|
|
1825
2511
|
function commandHelp2(name) {
|
|
@@ -1875,7 +2561,7 @@ function tokenize(line2) {
|
|
|
1875
2561
|
quote = null;
|
|
1876
2562
|
else
|
|
1877
2563
|
cur += ch;
|
|
1878
|
-
} else if (ch === '"' || ch === "'") {
|
|
2564
|
+
} else if ((ch === '"' || ch === "'") && cur === "") {
|
|
1879
2565
|
quote = ch;
|
|
1880
2566
|
} else if (/\s/.test(ch)) {
|
|
1881
2567
|
if (cur)
|
|
@@ -2104,9 +2790,10 @@ var VERSION2 = package_default.version;
|
|
|
2104
2790
|
function commandList() {
|
|
2105
2791
|
const groups = [
|
|
2106
2792
|
["Getting connected", ["login", "logout", "whoami", "skill"]],
|
|
2107
|
-
["Writing", ["write", "shape", "ask", "save", "capture"]],
|
|
2793
|
+
["Writing", ["write", "shape", "ask", "edit", "attach", "save", "capture"]],
|
|
2108
2794
|
["Looking", ["show", "ideas", "drafts", "open", "week", "context"]],
|
|
2109
|
-
["Shipping", ["queue", "unqueue"]]
|
|
2795
|
+
["Shipping", ["queue", "unqueue"]],
|
|
2796
|
+
["Tidying", ["delete", "restore"]]
|
|
2110
2797
|
];
|
|
2111
2798
|
const lines = [];
|
|
2112
2799
|
for (const [title, names] of groups) {
|
|
@@ -2140,6 +2827,10 @@ var COMMANDS3 = {
|
|
|
2140
2827
|
write,
|
|
2141
2828
|
shape,
|
|
2142
2829
|
ask,
|
|
2830
|
+
edit,
|
|
2831
|
+
attach,
|
|
2832
|
+
delete: deletePost,
|
|
2833
|
+
restore: restorePost,
|
|
2143
2834
|
save,
|
|
2144
2835
|
show,
|
|
2145
2836
|
drop,
|
|
@@ -2166,6 +2857,10 @@ async function main() {
|
|
|
2166
2857
|
return d.id ? "saved" : "unsaved";
|
|
2167
2858
|
},
|
|
2168
2859
|
run: async (name, rest) => {
|
|
2860
|
+
if (rest.includes("--help")) {
|
|
2861
|
+
out2(commandHelp(name) ?? HELP());
|
|
2862
|
+
return 0;
|
|
2863
|
+
}
|
|
2169
2864
|
const h = COMMANDS3[name];
|
|
2170
2865
|
return h ? h(rest) : 1;
|
|
2171
2866
|
}
|
|
@@ -2205,7 +2900,7 @@ main().then((code) => {
|
|
|
2205
2900
|
}
|
|
2206
2901
|
if (isApiError(err)) {
|
|
2207
2902
|
fail2(err.message);
|
|
2208
|
-
process.exitCode = err.isRefusal ? 3 : 1;
|
|
2903
|
+
process.exitCode = err.isNotConnected ? 2 : err.isRefusal ? 3 : 1;
|
|
2209
2904
|
return;
|
|
2210
2905
|
}
|
|
2211
2906
|
fail2(err instanceof Error ? err.message : String(err));
|
package/package.json
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "innernote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Write LinkedIn posts in your voice, from the terminal.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"linkedin",
|
|
7
|
+
"mcp",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"ai-agent",
|
|
11
|
+
"claude",
|
|
12
|
+
"claude-code",
|
|
13
|
+
"cursor",
|
|
14
|
+
"agent-skill",
|
|
15
|
+
"writing",
|
|
16
|
+
"content",
|
|
17
|
+
"personal-branding",
|
|
18
|
+
"ghostwriting",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://www.innernote.space/cli",
|
|
22
|
+
"license": "UNLICENSED",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://www.innernote.space/cli"
|
|
25
|
+
},
|
|
5
26
|
"type": "module",
|
|
6
27
|
"bin": {
|
|
7
28
|
"innernote": "dist/index.js"
|