🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@sakasegawa/ncli

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sakasegawa/ncli - npm Package Compare versions

Comparing version
0.2.0
to
0.3.0
+400
-3
dist/index.js

@@ -45,2 +45,41 @@ #!/usr/bin/env node

}
function restErrorToCliError(status, apiError) {
const message = apiError.message || "Unknown REST API error";
if (status === 401) {
return new CliError(
"REST API authentication failed",
message,
'Check your integration token. Set NOTION_API_KEY env var, or run "ncli rest login"'
);
}
if (status === 403) {
return new CliError(
"REST API access denied",
message,
"The integration may not have access to this resource. Check connection settings at notion.so/profile/integrations"
);
}
if (status === 404) {
return new CliError(
"REST API resource not found",
message,
"The integration may not have access to this page. Go to https://www.notion.so/profile/integrations/internal \u2192 select your integration \u2192 Content access tab \u2192 edit access to add pages."
);
}
if (status === 429) {
return new CliError(
"REST API rate limited",
message,
"Wait a moment and retry. The CLI retries automatically up to 3 times"
);
}
if (apiError.code === "validation_error") {
return new CliError(
"REST API validation error",
message,
'Check required fields. Run "ncli rest --help" for usage'
);
}
return new CliError(`REST API error (${status})`, message);
}
async function withRetry(fn, opts = {}) {

@@ -85,2 +124,5 @@ const { maxRetries = 3, baseDelayMs = 1e3 } = opts;

var AUTH_TIMEOUT_MS = 12e4;
var NOTION_API_BASE_URL = "https://api.notion.com/v1";
var NOTION_API_VERSION = "2026-03-11";
var REST_TOKEN_ENV_VAR = "NOTION_API_KEY";

@@ -275,2 +317,12 @@ // src/auth/callback-server.ts

}
readRestToken() {
const data = this.readJson("rest-token.json");
return data?.token;
}
saveRestToken(token) {
this.writeJson("rest-token.json", { token });
}
deleteRestToken() {
this.deleteFile("rest-token.json");
}
deleteAll() {

@@ -280,2 +332,3 @@ this.deleteTokens();

this.deleteCodeVerifier();
this.deleteRestToken();
}

@@ -463,2 +516,22 @@ };

}
function formatRestOutput(result, opts) {
if (opts.json) {
return JSON.stringify(result, null, 2);
}
if (opts.raw) {
return JSON.stringify(result);
}
return JSON.stringify(result, null, 2);
}
function isEmptyListResponse(result) {
return result.object === "list" && Array.isArray(result.results) && result.results.length === 0;
}
function printRestOutput(result, opts) {
console.log(formatRestOutput(result, opts));
if (isEmptyListResponse(result)) {
console.error(
"\nNote: No results found. If you expected results, ensure your integration has access to pages.\n Go to https://www.notion.so/profile/integrations/internal \u2192 select your integration \u2192 Content access tab \u2192 edit access to add pages."
);
}
}

@@ -693,2 +766,184 @@ // src/util/stdin.ts

// src/commands/file.ts
import path4 from "path";
// src/rest/client.ts
import fs2 from "fs";
import path3 from "path";
var MIME_TYPES = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".svg": "image/svg+xml",
".pdf": "application/pdf",
".txt": "text/plain",
".csv": "text/csv",
".json": "application/json",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".zip": "application/zip"
};
function guessMimeType(fileName) {
const ext = path3.extname(fileName).toLowerCase();
return MIME_TYPES[ext] || "application/octet-stream";
}
function buildRestUrl(basePath) {
const normalized = basePath.startsWith("/") ? basePath : `/${basePath}`;
return `${NOTION_API_BASE_URL}${normalized}`;
}
function buildRestHeaders(token, contentType) {
const headers = {
Authorization: `Bearer ${token}`,
"Notion-Version": NOTION_API_VERSION
};
if (contentType) {
headers["Content-Type"] = contentType;
}
return headers;
}
var NotionRestClient = class {
constructor(token) {
this.token = token;
}
async request(method, apiPath, body) {
const url = buildRestUrl(apiPath);
const headers = buildRestHeaders(this.token, body ? "application/json" : void 0);
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : void 0
});
const json = await response.json();
if (!response.ok) {
throw restErrorToCliError(response.status, json);
}
return json;
}
async uploadFile(filePath, displayName) {
const absolutePath = path3.resolve(filePath);
if (!fs2.existsSync(absolutePath)) {
throw new CliError(
"File not found",
`The file "${filePath}" does not exist`,
"Check the file path and try again"
);
}
const fileBuffer = fs2.readFileSync(absolutePath);
const fileName = displayName || path3.basename(absolutePath);
const mimeType = guessMimeType(fileName);
const createResult = await this.request("POST", "/file_uploads", {
mode: "single_part",
filename: fileName,
content_type: mimeType
});
const fileUploadId = createResult.id;
const formData = new FormData();
formData.append("file", new Blob([fileBuffer], { type: mimeType }), fileName);
const sendHeaders = buildRestHeaders(this.token);
const sendResponse = await fetch(`${NOTION_API_BASE_URL}/file_uploads/${fileUploadId}/send`, {
method: "POST",
headers: sendHeaders,
body: formData
});
const sendResult = await sendResponse.json();
if (!sendResponse.ok) {
throw restErrorToCliError(sendResponse.status, sendResult);
}
return sendResult;
}
};
// src/rest/with-rest-client.ts
function resolveRestToken() {
const envToken = process.env[REST_TOKEN_ENV_VAR];
if (envToken) {
return envToken;
}
const store = new TokenStore(CONFIG_DIR);
const storedToken = store.readRestToken();
if (storedToken) {
return storedToken;
}
throw new CliError(
"No REST API token configured",
"Notion REST API requires an integration token (Bearer token)",
`Set ${REST_TOKEN_ENV_VAR} env var, or run "ncli rest login" to save a token`
);
}
async function withRestClient(fn) {
const token = resolveRestToken();
const client = new NotionRestClient(token);
try {
return await withRetry(() => fn(client));
} catch (error) {
process.exitCode = 1;
throw error;
}
}
// src/commands/file.ts
function buildFileUploadArgs(filePath, opts) {
if (!filePath) {
throw new CliError(
"Missing file path",
"A local file path is required",
"Usage: ncli file upload <file-path>"
);
}
return { filePath, displayName: opts.name };
}
function buildAttachHint(fileUploadId, fileName) {
const block = JSON.stringify({
children: [
{
type: "file",
file: {
type: "file_upload",
file_upload: { id: fileUploadId },
name: fileName
}
}
]
});
return `
Attach to a page (appends to end):
ncli rest PATCH /blocks/<page-id>/children '${block}'
Insert after a specific block:
ncli rest PATCH /blocks/<page-id>/children '{"position":{"type":"after_block","after_block":{"id":"<block-id>"}},"children":[...]}'
Use "ncli fetch <page-id>" or "ncli rest GET /blocks/<page-id>/children" to find block IDs.`;
}
function registerFileCommands(program2) {
const file = program2.command("file").description("Upload files to Notion (REST API)");
file.command("upload").description("Upload a file to Notion (returns file_upload_id for attaching to pages)").argument("<file-path>", "Local file path to upload").option("--name <display-name>", "Display name for the file in Notion").addHelpText(
"after",
`
Examples:
ncli file upload ./screenshot.png
ncli file upload ./report.pdf --name "Q1 Report"
Requires REST API authentication:
ncli rest login # Save integration token (one-time)
NOTION_API_KEY=ntn_... # Or set env var
After uploading, attach the file to a page:
ncli rest PATCH /blocks/<page-id>/children '{"children":[{"type":"file","file":{"type":"file_upload","file_upload":{"id":"<file_upload_id>"},"name":"file.png"}}]}'
Insert after a specific block:
ncli rest PATCH /blocks/<page-id>/children '{"position":{"type":"after_block","after_block":{"id":"<block-id>"}},"children":[...]}'
Use "ncli fetch <page-id>" or "ncli rest GET /blocks/<page-id>/children" to find block IDs.`
).action(async (filePath, opts, cmd) => {
const args = buildFileUploadArgs(filePath, opts);
await withRestClient(async (client) => {
const result = await client.uploadFile(args.filePath, args.displayName);
printRestOutput(result, cmd.optsWithGlobals());
const fileUploadId = result.id;
const fileName = args.displayName || path4.basename(args.filePath);
console.error(buildAttachHint(fileUploadId, fileName));
});
});
}
// src/commands/login.ts

@@ -909,2 +1164,136 @@ function registerLoginCommands(program2) {

// src/commands/rest.ts
var VALID_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PATCH", "DELETE"]);
function buildRestCall(method, apiPath, jsonStr) {
const upperMethod = method.toUpperCase();
if (!VALID_METHODS.has(upperMethod)) {
throw new CliError(
`Invalid HTTP method: ${method}`,
"Supported methods are GET, POST, PATCH, DELETE",
"Example: ncli rest GET /pages/<id>"
);
}
const normalizedPath = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
if (!jsonStr || jsonStr.trim() === "") {
return { method: upperMethod, path: normalizedPath };
}
const body = parseJsonData(jsonStr);
return { method: upperMethod, path: normalizedPath, body };
}
function registerRestCommands(program2) {
const rest = program2.command("rest").description("Call Notion REST API directly \u2014 requires integration token").addHelpText(
"after",
`
Setup:
ncli rest login # Save integration token (one-time)
Usage:
ncli rest GET /users/me # Verify auth
ncli rest GET /pages/<page-id> # Get a page
ncli rest POST /search '{"query":"test"}' # Search
ncli file upload <page-id> ./image.png # Upload a file
The integration must have access to target pages.
Go to https://www.notion.so/profile/integrations/internal
\u2192 select your integration \u2192 Content access \u2192 add pages.`
);
rest.command("login").description("Save a Notion integration token for REST API access").addHelpText(
"after",
`
Examples:
ncli rest login # Enter token interactively
echo "ntn_..." | ncli rest login # Pipe token from stdin
Get your integration token at:
https://www.notion.so/profile/integrations`
).action(async () => {
let token;
if (!process.stdin.isTTY) {
token = (await readStdin()).trim();
} else {
console.log(`Get your integration token at: https://www.notion.so/profile/integrations
Create a new integration or copy the token from an existing one.
`);
process.stdout.write("Enter your Notion integration token: ");
token = await new Promise((resolve) => {
const chunks = [];
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding("utf-8");
const onData = (data) => {
for (const ch of data) {
if (ch === "\r" || ch === "\n") {
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdin.removeListener("data", onData);
process.stdout.write("\n");
resolve(Buffer.concat(chunks).toString("utf-8").trim());
return;
}
if (ch === "") {
process.stdin.setRawMode(false);
process.exit(1);
}
if (ch === "\x7F" || ch === "\b") {
if (chunks.length > 0) {
chunks.pop();
process.stdout.write("\b \b");
}
} else {
chunks.push(Buffer.from(ch));
process.stdout.write("*");
}
}
};
process.stdin.on("data", onData);
});
}
if (!token) {
throw new CliError(
"No token provided",
"An integration token is required",
"Get your token at https://www.notion.so/profile/integrations"
);
}
const store = new TokenStore(CONFIG_DIR);
store.saveRestToken(token);
console.log(`Token saved.
Next steps:
1. Open the Notion page you want to access
2. Click "..." (top right) \u2192 "Connections" \u2192 Add your integration
3. Verify: ncli rest GET /users/me`);
});
rest.command("logout").description("Remove saved REST API integration token").action(() => {
const store = new TokenStore(CONFIG_DIR);
store.deleteRestToken();
console.log("REST API token removed.");
});
rest.command("call", { isDefault: true }).description("Make a REST API request").argument("<method>", "HTTP method (GET, POST, PATCH, DELETE)").argument("<path>", "API path (e.g. /pages/<id>, /databases/<id>)").argument("[json]", "JSON request body").addHelpText(
"after",
`
Examples:
ncli rest GET /users/me
ncli rest GET /pages/<page-id>
ncli rest POST /pages '{"parent":{"page_id":"..."},"properties":{"title":[{"text":{"content":"New Page"}}]}}'
ncli rest PATCH /blocks/<block-id>/children '{"children":[...]}'
ncli rest DELETE /blocks/<block-id>
echo '{"query":"test"}' | ncli rest POST /search
REST API docs: https://developers.notion.com/reference`
).action(
async (method, apiPath, json, _opts, cmd) => {
let jsonStr = json;
if (!jsonStr && !process.stdin.isTTY) {
jsonStr = (await readStdin()).trim();
}
const call = buildRestCall(method, apiPath, jsonStr);
await withRestClient(async (client) => {
const result = await client.request(call.method, call.path, call.body);
printRestOutput(result, cmd.optsWithGlobals());
});
}
);
}
// src/commands/search.ts

@@ -1005,4 +1394,4 @@ function buildSearchCall(query) {

var program = new Command().name("ncli").version("0.2.0").description(
"ncli \u2014 read and write Notion from the terminal.\nAll commands support --json and --raw for structured output."
).option("--json", "Output as JSON (structured, parseable)").option("--raw", "Output raw MCP response (full server payload)").option("--verbose", "Verbose output").option("--no-color", "Disable colors").configureOutput({
"ncli \u2014 read and write Notion from the terminal.\nMCP commands use OAuth. REST API commands (rest, file) use integration token."
).option("--json", "Output as JSON (structured, parseable)").option("--raw", "Output raw response (full server payload)").option("--verbose", "Verbose output").option("--no-color", "Disable colors").configureOutput({
writeErr: (str) => {

@@ -1019,3 +1408,3 @@ process.stderr.write(str);

`
Quick start:
Quick start (MCP \u2014 OAuth auth):
ncli search "keyword" # Find pages/databases

@@ -1026,2 +1415,8 @@ ncli fetch <id> # Get content (use ID from search results)

Quick start (REST API \u2014 integration token):
ncli rest login # Save integration token (one-time)
ncli rest GET /users/me # Verify auth
ncli rest GET /pages/<id> # Get page via REST API
ncli file upload <page-id> ./image.png # Upload a file
Workflow: search \u2192 fetch (get IDs/schema) \u2192 create/update/query

@@ -1043,2 +1438,4 @@ For databases: always "ncli fetch <db-id>" first to get data_source_id.

registerApiCommand(program);
registerRestCommands(program);
registerFileCommands(program);

@@ -1045,0 +1442,0 @@ // src/index.ts

+2
-2
{
"name": "@sakasegawa/ncli",
"version": "0.2.0",
"description": "CLI wrapper for Notion's Remote MCP server",
"version": "0.3.0",
"description": "CLI for Notion — MCP + REST API support",
"type": "module",

@@ -6,0 +6,0 @@ "bin": {

@@ -11,3 +11,3 @@ # ncli

[Remote Notion MCP](https://mcp.notion.com/mcp) の CLI ラッパー — ターミナルから Notion を読み書きできます。
Notion 用 CLI — MCP + REST API 対応。ターミナルから Notion を読み書きできます。

@@ -19,2 +19,5 @@ 人間とコーディングエージェント(Claude Code, Codex 等)の両方に最適化。出力はすべて構造化 JSON、エラーにはリカバリーヒント付き。

- Notion ワークスペースへのフルアクセス: 検索、ページ、データベース、ビュー、コメント、ユーザー、チーム、ミーティングノート
- REST API 直接アクセス: `ncli rest` で任意の Notion API を呼び出し
- ファイルアップロード: `ncli file upload` でページにファイルを添付
- デュアル認証: OAuth(MCP コマンド)+ インテグレーショントークン(`NOTION_API_KEY` 環境変数 または `ncli rest login`)
- OAuth 2.0 + PKCE 認証(ブラウザベース、設定不要)

@@ -52,2 +55,16 @@ - Agent-first 設計: `--json` 出力、構造化エラーヒント(What + Why + Hint)

### REST API
```bash
# REST API セットアップ(インテグレーショントークン)
ncli rest login
# REST API を直接呼び出し
ncli rest GET /users/me
ncli rest GET /pages/<page-id>
# ファイルをアップロード
ncli file upload ./image.png
```
## コマンド一覧

@@ -76,2 +93,6 @@

| `ncli meeting-notes query` | ミーティングノートをフィルタ付きでクエリ |
| `ncli rest login` | REST API インテグレーショントークンを保存 |
| `ncli rest logout` | 保存済み REST API トークンを削除 |
| `ncli rest <METHOD> <path> [json]` | 任意の Notion REST API エンドポイントを呼び出し |
| `ncli file upload <file-path>` | ファイルをアップロード(file_upload_id を返し、ページへの添付方法を案内) |
| `ncli api <tool> [json]` | MCP ツールを直接呼び出し(エスケープハッチ) |

@@ -153,2 +174,3 @@

- Notion アカウント(ブラウザ経由の OAuth 認証)
- Notion インテグレーショントークン(REST API コマンド用 — https://www.notion.so/profile/integrations で取得)

@@ -155,0 +177,0 @@ ## 法的情報

@@ -11,3 +11,3 @@ # ncli

CLI wrapper for [Remote Notion MCP](https://mcp.notion.com/mcp) — read and write Notion from the terminal.
CLI for Notion — MCP + REST API support. Read and write Notion from the terminal.

@@ -19,2 +19,5 @@ Designed for both humans and coding agents (Claude Code, Codex, etc.). All output is structured JSON with recovery hints on errors.

- Full Notion workspace access: search, pages, databases, views, comments, users, teams, meeting notes
- REST API direct access: `ncli rest` for arbitrary Notion API calls
- File upload: `ncli file upload` for attaching files to pages
- Dual auth: OAuth (MCP commands) + integration token (`NOTION_API_KEY` env var or `ncli rest login`)
- OAuth 2.0 + PKCE authentication (browser-based, zero-config)

@@ -52,2 +55,16 @@ - Agent-first design: `--json` output, structured error hints (What + Why + Hint)

### REST API
```bash
# REST API setup (integration token)
ncli rest login
# Call REST API directly
ncli rest GET /users/me
ncli rest GET /pages/<page-id>
# Upload a file
ncli file upload ./image.png
```
## Commands

@@ -76,2 +93,6 @@

| `ncli meeting-notes query` | Query meeting notes with filters |
| `ncli rest login` | Save REST API integration token |
| `ncli rest logout` | Remove saved REST API token |
| `ncli rest <METHOD> <path> [json]` | Call any Notion REST API endpoint directly |
| `ncli file upload <file-path>` | Upload a file (returns file_upload_id for attaching to pages) |
| `ncli api <tool> [json]` | Call any MCP tool directly (escape hatch) |

@@ -153,2 +174,3 @@

- A Notion account (OAuth authentication via browser)
- Notion integration token (for REST API commands — get at https://www.notion.so/profile/integrations)

@@ -155,0 +177,0 @@ ## Legal