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 +13 -3
- package/dist/index.js +249 -103
- package/package.json +2 -1
- package/skills/innernote-cli/SKILL.md +125 -0
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,
|
|
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",
|
|
290
|
-
const room = (process.stdout.columns
|
|
291
|
-
const
|
|
292
|
-
const
|
|
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
|
|
296
|
-
|
|
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(
|
|
301
|
+
return right ? `${caramel(r.padEnd(ink))}${" ".repeat(gap)}${bold2(caramel(right))}` : caramel(r);
|
|
300
302
|
});
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
return left +
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
rows
|
|
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",
|
|
1164
|
-
const room = (process.stdout.columns
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
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
|
|
1170
|
-
|
|
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(
|
|
1231
|
+
return right ? `${caramel2(r.padEnd(ink))}${" ".repeat(gap)}${bold4(caramel2(right))}` : caramel2(r);
|
|
1174
1232
|
});
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (
|
|
1189
|
-
return left +
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
rows
|
|
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,
|
|
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 >
|
|
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
|
|
1373
|
-
if (state.
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
const sub = [];
|
|
1440
|
+
const small = [];
|
|
1441
|
+
if (state.plan)
|
|
1442
|
+
small.push(state.plan);
|
|
1377
1443
|
if (state.powers)
|
|
1378
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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(
|
|
1464
|
+
rows.push(dim2(counts.join(" ")));
|
|
1396
1465
|
if (state.next) {
|
|
1397
|
-
rows.push(
|
|
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",
|
|
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
|
-
`${
|
|
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:
|
|
1488
|
+
return { command: "ideas", why: "nothing scheduled, and you have things saved" };
|
|
1420
1489
|
}
|
|
1421
1490
|
if (state.scheduled === 0) {
|
|
1422
|
-
return { command:
|
|
1491
|
+
return { command: "write", why: "the week is empty" };
|
|
1423
1492
|
}
|
|
1424
1493
|
if (state.ideas >= 5) {
|
|
1425
|
-
return { command:
|
|
1494
|
+
return { command: "ideas", why: "your inbox is filling up" };
|
|
1426
1495
|
}
|
|
1427
|
-
return { command:
|
|
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
|
|
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:
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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,
|
|
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 >
|
|
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
|
-
|
|
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
|
|
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.
|