byuckchon-frontend-cli 1.6.1 → 1.7.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 +5 -1
- package/bin/index.js +1 -1
- package/package.json +1 -1
- package/src/ai/tools.js +87 -1
- package/src/commands/chat.js +10 -4
- package/src/openapi/lookup.js +135 -0
- package/src/openapi/summary.js +26 -30
- package/src/ui/ChatApp.js +1 -0
package/README.md
CHANGED
|
@@ -74,8 +74,11 @@ bc init
|
|
|
74
74
|
| `read_file` | 프로젝트 내 파일/디렉터리 내용 읽기 |
|
|
75
75
|
| `list_files` | 글롭 패턴으로 파일 나열 |
|
|
76
76
|
| `search_code` | RAG 인덱스 의미 기반 검색 (인덱스 있어야 함) |
|
|
77
|
+
| `search_openapi` | OpenAPI 스펙에서 엔드포인트 검색 (path/summary/tag) — 큰 스펙도 OK |
|
|
78
|
+
| `get_openapi_endpoint` | 특정 엔드포인트 상세 (params/requestBody/responses, `$ref` 인라인) |
|
|
77
79
|
| `write_file` | 새 파일 생성 또는 통째 덮어쓰기 |
|
|
78
80
|
| `edit_file` | 유일한 `old_string → new_string` 으로 부분 수정 (안전) |
|
|
81
|
+
| `fetch_figma` / `fetch_figma_image` / `fetch_figma_styles` | Figma 디자인/이미지/토큰 |
|
|
79
82
|
|
|
80
83
|
모델은 한 턴 안에서 **최대 12 step** 까지 툴을 자유롭게 호출합니다. 일반적인 흐름:
|
|
81
84
|
1. `list_files` 로 `src/api/` 구조 파악
|
|
@@ -351,7 +354,8 @@ bc config set-ui ink # 풀 TUI 복귀
|
|
|
351
354
|
- [x] v1.5.0 — 에이전트 모드 (read/list/search/write/edit 툴) — AI 가 실제 파일을 만든다
|
|
352
355
|
- [x] v1.6.0 — Figma 툴 (fetch_figma / image / styles), 한글 IME 안정 plain 모드 (`bc config set-ui plain`)
|
|
353
356
|
- [x] v1.6.1 — 툴 스키마 `jsonSchema()` 래핑 (`schema is not a function` 수정), plain 모드 이미지 첨부(`/image`·`/paste`) + iTerm2/kitty 인라인 썸네일
|
|
354
|
-
- [
|
|
357
|
+
- [x] v1.7.0 — OpenAPI 검색 툴 (`search_openapi` / `get_openapi_endpoint`) — 큰 스펙(수백 엔드포인트)에서도 정확한 경로/스키마 조회. 요약도 path 당 1줄로 압축 + 한도 상향
|
|
358
|
+
- [ ] v1.8.0 — write/edit 승인 게이트 (`y/n/v/q`), diff 미리보기
|
|
355
359
|
- [ ] Phase 3c-2: Figma 실 fetch (URL → 노드 트리 → 컴포넌트 인텐트)
|
|
356
360
|
- [ ] Phase 4: `bc gen component/page` (AST 편집 + 검증 루프), `/apply` diff 미리보기
|
|
357
361
|
|
package/bin/index.js
CHANGED
package/package.json
CHANGED
package/src/ai/tools.js
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from '../figma/api.js';
|
|
13
13
|
import { simplifyFetchNodes } from '../figma/simplify.js';
|
|
14
14
|
import { parseFigmaUrl } from '../figma/url.js';
|
|
15
|
+
import { getCachedOpenApi } from '../openapi/cache.js';
|
|
16
|
+
import { searchEndpoints, getEndpoint } from '../openapi/lookup.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Agentic chat 용 툴 정의.
|
|
@@ -27,9 +29,21 @@ import { parseFigmaUrl } from '../figma/url.js';
|
|
|
27
29
|
* const tools = buildTools({ projectRoot, effective, onEvent });
|
|
28
30
|
* streamText({ tools, stopWhen: stepCountIs(12), ... });
|
|
29
31
|
*/
|
|
30
|
-
export function buildTools({ projectRoot, effective, onEvent = () => {} }) {
|
|
32
|
+
export function buildTools({ projectRoot, effective, onEvent = () => {}, openapiSource = null }) {
|
|
31
33
|
const root = path.resolve(projectRoot);
|
|
32
34
|
|
|
35
|
+
// OpenAPI 스펙은 한 번만 로드해서 캐시 (큰 파일이라 반복 파싱 비쌈).
|
|
36
|
+
let _openapiDocPromise = null;
|
|
37
|
+
function loadOpenApiDoc() {
|
|
38
|
+
if (!openapiSource) return Promise.resolve(null);
|
|
39
|
+
if (!_openapiDocPromise) {
|
|
40
|
+
_openapiDocPromise = getCachedOpenApi(openapiSource)
|
|
41
|
+
.then((res) => res.doc ?? null)
|
|
42
|
+
.catch(() => null);
|
|
43
|
+
}
|
|
44
|
+
return _openapiDocPromise;
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
function safePath(p) {
|
|
34
48
|
if (!p || typeof p !== 'string') {
|
|
35
49
|
throw new Error('path 가 비어있습니다');
|
|
@@ -171,6 +185,44 @@ export function buildTools({ projectRoot, effective, onEvent = () => {} }) {
|
|
|
171
185
|
return { ok: true, path: rel, action: 'edited' };
|
|
172
186
|
}
|
|
173
187
|
|
|
188
|
+
// ─────────── OpenAPI 툴 ───────────
|
|
189
|
+
|
|
190
|
+
async function searchOpenApi({ query, limit = 40 }) {
|
|
191
|
+
const doc = await loadOpenApiDoc();
|
|
192
|
+
if (!doc) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
error:
|
|
196
|
+
'OpenAPI 스펙을 불러올 수 없습니다. bc.config.json 의 api.openapi 가 올바른 JSON 스펙 URL 인지 확인하세요 ' +
|
|
197
|
+
'(NestJS 는 보통 /api/docs 가 아니라 /api/docs-json).',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const hits = searchEndpoints(doc, query, { limit });
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
query,
|
|
204
|
+
count: hits.length,
|
|
205
|
+
endpoints: hits.map((e) => ({
|
|
206
|
+
method: e.method,
|
|
207
|
+
path: e.path,
|
|
208
|
+
summary: e.summary,
|
|
209
|
+
tags: e.tags,
|
|
210
|
+
})),
|
|
211
|
+
hint:
|
|
212
|
+
hits.length === 0
|
|
213
|
+
? '매치 없음. 다른 키워드로 재시도하거나, query 를 비워 전체 목록을 받아 path 를 직접 고르세요.'
|
|
214
|
+
: '상세 스키마가 필요하면 get_openapi_endpoint(path, method) 를 호출하세요.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function getOpenApiEndpoint({ path: epPath, method }) {
|
|
219
|
+
const doc = await loadOpenApiDoc();
|
|
220
|
+
if (!doc) {
|
|
221
|
+
return { ok: false, error: 'OpenAPI 스펙을 불러올 수 없습니다.' };
|
|
222
|
+
}
|
|
223
|
+
return getEndpoint(doc, epPath, method);
|
|
224
|
+
}
|
|
225
|
+
|
|
174
226
|
// ─────────── Figma 툴 ───────────
|
|
175
227
|
|
|
176
228
|
async function fetchFigma({ url, depth = 4 }) {
|
|
@@ -295,6 +347,40 @@ export function buildTools({ projectRoot, effective, onEvent = () => {} }) {
|
|
|
295
347
|
}),
|
|
296
348
|
execute: searchCode,
|
|
297
349
|
}),
|
|
350
|
+
search_openapi: tool({
|
|
351
|
+
description:
|
|
352
|
+
'연결된 OpenAPI(Swagger) 스펙에서 엔드포인트를 검색한다. path 일부("admin/inquiries"), ' +
|
|
353
|
+
'한글 summary("문의"), tag, operationId 어느 걸로도 검색 가능. ' +
|
|
354
|
+
'API 코드를 짜기 전에 반드시 이 툴로 정확한 경로/메서드를 먼저 확인할 것. ' +
|
|
355
|
+
'시스템 프롬프트의 요약은 잘려 있을 수 있으므로 "엔드포인트가 안 보인다" 싶으면 이 툴로 찾는다.',
|
|
356
|
+
inputSchema: jsonSchema({
|
|
357
|
+
type: 'object',
|
|
358
|
+
properties: {
|
|
359
|
+
query: { type: 'string', description: '검색어 (path 일부/summary/tag). 비우면 전체 목록' },
|
|
360
|
+
limit: { type: 'number', default: 40 },
|
|
361
|
+
},
|
|
362
|
+
additionalProperties: false,
|
|
363
|
+
}),
|
|
364
|
+
execute: searchOpenApi,
|
|
365
|
+
}),
|
|
366
|
+
get_openapi_endpoint: tool({
|
|
367
|
+
description:
|
|
368
|
+
'특정 엔드포인트의 상세(parameters / requestBody / responses 스키마, $ref 인라인됨) 를 가져온다. ' +
|
|
369
|
+
'search_openapi 로 찾은 정확한 path 와 method 를 넘긴다. 이 결과로 zod 스키마/타입/요청 함수를 정확히 생성.',
|
|
370
|
+
inputSchema: jsonSchema({
|
|
371
|
+
type: 'object',
|
|
372
|
+
properties: {
|
|
373
|
+
path: { type: 'string', description: '정확한 경로. 예: /api/admin/inquiries' },
|
|
374
|
+
method: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
description: 'GET/POST/... 생략 시 해당 path 의 모든 메서드',
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
required: ['path'],
|
|
380
|
+
additionalProperties: false,
|
|
381
|
+
}),
|
|
382
|
+
execute: getOpenApiEndpoint,
|
|
383
|
+
}),
|
|
298
384
|
write_file: tool({
|
|
299
385
|
description:
|
|
300
386
|
'새 파일을 만들거나 기존 파일을 통째로 덮어쓴다. 새 파일을 만들기 전에 반드시 1) 비슷한 기존 파일을 read_file 로 보고 2) 같은 폴더 컨벤션(barrel 파일, 네이밍, import 순서) 을 따른다.',
|
package/src/commands/chat.js
CHANGED
|
@@ -86,11 +86,15 @@ export async function chatCommand(opts = {}) {
|
|
|
86
86
|
};
|
|
87
87
|
system =
|
|
88
88
|
baseSystem +
|
|
89
|
-
'\n\n---\nOpenAPI 스펙 (
|
|
89
|
+
'\n\n---\nOpenAPI 스펙 개요 (아래 목록은 길면 잘려 있을 수 있음):\n' +
|
|
90
90
|
summary +
|
|
91
|
-
'\n\n
|
|
92
|
-
'
|
|
93
|
-
'
|
|
91
|
+
'\n\n**중요**: 위 목록은 일부만 보일 수 있다. 사용자가 특정 엔드포인트를 말하거나 ' +
|
|
92
|
+
'위 목록에서 안 보이면, 추측하지 말고 반드시 `search_openapi(query)` 로 정확한 ' +
|
|
93
|
+
'path/method 를 검색한 뒤 `get_openapi_endpoint(path, method)` 로 상세 스키마를 ' +
|
|
94
|
+
'가져와서 zod/타입/요청 함수를 만든다. ' +
|
|
95
|
+
'예: 사용자가 "inquiries" 라고 하면 search_openapi("inquiries") 로 ' +
|
|
96
|
+
'`/api/admin/inquiries` 같은 실제 경로를 찾아낸다. ' +
|
|
97
|
+
'이미 `*.gen.ts` 가 있으면 그걸 import 해서 쓰는 것도 좋다.';
|
|
94
98
|
}
|
|
95
99
|
} catch {
|
|
96
100
|
/* 비정상 URL/네트워크 실패 — 무시하고 계속. */
|
|
@@ -204,6 +208,7 @@ async function runOnce({ cfg, resolved, system, prompt }) {
|
|
|
204
208
|
const tools = buildTools({
|
|
205
209
|
projectRoot,
|
|
206
210
|
effective: cfg.effective,
|
|
211
|
+
openapiSource: cfg.effective.api?.openapi ?? null,
|
|
207
212
|
onEvent: (ev) => {
|
|
208
213
|
const label =
|
|
209
214
|
ev.kind === 'write_created'
|
|
@@ -378,6 +383,7 @@ async function runReadlineFallback({ cfg, resolved, system, session, openapiInfo
|
|
|
378
383
|
const tools = buildTools({
|
|
379
384
|
projectRoot,
|
|
380
385
|
effective: cfg.effective,
|
|
386
|
+
openapiSource: cfg.effective.api?.openapi ?? null,
|
|
381
387
|
onEvent: (ev) => {
|
|
382
388
|
const label =
|
|
383
389
|
ev.kind === 'write_created'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 스펙을 "쿼리" 하는 헬퍼.
|
|
3
|
+
*
|
|
4
|
+
* 큰 스펙(수백 개 엔드포인트, 수백 KB) 은 시스템 프롬프트 요약에 다 담을 수도 없고,
|
|
5
|
+
* 캐시 파일을 통째로 모델에 읽힐 수도 없다(토큰 폭발). 그래서 모델이 필요한 부분만
|
|
6
|
+
* 골라 가져갈 수 있게 검색/상세조회 함수를 제공한다.
|
|
7
|
+
*
|
|
8
|
+
* - searchEndpoints: path/summary/tag/operationId 부분일치로 후보 나열
|
|
9
|
+
* - getEndpoint: 특정 path(+method) 의 상세를 $ref 해석해서 반환
|
|
10
|
+
*/
|
|
11
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
12
|
+
|
|
13
|
+
/** "#/components/schemas/Foo" → doc.components.schemas.Foo */
|
|
14
|
+
function resolveRef(doc, ref, seen = new Set()) {
|
|
15
|
+
if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
|
|
16
|
+
const parts = ref.slice(2).split('/');
|
|
17
|
+
let cur = doc;
|
|
18
|
+
for (const p of parts) {
|
|
19
|
+
if (cur == null) return null;
|
|
20
|
+
cur = cur[p];
|
|
21
|
+
}
|
|
22
|
+
return cur ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 객체 안의 $ref 를 재귀적으로 해석해 인라인한다.
|
|
27
|
+
* 깊이/순환 방지: maxDepth, seen(ref 경로) 로 컷.
|
|
28
|
+
*/
|
|
29
|
+
function deref(doc, node, { maxDepth = 6, depth = 0, seen = new Set() } = {}) {
|
|
30
|
+
if (node == null || typeof node !== 'object') return node;
|
|
31
|
+
if (depth > maxDepth) return node;
|
|
32
|
+
|
|
33
|
+
if (node.$ref) {
|
|
34
|
+
if (seen.has(node.$ref)) return { $ref: node.$ref, note: '(순환 참조 생략)' };
|
|
35
|
+
const target = resolveRef(doc, node.$ref);
|
|
36
|
+
if (!target) return node;
|
|
37
|
+
const nextSeen = new Set(seen);
|
|
38
|
+
nextSeen.add(node.$ref);
|
|
39
|
+
return deref(doc, target, { maxDepth, depth: depth + 1, seen: nextSeen });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(node)) {
|
|
43
|
+
return node.map((n) => deref(doc, n, { maxDepth, depth: depth + 1, seen }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const [k, v] of Object.entries(node)) {
|
|
48
|
+
out[k] = deref(doc, v, { maxDepth, depth: depth + 1, seen });
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 모든 엔드포인트의 가벼운 인덱스 (path, method, summary, tags, operationId). */
|
|
54
|
+
export function listEndpoints(doc) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const [pathStr, methods] of Object.entries(doc?.paths ?? {})) {
|
|
57
|
+
if (!methods || typeof methods !== 'object') continue;
|
|
58
|
+
for (const m of METHODS) {
|
|
59
|
+
const op = methods[m];
|
|
60
|
+
if (!op || typeof op !== 'object') continue;
|
|
61
|
+
out.push({
|
|
62
|
+
method: m.toUpperCase(),
|
|
63
|
+
path: pathStr,
|
|
64
|
+
summary: op.summary ?? '',
|
|
65
|
+
operationId: op.operationId ?? '',
|
|
66
|
+
tags: op.tags ?? [],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 부분일치 검색. query 의 각 토큰이 path/summary/operationId/tag 어딘가에
|
|
75
|
+
* (대소문자 무시) 들어가면 매치. 점수 = 매치한 필드 수 + path 정확 포함 가산점.
|
|
76
|
+
*/
|
|
77
|
+
export function searchEndpoints(doc, query, { limit = 40 } = {}) {
|
|
78
|
+
const all = listEndpoints(doc);
|
|
79
|
+
if (!query || !query.trim()) return all.slice(0, limit);
|
|
80
|
+
|
|
81
|
+
const tokens = query.toLowerCase().split(/[\s/]+/).filter(Boolean);
|
|
82
|
+
const scored = [];
|
|
83
|
+
for (const e of all) {
|
|
84
|
+
const hay = [
|
|
85
|
+
e.path.toLowerCase(),
|
|
86
|
+
e.summary.toLowerCase(),
|
|
87
|
+
e.operationId.toLowerCase(),
|
|
88
|
+
e.tags.join(' ').toLowerCase(),
|
|
89
|
+
];
|
|
90
|
+
let score = 0;
|
|
91
|
+
for (const tok of tokens) {
|
|
92
|
+
if (hay[0].includes(tok)) score += 3; // path 매치 가중
|
|
93
|
+
else if (hay.some((h) => h.includes(tok))) score += 1;
|
|
94
|
+
}
|
|
95
|
+
if (e.path.toLowerCase().includes(query.toLowerCase())) score += 2;
|
|
96
|
+
if (score > 0) scored.push({ ...e, score });
|
|
97
|
+
}
|
|
98
|
+
scored.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
99
|
+
return scored.slice(0, limit);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 특정 path(+method) 의 상세. $ref 를 인라인해서 request/response 스키마까지 본다.
|
|
104
|
+
* method 생략 시 그 path 의 모든 메서드 반환.
|
|
105
|
+
*/
|
|
106
|
+
export function getEndpoint(doc, targetPath, method) {
|
|
107
|
+
const methods = doc?.paths?.[targetPath];
|
|
108
|
+
if (!methods) {
|
|
109
|
+
// 끝 슬래시/대소문자 보정 한 번 시도
|
|
110
|
+
const found = Object.keys(doc?.paths ?? {}).find(
|
|
111
|
+
(p) => p.toLowerCase() === String(targetPath).toLowerCase(),
|
|
112
|
+
);
|
|
113
|
+
if (!found) return { ok: false, error: `해당 path 없음: ${targetPath}` };
|
|
114
|
+
targetPath = found;
|
|
115
|
+
}
|
|
116
|
+
const pathItem = doc.paths[targetPath];
|
|
117
|
+
const wanted = method ? [method.toLowerCase()] : METHODS;
|
|
118
|
+
const operations = {};
|
|
119
|
+
for (const m of wanted) {
|
|
120
|
+
const op = pathItem[m];
|
|
121
|
+
if (!op) continue;
|
|
122
|
+
operations[m.toUpperCase()] = {
|
|
123
|
+
summary: op.summary,
|
|
124
|
+
operationId: op.operationId,
|
|
125
|
+
tags: op.tags,
|
|
126
|
+
parameters: deref(doc, op.parameters),
|
|
127
|
+
requestBody: deref(doc, op.requestBody),
|
|
128
|
+
responses: deref(doc, op.responses),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(operations).length === 0) {
|
|
132
|
+
return { ok: false, error: `${targetPath} 에 ${method ?? ''} 메서드 없음` };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, path: targetPath, operations };
|
|
135
|
+
}
|
package/src/openapi/summary.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* ...
|
|
15
15
|
*/
|
|
16
16
|
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
17
|
-
const
|
|
18
|
-
const MAX_BYTES =
|
|
17
|
+
const MAX_PATHS = 400;
|
|
18
|
+
const MAX_BYTES = 16 * 1024;
|
|
19
19
|
|
|
20
20
|
export function summarizeOpenApi(doc) {
|
|
21
21
|
if (!doc || typeof doc !== 'object') return null;
|
|
@@ -30,46 +30,42 @@ export function summarizeOpenApi(doc) {
|
|
|
30
30
|
lines.push(`base: ${servers.slice(0, 3).join(', ')}`);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// path 하나당 한 줄로 메서드를 합친다 (줄 수 절반 + 모델이 path 단위로 보기 쉬움).
|
|
34
|
+
const byPath = [];
|
|
34
35
|
for (const [pathStr, methods] of Object.entries(doc.paths ?? {})) {
|
|
35
36
|
if (!methods || typeof methods !== 'object') continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
method: m.toUpperCase(),
|
|
42
|
-
path: pathStr,
|
|
43
|
-
desc: desc.split('\n')[0].slice(0, 90),
|
|
44
|
-
});
|
|
45
|
-
}
|
|
37
|
+
const verbs = METHODS.filter((m) => methods[m] && typeof methods[m] === 'object').map((m) =>
|
|
38
|
+
m.toUpperCase(),
|
|
39
|
+
);
|
|
40
|
+
if (verbs.length === 0) continue;
|
|
41
|
+
byPath.push({ path: pathStr, verbs });
|
|
46
42
|
}
|
|
43
|
+
byPath.sort((a, b) => a.path.localeCompare(b.path));
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const shown = endpoints.slice(0, MAX_ENDPOINTS);
|
|
45
|
+
const total = byPath.length;
|
|
46
|
+
const shown = byPath.slice(0, MAX_PATHS);
|
|
51
47
|
|
|
52
|
-
lines.push(`
|
|
48
|
+
lines.push(`paths: (${shown.length}/${total})`);
|
|
53
49
|
for (const e of shown) {
|
|
54
|
-
lines.push(` ${e.
|
|
50
|
+
lines.push(` ${e.verbs.join(',').padEnd(20)} ${e.path}`);
|
|
55
51
|
}
|
|
56
52
|
if (shown.length < total) {
|
|
57
|
-
lines.push(` ... and ${total - shown.length} more`);
|
|
53
|
+
lines.push(` ... and ${total - shown.length} more (search_openapi 로 검색하세요)`);
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
let result = lines.join('\n');
|
|
57
|
+
// 바이트 초과 시 뒤에서부터 잘라낸다 (search_openapi 가 있으니 전부 못 담아도 안전).
|
|
61
58
|
if (Buffer.byteLength(result, 'utf8') > MAX_BYTES) {
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
result = compact;
|
|
59
|
+
const header = [lines[0], servers.length ? lines[1] : null].filter(Boolean);
|
|
60
|
+
const out = [...header, `paths: (truncated/${total}) — 전체는 search_openapi 로 검색`];
|
|
61
|
+
let bytes = Buffer.byteLength(out.join('\n'), 'utf8');
|
|
62
|
+
for (const e of shown) {
|
|
63
|
+
const line = ` ${e.verbs.join(',').padEnd(20)} ${e.path}`;
|
|
64
|
+
bytes += Buffer.byteLength(line + '\n', 'utf8');
|
|
65
|
+
if (bytes > MAX_BYTES) break;
|
|
66
|
+
out.push(line);
|
|
67
|
+
}
|
|
68
|
+
result = out.join('\n');
|
|
73
69
|
}
|
|
74
70
|
return result;
|
|
75
71
|
}
|
package/src/ui/ChatApp.js
CHANGED