innernote
v0.3.4
Published
Write LinkedIn posts in your voice, from the terminal.
Maintainers
Readme
innernote CLI
Write LinkedIn posts in your voice, from the terminal.
innernote capture "the thing you just thought of"
innernote write "why most onboarding asks for too much upfront" --save
innernote weekInstall
Needs Node 20 or newer. Nothing else.
npx innernoteRun it without installing anything, or put it on your PATH for good:
npm install -g innernoteThen connect the machine to your account:
innernote loginIt asks for a pairing code, which you get from Settings. The key it saves lasts 90 days and can be revoked from the same page.
Working on it
The source is TypeScript and runs on Bun:
bun run src/index.tsbun run build produces the published bundle in dist/, which is what npm
ships and what bun test exercises in dist.test.ts. The published file runs
on node; the source does not.
The session
Typing innernote on its own opens a session you stay in, the way claude or
psql do. Your week, then a prompt:
▍ week
▍ write "hiring your first engineer" --save
▍ capture the thing that just came up
▍ exitUp arrow for history, tab to complete a command, help for what there is.
Ctrl-C once cancels what you are typing; twice leaves.
Piped or redirected it prints your week and exits instead, so it never hangs a script waiting for input nobody is going to type.
Connect
innernote loginIt prints a URL. Open it in a browser where you are already signed in, copy the eight-character pairing code, and paste it back. The code works once and expires in ten minutes.
The token it gives you is stored at ~/.innernote/config.json, owner-read-only.
It is opaque, it is not your password, and you can revoke it from Settings at
any time.
Commands
The table below is asserted by cli/src/parity.test.ts: a command missing
from it fails the suite, so what you read here is what the binary has.
| Command | What it does |
| --- | --- |
| login [code] | connect this machine |
| logout | forget the token stored here |
| whoami | show which account is connected, and what this key can do |
| context | what you post about: your pillars and your series |
| capture <text> | save an idea to your inbox |
| write [topic] | draft a post in your voice |
| shape <shape...> | push a post shorter, punchier, warmer |
| ask <anything> | talk to the post in your own words |
| save | keep the draft you are holding |
| show | reprint the post you are holding |
| drop | put down whatever you are holding |
| open <n or id> | pull a post out of the last list and work on it |
| ideas [status] | list what you have captured |
| drafts [status] | list your posts, numbered |
| week | what is scheduled for the next seven days |
| queue <post-id> | schedule a post into your next open slot |
| unqueue <post-id> | pull it back out before it goes live |
| skill [--install] | print the agent skill, or register it for Claude Code |
write
innernote write # picks the pillar you have covered least
innernote write "hiring your first engineer"
innernote write --thought "$(cat notes/monday.md)"
innernote write "shipping fast" --format story --save
cat brief.md | innernote writeWithout --save the post is printed and nothing is stored, so you can pipe it:
innernote write "onboarding" | pbcopyThe draft comes from the same generation the app runs: same voice, same craft rules, same check against what you have already published. It is not a lighter version.
shape
The same twelve moves the app's editor has. It reads the post from stdin, so it composes:
pbpaste | innernote shape shorter punchier | pbcopy
innernote write "onboarding" --json | jq -r .content | innernote shape deslopShapes: hook example specific punchier shorter ending warmer
deslop mobile question takeaway jargon
The names are the server's. The CLI never writes the instruction itself, so "shorter" means the same thing here as it does in the editor and inside a chat.
queue
innernote queue p17a1yk936ywnan10643efjtc58dhkahA queued post goes live on its own at your next cadence slot. Nobody presses anything again. So it needs a connection you explicitly allowed to publish: tick "Let it queue posts" in Settings when you generate the pairing code. Without it you get a clear refusal, not a mystery.
innernote inside a chat can queue too, if you tick the same box for that connection. It can only queue a post you already saved and read, and your chat client asks you before it runs. Publish is in nobody's default grant, so every connection that can schedule was one you deliberately allowed.
Piping
Every command takes --json and prints the API's own payload, nothing else.
Human output and errors go to stderr, so stdout stays clean:
innernote ideas --json | jq -r '.[].content'
innernote week --json | jq 'length'
pbpaste | innernote captureExit codes
| Code | Meaning |
| --- | --- |
| 0 | it worked |
| 1 | it failed; the reason is on stderr |
| 2 | not connected, run innernote login |
| 3 | your account cannot do this right now |
Code 3 is separate on purpose. A scheduled job should be able to tell "innernote is broken" from "your trial ended", because only one of those is worth waking someone up for.
Environment
| Variable | Effect |
| --- | --- |
| INNERNOTE_TOKEN | use this token, ignore the stored one |
| INNERNOTE_API_URL | talk to another host, for development |
| NO_COLOR | plain output |
Both token variables override the config file and are never written back, which is what makes the CLI usable in CI without a second config format.
How it works
The CLI is a thin client over the same HTTP API the web app uses. It holds no database credentials and implements no product logic. Everything it can do is something the API already lets your account do, so entitlement, limits and voice all behave exactly as they do in the app, because the work happens server-side in one place.
The writer, here
▍ drafts
▍ open 3
▍ ask cut the second half and land it harder
▍ saveshape covers the twelve named moves. ask covers everything else, in your own
words: it is the same assistant the app's composer runs, so it can change the
post, answer a question about it, or say it would rather not.
show reprints whatever you are holding, and the prompt says whether it is
saved, so you always know where you are. drop puts it down.
Checking the whole flow
bun run cli/e2e.tsRuns the real user journey against a real server: connect, read pillars and series, capture an idea, draft a post, reshape it, save it, then delete everything it made. It reports what passed and cleans up after itself.
It never publishes. There is no queue step and there will not be one. A test that can publish eventually will, on a real account, to real people. Queueing is verified by asserting the refusal on a key that was not granted publish, which proves the gate without touching it.
Point it anywhere:
INNERNOTE_API_URL=http://localhost:3000 bun run cli/e2e.tsDevelopment
bun run src/index.ts --help
bun test
bun run typecheckPoint it at a local server:
INNERNOTE_API_URL=http://localhost:3000 bun run src/index.ts whoamiThis package is deliberately outside the app's workspace and has no runtime dependencies, so it cannot affect the web build.
