rootr-cli 0.34.1 → 0.35.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.ko.md CHANGED
@@ -482,7 +482,7 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
482
482
  | 도구 | 설명 |
483
483
  |---|---|
484
484
  | `rootr_create_issue_tracker` | 이슈 트래커 생성 |
485
- | `rootr_list_issues` | 이슈 목록(state/label/type/assignee/검색어 필터) |
485
+ | `rootr_list_issues` | 이슈 목록(state/label/type/assignee/검색어 필터). 나눠 받기: `limit`(1~200)·`cursor`(직전 `nextCursor`) → `nextCursor`·`total` |
486
486
  | `rootr_create_issue` | 이슈 생성(labels/type/parentIssueId 지원) |
487
487
  | `rootr_get_issue` | 이슈 상세 조회 |
488
488
  | `rootr_update_issue` | 이슈 갱신(닫기: state=CLOSED + stateReason) |
@@ -490,12 +490,14 @@ rootr memory forget <id> # 폐기(--hard면 행 삭제)
490
490
  | `rootr_update_issue_tracker` | 트래커 설정 — **외부 참여**(`read` 보기만 · `login` 로그인한 사람만 작성 · `anyone` 누구나 작성), 라벨·유형 |
491
491
  | `rootr_submit_public_issue` | **웹게시한** 트래커에 밖에서 이슈 올리기(공개 주소로, `asGuest`+`guestName` 이면 손님으로, 페이지 블록은 `blockId`, `files:[{filename, contentBase64}]` 첨부) |
492
492
  | `rootr_comment_public_issue` | 웹게시한 트래커의 이슈에 댓글(`files` 첨부, `read:true` 면 공개 댓글 읽기) |
493
- | `rootr_search_public_issues` | 웹게시한 트래커를 밖에서 찾기 — `q`(제목·본문·#번호·쓴 사람), `labels`(모두 가진 것), `state`, `sort`(newest·oldest·comments) |
493
+ | `rootr_search_public_issues` | 웹게시한 트래커를 밖에서 찾기 — `q`(제목·본문·#번호·쓴 사람), `labels`(모두 가진 것), `state`, `sort`(newest·oldest·comments), `limit`·`cursor` 로 나눠 받기 |
494
494
 
495
495
  CLI: `rootr issue tracker <트래커> --participation anyone` · `rootr issue list|create|comment …` ·
496
496
  `rootr issue list <공개 주소> --q "결제" --label 버그` · `rootr issue submit <공개 주소> --title "…" --name <이름> --file 화면.png --file 로그.txt` ·
497
497
  `rootr issue reply <공개 주소> <#번호> "<댓글>" [--file <경로>]`.
498
- 첨부는 그림(png·jpg·gif·webp)·pdf·txt·csv·log·docx·xlsx·pptx·zip, 한 파일 10MB·글 하나에 10개까지입니다(확장자와 파일 첫 바이트가 맞아야 함).
498
+ 목록은 **나눠 받습니다**(CLI 0.35+): 깃발이 없으면 예전처럼 최신 500건, `--limit 1~200` 은 한 페이지(맨 아래에 다음 `--cursor` 를 알려 줌),
499
+ `--all` 은 끝까지 이어 받습니다 — 예: `rootr issue list /지원/피드백 --state ALL --all --json`.
500
+ 첨부는 그림(png·jpg·gif·webp)·pdf·txt·csv·log·docx·xlsx·pptx·zip, 한 파일 75MB(손님 25MB)·글 하나에 10개까지입니다(확장자와 파일 첫 바이트가 맞아야 함).
499
501
  외부 참여는 **트래커 설정**이라 페이지(PAGE)에 `issues` 블록으로 끼운 트래커도 같은 규칙을 따릅니다.
500
502
 
501
503
  #### 웹훅 (webhooks:manage 스코프)
package/README.md CHANGED
@@ -314,9 +314,17 @@ rootr issue submit https://rootr.io/ko/p/AbCd123 --title "Button does nothing" -
314
314
  rootr issue submit https://rootr.io/ko/p/AbCd123 --title "Crash" --file shot.png --file app.log --name "Kim" # with files
315
315
  rootr issue reply https://rootr.io/ko/p/AbCd123 12 "Same here" --name "Lee"
316
316
  rootr issue list https://rootr.io/ko/p/AbCd123 --q "payment" --label bug --sort comments # search a published tracker
317
+ rootr issue list /support/feedback --state ALL --limit 100 # one page; the last line prints the next --cursor
318
+ rootr issue list /support/feedback --state ALL --limit 100 --cursor <next cursor>
319
+ rootr issue list /support/feedback --state ALL --all --json # every issue, page by page (200 at a time)
317
320
  ```
318
321
 
319
- Outside attachments: images (png·jpg·gif·webp), pdf, txt·csv·log, docx·xlsx·pptx, zip — 10MB per file, 10 per
322
+ Issue lists are **paged** (CLI 0.35+): without flags you get the newest 500 as before; `--limit 1..200` gives
323
+ one page and prints `--cursor <…>` for the next, `--all` follows the cursors to the end. MCP
324
+ `rootr_list_issues` / `rootr_search_public_issues` take `limit` and `cursor` and return `nextCursor` (null on the
325
+ last page) and `total`.
326
+
327
+ Outside attachments: images (png·jpg·gif·webp), pdf, txt·csv·log, docx·xlsx·pptx, zip — 75MB per file signed in (25MB as a guest), 10 per
320
328
  issue/comment; the extension and the file's first bytes must agree. MCP: `rootr_submit_public_issue` /
321
329
  `rootr_comment_public_issue` take `files: [{ filename, contentBase64 }]` (contents, never a path), and
322
330
  `rootr_search_public_issues` searches a published tracker the way a visitor does.
package/bin/rootr.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  import { setGlobalDocMailSink } from '../lib/context.js';
19
19
  import { resolveTarget, requireResolved, resolveMoveDestination } from '../lib/resolve.js';
20
20
  import { t as tr, lang } from '../lib/i18n.js';
21
+ import { issuePageArgs, collectIssuePages } from '../lib/issue-paging.js';
21
22
  import { domainPaymentHelp, slotLine } from '../lib/domain-billing.js';
22
23
 
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -89,6 +90,7 @@ const USAGE_KO = `Rootr CLI v${pkg.version}
89
90
  rootr issue tracker <트래커> [--participation read|login|anyone] [--json] 트래커 설정 보기·외부 참여 바꾸기(MANAGE)
90
91
  rootr issue list <트래커|공개 주소> [--state OPEN|CLOSED|ALL] [--q <검색어>] [--label <라벨>]… [--sort newest|oldest|comments] [--json]
91
92
  이슈 목록·검색(공개 주소면 밖에서 — 제목·본문·#번호·쓴 사람, 페이지 블록은 --block)
93
+ [--limit <1~200>] [--cursor <다음 커서>] [--all] 나눠 받기 — 깃발 없으면 최신 500건, --all 은 끝까지
92
94
  rootr issue create <트래커> --title "<제목>" [--body "<본문>"] 이슈 올리기(내 계정으로)
93
95
  rootr issue comment <트래커> <#번호> "<댓글>" 이슈에 댓글
94
96
  rootr issue submit <공개 주소> --title "<제목>" [--body …] [--file <경로>]… [--name <이름>] [--block <블록 id>] [--password <비번>]
@@ -245,6 +247,7 @@ usage:
245
247
  rootr issue tracker <tracker> [--participation read|login|anyone] [--json] show tracker settings / change outside participation (MANAGE)
246
248
  rootr issue list <tracker|public URL> [--state OPEN|CLOSED|ALL] [--q <text>] [--label <name>]… [--sort newest|oldest|comments] [--json]
247
249
  list/search issues (a public URL searches a PUBLISHED tracker from outside — title, body, #number, author; --block for a page)
250
+ [--limit <1-200>] [--cursor <next cursor>] [--all] paging — no flag = newest 500, --all = everything
248
251
  rootr issue create <tracker> --title "<title>" [--body "<body>"] file an issue (as your account)
249
252
  rootr issue comment <tracker> <#number> "<comment>" comment on an issue
250
253
  rootr issue submit <public URL> --title "<title>" [--body …] [--file <path>]… [--name <name>] [--block <block id>] [--password <pw>]
@@ -2216,6 +2219,23 @@ function issuesOf(res) {
2216
2219
  return Array.isArray(res) ? res : Array.isArray(res?.issues) ? res.issues : [];
2217
2220
  }
2218
2221
 
2222
+ /** 목록 아래 한 줄 — 몇 건 중 몇 건, 다음 페이지 받는 법. */
2223
+ function issuePageFooter(res, shown) {
2224
+ const total = typeof res?.total === 'number' ? res.total : res?.truncated?.total;
2225
+ if (res?.nextCursor) {
2226
+ console.log(
2227
+ tr(
2228
+ `… 전체 ${total ?? '?'}건 중 ${shown}건. 다음 페이지: 같은 조건에 --cursor ${res.nextCursor} (끝까지: --all)`,
2229
+ `… ${shown} of ${total ?? '?'}. Next page: same filters plus --cursor ${res.nextCursor} (everything: --all)`,
2230
+ ),
2231
+ );
2232
+ } else if (res?.truncated) {
2233
+ console.log(tr(`… ${res.truncated.total}건 중 ${res.truncated.shown}건 (나머지는 --limit/--cursor 또는 --all)`, `… ${res.truncated.shown} of ${res.truncated.total} (rest: --limit/--cursor or --all)`));
2234
+ } else if (typeof total === 'number' && total > shown) {
2235
+ console.log(tr(`… 전체 ${total}건 중 ${shown}건`, `… ${shown} of ${total}`));
2236
+ }
2237
+ }
2238
+
2219
2239
  function issueNumberOf(raw) {
2220
2240
  const n = Number(String(raw ?? '').replace(/^#/, ''));
2221
2241
  if (!Number.isSafeInteger(n) || n < 1) throw new Error(tr(`이슈 번호가 아닙니다: ${raw}`, `Not an issue number: ${raw}`));
@@ -2283,19 +2303,26 @@ async function cmdIssue(args) {
2283
2303
  }
2284
2304
  if (sub === 'list') {
2285
2305
  const target = args._[1];
2286
- if (!target) throw new Error(usage('rootr issue list <path|id|공개 주소> [--state OPEN|CLOSED|ALL] [--q <text>] [--label <이름>]… [--sort newest|oldest|comments] [--json]'));
2306
+ if (!target) throw new Error(usage('rootr issue list <path|id|공개 주소> [--state OPEN|CLOSED|ALL] [--q <text>] [--label <이름>]… [--sort newest|oldest|comments] [--limit <1~200>] [--cursor <다음 커서>] [--all] [--json]'));
2307
+ const pg = issuePageArgs(args);
2287
2308
  // ISSUE-PUBLIC-SEARCH — 공개 주소(…/p/{slug})나 --block 이면 웹게시한 트래커를 **밖에서** 찾는다(키 없이, 서버 검색).
2288
2309
  if (/\/p\//.test(String(target)) || typeof args.block === 'string') {
2289
2310
  const slug = publicSlugOf(target);
2290
2311
  const pass = typeof args.password === 'string' ? await client.unlockPublicNode(slug, args.password) : undefined;
2291
- const res = await client.searchPublicIssues(slug, {
2292
- blockId: typeof args.block === 'string' ? args.block : undefined,
2293
- q: typeof args.q === 'string' ? args.q : undefined,
2294
- state: typeof args.state === 'string' ? args.state : undefined,
2295
- labels: args.__all.label || [],
2296
- sort: typeof args.sort === 'string' ? args.sort : undefined,
2297
- pass,
2298
- });
2312
+ const res = await collectIssuePages(
2313
+ ({ limit, cursor }) =>
2314
+ client.searchPublicIssues(slug, {
2315
+ blockId: typeof args.block === 'string' ? args.block : undefined,
2316
+ q: typeof args.q === 'string' ? args.q : undefined,
2317
+ state: typeof args.state === 'string' ? args.state : undefined,
2318
+ labels: args.__all.label || [],
2319
+ sort: typeof args.sort === 'string' ? args.sort : undefined,
2320
+ pass,
2321
+ limit,
2322
+ cursor,
2323
+ }),
2324
+ pg,
2325
+ );
2299
2326
  if (args.json) return console.log(JSON.stringify(res, null, 2));
2300
2327
  const list = res?.issues || [];
2301
2328
  if (!list.length) return console.log(tr('조건에 맞는 이슈가 없습니다.', 'No matching issues.'));
@@ -2304,17 +2331,23 @@ async function cmdIssue(args) {
2304
2331
  const labels = (i.labels || []).map((l) => l.name).join(',');
2305
2332
  console.log(`#${i.number}\t${i.state}\t${i.title}${labels ? `\t[${labels}]` : ''}${who ? `\t${who}` : ''}${i.commentCount ? `\t💬${i.commentCount}` : ''}`);
2306
2333
  }
2307
- if (res.truncated) console.log(tr(`… ${res.truncated.total}건 중 ${res.truncated.shown}건`, `… ${res.truncated.shown} of ${res.truncated.total}`));
2334
+ issuePageFooter(res, list.length);
2308
2335
  return;
2309
2336
  }
2310
2337
  const id = await resolveNodeId(client, target);
2311
- const list = issuesOf(await client.listIssues(id, { state: args.state, q: typeof args.q === 'string' ? args.q : undefined }));
2312
- if (args.json) return console.log(JSON.stringify(list, null, 2));
2338
+ const res = await collectIssuePages(
2339
+ ({ limit, cursor }) => client.listIssues(id, { state: args.state, q: typeof args.q === 'string' ? args.q : undefined, limit, cursor }),
2340
+ pg,
2341
+ );
2342
+ const list = issuesOf(res);
2343
+ // --json: 깃발 없이는 예전처럼 배열(스크립트 호환), --all 도 배열(전부). --limit/--cursor 면 다음 커서가 필요하니 { issues, nextCursor, total }.
2344
+ if (args.json) return console.log(JSON.stringify(pg.paged && !pg.all ? { issues: list, nextCursor: res?.nextCursor ?? null, total: res?.total } : list, null, 2));
2313
2345
  if (!list.length) return console.log(tr('이슈가 없습니다.', 'No issues.'));
2314
2346
  for (const i of list) {
2315
2347
  const who = i.guestName ? tr(`${i.guestName} (외부)`, `${i.guestName} (guest)`) : i.author?.name ?? '';
2316
2348
  console.log(`#${i.number}\t${i.state}\t${i.title}${who ? `\t${who}` : ''}${i.commentCount ? `\t💬${i.commentCount}` : ''}`);
2317
2349
  }
2350
+ issuePageFooter(res, list.length);
2318
2351
  return;
2319
2352
  }
2320
2353
  if (sub === 'create') {
package/lib/client.js CHANGED
@@ -1038,12 +1038,15 @@ export class RootrClient {
1038
1038
  * ISSUE-PUBLIC-SEARCH — 공개 트래커(또는 페이지 블록)의 목록을 서버에서 찾는다. `q` 는 제목·본문·번호(#12)·(이름을 싣는 모드면) 쓴 사람,
1039
1039
  * `labels` 는 이름(모두 가진 것), `state` OPEN|CLOSED, `sort` newest|oldest|comments. 키 없이 부른다(공개 읽기).
1040
1040
  */
1041
- async searchPublicIssues(slug, { blockId, q, state, labels, sort, pass } = {}) {
1041
+ async searchPublicIssues(slug, { blockId, q, state, labels, sort, pass, limit, cursor } = {}) {
1042
1042
  const qs = new URLSearchParams();
1043
1043
  if (q) qs.set('q', q);
1044
1044
  if (state && String(state).toUpperCase() !== 'ALL') qs.set('state', String(state).toUpperCase());
1045
1045
  for (const l of [].concat(labels || [])) if (l) qs.append('label', l);
1046
1046
  if (sort) qs.set('sort', sort);
1047
+ // ISSUE-PAGING — limit(1~200)·cursor(직전 응답 nextCursor). 둘 다 없으면 서버가 예전처럼 500건.
1048
+ if (limit !== undefined && limit !== null) qs.set('limit', String(limit));
1049
+ if (cursor) qs.set('cursor', cursor);
1047
1050
  if (pass) qs.set('pass', pass);
1048
1051
  const tail = qs.toString();
1049
1052
  return this.request(`${this._publicIssuesBase(slug, blockId)}${tail ? `?${tail}` : ''}`, { noAuth: true });
@@ -1073,13 +1076,16 @@ export class RootrClient {
1073
1076
  return this.request(`${this._publicIssuesBase(slug, blockId)}/${encodeURIComponent(String(number))}${qs}`, { noAuth: true });
1074
1077
  }
1075
1078
 
1076
- async listIssues(trackerId, { state, label, type, assigneeId, q } = {}) {
1079
+ async listIssues(trackerId, { state, label, type, assigneeId, q, limit, cursor } = {}) {
1077
1080
  const params = new URLSearchParams();
1078
1081
  if (state) params.set('state', state);
1079
1082
  if (label) params.set('label', label);
1080
1083
  if (type) params.set('type', type);
1081
1084
  if (assigneeId) params.set('assigneeId', assigneeId);
1082
1085
  if (q) params.set('q', q);
1086
+ // ISSUE-PAGING — 응답 { issues, nextCursor, total }. 둘 다 없으면 예전처럼 최신 500개.
1087
+ if (limit !== undefined && limit !== null) params.set('limit', String(limit));
1088
+ if (cursor) params.set('cursor', cursor);
1083
1089
  const qs = params.toString();
1084
1090
  return this.request(`/issue-trackers/${encodeURIComponent(trackerId)}/issues${qs ? `?${qs}` : ''}`);
1085
1091
  }
@@ -0,0 +1,44 @@
1
+ import { t as tr } from './i18n.js';
2
+
3
+ /**
4
+ * ISSUE-PAGING — `--limit <1~200>` · `--cursor <다음 커서>` · `--all`(끝까지). 깃발이 없으면 예전처럼 한 번(서버가 최신 500개).
5
+ * `--all` 은 200개씩 nextCursor 가 없을 때까지 받는다. 옛 서버(페이지 모르는)는 nextCursor 를 안 주므로 한 번에 멈춘다.
6
+ */
7
+ export function issuePageArgs(args) {
8
+ const out = { all: args.all === true, limit: undefined, cursor: undefined };
9
+ if (args.limit !== undefined) {
10
+ const n = Number(args.limit);
11
+ if (args.limit === true || !Number.isInteger(n) || n < 1 || n > 200) {
12
+ throw new Error(tr('--limit 은 1~200 사이 정수입니다(예: --limit 100). 끝까지 받으려면 --all.', '--limit must be an integer 1–200 (e.g. --limit 100). Use --all to fetch everything.'));
13
+ }
14
+ out.limit = n;
15
+ }
16
+ if (args.cursor !== undefined) {
17
+ if (typeof args.cursor !== 'string' || !args.cursor) throw new Error(tr('--cursor 에는 직전 결과의 다음 커서를 넣으세요.', '--cursor needs the previous result\'s next cursor.'));
18
+ out.cursor = args.cursor;
19
+ }
20
+ out.paged = out.all || out.limit !== undefined || out.cursor !== undefined;
21
+ return out;
22
+ }
23
+
24
+ /** 페이지를 이어 받는다. fetchPage({limit, cursor}) → { issues, nextCursor?, total?, truncated? }. */
25
+ export async function collectIssuePages(fetchPage, { all, limit, cursor }) {
26
+ if (!all) return fetchPage({ limit, cursor });
27
+ const size = limit ?? 200;
28
+ let cur = cursor;
29
+ let last = null;
30
+ const issues = [];
31
+ for (let guard = 0; guard < 100_000; guard++) {
32
+ const res = await fetchPage({ limit: size, cursor: cur });
33
+ issues.push(...(Array.isArray(res?.issues) ? res.issues : Array.isArray(res) ? res : []));
34
+ last = res;
35
+ const next = res?.nextCursor;
36
+ if (!next || next === cur) break;
37
+ cur = next;
38
+ }
39
+ const out = { ...(last && !Array.isArray(last) ? last : {}), issues, nextCursor: null };
40
+ delete out.truncated;
41
+ if (typeof out.total !== 'number') out.total = issues.length;
42
+ return out;
43
+ }
44
+
@@ -31,6 +31,26 @@ function slugOf(target) {
31
31
  return m ? decodeURIComponent(m[1]) : t;
32
32
  }
33
33
 
34
+ /**
35
+ * ISSUE-PAGING — 다음 페이지가 있으면 에이전트가 놓치지 않게 한 줄 안내를 붙인다(서버 응답은 그대로 두고 `next` 만 더한다).
36
+ * 옛 서버(페이지 모르는)는 nextCursor 가 없어 안내도 없다 — 대신 truncated 가 있으면 그걸로 알린다.
37
+ */
38
+ export function withPageHint(result) {
39
+ if (!result || typeof result !== 'object' || !Array.isArray(result.issues)) return result;
40
+ const n = result.issues.length;
41
+ if (result.nextCursor) {
42
+ const total = typeof result.total === 'number' ? ` of ${result.total}` : '';
43
+ return {
44
+ ...result,
45
+ next: `More issues remain (this page ${n}${total}). Call again with cursor="${result.nextCursor}" and the same filters to get the next page.`,
46
+ };
47
+ }
48
+ if (result.truncated && result.nextCursor === undefined) {
49
+ return { ...result, next: `Only ${result.truncated.shown} of ${result.truncated.total} returned — pass limit (1–200) and then cursor=<nextCursor> to page through the rest.` };
50
+ }
51
+ return result;
52
+ }
53
+
34
54
  /** Issue tracker tools: create tracker, list/create/get/update issues, comment. */
35
55
  export function registerIssueTools(server) {
36
56
  server.registerTool(
@@ -68,8 +88,11 @@ export function registerIssueTools(server) {
68
88
  {
69
89
  title: 'List issues in a tracker',
70
90
  description:
71
- 'List issues in a Rootr (루터) issue tracker, optionally filtered by state (OPEN/CLOSED/ALL), label id, ' +
72
- 'type id, or a text query. Use rootr_get_issue for full details of one issue.',
91
+ 'List issues in a Rootr (루터) issue tracker, newest number first, optionally filtered by state (OPEN/CLOSED/ALL), ' +
92
+ 'label id, type id, or a text query. Paged: pass `limit` (1–200) and, for the next page, `cursor` = the previous ' +
93
+ 'result\'s `nextCursor` (keep the same filters). Without limit/cursor you get the newest 500. Returns ' +
94
+ '{ issues, nextCursor, total } — `nextCursor` null means the last page; `total` = all issues matching the filters. ' +
95
+ 'Use rootr_get_issue for full details of one issue.',
73
96
  annotations: { readOnlyHint: true, destructiveHint: false },
74
97
  inputSchema: {
75
98
  trackerId: z.string().describe(`Issue tracker node id${NODE_TARGET_HINT}`),
@@ -77,14 +100,16 @@ export function registerIssueTools(server) {
77
100
  q: z.string().optional().describe('Free-text search within issues'),
78
101
  label: z.string().optional().describe('Label id to filter by'),
79
102
  type: z.string().optional().describe('Issue type id to filter by'),
103
+ limit: z.number().int().min(1).max(200).optional().describe('Page size 1–200 (omit with cursor too = newest 500, as before)'),
104
+ cursor: z.string().max(400).optional().describe('`nextCursor` from the previous call, unchanged (same filters) — next page'),
80
105
  },
81
106
  },
82
- async ({ trackerId, state, q, label, type }) => {
107
+ async ({ trackerId, state, q, label, type, limit, cursor }) => {
83
108
  try {
84
109
  const client = makeClient();
85
110
  trackerId = await resolveNodeId(client, trackerId);
86
- const result = await client.listIssues(trackerId, { state, q, label, type });
87
- return jsonResult(result);
111
+ const result = await client.listIssues(trackerId, { state, q, label, type, limit, cursor });
112
+ return jsonResult(withPageHint(result));
88
113
  } catch (err) {
89
114
  return errorResult(err);
90
115
  }
@@ -311,8 +336,9 @@ export function registerIssueTools(server) {
311
336
  'what an outside visitor sees. `q` matches title, body, number ("#12" or "12") and, when the tracker shows authors ' +
312
337
  '(participation login/anyone), the author name; `labels` = label names (issues having all of them); `state` OPEN|CLOSED; ' +
313
338
  '`sort` newest (default) | oldest | comments. Returns { participation, issues:[{number,title,state,body,labels,createdAt,' +
314
- 'closedAt,commentCount,author}], truncated? } (≤500 per call). No key needed. For a tracker in YOUR workspace prefer ' +
315
- 'rootr_list_issues (internal fields).',
339
+ 'closedAt,commentCount,author}], truncated?, nextCursor?, total? }. Paged: `limit` (1–200) and `cursor` = the previous ' +
340
+ 'result\'s `nextCursor` (same q/state/labels/sort); without them you get up to 500 (truncated tells you there were more). ' +
341
+ 'No key needed. For a tracker in YOUR workspace prefer rootr_list_issues (internal fields).',
316
342
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
317
343
  inputSchema: {
318
344
  url: PUBLIC_TARGET.url,
@@ -322,14 +348,16 @@ export function registerIssueTools(server) {
322
348
  state: z.enum(['OPEN', 'CLOSED', 'ALL']).optional().describe('Default ALL'),
323
349
  labels: z.array(z.string().max(100)).max(20).optional().describe('Label names — issues having all of them'),
324
350
  sort: z.enum(['newest', 'oldest', 'comments']).optional(),
351
+ limit: z.number().int().min(1).max(200).optional().describe('Page size 1–200'),
352
+ cursor: z.string().max(400).optional().describe('`nextCursor` from the previous call, unchanged (same q/state/labels/sort)'),
325
353
  },
326
354
  },
327
- async ({ url, blockId, password, q, state, labels, sort }) => {
355
+ async ({ url, blockId, password, q, state, labels, sort, limit, cursor }) => {
328
356
  try {
329
357
  const client = makeClient();
330
358
  const slug = slugOf(url);
331
359
  const pass = password ? await client.unlockPublicNode(slug, password) : undefined;
332
- return jsonResult(await client.searchPublicIssues(slug, { blockId, q, state, labels, sort, pass }));
360
+ return jsonResult(withPageHint(await client.searchPublicIssues(slug, { blockId, q, state, labels, sort, pass, limit, cursor })));
333
361
  } catch (err) {
334
362
  return errorResult(err);
335
363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rootr-cli",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
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": {