rootr-cli 0.9.0 → 0.13.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/lib/client.js CHANGED
@@ -46,7 +46,23 @@ function hintFor(status, body) {
46
46
  '(rootr_workspaces 로 어떤 워크스페이스가 실제로 열리는지 확인할 수 있습니다.)'
47
47
  );
48
48
  }
49
- if (status === 401 || status === 403) {
49
+ // HINT-WRONG-CAUSE — 서버가 이미 원인을 정확히 말했으면 **덮어쓰지 않는다**.
50
+ // 예전에는 403 이면 무조건 "API 키/스코프를 확인하세요"를 붙였다. 그래서
51
+ // "무료 워크스페이스는 5개까지" 라는 정확한 메시지 밑에 엉뚱한 조치가 붙어,
52
+ // 받는 쪽이 키를 한참 뒤졌다(외부 에이전트 실측 2026-08-30).
53
+ if (status === 401 || status === 403 || status === 402) {
54
+ const msg = String((body && (body.message || body.error)) || '');
55
+ if (/workspace/i.test(msg) && /(at most|limit|한도|최대)/i.test(msg)) {
56
+ return (
57
+ '한도 문제이지 키 문제가 아닙니다. 안 쓰는 워크스페이스를 rootr.io > 설정 > 워크스페이스에서 ' +
58
+ '지우거나, 기존 워크스페이스 **안에** 폴더로 지으세요(rootr_scaffold_apply 에 rootPath 를 주면 됩니다). ' +
59
+ '요금제를 올려도 됩니다.'
60
+ );
61
+ }
62
+ // 서버가 무슨 일인지 이미 구체적으로 말했으면 그대로 둔다.
63
+ if (msg && !/^(forbidden|unauthorized|invalid api key|access denied)\.?$/i.test(msg.trim())) {
64
+ return undefined;
65
+ }
50
66
  return (
51
67
  'API 키 또는 워크스페이스 스코프를 확인하세요 (rootr config로 현재 설정 확인). ' +
52
68
  '워크스페이스 생성(rootr_create_workspace)은 계정 키(PAT, workspaces:create 스코프)가 필요합니다 — ' +
@@ -348,6 +364,35 @@ export class RootrClient {
348
364
  });
349
365
  }
350
366
 
367
+ // ── PUBLIC-ANY — 어떤 타입이든 공개 발행 ─────────────────────────────
368
+ //
369
+ // 문서·덱만 되던 것을 전 타입으로 열었다(창업자 지시 2026-08-27). 문서는 이
370
+ // 주소로 불러도 예전 경로로 넘어가므로, **무엇을 공개하든 이것 하나만 쓰면 된다.**
371
+
372
+ /**
373
+ * 공개 발행한다. `{ includeChildren: true }`를 주면 **그 아래 전부**(손자·증손자
374
+ * 포함)가 각자 공개 주소를 받는다. 안 주면 그 노드 하나만 공개된다.
375
+ */
376
+ async publishNode(nodeId, opts = {}) {
377
+ return this.request(`/nodes/${encodeURIComponent(nodeId)}/publish`, {
378
+ method: 'POST',
379
+ body: opts,
380
+ });
381
+ }
382
+
383
+ /** 공개 설정을 고친다. `{ isPublic: false }`면 닫는다(딸려 나갔던 하위도 같이 닫힌다). */
384
+ async setNodePublic(nodeId, settings = {}) {
385
+ return this.request(`/nodes/${encodeURIComponent(nodeId)}/public`, {
386
+ method: 'PATCH',
387
+ body: settings,
388
+ });
389
+ }
390
+
391
+ /** 지금 공개돼 있는지, 주소는 뭔지, 하위까지 공개인지. */
392
+ async nodePublicStatus(nodeId) {
393
+ return this.request(`/nodes/${encodeURIComponent(nodeId)}/public-status`);
394
+ }
395
+
351
396
  // ── DB-PERM-001 / DB-WATCH — 표에 거는 자물쇠와 알림 규칙 ──────────────
352
397
  //
353
398
  // 노션에 없는 것들이라, 에이전트가 표를 만들어 놓고도 "지원서는 서로 못 보게" 같은
@@ -435,6 +480,37 @@ export class RootrClient {
435
480
  });
436
481
  }
437
482
 
483
+ /**
484
+ * Check a designed tree BEFORE building it — the compiler pass of scaffolding.
485
+ * Needs no workspace, creates nothing. Returns { score, counts, findings, summary }.
486
+ */
487
+ async scaffoldReview(tree) {
488
+ return this.request('/scaffold/review', { method: 'POST', body: { tree } });
489
+ }
490
+
491
+ /**
492
+ * Check the workspace that was ACTUALLY built (not the tree you sent).
493
+ * Optional rootPath narrows it to one subtree.
494
+ */
495
+ async scaffoldReviewBuilt(workspace, rootPath) {
496
+ return this.request(`/workspaces/${encodeURIComponent(workspace)}/scaffold/review-built`, {
497
+ method: 'POST',
498
+ body: { rootPath },
499
+ });
500
+ }
501
+
502
+ /**
503
+ * Run the built workspace: do relations point at real rows, do rollups/formulas
504
+ * produce values, do views reference real columns, can the form actually collect,
505
+ * are sheet formulas computed and error-free. Writes nothing.
506
+ */
507
+ async scaffoldSelfTest(workspace, rootPath) {
508
+ return this.request(`/workspaces/${encodeURIComponent(workspace)}/scaffold/selftest`, {
509
+ method: 'POST',
510
+ body: { rootPath },
511
+ });
512
+ }
513
+
438
514
  /** Apply a scaffold tree to an existing workspace. */
439
515
  async scaffoldApply(workspace, { rootPath, tree } = {}) {
440
516
  return this.request(`/workspaces/${encodeURIComponent(workspace)}/scaffold/apply`, {
@@ -644,10 +720,21 @@ export class RootrClient {
644
720
  * building a table by adding columns one at a time made the order depend on
645
721
  * the order of the calls, which is how migrated tables came out scrambled.
646
722
  */
647
- async createDatabase(workspace, { name, parentId, path, createParents, autoRename, icon, properties, views, rows } = {}) {
723
+ async createDatabase(workspace, { name, parentId, path, createParents, autoRename, icon, properties, views, rows, rowPageType } = {}) {
648
724
  return this.request(`/workspaces/${encodeURIComponent(workspace)}/databases`, {
649
725
  method: 'POST',
650
- body: { name, parentId, path, createParents, autoRename, icon, properties, views, rows },
726
+ body: { name, parentId, path, createParents, autoRename, icon, properties, views, rows, rowPageType },
727
+ });
728
+ }
729
+
730
+ /**
731
+ * SIGN-001 — 이 표의 행을 열었을 때 나오는 상세 문서를 서명문서로 바꾼다.
732
+ * 그 뒤로 만들어지는 행마다 서명문서(연구노트)가 붙는다.
733
+ */
734
+ async setDatabaseRowPageType(databaseId, rowPageType) {
735
+ return this.request(`/databases/${encodeURIComponent(databaseId)}`, {
736
+ method: 'PATCH',
737
+ body: { rowPageType },
651
738
  });
652
739
  }
653
740
 
@@ -655,10 +742,13 @@ export class RootrClient {
655
742
  * 뷰를 하나 **덧붙인다**(기존 뷰는 그대로). 표를 만들 때만 뷰를 정할 수 있어서,
656
743
  * 노션에서 뷰가 넷인 표를 옮기면 하나밖에 못 두던 문제를 푼다.
657
744
  */
658
- async createDatabaseView(databaseId, { name, type, groupBy, dateProp, endDateProp } = {}) {
745
+ async createDatabaseView(
746
+ databaseId,
747
+ { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths } = {},
748
+ ) {
659
749
  return this.request(`/databases/${encodeURIComponent(databaseId)}/views`, {
660
750
  method: 'POST',
661
- body: { name, type, groupBy, dateProp, endDateProp },
751
+ body: { name, type, groupBy, dateProp, endDateProp, filters, sort, colWidths },
662
752
  });
663
753
  }
664
754
 
@@ -712,6 +802,74 @@ export class RootrClient {
712
802
  );
713
803
  }
714
804
 
805
+ /**
806
+ * DB-BULK — 고른 행 **전부**에 같은 값을 덮어쓴다(에이랩스 요청 2026-08-27).
807
+ * 하나라도 잠겨 있거나 남의 행이면 **아무것도 바꾸지 않고** 실패한다.
808
+ */
809
+ /**
810
+ * DB-BULK — 여러 행을 **한 번에 넣는다**. 전부 아니면 전무다.
811
+ * 칸은 이름으로도 id로도 적을 수 있다.
812
+ */
813
+ async bulkCreateRows(databaseId, rows) {
814
+ return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/bulk`, {
815
+ method: 'POST',
816
+ body: { rows },
817
+ });
818
+ }
819
+
820
+ /** DB-BULK — **행마다 다른 값**으로 한 번에 고친다. */
821
+ async bulkUpdateRowsEach(databaseId, updates) {
822
+ return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/each`, {
823
+ method: 'PATCH',
824
+ body: { updates },
825
+ });
826
+ }
827
+
828
+ async bulkUpdateRows(databaseId, rowIds, values) {
829
+ return this.request(`/databases/${encodeURIComponent(databaseId)}/rows`, {
830
+ method: 'PATCH',
831
+ body: { rowIds, values },
832
+ });
833
+ }
834
+
835
+ /** DB-BULK — 고른 행을 한 번에 지운다. 서명이 찍힌 행이 섞여 있으면 통째로 거절된다. */
836
+ async bulkDeleteRows(databaseId, rowIds) {
837
+ return this.request(`/databases/${encodeURIComponent(databaseId)}/rows/bulk-delete`, {
838
+ method: 'POST',
839
+ body: { rowIds },
840
+ });
841
+ }
842
+
843
+ /**
844
+ * ROW-PAGE-PICK — 행 하나에 상세 문서를 붙인다.
845
+ *
846
+ * `type`을 주면 **이 행만** 그 타입으로 태어난다(문서·서명문서·스프레드시트·
847
+ * 화이트보드·발표자료·녹음·폼·이슈). 안 주면 표의 기본값(`rowPageType`).
848
+ * 이미 붙어 있으면 그대로 돌려준다 — 타입을 소급해 바꾸지는 않는다.
849
+ */
850
+ async createRowPage(rowId, type) {
851
+ return this.request(`/database-rows/${encodeURIComponent(rowId)}/page`, {
852
+ method: 'POST',
853
+ body: type ? { type } : {},
854
+ });
855
+ }
856
+
857
+ /**
858
+ * ROW-PAGE-LINK — **이미 있는 문서를 행에 잇는다**(창업자 지적 2026-08-27).
859
+ * 문서는 옮기지 않는다 — 있던 자리에 그대로 두고 가리키기만 한다.
860
+ */
861
+ async linkRowPage(rowId, nodeId) {
862
+ return this.request(`/database-rows/${encodeURIComponent(rowId)}/page`, {
863
+ method: 'POST',
864
+ body: { nodeId },
865
+ });
866
+ }
867
+
868
+ /** ROW-PAGE-LINK — 연결만 끊는다. 문서는 지우지 않는다. */
869
+ async unlinkRowPage(rowId) {
870
+ return this.request(`/database-rows/${encodeURIComponent(rowId)}/page`, { method: 'DELETE' });
871
+ }
872
+
715
873
  async deleteRow(databaseId, rowId) {
716
874
  return this.request(
717
875
  `/databases/${encodeURIComponent(databaseId)}/rows/${encodeURIComponent(rowId)}`,
@@ -719,6 +877,26 @@ export class RootrClient {
719
877
  );
720
878
  }
721
879
 
880
+ // ---- READ-ACK: 읽음 확인 -------------------------------------------------
881
+ // 문서·표·폴더에 "읽고 확인 눌러야 한다"를 걸고, 얼마나 읽었는지 본다.
882
+
883
+ async readRequirement(nodeId) {
884
+ return this.request(`/nodes/${encodeURIComponent(nodeId)}/read-requirement`);
885
+ }
886
+
887
+ async setReadRequirement(nodeId, body) {
888
+ return this.request(`/nodes/${encodeURIComponent(nodeId)}/read-requirement`, {
889
+ method: 'PUT',
890
+ body,
891
+ });
892
+ }
893
+
894
+ async clearReadRequirement(nodeId) {
895
+ return this.request(`/nodes/${encodeURIComponent(nodeId)}/read-requirement`, {
896
+ method: 'DELETE',
897
+ });
898
+ }
899
+
722
900
  // ---- DB-COL: one column at a time --------------------------------------
723
901
  // PATCH /databases/:id replaces the whole properties array, so rebuilding it
724
902
  // client-side drops any column left out. These merge server-side instead.
@@ -1291,4 +1469,162 @@ export class RootrClient {
1291
1469
  if (data && Array.isArray(data.links)) return data.links;
1292
1470
  return [];
1293
1471
  }
1472
+
1473
+ // ── SIGN-001: 서명문서 · 연구노트 ────────────────────────────────────
1474
+ //
1475
+ // 마크다운 문서인데 올리는 순간 본문이 얼고, 결재라인에 적힌 사람들이 차례로
1476
+ // 서명하면 서명마다 시점확인(TSA)과 해시 체이닝이 붙는다. 서명·올리기·반려는
1477
+ // 메일로 받은 6자리 확인 코드를 함께 보내야 한다 — 서버가 개인키를 보관하는
1478
+ // 구조에서 "본인이 눌렀다"의 유일한 증거라, 코드 없이 대신 눌러 줄 수는 없다.
1479
+
1480
+ /** 서명문서 만들기 — 본문·모드·결재라인까지 한 번에. */
1481
+ async createSignedDoc(workspaceId, body = {}) {
1482
+ return this.request(`/workspaces/${encodeURIComponent(workspaceId)}/signed-docs`, {
1483
+ method: 'POST',
1484
+ body,
1485
+ });
1486
+ }
1487
+
1488
+ /** 지금 상태: 결재라인·단계별 진행·서명 목록·본문이 그대로인지. */
1489
+ async getSignedDoc(nodeId) {
1490
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}`);
1491
+ }
1492
+
1493
+ /** 결재라인 다시 짜기(올리기 전에만). steps 순서가 곧 서명 순서다. */
1494
+ async setApprovalLine(nodeId, steps) {
1495
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/approval-line`, {
1496
+ method: 'PUT',
1497
+ body: { steps },
1498
+ });
1499
+ }
1500
+
1501
+ /** 연구노트 프리셋: 기재자(나) → 점검자 두 단계. */
1502
+ async applyResearchNotePreset(nodeId, { reviewerUserId, noteMeta } = {}) {
1503
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/research-note-preset`, {
1504
+ method: 'POST',
1505
+ body: { reviewerUserId, noteMeta },
1506
+ });
1507
+ }
1508
+
1509
+ /** 확인 코드를 본인 메일로 보낸다. 문서 하나용. */
1510
+ async requestSigningCode(nodeId) {
1511
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/request-code`, {
1512
+ method: 'POST',
1513
+ body: {},
1514
+ });
1515
+ }
1516
+
1517
+ /** 확인 코드 — 묶음 서명용(워크스페이스 단위로 한 번). */
1518
+ async requestBulkSigningCode(workspaceId) {
1519
+ return this.request(`/workspaces/${encodeURIComponent(workspaceId)}/signed-docs/request-code`, {
1520
+ method: 'POST',
1521
+ body: {},
1522
+ });
1523
+ }
1524
+
1525
+ /** 올리기 = 본문을 얼리고 기안자가 첫 도장을 찍는다. */
1526
+ async submitSignedDoc(nodeId, { code } = {}) {
1527
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/submit`, {
1528
+ method: 'POST',
1529
+ body: { code },
1530
+ });
1531
+ }
1532
+
1533
+ /** 내 차례에 서명한다. */
1534
+ async signSignedDoc(nodeId, { code } = {}) {
1535
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/sign`, {
1536
+ method: 'POST',
1537
+ body: { code },
1538
+ });
1539
+ }
1540
+
1541
+ /** 반려 — 사유는 필수다. 반려되면 다시 고칠 수 있게 풀린다. */
1542
+ async rejectSignedDoc(nodeId, { comment, code } = {}) {
1543
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/reject`, {
1544
+ method: 'POST',
1545
+ body: { comment, code },
1546
+ });
1547
+ }
1548
+
1549
+ /** 아직 아무도 서명하지 않은 초안을 다시 열어 고칠 수 있게 한다. */
1550
+ async reopenSignedDoc(nodeId) {
1551
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/reopen`, { method: 'POST' });
1552
+ }
1553
+
1554
+ /** 개정본 만들기 — 서명된 문서는 고치는 게 아니라 새 판을 낸다. */
1555
+ async reviseSignedDoc(nodeId, note) {
1556
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/revise`, {
1557
+ method: 'POST',
1558
+ body: { note },
1559
+ });
1560
+ }
1561
+
1562
+ /** 위변조 검증. 로그인 없이도 열려 있다(본문은 안 내려온다). */
1563
+ async verifySignedDoc(nodeId) {
1564
+ return this.request(`/signed-docs/${encodeURIComponent(nodeId)}/verify`);
1565
+ }
1566
+
1567
+ /** 워크스페이스 서명문서 목록. pendingMine이면 "내 차례인 것"만. */
1568
+ async listSignedDocs(workspaceId, { status, mode, pendingMine } = {}) {
1569
+ const qs = new URLSearchParams();
1570
+ if (status) qs.set('status', status);
1571
+ if (mode) qs.set('mode', mode);
1572
+ if (pendingMine) qs.set('pendingMine', 'true');
1573
+ const q = qs.toString();
1574
+ return this.request(
1575
+ `/workspaces/${encodeURIComponent(workspaceId)}/signed-docs${q ? `?${q}` : ''}`,
1576
+ );
1577
+ }
1578
+
1579
+ /** 묶음 서명 — "이번 주 미점검 12건"을 한 번에. 코드 하나로 전부. */
1580
+ async bulkSignSignedDocs(workspaceId, { nodeIds, code } = {}) {
1581
+ return this.request(`/workspaces/${encodeURIComponent(workspaceId)}/signed-docs/bulk-sign`, {
1582
+ method: 'POST',
1583
+ body: { nodeIds, code },
1584
+ });
1585
+ }
1586
+
1587
+ /** 워크스페이스 원장(해시 체인) 전체 검증. */
1588
+ async verifyLedger(workspaceId) {
1589
+ return this.request(`/workspaces/${encodeURIComponent(workspaceId)}/ledger/verify`);
1590
+ }
1591
+
1592
+ /**
1593
+ * FEEDBACK — 제보를 보낸다. 다른 메서드와 달리 `request()`를 쓰지 않고 직접
1594
+ * fetch 한다: **API 키가 없어도, 키가 틀렸어도 보낼 수 있어야** 하기 때문이다.
1595
+ * 키가 잘못돼서 아무것도 못 하는 사람의 제보가 바로 우리가 제일 받고 싶은 제보다.
1596
+ * 키가 있으면 붙여서 누가 보냈는지 알 수 있게 하고, 없으면 익명으로 간다.
1597
+ */
1598
+ async sendFeedback({ body, title, kind, contactEmail, reporter, context, workspaceId, client } = {}) {
1599
+ const url = `${this.baseUrl}/feedback`;
1600
+ const headers = {
1601
+ 'Content-Type': 'application/json',
1602
+ 'x-rootr-client': client || 'cli',
1603
+ ...this.defaultHeaders,
1604
+ };
1605
+ if (this.apiKey) headers['x-api-key'] = this.apiKey;
1606
+ let res;
1607
+ try {
1608
+ res = await fetch(url, {
1609
+ method: 'POST',
1610
+ headers,
1611
+ body: JSON.stringify({ body, title, kind, contactEmail, reporter, context, workspaceId }),
1612
+ });
1613
+ } catch (err) {
1614
+ throw new RootrApiError(`네트워크 오류: ${err.message}`, { hint: `요청 URL: ${url}` });
1615
+ }
1616
+ const text = await res.text();
1617
+ let data;
1618
+ try {
1619
+ data = text ? JSON.parse(text) : {};
1620
+ } catch {
1621
+ data = { message: text };
1622
+ }
1623
+ if (!res.ok) {
1624
+ throw new RootrApiError(data.message || `제보를 보내지 못했습니다 (HTTP ${res.status})`, {
1625
+ status: res.status,
1626
+ });
1627
+ }
1628
+ return data;
1629
+ }
1294
1630
  }
package/lib/config.js CHANGED
@@ -43,6 +43,11 @@ export function loadConfig() {
43
43
  * its owner belongs to, so it is the right fallback).
44
44
  */
45
45
  export function keyForWorkspace(cfg, workspace) {
46
+ // ENV-WINS — 사람이 방금 환경변수로 준 키가 설정파일에 저장된 키보다 세다.
47
+ // 예전에는 반대였다: `ROOTR_API_KEY=... rootr mcp` 로 다른 서버를 가리켜도
48
+ // 파일에 워크스페이스로 묶여 있던 옛 키가 조용히 이겨서 "Invalid API key"만
49
+ // 떴다(2026-08-30 실측). 어느 키가 쓰였는지 화면에 안 나오니 원인이 안 보인다.
50
+ if (process.env.ROOTR_API_KEY) return process.env.ROOTR_API_KEY;
46
51
  if (workspace) {
47
52
  const bound = (cfg.keys || []).find((k) => k.workspace === workspace);
48
53
  if (bound) return bound.apiKey;