innernote 0.4.0 → 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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/dist/index.js +471 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,12 +86,16 @@ from it fails the suite, so what you read here is what the binary has.
86
86
  | `login [code]` | connect this machine |
87
87
  | `logout` | forget the token stored here |
88
88
  | `whoami` | show which account is connected, and what this key can do |
89
- | `context` | what you post about: your pillars and your series |
89
+ | `context` | what you post about: your pillars, your series and your products |
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
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
94
  | `edit <id>` | put new text, a title, a first comment, a series or a ready state on a saved post |
95
+ | `repurpose <link or text>` | write your own post from a link, a capture (`--capture <id>`) or pasted text |
96
+ | `captures` | your Capture page: list, read one, `add` a link or a thought, `delete` |
97
+ | `radar` | recent posts from the creators you watch |
98
+ | `history <id>` | every earlier version of a post; `--restore <n>` puts one back |
95
99
  | `attach <id> <file...>` | put an image, images, a PDF or a video on a saved post |
96
100
  | `save` | keep the draft you are holding |
97
101
  | `show [id]` | a post with everything the app knows about it, or the one you are holding |
package/dist/index.js CHANGED
@@ -714,13 +714,15 @@ async function week(args) {
714
714
  }
715
715
  async function context(args) {
716
716
  const { json } = parseFlags(args);
717
- const [pillars, series, notebook] = await Promise.all([
717
+ const [pillars, series, notebook, products] = await Promise.all([
718
718
  request("/api/pillars"),
719
719
  request("/api/campaigns"),
720
- request("/api/notebook/brief").catch(() => ({ brief: null }))
720
+ request("/api/notebook/brief").catch(() => ({ brief: null })),
721
+ request("/api/products").catch(() => ({ products: [] }))
721
722
  ]);
723
+ const productRows = products.products.map((p) => ({ name: p.name, oneLiner: p.oneLiner, postCount: p.postCount }));
722
724
  if (json) {
723
- out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns, brief: notebook.brief }, null, 2));
725
+ out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns, products: productRows, brief: notebook.brief }, null, 2));
724
726
  return 0;
725
727
  }
726
728
  out();
@@ -755,6 +757,15 @@ async function context(args) {
755
757
  } else {
756
758
  out(` ${dim2("No series running.")}`);
757
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
+ }
758
769
  out();
759
770
  return 0;
760
771
  }
@@ -1182,7 +1193,7 @@ async function open(args) {
1182
1193
  return 0;
1183
1194
  }
1184
1195
  async function ask(args) {
1185
- const { json, rest, value, has } = parseFlags(args, ["post", "passage", "preset", "shape"]);
1196
+ const { json, rest, value, has } = parseFlags(args, ["post", "passage", "preset", "shape", "product"]);
1186
1197
  const message = rest.join(" ").trim();
1187
1198
  const held = getDraft();
1188
1199
  const postRef = value("post");
@@ -1191,7 +1202,9 @@ async function ask(args) {
1191
1202
  passage: value("passage"),
1192
1203
  preset: value("preset"),
1193
1204
  shape: value("shape"),
1194
- region: has("hook") ? "hook" : has("ending") ? "ending" : undefined
1205
+ region: has("hook") ? "hook" : has("ending") ? "ending" : undefined,
1206
+ product: value("product"),
1207
+ productMode: has("rewrite") ? "rewrite" : "weave"
1195
1208
  };
1196
1209
  return askAboutPost(postRef, message, target, json);
1197
1210
  }
@@ -1342,6 +1355,9 @@ function askBody(content, message, t) {
1342
1355
  if (preset && ![...PRESETS.selection, ...PRESETS.whole, ...PRESETS.region].includes(preset)) {
1343
1356
  return `There is no preset called "${preset}". Presets: ${[...PRESETS.selection, ...PRESETS.whole, ...PRESETS.region].join(", ")}`;
1344
1357
  }
1358
+ if (t.productId) {
1359
+ return { text: content, productId: t.productId, productMode: t.productMode ?? "weave" };
1360
+ }
1345
1361
  if (t.shape) {
1346
1362
  if (!SHAPE_NAMES.includes(t.shape))
1347
1363
  return `There is no shape called "${t.shape}". Shapes: ${SHAPE_NAMES.join(", ")}`;
@@ -1389,6 +1405,16 @@ async function askAboutPost(refArg, message, target, json) {
1389
1405
  fail("That post is already live on LinkedIn, so it cannot be changed here.");
1390
1406
  return 1;
1391
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
+ }
1392
1418
  const body = askBody(post.content, message, target);
1393
1419
  if (typeof body === "string") {
1394
1420
  fail(body);
@@ -1754,14 +1780,16 @@ var COMMANDS = {
1754
1780
  },
1755
1781
  ask: {
1756
1782
  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]",
1783
+ usage: "innernote ask [--post <id>] [--passage <text> | --hook | --ending] [--preset <name> | --shape <name> | --product <name>] [what you want]",
1758
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.",
1759
1785
  options: [
1760
1786
  ["--post <id>", "a saved post, by id, instead of the one in hand"],
1761
1787
  ["--passage <text>", "change only these exact words of the post"],
1762
1788
  ["--hook, --ending", "change only the opening, or the closing"],
1763
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"],
1764
- ["--shape <name>", "story opinion listicle contrarian experience howto lessons casestudy framework question announcement prediction beliefbreak"]
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"]
1765
1793
  ],
1766
1794
  examples: [
1767
1795
  "ask cut the second half and land it harder",
@@ -1770,7 +1798,8 @@ var COMMANDS = {
1770
1798
  'innernote ask --post p17abc... --passage "we got lucky" "say what the luck actually was"',
1771
1799
  'innernote ask --post p17abc... --passage "we got lucky" --preset concrete',
1772
1800
  "innernote ask --post p17abc... --preset ending_question",
1773
- "innernote ask --post p17abc... --shape story"
1801
+ "innernote ask --post p17abc... --shape story",
1802
+ 'innernote ask --post p17abc... --product "innernote"'
1774
1803
  ]
1775
1804
  },
1776
1805
  edit: {
@@ -1792,6 +1821,60 @@ var COMMANDS = {
1792
1821
  'innernote edit p17abc... --series "Build in Public" --status ready'
1793
1822
  ]
1794
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
+ },
1795
1878
  attach: {
1796
1879
  summary: "put an image, images, a PDF or a video on a saved post",
1797
1880
  usage: "innernote attach <id> <file...> [--title <text>] | innernote attach <id> --remove",
@@ -1871,9 +1954,9 @@ var COMMANDS = {
1871
1954
  examples: ["innernote skill", "innernote skill --install"]
1872
1955
  },
1873
1956
  context: {
1874
- summary: "what you post about: your topics and your series",
1957
+ summary: "what you post about: your topics, your series and your products",
1875
1958
  usage: "innernote 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.",
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.",
1877
1960
  examples: ["innernote context", "innernote context --json"]
1878
1961
  },
1879
1962
  queue: {
@@ -1994,7 +2077,7 @@ function suggest(state) {
1994
2077
  // package.json
1995
2078
  var package_default = {
1996
2079
  name: "innernote",
1997
- version: "0.4.0",
2080
+ version: "0.5.0",
1998
2081
  description: "Write LinkedIn posts in your voice, from the terminal.",
1999
2082
  keywords: [
2000
2083
  "linkedin",
@@ -2285,17 +2368,323 @@ async function restorePost(args) {
2285
2368
  ok(`Restored. It is back in your posts. ${dim2(ref.id)}`);
2286
2369
  return 0;
2287
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
+ }
2288
2677
 
2289
2678
  // src/session.ts
2290
2679
  import { createInterface } from "node:readline";
2291
2680
 
2292
2681
  // src/ghost.ts
2293
- function ghostFor(line2, cursor, history, names) {
2682
+ function ghostFor(line2, cursor, history2, names) {
2294
2683
  if (!line2 || cursor !== line2.length)
2295
2684
  return null;
2296
2685
  if (line2.startsWith(" "))
2297
2686
  return null;
2298
- const fromHistory = history.find((h) => h.startsWith(line2) && h.length > line2.length);
2687
+ const fromHistory = history2.find((h) => h.startsWith(line2) && h.length > line2.length);
2299
2688
  if (fromHistory)
2300
2689
  return fromHistory.slice(line2.length);
2301
2690
  if (!line2.includes(" ")) {
@@ -2378,14 +2767,16 @@ var COMMANDS2 = {
2378
2767
  },
2379
2768
  ask: {
2380
2769
  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]",
2770
+ usage: "innernote ask [--post <id>] [--passage <text> | --hook | --ending] [--preset <name> | --shape <name> | --product <name>] [what you want]",
2382
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.",
2383
2772
  options: [
2384
2773
  ["--post <id>", "a saved post, by id, instead of the one in hand"],
2385
2774
  ["--passage <text>", "change only these exact words of the post"],
2386
2775
  ["--hook, --ending", "change only the opening, or the closing"],
2387
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"],
2388
- ["--shape <name>", "story opinion listicle contrarian experience howto lessons casestudy framework question announcement prediction beliefbreak"]
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"]
2389
2780
  ],
2390
2781
  examples: [
2391
2782
  "ask cut the second half and land it harder",
@@ -2394,7 +2785,8 @@ var COMMANDS2 = {
2394
2785
  'innernote ask --post p17abc... --passage "we got lucky" "say what the luck actually was"',
2395
2786
  'innernote ask --post p17abc... --passage "we got lucky" --preset concrete',
2396
2787
  "innernote ask --post p17abc... --preset ending_question",
2397
- "innernote ask --post p17abc... --shape story"
2788
+ "innernote ask --post p17abc... --shape story",
2789
+ 'innernote ask --post p17abc... --product "innernote"'
2398
2790
  ]
2399
2791
  },
2400
2792
  edit: {
@@ -2416,6 +2808,60 @@ var COMMANDS2 = {
2416
2808
  'innernote edit p17abc... --series "Build in Public" --status ready'
2417
2809
  ]
2418
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..."
2823
+ ]
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
+ },
2419
2865
  attach: {
2420
2866
  summary: "put an image, images, a PDF or a video on a saved post",
2421
2867
  usage: "innernote attach <id> <file...> [--title <text>] | innernote attach <id> --remove",
@@ -2495,9 +2941,9 @@ var COMMANDS2 = {
2495
2941
  examples: ["innernote skill", "innernote skill --install"]
2496
2942
  },
2497
2943
  context: {
2498
- summary: "what you post about: your topics and your series",
2944
+ summary: "what you post about: your topics, your series and your products",
2499
2945
  usage: "innernote 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.",
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.",
2501
2947
  examples: ["innernote context", "innernote context --json"]
2502
2948
  },
2503
2949
  queue: {
@@ -2790,8 +3236,9 @@ var VERSION2 = package_default.version;
2790
3236
  function commandList() {
2791
3237
  const groups = [
2792
3238
  ["Getting connected", ["login", "logout", "whoami", "skill"]],
2793
- ["Writing", ["write", "shape", "ask", "edit", "attach", "save", "capture"]],
2794
- ["Looking", ["show", "ideas", "drafts", "open", "week", "context"]],
3239
+ ["Writing", ["write", "repurpose", "shape", "ask", "edit", "attach", "save", "capture"]],
3240
+ ["Sources", ["captures", "radar"]],
3241
+ ["Looking", ["show", "history", "ideas", "drafts", "open", "week", "context"]],
2795
3242
  ["Shipping", ["queue", "unqueue"]],
2796
3243
  ["Tidying", ["delete", "restore"]]
2797
3244
  ];
@@ -2831,6 +3278,10 @@ var COMMANDS3 = {
2831
3278
  attach,
2832
3279
  delete: deletePost,
2833
3280
  restore: restorePost,
3281
+ history,
3282
+ captures,
3283
+ repurpose,
3284
+ radar,
2834
3285
  save,
2835
3286
  show,
2836
3287
  drop,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innernote",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Write LinkedIn posts in your voice, from the terminal.",
5
5
  "keywords": [
6
6
  "linkedin",