@octonote/cli 0.0.0-stage → 0.1.3
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 +53 -2
- package/dist/commands/agents.js +71 -0
- package/dist/commands/auth.js +155 -0
- package/dist/commands/canvas.js +102 -0
- package/dist/commands/items.js +225 -0
- package/dist/commands/note.js +111 -0
- package/dist/commands/notebook.js +45 -0
- package/dist/commands/project.js +93 -0
- package/dist/core/config.js +66 -0
- package/dist/core/context.js +64 -0
- package/dist/core/errors.js +52 -0
- package/dist/core/frontmatter.js +42 -0
- package/dist/core/http.js +124 -0
- package/dist/core/input.js +28 -0
- package/dist/core/login.js +119 -0
- package/dist/core/output.js +62 -0
- package/dist/core/refs.js +41 -0
- package/dist/core/resources.js +23 -0
- package/dist/core/run.js +19 -0
- package/dist/guide/agent-guide.js +90 -0
- package/dist/guide/getting-started.js +44 -0
- package/dist/index.js +73 -0
- package/dist/reference.js +48 -0
- package/dist/version.js +2 -0
- package/package.json +51 -4
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `octonote login`: open the browser on <web>/cli/connect, where you click
|
|
3
|
+
* Authorize; the page hands the new token to this process over a one-shot
|
|
4
|
+
* server on 127.0.0.1. The server only accepts a POST from the Octonote
|
|
5
|
+
* site carrying the random `state` we generated, then shuts down. If the
|
|
6
|
+
* browser can't reach it, the page shows the token to paste here instead.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { createServer, } from "node:http";
|
|
11
|
+
import { hostname } from "node:os";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import { CliError } from "./errors.js";
|
|
14
|
+
export const TOKEN_RE = /^ont_[A-Za-z0-9_-]{20,}$/;
|
|
15
|
+
export function openBrowser(url) {
|
|
16
|
+
const [cmd, args] = process.platform === "darwin"
|
|
17
|
+
? ["open", [url]]
|
|
18
|
+
: process.platform === "win32"
|
|
19
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
20
|
+
: ["xdg-open", [url]];
|
|
21
|
+
try {
|
|
22
|
+
const child = spawn(cmd, args, {
|
|
23
|
+
stdio: "ignore",
|
|
24
|
+
detached: true,
|
|
25
|
+
});
|
|
26
|
+
child.on("error", () => undefined);
|
|
27
|
+
child.unref();
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function browserLogin(opts) {
|
|
35
|
+
const state = randomBytes(24).toString("base64url");
|
|
36
|
+
const origin = new URL(opts.webUrl).origin;
|
|
37
|
+
let settle;
|
|
38
|
+
let fail;
|
|
39
|
+
const result = new Promise((res, rej) => {
|
|
40
|
+
settle = res;
|
|
41
|
+
fail = rej;
|
|
42
|
+
});
|
|
43
|
+
const server = createServer((req, res) => handle(req, res));
|
|
44
|
+
const cors = (res) => {
|
|
45
|
+
res.setHeader("access-control-allow-origin", origin);
|
|
46
|
+
res.setHeader("access-control-allow-methods", "POST, OPTIONS");
|
|
47
|
+
res.setHeader("access-control-allow-headers", "content-type");
|
|
48
|
+
// Chrome's Private Network Access: a public site may call localhost only if we say so.
|
|
49
|
+
res.setHeader("access-control-allow-private-network", "true");
|
|
50
|
+
res.setHeader("cache-control", "no-store");
|
|
51
|
+
};
|
|
52
|
+
function handle(req, res) {
|
|
53
|
+
cors(res);
|
|
54
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
55
|
+
if (url.pathname !== "/cb")
|
|
56
|
+
return void res.writeHead(404).end();
|
|
57
|
+
if (req.method === "OPTIONS")
|
|
58
|
+
return void res.writeHead(204).end();
|
|
59
|
+
if (req.method !== "POST" ||
|
|
60
|
+
(req.headers.origin && req.headers.origin !== origin)) {
|
|
61
|
+
return void res.writeHead(403).end();
|
|
62
|
+
}
|
|
63
|
+
let body = "";
|
|
64
|
+
req.on("data", (c) => {
|
|
65
|
+
body += c;
|
|
66
|
+
if (body.length > 10_000)
|
|
67
|
+
req.destroy();
|
|
68
|
+
});
|
|
69
|
+
req.on("end", () => {
|
|
70
|
+
let data = {};
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(body);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Fall through to the checks below.
|
|
76
|
+
}
|
|
77
|
+
if (data.state !== state || !data.token || !TOKEN_RE.test(data.token)) {
|
|
78
|
+
return void res.writeHead(400).end();
|
|
79
|
+
}
|
|
80
|
+
res
|
|
81
|
+
.writeHead(200, { "content-type": "application/json" })
|
|
82
|
+
.end('{"ok":true}');
|
|
83
|
+
settle({ token: data.token, email: data.email || null });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
await new Promise((res, rej) => {
|
|
87
|
+
server.once("error", rej);
|
|
88
|
+
server.listen(0, "127.0.0.1", () => res());
|
|
89
|
+
});
|
|
90
|
+
const port = server.address().port;
|
|
91
|
+
const url = `${opts.webUrl}/cli/connect?${new URLSearchParams({
|
|
92
|
+
cb: `http://127.0.0.1:${port}/cb`,
|
|
93
|
+
state,
|
|
94
|
+
name: hostname().split(".")[0] ?? "",
|
|
95
|
+
}).toString()}`;
|
|
96
|
+
opts.onUrl(url);
|
|
97
|
+
openBrowser(url);
|
|
98
|
+
// Paste fallback: a person can paste the token the page shows.
|
|
99
|
+
let rl = null;
|
|
100
|
+
if (process.stdin.isTTY) {
|
|
101
|
+
rl = createInterface({ input: process.stdin });
|
|
102
|
+
rl.on("line", (line) => {
|
|
103
|
+
const token = line.trim();
|
|
104
|
+
if (TOKEN_RE.test(token))
|
|
105
|
+
settle({ token, email: null });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const timer = setTimeout(() => fail(new CliError("Timed out waiting for the browser.", "LOGIN_TIMEOUT", {
|
|
109
|
+
hint: "Run `octonote login` again.",
|
|
110
|
+
})), opts.timeoutMs ?? 5 * 60_000);
|
|
111
|
+
try {
|
|
112
|
+
return await result;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
rl?.close();
|
|
117
|
+
server.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output rules (what agents rely on):
|
|
3
|
+
* - stdout carries only the result; progress and hints go to stderr.
|
|
4
|
+
* - JSON when stdout isn't a terminal (or with --json); readable text for
|
|
5
|
+
* people in a terminal (or with --pretty).
|
|
6
|
+
* - Errors: JSON `{ "error": { code, message, hint } }` on stdout when in
|
|
7
|
+
* JSON mode, else a readable line on stderr; non-zero exit code always.
|
|
8
|
+
*/
|
|
9
|
+
import { CliError } from "./errors.js";
|
|
10
|
+
let mode = process.stdout.isTTY ? "pretty" : "json";
|
|
11
|
+
let quiet = false;
|
|
12
|
+
export function setOutputMode(next) {
|
|
13
|
+
if (next.json)
|
|
14
|
+
mode = "json";
|
|
15
|
+
else if (next.pretty)
|
|
16
|
+
mode = "pretty";
|
|
17
|
+
quiet = Boolean(next.quiet);
|
|
18
|
+
}
|
|
19
|
+
export const outputMode = () => mode;
|
|
20
|
+
/** Print a result: JSON as-is, or `pretty(data)` for people. */
|
|
21
|
+
export function emit(data, pretty) {
|
|
22
|
+
if (mode === "json") {
|
|
23
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const text = pretty ? pretty(data) : JSON.stringify(data, null, 2);
|
|
27
|
+
if (text)
|
|
28
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
29
|
+
}
|
|
30
|
+
/** Progress / notes for people, on stderr so stdout stays clean. */
|
|
31
|
+
export function info(message) {
|
|
32
|
+
if (!quiet && mode === "pretty")
|
|
33
|
+
process.stderr.write(`${message}\n`);
|
|
34
|
+
}
|
|
35
|
+
export function reportError(err) {
|
|
36
|
+
const e = err instanceof CliError
|
|
37
|
+
? err
|
|
38
|
+
: new CliError(err instanceof Error ? err.message : String(err), "INTERNAL");
|
|
39
|
+
if (mode === "json") {
|
|
40
|
+
process.stdout.write(`${JSON.stringify(e.toJSON(), null, 2)}\n`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
process.stderr.write(`✗ ${e.message}${e.code !== "INTERNAL" ? ` (${e.code})` : ""}\n`);
|
|
44
|
+
if (e.hint)
|
|
45
|
+
process.stderr.write(` ${e.hint}\n`);
|
|
46
|
+
}
|
|
47
|
+
return e.exitCode;
|
|
48
|
+
}
|
|
49
|
+
/** A plain text table for people. */
|
|
50
|
+
export function table(rows, columns) {
|
|
51
|
+
if (rows.length === 0)
|
|
52
|
+
return "(none)";
|
|
53
|
+
const widths = columns.map((c) => Math.max(c.length, ...rows.map((r) => (r[c] ?? "").length)));
|
|
54
|
+
const line = (cells) => cells
|
|
55
|
+
.map((cell, i) => cell.padEnd(widths[i]))
|
|
56
|
+
.join(" ")
|
|
57
|
+
.trimEnd();
|
|
58
|
+
return [
|
|
59
|
+
line(columns.map((c) => c.toUpperCase())),
|
|
60
|
+
...rows.map((r) => line(columns.map((c) => r[c] ?? ""))),
|
|
61
|
+
].join("\n");
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Octonote ids carry their kind (pag_ note, cnv_ canvas, prj_ project,
|
|
3
|
+
* ntb_ notebook), so most commands accept any id and know what it is.
|
|
4
|
+
*/
|
|
5
|
+
import { usage } from "./errors.js";
|
|
6
|
+
export const KIND_BY_PREFIX = {
|
|
7
|
+
pag: "page",
|
|
8
|
+
cnv: "canvas",
|
|
9
|
+
prj: "project",
|
|
10
|
+
ntb: "notebook",
|
|
11
|
+
};
|
|
12
|
+
/** What people call each kind. */
|
|
13
|
+
export const KIND_NAME = {
|
|
14
|
+
page: "note",
|
|
15
|
+
canvas: "canvas",
|
|
16
|
+
project: "project",
|
|
17
|
+
notebook: "notebook",
|
|
18
|
+
};
|
|
19
|
+
const APP_PATH = {
|
|
20
|
+
page: "n",
|
|
21
|
+
canvas: "c",
|
|
22
|
+
project: "p",
|
|
23
|
+
notebook: "nb",
|
|
24
|
+
};
|
|
25
|
+
const ID_RE = /^(pag|cnv|prj|ntb)_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
26
|
+
/** Accepts a bare id or an app / share URL that contains one. */
|
|
27
|
+
export function parseRef(input, expect) {
|
|
28
|
+
const match = input.match(/(pag|cnv|prj|ntb)_[0-9a-f-]{36}/);
|
|
29
|
+
const id = match?.[0];
|
|
30
|
+
if (!id || !ID_RE.test(id)) {
|
|
31
|
+
throw usage(`"${input}" isn't an Octonote id.`, "Ids look like pag_… (note), cnv_… (canvas), prj_… (project) or ntb_… (notebook). `octonote list` shows them.");
|
|
32
|
+
}
|
|
33
|
+
const kind = KIND_BY_PREFIX[id.slice(0, 3)];
|
|
34
|
+
if (expect && kind !== expect) {
|
|
35
|
+
throw usage(`${id} is a ${KIND_NAME[kind]}, not a ${KIND_NAME[expect]}.`);
|
|
36
|
+
}
|
|
37
|
+
return { kind, id };
|
|
38
|
+
}
|
|
39
|
+
export function appUrl(webUrl, kind, id) {
|
|
40
|
+
return `${webUrl}/${APP_PATH[kind]}/${id}`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { appUrl, KIND_NAME } from "./refs.js";
|
|
2
|
+
export function item(ctx, kind, row) {
|
|
3
|
+
return {
|
|
4
|
+
kind: KIND_NAME[kind],
|
|
5
|
+
id: row.id,
|
|
6
|
+
title: row.title ?? row.name ?? "",
|
|
7
|
+
url: appUrl(ctx.config.webUrl, kind, row.id),
|
|
8
|
+
...(row.notebookId !== undefined ? { notebookId: row.notebookId } : {}),
|
|
9
|
+
...(row.updatedAt ? { updatedAt: row.updatedAt } : {}),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function setPublished(ctx, kind, id, published) {
|
|
13
|
+
return ctx.api.put(`/${kind}/${id}/publish`, { published });
|
|
14
|
+
}
|
|
15
|
+
export function describeItem(i) {
|
|
16
|
+
return [
|
|
17
|
+
`${i.kind} ${i.title ? `“${i.title}” ` : ""}${i.id}`,
|
|
18
|
+
` ${i.url}`,
|
|
19
|
+
i.publicUrl ? ` public: ${i.publicUrl}` : "",
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
package/dist/core/run.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createContext } from "./context.js";
|
|
2
|
+
import { reportError, setOutputMode } from "./output.js";
|
|
3
|
+
export function action(handler, opts = {}) {
|
|
4
|
+
return async (...args) => {
|
|
5
|
+
// Commander passes (...positional, options, command).
|
|
6
|
+
const command = args[args.length - 1];
|
|
7
|
+
const globals = command.optsWithGlobals();
|
|
8
|
+
setOutputMode(globals);
|
|
9
|
+
try {
|
|
10
|
+
const ctx = await createContext(globals, {
|
|
11
|
+
requireAuth: opts.auth !== false,
|
|
12
|
+
});
|
|
13
|
+
await handler(ctx, ...args.slice(0, -1));
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
process.exitCode = reportError(err);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guide agents read — one source for `octonote guide`, the section
|
|
3
|
+
* `octonote setup` writes into CLAUDE.md / AGENTS.md, and the Claude Code
|
|
4
|
+
* skill. Keep it short and task-first: agents act on the first screen.
|
|
5
|
+
*/
|
|
6
|
+
/** Short pointer for CLAUDE.md / AGENTS.md (the full guide is one command away). */
|
|
7
|
+
export const PROJECT_SECTION = `## Octonote (notes & canvases)
|
|
8
|
+
|
|
9
|
+
The user keeps notes, diagrams (canvases), projects and notebooks in Octonote. Use the \`octonote\` CLI to read and write them — never ask the user to copy-paste.
|
|
10
|
+
|
|
11
|
+
- Run \`octonote guide\` once per session before using it; \`octonote <command> --help\` for details.
|
|
12
|
+
- Write notes as markdown files (mermaid, \`$math$\`, code and tables work); draw diagrams as mermaid flowcharts (\`--from-mermaid\`), which become real, editable canvases.
|
|
13
|
+
- Output is JSON when piped: read \`id\`, \`url\` and \`publicUrl\` from it, and always give the user the link.
|
|
14
|
+
- Only publish, share or delete when the user asked to.
|
|
15
|
+
`;
|
|
16
|
+
export const AGENT_GUIDE = `# Octonote CLI — guide for agents
|
|
17
|
+
|
|
18
|
+
Octonote holds the user's **notes** (markdown documents), **canvases** (diagrams),
|
|
19
|
+
**projects** (one note + one canvas, side by side) and **notebooks** (folders).
|
|
20
|
+
The \`octonote\` CLI is how you read and write them on the user's behalf.
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
- Output is JSON when piped (force with --json). Errors print
|
|
24
|
+
\`{"error":{"code","message","hint"}}\` and exit non-zero; branch on \`code\`.
|
|
25
|
+
- Never prompts. Content comes from --file <path>, --stdin, or "-" as the path.
|
|
26
|
+
- Every result has \`id\`, \`url\` (app link) and, once published, \`publicUrl\`.
|
|
27
|
+
Give the user the link at the end.
|
|
28
|
+
- Ids carry their kind: pag_ note, cnv_ canvas, prj_ project, ntb_ notebook.
|
|
29
|
+
Notebooks can also be named by exact name (--notebook "Architecture").
|
|
30
|
+
- Publish, share, invite or delete only when the user asked for it.
|
|
31
|
+
\`delete\` requires --yes.
|
|
32
|
+
- Not signed in (code NOT_LOGGED_IN)? Ask the user to run \`octonote login\`
|
|
33
|
+
(or set OCTONOTE_TOKEN). Don't try to sign in yourself.
|
|
34
|
+
|
|
35
|
+
## Write a note
|
|
36
|
+
octonote note create "Title" --file doc.md [--notebook "Name"] [--publish]
|
|
37
|
+
cat doc.md | octonote note create "Title" --stdin
|
|
38
|
+
Markdown → rich note: headings, lists, checklists, tables, code, quotes,
|
|
39
|
+
images (by URL), \`$inline math$\`, \`$$display math$$\`, and \`\`\`mermaid blocks
|
|
40
|
+
(rendered diagrams inside the note).
|
|
41
|
+
|
|
42
|
+
## Read and edit a note
|
|
43
|
+
octonote note show <id> # markdown to stdout
|
|
44
|
+
octonote note pull <id> -o note.md # file with front matter (keeps the id)
|
|
45
|
+
# …edit note.md…
|
|
46
|
+
octonote note push note.md # replaces the note's content
|
|
47
|
+
octonote note append <id> --text "- new item" # or --file / --stdin
|
|
48
|
+
octonote note rename <id> "New title"
|
|
49
|
+
|
|
50
|
+
## Draw a canvas (diagram)
|
|
51
|
+
Write a mermaid **flowchart** and let Octonote lay it out as real shapes:
|
|
52
|
+
octonote canvas create "Login flow" --from-mermaid flow.mmd [--publish]
|
|
53
|
+
octonote canvas redraw <id> --from-mermaid flow.mmd
|
|
54
|
+
Supported: \`graph|flowchart TD|LR|BT|RL\`; nodes A[box] A(round) A([stadium])
|
|
55
|
+
A((circle)) A{decision} A[(database)] A{{hexagon}}; edges --> --- -.-> ==> <-->
|
|
56
|
+
with labels (A -->|yes| B or A -- yes --> B); chains; A & B --> C;
|
|
57
|
+
\`subgraph id [Title] … end\`. Other diagram types (sequence, gantt…) can't be
|
|
58
|
+
canvases — put them in a note as a \`\`\`mermaid block instead.
|
|
59
|
+
Raw tldraw JSON also works: \`canvas pull <id> -o c.json\` / \`canvas push c.json --id <id>\`.
|
|
60
|
+
|
|
61
|
+
## Project = note + canvas
|
|
62
|
+
octonote project create "Auth flow" --note auth.md --canvas-mermaid auth.mmd \\
|
|
63
|
+
[--view split|notes|canvas] [--notebook "Name"] [--publish]
|
|
64
|
+
octonote project show <id> # ids of its note and canvas (edit them as above)
|
|
65
|
+
|
|
66
|
+
## Organise
|
|
67
|
+
octonote list [--kind note|canvas|project|notebook] [--notebook "Name"]
|
|
68
|
+
octonote search <words> # by title
|
|
69
|
+
octonote notebook list | create "Name" | show "Name"
|
|
70
|
+
octonote move <id> --notebook "Name" # or --out
|
|
71
|
+
octonote delete <id> --yes
|
|
72
|
+
|
|
73
|
+
## Publish and share
|
|
74
|
+
octonote publish <id> # → publicUrl (a public, read-only page)
|
|
75
|
+
octonote unpublish <id>
|
|
76
|
+
octonote share <id> --email person@example.com --role viewer|editor
|
|
77
|
+
octonote share <id> --link view|edit|off # anyone with the app link
|
|
78
|
+
Publishing a notebook publishes everything inside it.
|
|
79
|
+
|
|
80
|
+
## Account
|
|
81
|
+
octonote whoami | workspace list | workspace use <name|id> | logout
|
|
82
|
+
octonote account rename "Full Name" # display name (author on published pages)
|
|
83
|
+
`;
|
|
84
|
+
/** Claude Code skill file (.claude/skills/octonote/SKILL.md). */
|
|
85
|
+
export const CLAUDE_SKILL = `---
|
|
86
|
+
name: octonote
|
|
87
|
+
description: Read and write the user's Octonote notes, canvases (diagrams), projects and notebooks with the octonote CLI — create notes from markdown, draw diagrams from mermaid flowcharts, publish and share them. Use when the user mentions Octonote, their notes or notebooks, or asks to document, diagram, write up or publish something there.
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
${AGENT_GUIDE}`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `octonote` prints with no command — right after install, too, since
|
|
3
|
+
* the install line is `npm i -g … && octonote`. (npm hides messages printed
|
|
4
|
+
* during install, so this is where setup help lives.) Everything needed to
|
|
5
|
+
* get going, without opening the docs.
|
|
6
|
+
*/
|
|
7
|
+
export const DOCS_URL = "https://octonote.ai/cli";
|
|
8
|
+
export function gettingStarted(opts) {
|
|
9
|
+
const signIn = opts.signedInAs
|
|
10
|
+
? ` ✓ Signed in as ${opts.signedInAs}`
|
|
11
|
+
: ` 1. Sign in octonote login
|
|
12
|
+
(no browser? set OCTONOTE_TOKEN from Settings → CLI & agents)`;
|
|
13
|
+
return `
|
|
14
|
+
octonote ${opts.version} — Octonote from your terminal, Claude Code and Codex
|
|
15
|
+
|
|
16
|
+
GET STARTED
|
|
17
|
+
${signIn}
|
|
18
|
+
${opts.signedInAs ? "1." : "2."} Teach your agent octonote setup claude # this project's CLAUDE.md
|
|
19
|
+
octonote setup claude --global --skill # every project + skill
|
|
20
|
+
octonote setup codex # this project's AGENTS.md
|
|
21
|
+
octonote setup codex --global
|
|
22
|
+
Then just ask: “write this up in Octonote”, “draw this as an Octonote canvas”.
|
|
23
|
+
|
|
24
|
+
Claude Code: allow it without prompts — add to ~/.claude/settings.json:
|
|
25
|
+
{ "permissions": { "allow": ["Bash(octonote:*)"] } }
|
|
26
|
+
Codex: its sandbox blocks the network by default — in ~/.codex/config.toml:
|
|
27
|
+
[sandbox_workspace_write]
|
|
28
|
+
network_access = true
|
|
29
|
+
|
|
30
|
+
COMMON COMMANDS
|
|
31
|
+
octonote note create "Title" --file doc.md note from markdown
|
|
32
|
+
octonote note pull <id> -o doc.md edit locally, then: note push doc.md
|
|
33
|
+
octonote canvas create "Flow" --from-mermaid flow.mmd draw a diagram
|
|
34
|
+
octonote project create "Name" --note doc.md --canvas-mermaid flow.mmd
|
|
35
|
+
octonote list · search <words> · notebook list
|
|
36
|
+
octonote publish <id> · share <id> --email … --role viewer|editor
|
|
37
|
+
|
|
38
|
+
HELP
|
|
39
|
+
octonote --help all commands
|
|
40
|
+
octonote <command> --help one command, e.g. octonote note create --help
|
|
41
|
+
octonote guide the full guide written for AI agents
|
|
42
|
+
${DOCS_URL} docs on the web
|
|
43
|
+
`;
|
|
44
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* octonote — the Octonote command-line tool, built for people and for
|
|
4
|
+
* agents (Claude Code, Codex). Commands live in ./commands; shared
|
|
5
|
+
* plumbing (config, HTTP, output, errors) in ./core; the agent guide in
|
|
6
|
+
* ./guide. The CLI only talks to the public Octonote API.
|
|
7
|
+
*/
|
|
8
|
+
import { realpathSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { registerAgents } from "./commands/agents.js";
|
|
12
|
+
import { registerAuth } from "./commands/auth.js";
|
|
13
|
+
import { registerCanvas } from "./commands/canvas.js";
|
|
14
|
+
import { registerItems } from "./commands/items.js";
|
|
15
|
+
import { registerNote } from "./commands/note.js";
|
|
16
|
+
import { registerNotebook } from "./commands/notebook.js";
|
|
17
|
+
import { registerProject } from "./commands/project.js";
|
|
18
|
+
import { resolveConfig } from "./core/config.js";
|
|
19
|
+
import { DOCS_URL, gettingStarted } from "./guide/getting-started.js";
|
|
20
|
+
import { VERSION } from "./version.js";
|
|
21
|
+
export { VERSION } from "./version.js";
|
|
22
|
+
export function buildProgram() {
|
|
23
|
+
const program = new Command()
|
|
24
|
+
.name("octonote")
|
|
25
|
+
.description("Octonote from your terminal — and from Claude Code or Codex.\nAgents: run `octonote guide` first.")
|
|
26
|
+
.version(VERSION)
|
|
27
|
+
.option("--json", "JSON output (default when piped)")
|
|
28
|
+
.option("--pretty", "human output (default in a terminal)")
|
|
29
|
+
.option("-q, --quiet", "no progress messages")
|
|
30
|
+
.option("-w, --workspace <id>", "workspace to use (default: yours)")
|
|
31
|
+
.option("--api-url <url>", "Octonote API (default: OCTONOTE_API_URL or the hosted API)")
|
|
32
|
+
.option("--web-url <url>", "Octonote web app (default: OCTONOTE_WEB_URL or https://octonote.ai)")
|
|
33
|
+
.showHelpAfterError("(run with --help for usage)")
|
|
34
|
+
.configureOutput({
|
|
35
|
+
// Usage errors: say what's wrong without dumping the full help.
|
|
36
|
+
outputError: (str, write) => write(str),
|
|
37
|
+
});
|
|
38
|
+
// `octonote` on its own: the getting-started screen (also shown right
|
|
39
|
+
// after install — the install line ends in `&& octonote`).
|
|
40
|
+
program.action(async () => {
|
|
41
|
+
const { token, tokenSource, email } = await resolveConfig();
|
|
42
|
+
const signedInAs = token
|
|
43
|
+
? (email ?? (tokenSource === "env" ? "OCTONOTE_TOKEN" : "your account"))
|
|
44
|
+
: null;
|
|
45
|
+
process.stdout.write(gettingStarted({ version: VERSION, signedInAs }));
|
|
46
|
+
});
|
|
47
|
+
registerAuth(program);
|
|
48
|
+
registerNote(program);
|
|
49
|
+
registerCanvas(program);
|
|
50
|
+
registerProject(program);
|
|
51
|
+
registerNotebook(program);
|
|
52
|
+
registerItems(program);
|
|
53
|
+
registerAgents(program);
|
|
54
|
+
program.addHelpText("after", `\nGetting started (sign-in, Claude Code & Codex setup): run \`octonote\` on its own.\nAgents: \`octonote guide\`. Docs: ${DOCS_URL}`);
|
|
55
|
+
return program;
|
|
56
|
+
}
|
|
57
|
+
// Run only when executed (npm's bin link resolves to this file), not when imported by tests.
|
|
58
|
+
function isMain() {
|
|
59
|
+
try {
|
|
60
|
+
return (realpathSync(process.argv[1] ?? "") === fileURLToPath(import.meta.url));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (isMain()) {
|
|
67
|
+
buildProgram()
|
|
68
|
+
.parseAsync(process.argv)
|
|
69
|
+
.catch((err) => {
|
|
70
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AGENT_GUIDE, PROJECT_SECTION } from "./guide/agent-guide.js";
|
|
2
|
+
function options(cmd) {
|
|
3
|
+
return cmd.options
|
|
4
|
+
.filter((o) => !["--help", "--version"].includes(o.long ?? ""))
|
|
5
|
+
.map((o) => ({
|
|
6
|
+
flags: o.flags,
|
|
7
|
+
description: o.description,
|
|
8
|
+
required: o.mandatory,
|
|
9
|
+
...(o.defaultValue !== undefined
|
|
10
|
+
? { default: String(o.defaultValue) }
|
|
11
|
+
: {}),
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
export function buildReference(program, version) {
|
|
15
|
+
const commands = [];
|
|
16
|
+
const walk = (cmd, prefix) => {
|
|
17
|
+
for (const sub of cmd.commands) {
|
|
18
|
+
const path = [...prefix, sub.name()];
|
|
19
|
+
if (sub.commands.length) {
|
|
20
|
+
walk(sub, path);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
commands.push({
|
|
24
|
+
name: path.join(" "),
|
|
25
|
+
aliases: sub.aliases(),
|
|
26
|
+
description: sub.description(),
|
|
27
|
+
arguments: sub.registeredArguments.map((a) => ({
|
|
28
|
+
name: a.name(),
|
|
29
|
+
description: a.description,
|
|
30
|
+
required: a.required,
|
|
31
|
+
variadic: a.variadic,
|
|
32
|
+
...(a.defaultValue !== undefined
|
|
33
|
+
? { default: String(a.defaultValue) }
|
|
34
|
+
: {}),
|
|
35
|
+
})),
|
|
36
|
+
options: options(sub),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
walk(program, []);
|
|
41
|
+
return {
|
|
42
|
+
version,
|
|
43
|
+
globalOptions: options(program),
|
|
44
|
+
commands,
|
|
45
|
+
guide: AGENT_GUIDE,
|
|
46
|
+
projectSection: PROJECT_SECTION,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octonote/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "The octonote command-line tool: lets you — and agents like Claude Code and Codex — create, edit, publish and share Octonote notes, canvases, projects and notebooks.",
|
|
5
|
+
"homepage": "https://octonote.ai/cli",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/CoderCouple/octonote-ai.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"octonote": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json && node scripts/gen-reference.mjs",
|
|
24
|
+
"dev": "tsx src/index.ts",
|
|
25
|
+
"lint": "echo \"lint pending\"",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"check:reference": "node scripts/gen-reference.mjs --check"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^12.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.10.2",
|
|
35
|
+
"tsx": "^4.19.2",
|
|
36
|
+
"typescript": "^5.7.2",
|
|
37
|
+
"vitest": "^4.1.9"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"octonote",
|
|
41
|
+
"cli",
|
|
42
|
+
"notes",
|
|
43
|
+
"canvas",
|
|
44
|
+
"mermaid",
|
|
45
|
+
"claude",
|
|
46
|
+
"codex",
|
|
47
|
+
"ai-agent"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|