ntion 0.1.2 → 0.2.0

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 CHANGED
@@ -35,6 +35,14 @@ A single "get all my tasks" workflow tells the whole story:
35
35
  2. **No schema bloat** — MCP's database fetch includes ~2 KB of SQLite DDL, ~800 B of XML boilerplate, and ~1.4 KB of base64 `collectionPropertyOption://` URLs that are never used for reads. ntion returns only actionable data.
36
36
  3. **Markdown-first** — Page content defaults to markdown, matching what agents actually consume. No manual format negotiation needed.
37
37
 
38
+ ## Agent skill
39
+
40
+ ntion ships with an [agent skill](https://docs.anthropic.com/en/docs/claude-code/skills) that teaches AI agents how to use the CLI. Install it with:
41
+
42
+ ```bash
43
+ npx skills add https://github.com/mbroton/notion-cli --skill ntion-cli
44
+ ```
45
+
38
46
  ## Install
39
47
 
40
48
  ```bash
@@ -9,13 +9,142 @@ function chunkText(content) {
9
9
  }
10
10
  return chunks;
11
11
  }
12
+ const DEFAULT_ANNOTATIONS = {
13
+ bold: false,
14
+ italic: false,
15
+ strikethrough: false,
16
+ code: false,
17
+ };
18
+ function findClosingStar(text, start) {
19
+ for (let i = start; i < text.length; i++) {
20
+ if (text[i] === "*" && text[i + 1] !== "*" && text[i - 1] !== "*") {
21
+ return i;
22
+ }
23
+ }
24
+ return -1;
25
+ }
26
+ function parseInlineSegments(text, inherited, linkUrl) {
27
+ const segments = [];
28
+ let i = 0;
29
+ let plain = "";
30
+ const flush = () => {
31
+ if (plain) {
32
+ segments.push({ content: plain, annotations: { ...inherited }, link: linkUrl });
33
+ plain = "";
34
+ }
35
+ };
36
+ while (i < text.length) {
37
+ // Inline code (no nesting inside)
38
+ if (text[i] === "`") {
39
+ const close = text.indexOf("`", i + 1);
40
+ if (close !== -1) {
41
+ flush();
42
+ segments.push({
43
+ content: text.slice(i + 1, close),
44
+ annotations: { ...inherited, code: true },
45
+ link: linkUrl,
46
+ });
47
+ i = close + 1;
48
+ continue;
49
+ }
50
+ }
51
+ // Link [text](url)
52
+ if (text[i] === "[") {
53
+ const bracketClose = text.indexOf("]", i + 1);
54
+ if (bracketClose !== -1 && text[bracketClose + 1] === "(") {
55
+ const parenClose = text.indexOf(")", bracketClose + 2);
56
+ if (parenClose !== -1) {
57
+ flush();
58
+ const linkText = text.slice(i + 1, bracketClose);
59
+ const url = text.slice(bracketClose + 2, parenClose);
60
+ segments.push(...parseInlineSegments(linkText, inherited, url));
61
+ i = parenClose + 1;
62
+ continue;
63
+ }
64
+ }
65
+ }
66
+ // *** bold+italic
67
+ if (text[i] === "*" && text[i + 1] === "*" && text[i + 2] === "*") {
68
+ const close = text.indexOf("***", i + 3);
69
+ if (close !== -1) {
70
+ flush();
71
+ segments.push(...parseInlineSegments(text.slice(i + 3, close), { ...inherited, bold: true, italic: true }, linkUrl));
72
+ i = close + 3;
73
+ continue;
74
+ }
75
+ }
76
+ // ** bold
77
+ if (text[i] === "*" && text[i + 1] === "*") {
78
+ const close = text.indexOf("**", i + 2);
79
+ if (close !== -1) {
80
+ flush();
81
+ segments.push(...parseInlineSegments(text.slice(i + 2, close), { ...inherited, bold: true }, linkUrl));
82
+ i = close + 2;
83
+ continue;
84
+ }
85
+ }
86
+ // * italic
87
+ if (text[i] === "*") {
88
+ const close = findClosingStar(text, i + 1);
89
+ if (close !== -1) {
90
+ flush();
91
+ segments.push(...parseInlineSegments(text.slice(i + 1, close), { ...inherited, italic: true }, linkUrl));
92
+ i = close + 1;
93
+ continue;
94
+ }
95
+ }
96
+ // ~~ strikethrough
97
+ if (text[i] === "~" && text[i + 1] === "~") {
98
+ const close = text.indexOf("~~", i + 2);
99
+ if (close !== -1) {
100
+ flush();
101
+ segments.push(...parseInlineSegments(text.slice(i + 2, close), { ...inherited, strikethrough: true }, linkUrl));
102
+ i = close + 2;
103
+ continue;
104
+ }
105
+ }
106
+ plain += text[i];
107
+ i++;
108
+ }
109
+ flush();
110
+ return segments;
111
+ }
112
+ function segmentToRichText(segment) {
113
+ return chunkText(segment.content).map((chunk) => {
114
+ const textObj = { content: chunk };
115
+ if (segment.link) {
116
+ textObj.link = { url: segment.link };
117
+ }
118
+ const obj = { type: "text", text: textObj };
119
+ const { bold, italic, strikethrough, code } = segment.annotations;
120
+ if (bold || italic || strikethrough || code) {
121
+ const annotations = {};
122
+ if (bold)
123
+ annotations.bold = true;
124
+ if (italic)
125
+ annotations.italic = true;
126
+ if (strikethrough)
127
+ annotations.strikethrough = true;
128
+ if (code)
129
+ annotations.code = true;
130
+ obj.annotations = annotations;
131
+ }
132
+ return obj;
133
+ });
134
+ }
12
135
  function toRichText(content) {
136
+ const normalized = content.length > 0 ? content : " ";
137
+ const segments = parseInlineSegments(normalized, DEFAULT_ANNOTATIONS);
138
+ if (segments.length === 0) {
139
+ return [{ type: "text", text: { content: " " } }];
140
+ }
141
+ return segments.flatMap(segmentToRichText);
142
+ }
143
+ function toPlainRichText(content) {
13
144
  const normalized = content.length > 0 ? content : " ";
14
145
  return chunkText(normalized).map((chunk) => ({
15
146
  type: "text",
16
- text: {
17
- content: chunk,
18
- },
147
+ text: { content: chunk },
19
148
  }));
20
149
  }
21
150
  function paragraphBlock(text) {
@@ -131,7 +260,7 @@ function codeBlock(text, language) {
131
260
  object: "block",
132
261
  type: "code",
133
262
  code: {
134
- rich_text: toRichText(text),
263
+ rich_text: toPlainRichText(text),
135
264
  language,
136
265
  },
137
266
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntion",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Token-efficient CLI for personal Notion workspaces",
5
5
  "type": "module",
6
6
  "license": "MIT",