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 +5 -3
- package/README.md +9 -1
- package/bin/rootr.js +45 -12
- package/lib/client.js +8 -2
- package/lib/issue-paging.js +44 -0
- package/lib/mcp-tools/issues.js +37 -9
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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
|
-
|
|
2334
|
+
issuePageFooter(res, list.length);
|
|
2308
2335
|
return;
|
|
2309
2336
|
}
|
|
2310
2337
|
const id = await resolveNodeId(client, target);
|
|
2311
|
-
const
|
|
2312
|
-
|
|
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
|
+
|
package/lib/mcp-tools/issues.js
CHANGED
|
@@ -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),
|
|
72
|
-
'type id, or a text query.
|
|
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? }
|
|
315
|
-
'
|
|
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
|
}
|