@brianlovin/notion-skills
Version:
Sync agent skills from a Notion database to Claude Code, Codex, OpenCode, Cursor, Gemini CLI.
160 lines • 6.33 kB
JavaScript
/**
* Error translation: turn raw exceptions from ntn / Notion / our internals
* into human-readable messages with optional recovery hints.
*
* Pattern matching is keyed off the message text rather than instanceof
* checks because errors cross language and process boundaries (ntn shells
* out, Notion returns JSON, we wrap with our own classes). The matchers
* are dumb regexes — when they fire, the original message is shown
* alongside the friendly version so we never hide context.
*
* Recoveries are PRINTED, not executed. Spawning a re-entrant CLI from
* inside a failing command was clever; printing the command the user can
* type is dumb and obviously correct.
*/
import chalk from "chalk";
const PATTERNS = [
{
match: (t) => /API token is invalid/i.test(t) || /NtnAuthError/.test(t),
build: () => ({
summary: "Notion auth has expired or `ntn` is in a stuck state.",
detail: "Sometimes `ntn doctor` reports a valid token while API calls still fail. " +
"The reliable fix is a full re-login.",
suggest: "ntn logout && ntn login",
}),
},
{
match: (t) => /ntn` is not installed/i.test(t) || /NtnNotInstalledError/.test(t),
build: () => ({
summary: "`ntn` isn't installed.",
detail: "notion-skills uses Notion's official CLI for API access.",
suggest: "Install: https://github.com/makenotion/cli",
}),
},
{
match: (t) => /is expected to be (select|rich_text|checkbox|multi_select|title)/i.test(t),
build: () => ({
summary: "Notion's database schema doesn't match what notion-skills expects.",
detail: "A property's type is different in Notion than in our schema. " +
"This usually happens after upgrading the CLI.",
suggest: "notion-skills upgrade",
}),
},
{
match: (t) => /Could not find database/i.test(t) || /Could not find page/i.test(t),
build: () => ({
summary: "Notion couldn't find the database or page you referenced.",
detail: "Either the URL/ID is wrong, the page was deleted, or `ntn` is " +
"logged in to a different workspace than the one containing it.",
}),
},
{
match: (t) => /Can't edit block that is archived/i.test(t),
build: () => ({
summary: "That page is in Notion's trash.",
detail: "Restore it from Notion's trash or recreate the page.",
}),
},
{
match: (t) => /ENOTFOUND|EAI_AGAIN|getaddrinfo|fetch failed/i.test(t),
build: () => ({
summary: "Couldn't reach the Notion API.",
detail: "Network looks unavailable. Check your connection and try again.",
}),
},
{
match: (t) => /No scope configured/i.test(t),
build: () => ({
summary: "notion-skills isn't configured yet.",
suggest: "notion-skills init",
}),
},
{
match: (t) => /Not logged in/i.test(t),
build: () => ({
summary: "Not logged in to Notion.",
suggest: "ntn login",
}),
},
{
match: (t) => /GitHub rate limit hit/i.test(t),
build: (t) => ({
summary: "GitHub rate limit hit while fetching the skill repo.",
detail: "The hint about reset time is in the raw error below. Anonymous " +
"requests cap at 60/hour; authenticated calls get 5,000/hour.",
suggest: t.includes("GITHUB_TOKEN")
? "export GITHUB_TOKEN=$(gh auth token) # or set a personal token"
: undefined,
}),
},
{
match: (t) => /GitHub fetch timed out/i.test(t),
build: () => ({
summary: "GitHub took too long to respond.",
detail: "Default timeout is 10s. If your network is slow or the file is unusually large, raise the limit:",
suggest: "NOTION_SKILLS_FETCH_TIMEOUT_MS=30000 notion-skills add ...",
}),
},
{
match: (t) => /GitHub API returned 404/i.test(t),
build: () => ({
summary: "GitHub couldn't find that repo, branch, or tag.",
detail: "Check the spelling. If the repo is private, set GITHUB_TOKEN " +
"(or run `gh auth login`) so we can authenticate.",
}),
},
{
match: (t) => /Only github\.com URLs are supported/i.test(t),
build: () => ({
summary: "Only github.com is supported as a source host.",
detail: "GitLab and self-hosted Git providers aren't wired up yet. If " +
"you have a use case for GitHub Enterprise, file an issue.",
}),
},
{
match: (t) => /Unknown source ".+"\. Configured/i.test(t),
build: () => ({
summary: "That source key isn't configured on this machine.",
suggest: "notion-skills source list",
}),
},
{
match: (t) => /Multiple sources configured and no default/i.test(t),
build: () => ({
summary: "Multiple sources configured; need to know which to use.",
detail: "Pass `--source <key>` for this command, or set a default with " +
"`notion-skills source default <key>` so unscoped commands route there.",
}),
},
];
export function translateError(err) {
const raw = err instanceof Error ? err.message : String(err);
for (const p of PATTERNS) {
if (p.match(raw)) {
return { ...p.build(raw), raw };
}
}
return { summary: raw, raw };
}
/**
* Print a translated error to stderr. Recovery commands are SUGGESTED,
* never executed automatically. Returns 1 (process exit code).
*/
export function reportError(err) {
const f = translateError(err);
console.error("");
console.error(chalk.red(`✗ ${f.summary}`));
if (f.detail) {
for (const line of f.detail.split("\n")) {
console.error(chalk.dim(` ${line}`));
}
}
if (f.suggest) {
console.error(chalk.dim(` → ${f.suggest}`));
}
if (f.summary !== f.raw) {
console.error(chalk.dim(` (raw: ${f.raw.split("\n")[0]})`));
}
return 1;
}
//# sourceMappingURL=errors.js.map