innernote 0.1.2 → 0.3.1

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 CHANGED
@@ -78,18 +78,28 @@ any time.
78
78
 
79
79
  ## Commands
80
80
 
81
+ The table below is asserted by `cli/src/parity.test.ts`: a command missing
82
+ from it fails the suite, so what you read here is what the binary has.
83
+
81
84
  | Command | What it does |
82
85
  | --- | --- |
83
86
  | `login [code]` | connect this machine |
84
87
  | `logout` | forget the token stored here |
85
- | `whoami` | show which account is connected |
88
+ | `whoami` | show which account is connected, and what this key can do |
89
+ | `context` | what you post about: your pillars and your series |
86
90
  | `capture <text>` | save an idea to your inbox |
87
91
  | `write [topic]` | draft a post in your voice |
92
+ | `shape <shape...>` | push a post shorter, punchier, warmer |
93
+ | `ask <anything>` | talk to the post in your own words |
94
+ | `save` | keep the draft you are holding |
95
+ | `show` | reprint the post you are holding |
96
+ | `drop` | put down whatever you are holding |
97
+ | `open <n or id>` | pull a post out of the last list and work on it |
88
98
  | `ideas [status]` | list what you have captured |
89
- | `drafts [status]` | list your posts, with their ids |
99
+ | `drafts [status]` | list your posts, numbered |
90
100
  | `week` | what is scheduled for the next seven days |
91
- | `shape <shape...>` | push a post shorter, punchier, warmer (reads stdin) |
92
101
  | `queue <post-id>` | schedule a post into your next open slot |
102
+ | `unqueue <post-id>` | pull it back out before it goes live |
93
103
 
94
104
  ### write
95
105
 
package/dist/index.js CHANGED
@@ -286,44 +286,44 @@ function wordmark(name, gap = 1) {
286
286
  function widest(rows) {
287
287
  return Math.max(...rows.map((r) => r.length));
288
288
  }
289
- function lockup(name = "innernote", tagline) {
290
- const room = (process.stdout.columns ?? 80) - 5;
291
- const mark = logo(2);
292
- const markWidth = widest(mark);
289
+ function lockup(name = "innernote", sub) {
290
+ const room = (process.stdout.columns || 80) - 5;
291
+ const subs = sub == null ? [] : Array.isArray(sub) ? sub : [sub];
292
+ const mark = logo(2).map((r) => r.trimEnd());
293
+ const ink = widest(mark);
293
294
  const word = wordmark(name);
294
295
  if (word) {
295
- const gap2 = 3;
296
- if (markWidth + gap2 + widest(word) <= room) {
296
+ const gap = 2;
297
+ const wide = Math.max(widest(word), ...subs.map((s) => width(s)));
298
+ if (ink + gap + wide <= room) {
297
299
  const rows2 = mark.map((r, i) => {
298
300
  const right = word[i] ?? "";
299
- return right ? `${caramel(r.padEnd(markWidth))}${" ".repeat(gap2)}${bold2(caramel(right))}` : caramel(r.trimEnd());
301
+ return right ? `${caramel(r.padEnd(ink))}${" ".repeat(gap)}${bold2(caramel(right))}` : caramel(r);
300
302
  });
301
- if (tagline)
302
- rows2.push("", ` ${dim2(tagline)}`);
303
+ const indent = " ".repeat(ink + gap);
304
+ for (const line of subs)
305
+ rows2.push(`${indent}${line}`);
303
306
  return rows2;
304
307
  }
305
308
  }
306
- const gap = 3;
307
- const beside = Math.max(name.length, tagline?.length ?? 0);
308
- if (markWidth + gap + beside <= room) {
309
- const mid = Math.floor(mark.length / 2);
310
- return mark.map((r, i) => {
311
- const left = caramel(r.padEnd(markWidth)) + " ".repeat(gap);
312
- if (i === mid)
313
- return left + bold2(caramel(name));
314
- if (i === mid + 1 && tagline)
315
- return left + dim2(tagline);
316
- return caramel(r.trimEnd());
317
- });
318
- }
319
- const rows = mark.map((r) => caramel(r.trimEnd()));
320
- rows.push("", bold2(caramel(name)));
321
- if (tagline)
322
- rows.push(dim2(tagline));
323
- return rows;
309
+ const mid = Math.floor(mark.length / 2);
310
+ const below = [];
311
+ const rows = mark.map((r, i) => {
312
+ const left = caramel(r.padEnd(ink)) + " ";
313
+ if (i === mid)
314
+ return left + bold2(caramel(name));
315
+ const sub2 = subs[i - mid - 1];
316
+ if (i > mid && sub2 !== undefined) {
317
+ if (ink + 3 + width(sub2) <= room)
318
+ return left + sub2;
319
+ below.push(sub2);
320
+ }
321
+ return caramel(r);
322
+ });
323
+ return [...rows, ...below];
324
324
  }
325
325
  function banner(tagline = "your voice, from the terminal") {
326
- return lockup("innernote", tagline).map((r) => " " + r).join(`
326
+ return lockup("innernote", dim2(tagline)).map((r) => " " + r).join(`
327
327
  `);
328
328
  }
329
329
  function bar(score, width2 = 10) {
@@ -609,6 +609,42 @@ async function week(args) {
609
609
  }
610
610
  return 0;
611
611
  }
612
+ async function context(args) {
613
+ const { json } = parseFlags(args);
614
+ const [pillars, series] = await Promise.all([
615
+ request("/api/pillars"),
616
+ request("/api/campaigns")
617
+ ]);
618
+ if (json) {
619
+ out(JSON.stringify({ pillars: pillars.pillars, series: series.campaigns }, null, 2));
620
+ return 0;
621
+ }
622
+ out();
623
+ if (pillars.pillars.length) {
624
+ out(` ${bold2("What you post about")}`);
625
+ for (const p of pillars.pillars) {
626
+ out(` ${caramel(p.name)}${typeof p.weight === "number" ? dim2(` ${p.weight}%`) : ""}`);
627
+ }
628
+ } else {
629
+ out(` ${dim2("No content pillars yet. Set them in Settings.")}`);
630
+ }
631
+ out();
632
+ if (series.campaigns.length) {
633
+ out(` ${bold2("Series")}${dim2(" write into one with")} ${caramel('write --series "<name>"')}`);
634
+ for (const c of series.campaigns) {
635
+ const count = typeof c.postCount === "number" && c.postCount > 0 ? dim2(` ${c.postCount} ${c.postCount === 1 ? "post" : "posts"}`) : "";
636
+ out(` ${caramel(c.name)}${count}`);
637
+ if (c.brief)
638
+ out(` ${dim2(oneLine(c.brief, 64))}`);
639
+ if (c.goal)
640
+ out(` ${dim2(`goal: ${oneLine(c.goal, 58)}`)}`);
641
+ }
642
+ } else {
643
+ out(` ${dim2("No series running.")}`);
644
+ }
645
+ out();
646
+ return 0;
647
+ }
612
648
 
613
649
  // src/commands/read.ts
614
650
  function parseFlags2(args) {
@@ -1078,6 +1114,23 @@ async function drop() {
1078
1114
  note(dim2("`write` to start something, or `drafts` to pick one up"));
1079
1115
  return 0;
1080
1116
  }
1117
+ async function unqueue(args) {
1118
+ const { json, rest } = parseFlags2(args);
1119
+ const ref = rest[0] ? resolveRef(rest[0]) : null;
1120
+ if (!ref) {
1121
+ fail("Which post?");
1122
+ note(dim2(`${CMD} unqueue <post-id> (\`${CMD} week\` shows what is queued)`));
1123
+ return 1;
1124
+ }
1125
+ const result = await request(`/api/linkedin/schedule/${encodeURIComponent(ref.id)}`, { method: "DELETE" });
1126
+ if (json) {
1127
+ out(JSON.stringify(result, null, 2));
1128
+ return 0;
1129
+ }
1130
+ ok("Pulled back out of the queue. It is a draft again.");
1131
+ note(dim2("`drafts` to find it, `queue` to send it back"));
1132
+ return 0;
1133
+ }
1081
1134
 
1082
1135
  // src/ui.ts
1083
1136
  var useColor2 = process.stdout.isTTY === true && !process.env.NO_COLOR && process.env.TERM !== "dumb";
@@ -1122,6 +1175,9 @@ var dim4 = sgr2("2");
1122
1175
  var green4 = sgr2("32");
1123
1176
  var yellow4 = sgr2("33");
1124
1177
  var red4 = sgr2("31");
1178
+ function width2(s) {
1179
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1180
+ }
1125
1181
  var canAnimate2 = isTTY2 && process.env.TERM !== "dumb" && !process.env.CI;
1126
1182
  var MARK_GRID2 = [
1127
1183
  "....#....",
@@ -1160,44 +1216,44 @@ function wordmark2(name, gap = 1) {
1160
1216
  function widest2(rows) {
1161
1217
  return Math.max(...rows.map((r) => r.length));
1162
1218
  }
1163
- function lockup2(name = "innernote", tagline) {
1164
- const room = (process.stdout.columns ?? 80) - 5;
1165
- const mark = logo2(2);
1166
- const markWidth = widest2(mark);
1219
+ function lockup2(name = "innernote", sub) {
1220
+ const room = (process.stdout.columns || 80) - 5;
1221
+ const subs = sub == null ? [] : Array.isArray(sub) ? sub : [sub];
1222
+ const mark = logo2(2).map((r) => r.trimEnd());
1223
+ const ink = widest2(mark);
1167
1224
  const word = wordmark2(name);
1168
1225
  if (word) {
1169
- const gap2 = 3;
1170
- if (markWidth + gap2 + widest2(word) <= room) {
1226
+ const gap = 2;
1227
+ const wide = Math.max(widest2(word), ...subs.map((s) => width2(s)));
1228
+ if (ink + gap + wide <= room) {
1171
1229
  const rows2 = mark.map((r, i) => {
1172
1230
  const right = word[i] ?? "";
1173
- return right ? `${caramel2(r.padEnd(markWidth))}${" ".repeat(gap2)}${bold4(caramel2(right))}` : caramel2(r.trimEnd());
1231
+ return right ? `${caramel2(r.padEnd(ink))}${" ".repeat(gap)}${bold4(caramel2(right))}` : caramel2(r);
1174
1232
  });
1175
- if (tagline)
1176
- rows2.push("", ` ${dim4(tagline)}`);
1233
+ const indent = " ".repeat(ink + gap);
1234
+ for (const line of subs)
1235
+ rows2.push(`${indent}${line}`);
1177
1236
  return rows2;
1178
1237
  }
1179
1238
  }
1180
- const gap = 3;
1181
- const beside = Math.max(name.length, tagline?.length ?? 0);
1182
- if (markWidth + gap + beside <= room) {
1183
- const mid = Math.floor(mark.length / 2);
1184
- return mark.map((r, i) => {
1185
- const left = caramel2(r.padEnd(markWidth)) + " ".repeat(gap);
1186
- if (i === mid)
1187
- return left + bold4(caramel2(name));
1188
- if (i === mid + 1 && tagline)
1189
- return left + dim4(tagline);
1190
- return caramel2(r.trimEnd());
1191
- });
1192
- }
1193
- const rows = mark.map((r) => caramel2(r.trimEnd()));
1194
- rows.push("", bold4(caramel2(name)));
1195
- if (tagline)
1196
- rows.push(dim4(tagline));
1197
- return rows;
1239
+ const mid = Math.floor(mark.length / 2);
1240
+ const below = [];
1241
+ const rows = mark.map((r, i) => {
1242
+ const left = caramel2(r.padEnd(ink)) + " ";
1243
+ if (i === mid)
1244
+ return left + bold4(caramel2(name));
1245
+ const sub2 = subs[i - mid - 1];
1246
+ if (i > mid && sub2 !== undefined) {
1247
+ if (ink + 3 + width2(sub2) <= room)
1248
+ return left + sub2;
1249
+ below.push(sub2);
1250
+ }
1251
+ return caramel2(r);
1252
+ });
1253
+ return [...rows, ...below];
1198
1254
  }
1199
1255
  function banner2(tagline = "your voice, from the terminal") {
1200
- return lockup2("innernote", tagline).map((r) => " " + r).join(`
1256
+ return lockup2("innernote", dim4(tagline)).map((r) => " " + r).join(`
1201
1257
  `);
1202
1258
  }
1203
1259
 
@@ -1313,6 +1369,18 @@ var COMMANDS = {
1313
1369
  usage: "innernote week",
1314
1370
  detail: "Seven days across, so the gaps are visible before you read a word. Published is filled, scheduled is open."
1315
1371
  },
1372
+ unqueue: {
1373
+ summary: "pull a post back out of the queue before it goes live",
1374
+ usage: "innernote unqueue <post-id>",
1375
+ detail: "The undo for queue, while there is still time. The post returns to your drafts and nothing goes live. Once it has been published this refuses, because a live post cannot be unsent this way.",
1376
+ examples: ["innernote unqueue p17abc...", "innernote unqueue 3"]
1377
+ },
1378
+ context: {
1379
+ summary: "what you post about: your pillars and your series",
1380
+ usage: "innernote context",
1381
+ detail: "Your standing context: content pillars 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.",
1382
+ examples: ["innernote context", "innernote context --json"]
1383
+ },
1316
1384
  queue: {
1317
1385
  summary: "schedule a post into your next open slot",
1318
1386
  usage: "innernote queue <post-id>",
@@ -1342,12 +1410,12 @@ function commandHelp(name) {
1342
1410
  return lines.join(`
1343
1411
  `);
1344
1412
  }
1345
- function wrap3(text, width2, indent) {
1413
+ function wrap3(text, width3, indent) {
1346
1414
  const words = text.split(/\s+/);
1347
1415
  const lines = [];
1348
1416
  let line = "";
1349
1417
  for (const w of words) {
1350
- if ((line + " " + w).trim().length > width2) {
1418
+ if ((line + " " + w).trim().length > width3) {
1351
1419
  lines.push(indent + line.trim());
1352
1420
  line = w;
1353
1421
  } else {
@@ -1369,67 +1437,68 @@ function margin(rows) {
1369
1437
  `);
1370
1438
  }
1371
1439
  function landing(state) {
1372
- const rows = ["", ...lockup("innernote"), ""];
1373
- if (state.name) {
1374
- rows.push(state.name + (state.plan ? dim2(` ${state.plan}`) : ""));
1375
- }
1376
- const sub = [];
1440
+ const small = [];
1441
+ if (state.plan)
1442
+ small.push(state.plan);
1377
1443
  if (state.powers)
1378
- sub.push(state.powers);
1379
- if (typeof state.expiresInDays === "number" && state.expiresInDays <= 14) {
1380
- sub.push(`key expires in ${state.expiresInDays} ${state.expiresInDays === 1 ? "day" : "days"}`);
1381
- }
1444
+ small.push(state.powers);
1382
1445
  if (state.version)
1383
- sub.push(`v${state.version}`);
1384
- if (sub.length)
1385
- rows.push(dim2(sub.join(" ")));
1386
- if (state.week?.length) {
1387
- rows.push("", ...state.week);
1446
+ small.push(`v${state.version}`);
1447
+ const sub = [];
1448
+ if (state.name)
1449
+ sub.push(`${bold2(state.name)} ${dim2(small.join(" "))}`);
1450
+ else if (small.length)
1451
+ sub.push(dim2(small.join(" ")));
1452
+ if (typeof state.expiresInDays === "number" && state.expiresInDays <= 14) {
1453
+ sub.push(yellow2(`key expires in ${state.expiresInDays} ${state.expiresInDays === 1 ? "day" : "days"}`));
1388
1454
  }
1455
+ const rows = ["", ...lockup("innernote", sub), ""];
1456
+ if (state.week?.length)
1457
+ rows.push(...state.week, "");
1389
1458
  const counts = [];
1390
1459
  if (state.ideas)
1391
1460
  counts.push(`${state.ideas} ${state.ideas === 1 ? "idea" : "ideas"} waiting`);
1392
1461
  if (state.drafts)
1393
- counts.push(`${state.drafts} drafts`);
1462
+ counts.push(`${state.drafts} ${state.drafts === 1 ? "draft" : "drafts"}`);
1394
1463
  if (counts.length)
1395
- rows.push("", dim2(counts.join(" ")));
1464
+ rows.push(dim2(counts.join(" ")));
1396
1465
  if (state.next) {
1397
- rows.push("", `${caramel(state.next.command)}${pad("", Math.max(1, 20 - width(state.next.command)))}${dim2(state.next.why)}`);
1466
+ rows.push(`${caramel(state.next.command)} ${dim2(state.next.why)}`);
1398
1467
  }
1399
1468
  rows.push("");
1400
1469
  return margin(rows);
1401
1470
  }
1402
1471
  function landingDisconnected(apiUrl, version) {
1472
+ const host = apiUrl.replace(/^https?:\/\//, "");
1473
+ const sub = [dim2("LinkedIn posts in your voice, from the terminal")];
1474
+ if (version)
1475
+ sub.push(dim2(`v${version}`));
1403
1476
  return margin([
1404
1477
  "",
1405
- ...lockup("innernote", "your voice, from the terminal"),
1406
- ...version ? [` ${dim2(`v${version}`)}`] : [],
1407
- "",
1408
- dim2("Write LinkedIn posts in your voice, without leaving here."),
1409
- "",
1410
- `${caramel(`${CMD} login`)}${pad("", Math.max(3, 20 - width(`${CMD} login`)))}${dim2("connect this machine")}`,
1411
- dim2(`Get a code at ${apiUrl.replace(/^https?:\/\//, "")}/dashboard/settings`),
1478
+ ...lockup("innernote", sub),
1412
1479
  "",
1413
- `${dim2("No account yet?")} ${caramel(apiUrl.replace(/^https?:\/\//, ""))}`,
1480
+ `${caramel(`${CMD} login`)} ${dim2("connect this machine")}`,
1481
+ dim2(`Get a code at ${host}/dashboard/settings`),
1482
+ `${dim2("No account yet?")} ${caramel(host)}`,
1414
1483
  ""
1415
1484
  ]);
1416
1485
  }
1417
1486
  function suggest(state) {
1418
1487
  if (state.scheduled === 0 && state.ideas > 0) {
1419
- return { command: `${CMD} ideas`, why: "nothing scheduled, and you have things saved" };
1488
+ return { command: "ideas", why: "nothing scheduled, and you have things saved" };
1420
1489
  }
1421
1490
  if (state.scheduled === 0) {
1422
- return { command: `${CMD} write`, why: "the week is empty" };
1491
+ return { command: "write", why: "the week is empty" };
1423
1492
  }
1424
1493
  if (state.ideas >= 5) {
1425
- return { command: `${CMD} ideas`, why: "your inbox is filling up" };
1494
+ return { command: "ideas", why: "your inbox is filling up" };
1426
1495
  }
1427
- return { command: `${CMD} write`, why: "start something" };
1496
+ return { command: "write", why: "start something" };
1428
1497
  }
1429
1498
  // package.json
1430
1499
  var package_default = {
1431
1500
  name: "innernote",
1432
- version: "0.1.2",
1501
+ version: "0.3.1",
1433
1502
  description: "Write LinkedIn posts in your voice, from the terminal.",
1434
1503
  type: "module",
1435
1504
  bin: {
@@ -1437,6 +1506,7 @@ var package_default = {
1437
1506
  },
1438
1507
  files: [
1439
1508
  "dist",
1509
+ "skills",
1440
1510
  "README.md"
1441
1511
  ],
1442
1512
  scripts: {
@@ -1466,7 +1536,7 @@ function powers2(scopes) {
1466
1536
  return scopes[0];
1467
1537
  return `${scopes.slice(0, -1).join(", ")} and ${scopes[scopes.length - 1]}`;
1468
1538
  }
1469
- async function home() {
1539
+ async function home(inSession = false) {
1470
1540
  const config = await loadConfig();
1471
1541
  if (!config.token) {
1472
1542
  out();
@@ -1498,11 +1568,14 @@ async function home() {
1498
1568
  `).map((l) => l.replace(/^ {2}/, "")) : null,
1499
1569
  ideas: ideas2?.ideas.length,
1500
1570
  drafts: drafts2?.posts.length,
1501
- next: suggest({
1502
- scheduled,
1503
- ideas: ideas2?.ideas.length ?? 0,
1504
- drafts: drafts2?.posts.length ?? 0
1505
- })
1571
+ next: (() => {
1572
+ const n = suggest({
1573
+ scheduled,
1574
+ ideas: ideas2?.ideas.length ?? 0,
1575
+ drafts: drafts2?.posts.length ?? 0
1576
+ });
1577
+ return inSession ? n : { ...n, command: `${CMD} ${n.command}` };
1578
+ })()
1506
1579
  }));
1507
1580
  out();
1508
1581
  return 0;
@@ -1511,6 +1584,30 @@ async function home() {
1511
1584
  // src/session.ts
1512
1585
  import { createInterface } from "node:readline";
1513
1586
 
1587
+ // src/ghost.ts
1588
+ function ghostFor(line2, cursor, history, names) {
1589
+ if (!line2 || cursor !== line2.length)
1590
+ return null;
1591
+ if (line2.startsWith(" "))
1592
+ return null;
1593
+ const fromHistory = history.find((h) => h.startsWith(line2) && h.length > line2.length);
1594
+ if (fromHistory)
1595
+ return fromHistory.slice(line2.length);
1596
+ if (!line2.includes(" ")) {
1597
+ const name = names.find((n) => n.startsWith(line2) && n.length > line2.length);
1598
+ return name ? name.slice(line2.length) : null;
1599
+ }
1600
+ if (line2.startsWith("help ")) {
1601
+ const arg = line2.slice(5);
1602
+ if (arg && !arg.includes(" ")) {
1603
+ const name = names.find((n) => n.startsWith(arg) && n.length > arg.length);
1604
+ if (name)
1605
+ return name.slice(arg.length);
1606
+ }
1607
+ }
1608
+ return null;
1609
+ }
1610
+
1514
1611
  // src/help.ts
1515
1612
  var COMMANDS2 = {
1516
1613
  login: {
@@ -1623,6 +1720,18 @@ var COMMANDS2 = {
1623
1720
  usage: "innernote week",
1624
1721
  detail: "Seven days across, so the gaps are visible before you read a word. Published is filled, scheduled is open."
1625
1722
  },
1723
+ unqueue: {
1724
+ summary: "pull a post back out of the queue before it goes live",
1725
+ usage: "innernote unqueue <post-id>",
1726
+ detail: "The undo for queue, while there is still time. The post returns to your drafts and nothing goes live. Once it has been published this refuses, because a live post cannot be unsent this way.",
1727
+ examples: ["innernote unqueue p17abc...", "innernote unqueue 3"]
1728
+ },
1729
+ context: {
1730
+ summary: "what you post about: your pillars and your series",
1731
+ usage: "innernote context",
1732
+ detail: "Your standing context: content pillars 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.",
1733
+ examples: ["innernote context", "innernote context --json"]
1734
+ },
1626
1735
  queue: {
1627
1736
  summary: "schedule a post into your next open slot",
1628
1737
  usage: "innernote queue <post-id>",
@@ -1652,12 +1761,12 @@ function commandHelp2(name) {
1652
1761
  return lines.join(`
1653
1762
  `);
1654
1763
  }
1655
- function wrap4(text, width2, indent) {
1764
+ function wrap4(text, width3, indent) {
1656
1765
  const words = text.split(/\s+/);
1657
1766
  const lines = [];
1658
1767
  let line2 = "";
1659
1768
  for (const w of words) {
1660
- if ((line2 + " " + w).trim().length > width2) {
1769
+ if ((line2 + " " + w).trim().length > width3) {
1661
1770
  lines.push(indent + line2.trim());
1662
1771
  line2 = w;
1663
1772
  } else {
@@ -1729,6 +1838,40 @@ function indented(run) {
1729
1838
  };
1730
1839
  return run().finally(restore);
1731
1840
  }
1841
+ function armGhost(rl, names, promptWidth) {
1842
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
1843
+ return;
1844
+ if (dim2("x") === "x")
1845
+ return;
1846
+ const inner = rl;
1847
+ let shown = null;
1848
+ const clear = () => {
1849
+ if (shown == null)
1850
+ return;
1851
+ process.stdout.write("\x1B[K");
1852
+ shown = null;
1853
+ };
1854
+ const draw = () => {
1855
+ if (process.stdin.isPaused())
1856
+ return;
1857
+ const rest = ghostFor(inner.line ?? "", inner.cursor ?? 0, inner.history ?? [], names);
1858
+ if (!rest)
1859
+ return;
1860
+ const cols = process.stdout.columns || 80;
1861
+ if (promptWidth() + (inner.line?.length ?? 0) + rest.length >= cols - 1)
1862
+ return;
1863
+ process.stdout.write(`${dim2(rest)}\x1B[${rest.length}D`);
1864
+ shown = rest;
1865
+ };
1866
+ process.stdin.prependListener("keypress", (_ch, key) => {
1867
+ const offered = shown;
1868
+ clear();
1869
+ if (key?.name === "right" && offered && inner.cursor === inner.line.length) {
1870
+ rl.write(offered);
1871
+ }
1872
+ setImmediate(draw);
1873
+ });
1874
+ }
1732
1875
  async function session(opts) {
1733
1876
  await opts.intro();
1734
1877
  const names = [...Object.keys(COMMANDS2), ...BUILTINS];
@@ -1746,7 +1889,8 @@ async function session(opts) {
1746
1889
  return [hits.length ? hits : names, line2];
1747
1890
  }
1748
1891
  });
1749
- console.log(` ${dim2("Type a command, or")} ${caramel("help")}${dim2(". Ctrl-C twice to leave.")}
1892
+ armGhost(rl, names, () => width(promptFor()));
1893
+ console.log(` ${dim2("Type a command, or")} ${caramel("help")}${dim2(". Right arrow takes the hint. Ctrl-C twice to leave.")}
1750
1894
  `);
1751
1895
  rl.prompt();
1752
1896
  return new Promise((resolve) => {
@@ -1848,8 +1992,8 @@ function whatThereIs() {
1848
1992
  ];
1849
1993
  const groups = [
1850
1994
  ["Writing", ["write", "shape", "ask", "save", "capture"]],
1851
- ["Looking", ["show", "ideas", "drafts", "open", "week", "whoami"]],
1852
- ["Shipping", ["queue"]],
1995
+ ["Looking", ["show", "ideas", "drafts", "open", "week", "context", "whoami"]],
1996
+ ["Shipping", ["queue", "unqueue"]],
1853
1997
  ["Connection", ["login", "logout"]]
1854
1998
  ];
1855
1999
  for (const [title, cmds] of groups) {
@@ -1878,8 +2022,8 @@ function commandList() {
1878
2022
  const groups = [
1879
2023
  ["Getting connected", ["login", "logout", "whoami"]],
1880
2024
  ["Writing", ["write", "shape", "ask", "save", "capture"]],
1881
- ["Looking", ["show", "ideas", "drafts", "open", "week"]],
1882
- ["Shipping", ["queue"]]
2025
+ ["Looking", ["show", "ideas", "drafts", "open", "week", "context"]],
2026
+ ["Shipping", ["queue", "unqueue"]]
1883
2027
  ];
1884
2028
  const lines = [];
1885
2029
  for (const [title, names] of groups) {
@@ -1904,6 +2048,7 @@ var HELP = () => [
1904
2048
  ].join(`
1905
2049
  `);
1906
2050
  var COMMANDS3 = {
2051
+ context,
1907
2052
  login,
1908
2053
  logout: () => logout(),
1909
2054
  whoami,
@@ -1918,7 +2063,8 @@ var COMMANDS3 = {
1918
2063
  drafts,
1919
2064
  open,
1920
2065
  week,
1921
- queue
2066
+ queue,
2067
+ unqueue
1922
2068
  };
1923
2069
  async function main() {
1924
2070
  const [, , command, ...args] = process.argv;
@@ -1927,7 +2073,7 @@ async function main() {
1927
2073
  return home();
1928
2074
  return session({
1929
2075
  intro: async () => {
1930
- await home();
2076
+ await home(true);
1931
2077
  },
1932
2078
  status: () => {
1933
2079
  const d = getDraft2();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innernote",
3
- "version": "0.1.2",
3
+ "version": "0.3.1",
4
4
  "description": "Write LinkedIn posts in your voice, from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
+ "skills",
11
12
  "README.md"
12
13
  ],
13
14
  "scripts": {
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: innernote-cli
3
+ description: Drive the innernote CLI (`npx innernote`) to help someone write, save and schedule LinkedIn posts in their own voice from the terminal. Use this whenever the user mentions innernote, asks to draft or queue a LinkedIn post from the command line, wants their posting week or captured ideas checked, or asks you to capture a thought for later, even if they do not name the CLI. Also use it before touching any `innernote` command in a script.
4
+ ---
5
+
6
+ # Driving the innernote CLI
7
+
8
+ innernote holds this person's writing voice: how they write, what they post
9
+ about, and their posting schedule. The CLI is a thin client over the same
10
+ service the app uses, so a draft made here sounds like THEM, not like you,
11
+ and everything you save appears in their app.
12
+
13
+ ## The contract, before any command
14
+
15
+ - **stdout is data, stderr is narration.** The post text, JSON and ids go to
16
+ stdout; spinners, notes and errors go to stderr. Pipe stdout with
17
+ confidence, and never parse the human-facing view: every read command takes
18
+ `--json`, which prints the API payload and nothing else.
19
+ - **Exit codes are the protocol.** `0` worked. `1` failed, reason on stderr.
20
+ `2` not connected: stop and ask the user to run `npx innernote login`.
21
+ `3` the account cannot do this right now: a paywall or a spent allowance,
22
+ never a bug. Only `1` is worth debugging.
23
+ - **A refusal is a sentence for the user, not an obstacle for you.** When a
24
+ command refuses (a limit, a missing cadence, no such post), the message is
25
+ already written for the person. Show it to them verbatim and stop. Do not
26
+ retry, and do not work around it.
27
+
28
+ ## Connecting
29
+
30
+ Check with `innernote whoami` (exit `2` means not connected). Connecting
31
+ needs a pairing code from the user's Settings page at
32
+ innernote.space/dashboard/settings; you cannot get one yourself. Ask them
33
+ for it, then run `innernote login THEIR-CODE`. The code works once and
34
+ expires in ten minutes. `INNERNOTE_TOKEN` in the environment overrides the
35
+ stored key, which is how CI and scripts authenticate.
36
+
37
+ ## Read before you write
38
+
39
+ Suggestions that ignore what this person posts about are generic advice,
40
+ which is the one thing innernote exists to prevent.
41
+
42
+ ```bash
43
+ innernote context --json # their content pillars and running series
44
+ innernote week --json # what is scheduled over the next seven days
45
+ innernote ideas --json # thoughts they captured and have not written
46
+ innernote drafts --json # saved posts, newest first, with ids
47
+ ```
48
+
49
+ Start with `context` and `week` when helping them decide what to write: an
50
+ empty week is a real answer, and a post should land inside a pillar or a
51
+ series they have committed to.
52
+
53
+ ## The writing loop
54
+
55
+ **Capture** the moment something is worth posting about later:
56
+
57
+ ```bash
58
+ innernote capture "their thought, in their words"
59
+ git log --oneline -20 | innernote capture # stdin works too
60
+ ```
61
+
62
+ Capture their framing, not your tidied summary. Their phrasing carries their
63
+ voice; a summary throws it away.
64
+
65
+ **Draft and keep** in one command:
66
+
67
+ ```bash
68
+ innernote write "their rough thought or topic, in their words" --save
69
+ innernote write "next part of the hiring story" --series "Build in Public" --save
70
+ ```
71
+
72
+ Pass their OWN WORDS as the argument, as fully as you have them. `write`
73
+ takes a whole messy thought, and any shaping you already know they want
74
+ (shorter, no list, warmer ending) belongs in that ask, phrased plainly.
75
+ `--series` takes the series NAME; a wrong name refuses with the real list,
76
+ so correct from that and retry once. The saved id is printed on stderr and
77
+ `innernote drafts --json` lists it first.
78
+
79
+ **Shaping an already-saved post is not yours to finish here.** Each CLI
80
+ invocation is a fresh process, so the flag form of `shape` transforms
81
+ stdin to stdout without saving:
82
+
83
+ ```bash
84
+ innernote shape shorter punchier < draft.txt # transformed text, NOT saved
85
+ ```
86
+
87
+ Use that for showing the person options. To revise and keep, fold the
88
+ direction into a fresh `write ... --save`, or hand them to the app or the
89
+ interactive session (`innernote` on its own), where reshaping a held draft
90
+ does persist.
91
+
92
+ ## Scheduling: the one action that reaches other people
93
+
94
+ `innernote queue <id>` schedules a saved post into their next open slot,
95
+ and it then goes live on LinkedIn on its own. Nobody presses anything
96
+ again, and a live post cannot be unsent.
97
+
98
+ So queue ONLY when the user has clearly told you to queue that specific
99
+ post. Never as a helpful next step, never to tidy up. If they have not said
100
+ so, tell them it is saved and stop. The undo exists while it is still
101
+ pending:
102
+
103
+ ```bash
104
+ innernote queue p17abc... # their explicit ask, and only then
105
+ innernote unqueue p17abc... # back to drafts, nothing goes out
106
+ ```
107
+
108
+ If `queue` exits `3`, their plan or allowance is the reason; show the
109
+ message, which says where to fix it, and leave the decision with them.
110
+
111
+ ## Worked example
112
+
113
+ The user says: "turn what I told you about interview homework into a post
114
+ for my build-in-public series, keep it short, and line it up for this week."
115
+
116
+ ```bash
117
+ innernote context --json # confirm the series name
118
+ innernote week --json # is there an open day?
119
+ innernote write "interview homework filters for free time, not talent. we swapped ours for a paid 40 minute working session and offer accepts went up. keep it short." --series "Build in Public" --save
120
+ innernote drafts --json # take the first id
121
+ innernote queue <that-id> # they said "line it up": that is the go-ahead
122
+ ```
123
+
124
+ Then tell them what was drafted, where it was queued, and that `unqueue`
125
+ takes it back any time before it goes live.