deel-local-cli 1.15.0 → 1.16.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 +70 -13
- package/README.md +71 -13
- package/bin/deel.js +45 -1
- package/package.json +1 -1
- package/src/acp/jsonrpc.js +23 -0
- package/src/acp/serve.js +62 -5
- package/src/agent/asks.js +58 -2
- package/src/agent/budget.js +33 -0
- package/src/agent/effort.js +69 -0
- package/src/agent/loop.js +10 -2
- package/src/agent/modes.js +179 -17
- package/src/agent/route.js +119 -8
- package/src/backend/adapter.js +56 -5
- package/src/cmdnames.js +1 -0
- package/src/commands.js +31 -13
- package/src/completion.js +1 -0
- package/src/i18n/en.js +13 -0
- package/src/i18n/ja.js +13 -0
- package/src/i18n/ko.js +13 -0
- package/src/i18n/zh.js +13 -0
- package/src/tools/doc2md.js +266 -0
- package/src/tools/docs.js +14 -4
- package/src/tools/fig.js +393 -0
- package/src/tools/index.js +49 -8
- package/src/tools/kiwi.js +293 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 문서 → 마크다운.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 글이 아니라 마크다운인가 ─────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* deel 은 이미 hwpx·docx·pptx·xlsx·pdf 와 Figma `.fig` 를 제 손으로 읽는다
|
|
7
|
+
* (docs.js · xlsx.js · pdf.js · fig.js). 다만 돌려주던 것이 **평평한 글**이었다.
|
|
8
|
+
* 표는 `이름 | 값 | 비고` 한 줄로 펴져 나갔고, 장 구분은 `--- 3장 ---` 이었다.
|
|
9
|
+
*
|
|
10
|
+
* 그게 왜 손해인가 — 읽는 쪽이 사람이 아니라 모델이기 때문이다. 마크다운
|
|
11
|
+
* 표는 모델이 「첫 줄이 머리글이고 셋째 칸이 비고」 라는 것을 그냥 안다.
|
|
12
|
+
* 평평한 줄에서는 그걸 매번 다시 짐작해야 하고, 칸이 빈 행이 하나 섞이면
|
|
13
|
+
* 거기서부터 어긋난다. 같은 바이트로 더 많이 전하는 쪽이 마크다운이다.
|
|
14
|
+
*
|
|
15
|
+
* 그리고 사람도 그대로 쓴다. `.md` 로 떨어뜨려 두면 그게 곧 문서다.
|
|
16
|
+
*
|
|
17
|
+
* ── 무엇을 새로 들이지 않나 ─────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* 아무것도. 읽는 일은 이미 있는 읽개들이 그대로 하고, 이 파일은 그것이
|
|
20
|
+
* 돌려준 것을 **그리기만** 한다. 의존성 0개는 그대로다.
|
|
21
|
+
*
|
|
22
|
+
* ── 표를 언제 표로 안 그리나 ────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* 칸이 열두 개를 넘으면 마크다운 표는 읽는 것이 아니라 가로로 흐르는 벽이
|
|
25
|
+
* 된다. 그때는 CSV 울타리로 떨어뜨리고 **왜 그랬는지 적는다.** 조용히
|
|
26
|
+
* 모양을 바꾸면, 표가 안 나오는 것이 파일 탓인지 우리 탓인지 알 길이 없다.
|
|
27
|
+
*/
|
|
28
|
+
import { basename, extname } from 'node:path';
|
|
29
|
+
|
|
30
|
+
import { isDocPath, readDoc, summarize as docSummary } from './docs.js';
|
|
31
|
+
import { isPdfPath, readPdf, summarize as pdfSummary } from './pdf.js';
|
|
32
|
+
import { isFigPath, readFig, summarize as figSummary } from './fig.js';
|
|
33
|
+
import { readXlsx, toCsv } from './xlsx.js';
|
|
34
|
+
import { readFileSync } from 'node:fs';
|
|
35
|
+
|
|
36
|
+
/** 엑셀 확장자. tools/index.js 가 쓰는 것과 같은 목록이다. */
|
|
37
|
+
const 엑셀 = new Set(['.xlsx', '.xlsm']);
|
|
38
|
+
|
|
39
|
+
/** 이 길로 마크다운을 만들 수 있는 파일인가. */
|
|
40
|
+
export function 바꿀수있나(경로) {
|
|
41
|
+
const p = String(경로 ?? '');
|
|
42
|
+
return isDocPath(p) || isPdfPath(p) || isFigPath(p) || 엑셀.has(extname(p).toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 우리가 직접 읽는 갈래 목록 — 화면과 도움말이 같은 곳에서 가져간다. */
|
|
46
|
+
export const 읽는갈래 = ['hwpx', 'docx', 'pptx', 'xlsx', 'pdf', 'fig'];
|
|
47
|
+
|
|
48
|
+
// 다 실어 봐야 창만 찬다. 글로 낼 때와 같은 상한을 쓴다(docs.js · pdf.js).
|
|
49
|
+
const 최대글자 = 60000;
|
|
50
|
+
// 이보다 칸이 많으면 마크다운 표로 안 그린다 — 가로로 흐르는 벽이 된다.
|
|
51
|
+
const 최대칸 = 12;
|
|
52
|
+
|
|
53
|
+
/** 마크다운 표 칸 하나. `|` 와 줄바꿈은 표를 부수므로 바꿔 준다. */
|
|
54
|
+
function 칸(v) {
|
|
55
|
+
return String(v ?? '')
|
|
56
|
+
.replace(/\|/g, '\\|')
|
|
57
|
+
.replace(/\r?\n/g, '<br>')
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 이 행들이 다 비었나. 빈 표를 그리면 머리글만 있는 유령 표가 남는다. */
|
|
62
|
+
const 다비었나 = (행들) => 행들.every((r) => r.every((c) => !String(c ?? '').trim()));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 행 목록을 마크다운 표로. 첫 행이 머리글이다.
|
|
66
|
+
*
|
|
67
|
+
* 칸 수가 행마다 다른 문서가 흔하다(합쳐진 칸). 제일 넓은 행에 맞춰 채운다 —
|
|
68
|
+
* 안 채우면 마크다운이 그 행부터 표로 안 읽는다.
|
|
69
|
+
*/
|
|
70
|
+
export function 표그리기(행들) {
|
|
71
|
+
const 있는것 = (행들 ?? []).filter((r) => Array.isArray(r) && r.length);
|
|
72
|
+
if (!있는것.length || 다비었나(있는것)) return '';
|
|
73
|
+
|
|
74
|
+
const 폭 = Math.max(...있는것.map((r) => r.length));
|
|
75
|
+
if (폭 > 최대칸) {
|
|
76
|
+
return ['```csv', toCsv(있는것.map((r) => [...r, ...Array(폭 - r.length).fill('')])), '```',
|
|
77
|
+
`(칸이 ${폭}개라 마크다운 표 대신 CSV 로 냈습니다 — 표로 그리면 가로로 흘러 못 읽습니다.)`,
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const 채워 = (r) => [...r.map(칸), ...Array(폭 - r.length).fill('')];
|
|
82
|
+
const [머리, ...몸] = 있는것;
|
|
83
|
+
return [
|
|
84
|
+
`| ${채워(머리).join(' | ')} |`,
|
|
85
|
+
`|${' --- |'.repeat(폭)}`,
|
|
86
|
+
...몸.map((r) => `| ${채워(r).join(' | ')} |`),
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 글자 수를 세면서 조각을 담는 자. 넘치면 **넘쳤다고 말하고** 멈춘다.
|
|
92
|
+
*
|
|
93
|
+
* 조용히 자르면 모델은 그게 전부인 줄 알고 "문서에 그런 내용 없다" 고 답한다.
|
|
94
|
+
* 이 파일이 읽개들에서 그대로 물려받는 규칙이다.
|
|
95
|
+
*/
|
|
96
|
+
function 담개(maxChars) {
|
|
97
|
+
const 조각 = [];
|
|
98
|
+
const 말 = [];
|
|
99
|
+
let 셈 = 0;
|
|
100
|
+
let 찼나 = false;
|
|
101
|
+
return {
|
|
102
|
+
담기(글) {
|
|
103
|
+
if (찼나) return false;
|
|
104
|
+
const s = String(글 ?? '');
|
|
105
|
+
if (셈 + s.length > maxChars) {
|
|
106
|
+
찼나 = true;
|
|
107
|
+
말.push(`${maxChars.toLocaleString('en-US')}자에서 잘랐습니다 — 뒷부분은 안 실렸습니다`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
조각.push(s);
|
|
111
|
+
셈 += s.length + 1;
|
|
112
|
+
return true;
|
|
113
|
+
},
|
|
114
|
+
get 찼나() { return 찼나; },
|
|
115
|
+
끝내기: () => ({ md: 조각.join('\n\n'), 말 }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 덩이 하나(장·구획·쪽)를 담는다.
|
|
121
|
+
*
|
|
122
|
+
* 표 행({행:[...]})이 잇달아 나오면 한 표로 묶는다. 사이에 글이 끼면 거기서
|
|
123
|
+
* 표가 끊긴 것이다 — 문서에서 표가 두 개로 나뉜 자리가 정확히 그 모양이다.
|
|
124
|
+
*/
|
|
125
|
+
function 덩이담기(담, 문단들, 머리글) {
|
|
126
|
+
if (머리글 && !담.담기(머리글)) return;
|
|
127
|
+
let 표 = [];
|
|
128
|
+
const 표비우기 = () => {
|
|
129
|
+
if (!표.length) return true;
|
|
130
|
+
const 그림 = 표그리기(표);
|
|
131
|
+
표 = [];
|
|
132
|
+
return 그림 ? 담.담기(그림) : true;
|
|
133
|
+
};
|
|
134
|
+
for (const 문단 of 문단들 ?? []) {
|
|
135
|
+
if (문단 && typeof 문단 === 'object' && Array.isArray(문단.행)) { 표.push(문단.행); continue; }
|
|
136
|
+
if (!표비우기()) return;
|
|
137
|
+
if (!담.담기(String(문단))) return;
|
|
138
|
+
}
|
|
139
|
+
표비우기();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 마크다운 제목에 파일 이름을 그대로 쓴다 — 어느 파일에서 나온 글인지가 남아야 한다. */
|
|
143
|
+
const 제목 = (경로) => `# ${basename(String(경로 ?? ''))}`;
|
|
144
|
+
|
|
145
|
+
function 문서를마크다운(경로, maxChars) {
|
|
146
|
+
const r = readDoc(경로, { 표를따로: true });
|
|
147
|
+
if (!r.ok) return { ok: false, error: r.error, 끝났다: r.끝났다 };
|
|
148
|
+
|
|
149
|
+
const 담 = 담개(maxChars);
|
|
150
|
+
담.담기(제목(경로));
|
|
151
|
+
const 여럿 = r.덩이들.length > 1;
|
|
152
|
+
for (const d of r.덩이들) {
|
|
153
|
+
덩이담기(담, d.문단들, 여럿 ? `## ${d.이름}` : '');
|
|
154
|
+
if (담.찼나) break;
|
|
155
|
+
}
|
|
156
|
+
const { md, 말 } = 담.끝내기();
|
|
157
|
+
return { ok: true, 갈래: r.갈래, md, 말, summary: docSummary(r) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function 엑셀을마크다운(경로, maxChars) {
|
|
161
|
+
let r;
|
|
162
|
+
try {
|
|
163
|
+
r = readXlsx(readFileSync(경로));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return { ok: false, error: `엑셀을 읽지 못했습니다 — ${err.message}` };
|
|
166
|
+
}
|
|
167
|
+
const 담 = 담개(maxChars);
|
|
168
|
+
담.담기(제목(경로));
|
|
169
|
+
for (const s of r.sheets) {
|
|
170
|
+
// 숨긴 시트도 낸다. 다만 숨겨져 있었다고 적는다 — 그 사실이 값을 읽는
|
|
171
|
+
// 방식을 바꾼다(옛 판·임시 계산용인 일이 많다).
|
|
172
|
+
담.담기(`## ${s.name}${s.hidden ? ' (숨김 시트)' : ''}`);
|
|
173
|
+
const 그림 = 표그리기(s.rows);
|
|
174
|
+
if (!담.담기(그림 || '(빈 시트입니다.)')) break;
|
|
175
|
+
}
|
|
176
|
+
const { md, 말 } = 담.끝내기();
|
|
177
|
+
const 시트말 = `xlsx · 시트 ${r.sheets.length}개`;
|
|
178
|
+
return { ok: true, 갈래: 'xlsx', md, 말: [...말, ...(r.notes ?? [])], summary: 시트말 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Figma 시안을 마크다운으로.
|
|
183
|
+
*
|
|
184
|
+
* 쪽마다 `## 쪽이름` 이 되고, 그 밑이 그대로 마크다운 목록이 된다 — 시안의
|
|
185
|
+
* 짜임이 곧 목록의 들여쓰기라, 옮겨 그릴 것이 없다.
|
|
186
|
+
*/
|
|
187
|
+
function fig를마크다운(경로, maxChars) {
|
|
188
|
+
const r = readFig(경로);
|
|
189
|
+
if (!r.ok) return { ok: false, error: r.error, 끝났다: r.끝났다 };
|
|
190
|
+
|
|
191
|
+
const 담 = 담개(maxChars);
|
|
192
|
+
담.담기(제목(경로));
|
|
193
|
+
for (const d of r.덩이들) {
|
|
194
|
+
if (!담.담기(`## ${d.이름}`)) break;
|
|
195
|
+
/*
|
|
196
|
+
* 나무 줄은 **붙여서** 한 덩이로 낸다.
|
|
197
|
+
*
|
|
198
|
+
* 여기만 다른 읽개와 다르다. 문서의 문단은 사이가 벌어져야 문단인데,
|
|
199
|
+
* 목록은 사이가 벌어지면 들여쓰기가 뜻을 잃는다. 그렇다고 쪽 하나를
|
|
200
|
+
* 통째로 한 덩이로 만들면, 넘칠 때 그 쪽이 통째로 사라진다 — 조용히
|
|
201
|
+
* 빠진 자리를 안 만드는 것이 이 파일의 규칙이라 그럴 수는 없다.
|
|
202
|
+
* 그래서 8천 자쯤에서 끊는다. 자를 자리가 줄 단위로 남는다.
|
|
203
|
+
*/
|
|
204
|
+
let 뭉치 = [];
|
|
205
|
+
let 셈 = 0;
|
|
206
|
+
const 비우기 = () => {
|
|
207
|
+
if (!뭉치.length) return true;
|
|
208
|
+
const 글 = 뭉치.join('\n');
|
|
209
|
+
뭉치 = [];
|
|
210
|
+
셈 = 0;
|
|
211
|
+
return 담.담기(글);
|
|
212
|
+
};
|
|
213
|
+
for (const 줄 of d.문단들 ?? []) {
|
|
214
|
+
뭉치.push(String(줄));
|
|
215
|
+
셈 += String(줄).length + 1;
|
|
216
|
+
if (셈 >= 8000 && !비우기()) break;
|
|
217
|
+
}
|
|
218
|
+
if (!비우기()) break;
|
|
219
|
+
}
|
|
220
|
+
const { md, 말 } = 담.끝내기();
|
|
221
|
+
return { ok: true, 갈래: 'fig', md, 말: [...(r.말 ?? []), ...말], summary: figSummary(r) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function pdf를마크다운(경로, maxChars) {
|
|
225
|
+
const r = readPdf(경로);
|
|
226
|
+
if (!r.ok) return { ok: false, error: r.error, 끝났다: r.끝났다 };
|
|
227
|
+
|
|
228
|
+
const 담 = 담개(maxChars);
|
|
229
|
+
담.담기(제목(경로));
|
|
230
|
+
const 왜표 = new Map((r.못읽은쪽 ?? []).map((x) => [x.번호, x]));
|
|
231
|
+
for (const d of r.덩이들) {
|
|
232
|
+
const 번호 = Number((d.이름.match(/^(\d+)/) ?? [])[1]);
|
|
233
|
+
담.담기(`## ${d.이름}`);
|
|
234
|
+
const 못 = 왜표.get(번호);
|
|
235
|
+
// 못 읽은 쪽을 조용히 건너뛰면 **없는 문서**가 된다. 쪽 자리에 왜 비었는지 적는다.
|
|
236
|
+
if (못 && !못.일부) { 담.담기(`> 이 쪽은 글로 못 읽었습니다 — ${못.왜}`); continue; }
|
|
237
|
+
덩이담기(담, d.문단들, '');
|
|
238
|
+
if (못?.일부) 담.담기(`> 이 쪽은 일부만 읽었습니다 — ${못.왜}`);
|
|
239
|
+
if (담.찼나) break;
|
|
240
|
+
}
|
|
241
|
+
const { md, 말 } = 담.끝내기();
|
|
242
|
+
return { ok: true, 갈래: 'pdf', md, 말, summary: pdfSummary(r) };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 문서 하나를 마크다운으로.
|
|
247
|
+
*
|
|
248
|
+
* 던지지 않는다. 깨진 파일은 도구 실행 한가운데서 만나는 것이라, 예외가 나면
|
|
249
|
+
* 「문서가 깨졌다」 가 「도구가 터졌다」 로 보고된다 — 읽개들이 다 지키는 규칙이다.
|
|
250
|
+
*
|
|
251
|
+
* @returns {{ok:true, 갈래:string, md:string, 말:string[], summary:string}
|
|
252
|
+
* |{ok:false, error:string, 끝났다?:boolean}}
|
|
253
|
+
*/
|
|
254
|
+
export function 마크다운(경로, { maxChars = 최대글자 } = {}) {
|
|
255
|
+
const p = String(경로 ?? '');
|
|
256
|
+
if (엑셀.has(extname(p).toLowerCase())) return 엑셀을마크다운(p, maxChars);
|
|
257
|
+
if (isPdfPath(p)) return pdf를마크다운(p, maxChars);
|
|
258
|
+
if (isFigPath(p)) return fig를마크다운(p, maxChars);
|
|
259
|
+
if (isDocPath(p)) return 문서를마크다운(p, maxChars);
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
error: `마크다운으로 바꿀 수 있는 갈래가 아닙니다: ${basename(p) || p}\n`
|
|
263
|
+
+ ` 직접 읽는 것: ${읽는갈래.join(' · ')}\n`
|
|
264
|
+
+ ' 옛 형식(.ppt·.doc·.xls·.hwp)은 이 PC 에 LibreOffice 가 있으면 Read 가 빌려 읽습니다.',
|
|
265
|
+
};
|
|
266
|
+
}
|
package/src/tools/docs.js
CHANGED
|
@@ -162,7 +162,14 @@ const 끝이름 = (name) => {
|
|
|
162
162
|
* "이름값가1" 같은 덩어리가 되는데, 그건 뜻이 사라진 글이라 안 읽은 것보다
|
|
163
163
|
* 나쁘다 — 모델이 그걸 근거로 답한다.
|
|
164
164
|
*/
|
|
165
|
-
|
|
165
|
+
/**
|
|
166
|
+
* @param {object} [옵션]
|
|
167
|
+
* @param {boolean} [옵션.표를따로] 표 행을 { 행: [칸...] } 로 돌려준다.
|
|
168
|
+
* 기본값은 여태와 똑같이 `a | b` 한 줄이다. 마크다운으로 낼 때만 켠다
|
|
169
|
+
* (tools/doc2md.js) — 마크다운 표는 칸이 몇 개인지 알아야 그릴 수 있고,
|
|
170
|
+
* 이어 붙인 한 줄에서 그걸 되짚으면 글에 든 | 하나에 표가 어긋난다.
|
|
171
|
+
*/
|
|
172
|
+
export function 문단뽑기(xml, 갈래, { 표를따로 = false } = {}) {
|
|
166
173
|
const 표기 = 이름표[갈래];
|
|
167
174
|
const 문단들 = [];
|
|
168
175
|
|
|
@@ -195,7 +202,10 @@ export function 문단뽑기(xml, 갈래) {
|
|
|
195
202
|
if (이름 === 표기.표) { 표깊이 += t.closing ? -1 : (t.selfClosing ? 0 : 1); continue; }
|
|
196
203
|
if (표깊이 > 0 && 이름 === 표기.행) {
|
|
197
204
|
if (t.closing) {
|
|
198
|
-
if (행칸들)
|
|
205
|
+
if (행칸들) {
|
|
206
|
+
const 칸 = 행칸들.map((x) => x.trim());
|
|
207
|
+
문단들.push(표를따로 ? { 행: 칸 } : 칸.join(' | '));
|
|
208
|
+
}
|
|
199
209
|
행칸들 = null;
|
|
200
210
|
} else {
|
|
201
211
|
행칸들 = [];
|
|
@@ -237,7 +247,7 @@ const 알맹이 = {
|
|
|
237
247
|
* 던지지 않는다 — 깨진 파일은 도구 실행 한가운데서 만나는 것이라,
|
|
238
248
|
* 예외가 나면 "문서가 깨졌다" 가 "도구가 터졌다" 로 보고된다.
|
|
239
249
|
*/
|
|
240
|
-
export function readDoc(
|
|
250
|
+
export function readDoc(경로또는버퍼, { 표를따로 = false } = {}) {
|
|
241
251
|
const 갈래 = Buffer.isBuffer(경로또는버퍼) ? null : 종류(경로또는버퍼);
|
|
242
252
|
let buf;
|
|
243
253
|
try {
|
|
@@ -270,7 +280,7 @@ export function readDoc(경로또는버퍼) {
|
|
|
270
280
|
|
|
271
281
|
const 덩이들 = 찾은.map((s, i) => ({
|
|
272
282
|
이름: 구획이름(i, s.번호),
|
|
273
|
-
문단들: 문단뽑기(s.몸.toString('utf8'),
|
|
283
|
+
문단들: 문단뽑기(s.몸.toString('utf8'), 갈래, { 표를따로 }),
|
|
274
284
|
}));
|
|
275
285
|
return { ok: true, 갈래, 덩이들 };
|
|
276
286
|
}
|