rootr-cli 0.29.0 → 0.30.1

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
@@ -11,7 +11,7 @@ citations back to the exact paragraph.
11
11
  *(한국어 문서: [README.ko.md](https://github.com/gwmage/rootr-cli/blob/main/README.ko.md))*
12
12
 
13
13
  - `rootr` CLI — documents, databases, forms, signed documents, publishing, permissions, memory
14
- - `rootr mcp` — stdio MCP server, **214 tools** (see [MCP tools](#mcp-tools))
14
+ - `rootr mcp` — stdio MCP server, **217 tools** (see [MCP tools](#mcp-tools))
15
15
 
16
16
  Plain Node.js ESM, no build step. Node 18+ (uses the built-in `fetch`).
17
17
 
@@ -331,6 +331,21 @@ cannot hold a CNAME — connect `www.` and forward the root to it (the guide sho
331
331
  Only when the same name was registered before by a *different* workspace, one extra `TXT` line
332
332
  (`_rootr-verify.www`) is required, so nobody can take over a domain whose CNAME was left behind.
333
333
 
334
+ ### Page visit tracking
335
+
336
+ Turn on GA-style tracking for a published PAGE. The numbers land in ordinary workspace nodes next to the page —
337
+ an "Analytics" folder with a visit **LOG** (one entry per visit, scroll depth, block seen, click, form / booking
338
+ step, time on page — with channel, utm, device, in-app browser, country), a **daily visits table** (one row per
339
+ KST day, refilled every 15 minutes) and a **dashboard page** of live charts. Only the published address records;
340
+ no cookies, IPs are not stored.
341
+
342
+ ```bash
343
+ rootr page track /홈페이지 on # create the folder + log + daily table + dashboard and start recording
344
+ rootr page stats /홈페이지 # last 7 days visitors / sessions / conversions + where the nodes are
345
+ rootr page recount /홈페이지 --days 30 # recount the daily table now
346
+ rootr page track /홈페이지 off # stop recording (every node stays)
347
+ ```
348
+
334
349
  ### Signed documents and research notes
335
350
 
336
351
  A signed document is a markdown document whose body **freezes the moment it is submitted**; the
@@ -489,7 +504,7 @@ is refused and says when the grace was used.
489
504
 
490
505
  ## MCP tools
491
506
 
492
- `rootr mcp` speaks MCP over stdio. 214 tools:
507
+ `rootr mcp` speaks MCP over stdio. 217 tools:
493
508
 
494
509
  | Group | Count | Tools |
495
510
  |---|---|---|
@@ -501,7 +516,7 @@ is refused and says when the grace was used.
501
516
  | Forms | 10 | create, read, update, responses, share links, collectors |
502
517
  | Presentations & images | 10 | decks, slides, diagram slides, image generation and background removal |
503
518
  | Workspaces & scaffolding | 10 | list, create, delete, switch, set the emblem (sidebar logo), plan / review / apply a document tree, health check |
504
- | Pages | 6 | HTML page nodes: create, read, update, append and patch blocks, live data |
519
+ | Pages | 7 | HTML page nodes: create, read, update, append and patch blocks, live data, visit tracking (GA-style visits / scroll / conversions stored as a LOG, a daily table and a dashboard) |
505
520
  | Spreadsheets | 7 | create, read, update, per-sheet create/update/delete, patch cells & formulas |
506
521
  | Recordings | 7 | create, upload & download audio, transcribe, summarize, rename speakers |
507
522
  | Issue tracker | 6 | create tracker, list / create / get / update issue, comment |
package/bin/rootr.js CHANGED
@@ -120,6 +120,9 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
120
120
  rootr mail webhook add <url> [--events …] | list | rm <id> | test <id>
121
121
  받은 문서·회수·읽음을 내 주소로 알려 주는 웹훅(내 계정 것, HMAC 서명)
122
122
  ※ 다른 명령 뒤 새로 받은 문서가 있으면 stderr 로 한 줄 알림(끄기: ROOTR_MAIL_NOTICE=0)
123
+ rootr page stats <페이지 path|id> [--json] 방문 기록 상태 — 최근 7일 방문자·방문·전환, 기록(LOG)·일별 표·대시보드 위치
124
+ rootr page track <페이지 path|id> on|off [--locale ko|en] 방문 기록 켜기(페이지 옆에 「방문 분석」 폴더: 방문 기록 LOG·일별 방문 표·대시보드)·끄기(노드는 남음)
125
+ rootr page recount <페이지 path|id> [--days 90] 일별 방문 표를 지금 다시 세기(보통은 15분마다 자동)
123
126
  rootr domain list 내 도메인 목록 — 종류(내 도메인·무료 주소)·상태와 가리키는 페이지, 자리 사용(유료 n/m·무료 n/3)
124
127
  rootr domain detect <도메인> [--provider <업체>] 등록 전에 — DNS 를 어디서 관리하는지(가비아·Cloudflare…)와 넣을 한 줄·단계 안내
125
128
  rootr domain add <도메인> <페이지·문서 path|id> [--provider <업체>] [--confirm-charge] 발행한 페이지·문서를 내 도메인(www.회사.co.kr)으로 연결 (Team 플랜, 도메인 1개당 월 $10·연 결제 시 월 $8 — 자리가 꽉 찼으면 --confirm-charge 로 1개 추가 결제)
@@ -256,6 +259,9 @@ usage:
256
259
  rootr mail access <id> read|comment|write change what the recipient of a document you sent can do
257
260
  rootr mail revoke <id> withdraw a document you sent — the recipient can no longer open the original
258
261
  rootr mail hide <id> [--box inbox|sent] remove from your own mailbox (a received one then stops opening for your account)
262
+ rootr page stats <page path|id> [--json] visit tracking status — last 7 days visitors/sessions/conversions, where the log, daily table and dashboard are
263
+ rootr page track <page path|id> on|off [--locale ko|en] turn visit tracking on (an "Analytics" folder next to the page: visit LOG, daily visits table, dashboard) or off (nodes stay)
264
+ rootr page recount <page path|id> [--days 90] recount the daily visits table now (normally every 15 minutes)
259
265
  rootr domain list your custom domains — status (waiting for DNS / issuing certificate / connected) and page
260
266
  rootr domain detect <domain> [--provider <id>] before adding — where the DNS is managed (Gabia, Cloudflare…), the one line to add, step by step
261
267
  rootr domain add <domain> <page|doc path|id> [--provider <id>] [--confirm-charge] open a published page or document at your own domain (Team plan; $10/month per domain, $8 billed yearly — when slots are full, --confirm-charge buys one more)
@@ -3287,6 +3293,16 @@ async function cmdDbRowPageType(args) {
3287
3293
  }
3288
3294
 
3289
3295
  /** 경로로 줘도 되게 — 표 인자도 문서 도구들과 같은 규칙을 따른다. */
3296
+ /**
3297
+ * 노드 id·"/경로"·루터 주소 어느 것이든 노드 id 로. (예전엔 이 이름을 부르기만 하고 정의가 없어 `rootr form mapping` 이
3298
+ * "resolveNodeId is not defined" 로 죽었다 — 2026-10-25 page 명령을 실제로 불러 보다 발견.)
3299
+ */
3300
+ async function resolveNodeId(client, target) {
3301
+ const resolved = await resolveTarget(client, target);
3302
+ if (resolved.existed === false) throw new Error(tr(`찾을 수 없습니다: ${target}`, `Not found: ${target}`));
3303
+ return resolved.id ?? target;
3304
+ }
3305
+
3290
3306
  async function resolveDatabaseId(client, target) {
3291
3307
  if (typeof target === 'string' && target.startsWith('/')) {
3292
3308
  const resolved = await resolveTarget(client, target);
@@ -3845,6 +3861,46 @@ function printDomainStatus(d) {
3845
3861
  if (d.message || d.lastError) console.log(` ${d.message || d.lastError}`);
3846
3862
  }
3847
3863
 
3864
+ /**
3865
+ * PAGE-ANALYTICS — `rootr page stats|track|recount <page>`. 방문 기록은 워크스페이스 노드(LOG·일별 표·대시보드)에 쌓인다 —
3866
+ * 숫자를 더 깊게 보려면 `rootr db rows <일별 표>`·`rootr log query <방문 기록>` 을 그대로 쓴다.
3867
+ */
3868
+ async function cmdPage(args) {
3869
+ const sub = args._[0];
3870
+ const target = args._[1];
3871
+ const usageText = tr(
3872
+ 'rootr page stats <페이지> | rootr page track <페이지> on|off [--locale ko|en] | rootr page recount <페이지> [--days 90]',
3873
+ 'rootr page stats <page> | rootr page track <page> on|off [--locale ko|en] | rootr page recount <page> [--days 90]',
3874
+ );
3875
+ if (!target || !['stats', 'track', 'recount'].includes(sub)) throw new Error(usageText);
3876
+ const client = makeClient();
3877
+ const id = await resolveNodeId(client, target);
3878
+ let out;
3879
+ if (sub === 'track') {
3880
+ const mode = args._[2];
3881
+ if (mode !== 'on' && mode !== 'off') throw new Error(usageText);
3882
+ out = await client.setPageAnalytics(id, { enabled: mode === 'on', locale: args.locale });
3883
+ } else if (sub === 'recount') {
3884
+ out = (await client.refreshPageAnalytics(id, args.days)).status;
3885
+ } else {
3886
+ out = await client.getPageAnalytics(id);
3887
+ }
3888
+ if (args.json) return console.log(JSON.stringify(out, null, 2));
3889
+ console.log(out.enabled ? tr('방문 기록: 켜짐', 'Visit tracking: on') : tr('방문 기록: 꺼짐', 'Visit tracking: off'));
3890
+ if (out.last7d) {
3891
+ console.log(tr(
3892
+ `최근 7일 방문자 ${out.last7d.visitors} · 방문 ${out.last7d.sessions} · 전환 ${out.last7d.conversions}`,
3893
+ `Last 7 days visitors ${out.last7d.visitors} · sessions ${out.last7d.sessions} · conversions ${out.last7d.conversions}`,
3894
+ ));
3895
+ }
3896
+ for (const [label, n] of [[tr('대시보드', 'Dashboard'), out.dashboard], [tr('일별 표', 'Daily table'), out.daily], [tr('방문 기록', 'Visit log'), out.log]]) {
3897
+ if (n) console.log(` ${label}\t${n.name}\t${n.id}${n.url ? `\t${n.url}` : ''}`);
3898
+ }
3899
+ if (out.enabled && !out.lastEventAt) {
3900
+ console.log(tr(' 아직 기록 없음 — 웹에 게시된 주소에서만 기록됩니다(rootr publish).', ' No visits yet — only the published address records (rootr publish).'));
3901
+ }
3902
+ }
3903
+
3848
3904
  async function cmdDomain(args) {
3849
3905
  const sub = args._[0];
3850
3906
  const client = makeClient();
@@ -4040,6 +4096,9 @@ async function main() {
4040
4096
  case 'domain':
4041
4097
  case 'domains':
4042
4098
  return cmdDomain(args);
4099
+ case 'page':
4100
+ case 'pages':
4101
+ return cmdPage(args);
4043
4102
  case 'feedback':
4044
4103
  return cmdFeedback(args);
4045
4104
  case 'scaffold':
package/lib/client.js CHANGED
@@ -1700,6 +1700,20 @@ export class RootrClient {
1700
1700
  return this.request(`/pages/${encodeURIComponent(pageId)}/live-data`);
1701
1701
  }
1702
1702
 
1703
+ // PAGE-ANALYTICS — 방문 기록(켜면 LOG·일별 표·대시보드가 페이지 옆에 선다)
1704
+ async getPageAnalytics(pageId) {
1705
+ return this.request(`/pages/${encodeURIComponent(pageId)}/analytics`);
1706
+ }
1707
+
1708
+ async setPageAnalytics(pageId, { enabled, locale } = {}) {
1709
+ return this.request(`/pages/${encodeURIComponent(pageId)}/analytics`, { method: 'POST', body: { enabled, ...(locale ? { locale } : {}) } });
1710
+ }
1711
+
1712
+ async refreshPageAnalytics(pageId, days) {
1713
+ const q = days ? `?days=${encodeURIComponent(days)}` : '';
1714
+ return this.request(`/pages/${encodeURIComponent(pageId)}/analytics/refresh${q}`, { method: 'POST' });
1715
+ }
1716
+
1703
1717
  // ---- Presentations (slide decks) ----------------------------------------
1704
1718
 
1705
1719
  async createPresentation(workspaceId, { name, parentId, theme, slides, config, icon } = {}) {
@@ -100,8 +100,9 @@ const BLOCK_GUIDE =
100
100
  'that name. The server resolves it and stores the id (the response shows the id). A path or name it cannot resolve — ' +
101
101
  'missing, not readable by you, wrong node type, or a name shared by two nodes — is rejected with 400 and NOTHING is ' +
102
102
  'saved; it is never stored as a silently empty block. `column`/`labelColumn`/' +
103
- '`valueColumn`/`orderBy.column` are the column NAMES as shown in the table (not column ids). A table or form that ' +
104
- 'sits in a PRIVATE subtree is silently skipped on the published page — that is deliberate, never a bug to work around.\n' +
103
+ '`valueColumn`/`orderBy.column` are the column NAMES as shown in the table (not column ids). A live-block source table ' +
104
+ 'that sits in a PRIVATE subtree is silently skipped on the published page (inside the app whoever can open it still sees ' +
105
+ 'it) — that is deliberate, never a bug to work around. Form and booking blocks are not affected by PRIVATE.\n' +
105
106
  'LINKS: every `target` is {kind:"url", href} (absolute http(s)://, mailto: or tel: only), {kind:"node", nodeId} ' +
106
107
  'for a document in the same workspace, or {kind:"block", blockId} to JUMP TO ANOTHER BLOCK ON THIS SAME PAGE ' +
107
108
  '(e.g. a hero button "Book a free consult" → the booking block further down: smooth scroll in the same tab, no new tab — ' +
@@ -293,7 +294,7 @@ export function registerPageTools(server) {
293
294
  '(category {labels, series[]} · single {value, previous, goal, spark} · xy · distribution · tree · flow · ohlc · range). ' +
294
295
  'A chart that cannot draw says why: `{shape:"empty", reason:"column_missing"|"no_rows"|"needs_number", column}` — ' +
295
296
  'fix that column name or filter. A block missing from the result means its source was not found, is deleted, ' +
296
- 'or sits in a PRIVATE subtree.',
297
+ 'or you cannot open it.',
297
298
  annotations: { readOnlyHint: true, destructiveHint: false },
298
299
  inputSchema: {
299
300
  pageId: z.string().describe(`PAGE node id${NODE_TARGET_HINT}`),
@@ -309,4 +310,41 @@ export function registerPageTools(server) {
309
310
  }
310
311
  },
311
312
  );
313
+ server.registerTool(
314
+ 'rootr_page_analytics',
315
+ {
316
+ title: 'Turn on / read visit tracking for a published page',
317
+ description:
318
+ 'GA-style visit tracking for a Rootr (루터) PAGE, stored in the workspace itself. action "enable" creates, next to the page, ' +
319
+ 'a folder "<page> · 방문 분석" (Analytics) holding: a LOG node (one entry per visitor action — visit, scroll 25/50/75/90%, ' +
320
+ 'block seen, click, form seen/started/submitted, booking steps, time on page — with channel/utm/referrer/device/browser/' +
321
+ 'country columns), a DATABASE "일별 방문" (Daily visits: one row per KST day — visitors, sessions, new visitors, page views, ' +
322
+ 'avg seconds, read-half/read-to-end sessions, clicks, form submits, bookings, conversions, conversion rate; filled every 15 min), ' +
323
+ 'and a PAGE dashboard of live charts over both. Read the numbers with rootr_read_database (daily table), ' +
324
+ 'rootr_query_log_entries / rootr_log_stats (the log) or rootr_read_page_live_data (the dashboard). ' +
325
+ 'Only the PUBLISHED page records (rootr_publish_node, Team plan); the editor preview never does. "disable" stops recording ' +
326
+ 'and keeps every node. "refresh" recounts the daily table now (default 90 days). "status" (default) returns the node ids, ' +
327
+ 'last visit time and last-7-day totals. Field names follow `locale` (ko|en, first enable only). No cookies; visitors are ' +
328
+ 'random per-page tags, IPs are never stored.',
329
+ annotations: { readOnlyHint: false, destructiveHint: false },
330
+ inputSchema: {
331
+ pageId: z.string().describe(`PAGE node id${NODE_TARGET_HINT}`),
332
+ action: z.enum(['status', 'enable', 'disable', 'refresh']).optional().describe('default "status"'),
333
+ locale: z.enum(['ko', 'en']).optional().describe('column/node names on first enable (default ko)'),
334
+ days: z.number().int().min(1).max(400).optional().describe('refresh: how many days back to recount (default 90)'),
335
+ },
336
+ },
337
+ async ({ pageId, action, locale, days }) => {
338
+ try {
339
+ const client = makeClient();
340
+ pageId = await resolveNodeId(client, pageId);
341
+ if (action === 'enable') return jsonResult(await client.setPageAnalytics(pageId, { enabled: true, locale }));
342
+ if (action === 'disable') return jsonResult(await client.setPageAnalytics(pageId, { enabled: false }));
343
+ if (action === 'refresh') return jsonResult(await client.refreshPageAnalytics(pageId, days));
344
+ return jsonResult(await client.getPageAnalytics(pageId));
345
+ } catch (err) {
346
+ return errorResult(err);
347
+ }
348
+ },
349
+ );
312
350
  }
@@ -54,6 +54,7 @@ const ACCESS_HINT =
54
54
  'Any property may carry `align: "left"|"center"|"right"` and `wrap: true`; a date property `highlightOverdue: true` ' +
55
55
  '(past dates red — off by default), `allowTime: false` (no time) and `allowRange: false` (no end date). A table view ' +
56
56
  'config may carry `density: "compact"` and `frozenColumns: 1` (first N visible columns fixed while scrolling sideways). ' +
57
+ 'A PAGE node may carry `page.analytics: true` (visit tracking — an "Analytics" folder with a visit LOG, a daily visits table and a dashboard is created next to it; records once published). ' +
57
58
  'A DOCUMENT\'s `content` may embed a live ' +
58
59
  'table/sheet/deck with a line `[[embed:<name of a node in this same tree>]]` or `[[embed:/absolute/path]]` — it is ' +
59
60
  'linked to the real node after the whole tree is built (an unknown name rolls the whole tree back).';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "Official Rootr CLI + MCP server — read, write, edit and search Rootr documents from the shell or from an AI agent.",
5
5
  "type": "module",
6
6
  "bin": {