deel-local-cli 1.1.1 → 1.2.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.en.md +347 -9
- package/README.md +231 -4
- package/package.json +2 -2
- package/src/agent/budget.js +0 -14
- package/src/agent/compact.js +87 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/loop.js +38 -1
- package/src/agent/modes.js +2 -0
- package/src/agent/session.js +145 -1
- package/src/agent/threads.js +154 -0
- package/src/commands.js +167 -5
- package/src/config.js +10 -1
- package/src/oneshot.js +4 -0
- package/src/repl.js +83 -17
- package/src/safety/guard.js +37 -2
- package/src/safety/undo.js +1 -1
- package/src/setup.js +2 -2
- package/src/skills/discover.js +1 -1
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +1 -1
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +6 -2
- package/src/ui/inputbox.js +32 -6
- package/src/ui/md.js +227 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +14 -1
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/commands.js
CHANGED
|
@@ -54,6 +54,8 @@ export const COMMANDS = {
|
|
|
54
54
|
cost: { desc: '이번 세션 사용량' },
|
|
55
55
|
status: { desc: '연결 상태' },
|
|
56
56
|
scan: { desc: '이 PC 의 로컬 모델 서버 훑기', arg: '[save]' },
|
|
57
|
+
thread: { desc: '대화 갈래 — 곁가지를 딴 자리에서. 연결·되돌리기는 같이 씀', arg: '[new|fork|close|번호|이름]' },
|
|
58
|
+
learned: { desc: '쓰면서 저절로 알게 된 것 — 되는 명령·이 모델의 버릇', arg: '[지우기]' },
|
|
57
59
|
sessions:{ desc: '이 폴더의 지난 대화 목록' },
|
|
58
60
|
recall: { desc: '지난 대화에서 찾기 — 목록 말고 내용으로', arg: '<찾을 말>' },
|
|
59
61
|
mcp: { desc: '밖에서 붙인 도구(MCP) 서버 보기' },
|
|
@@ -530,6 +532,12 @@ export async function handle(line, session, ctx) {
|
|
|
530
532
|
return { handled: true };
|
|
531
533
|
}
|
|
532
534
|
|
|
535
|
+
case 'thread':
|
|
536
|
+
case '갈래': return 갈래명령(session, ctx, arg), { handled: true };
|
|
537
|
+
|
|
538
|
+
case 'learned':
|
|
539
|
+
case '배움': return 배움명령(session, ctx, arg), { handled: true };
|
|
540
|
+
|
|
533
541
|
case 'sessions': {
|
|
534
542
|
const rows = listSessions(session.root, { limit: 12 });
|
|
535
543
|
rule('이 폴더의 지난 대화', 70);
|
|
@@ -701,10 +709,6 @@ export async function 미리보기끄기() {
|
|
|
701
709
|
}
|
|
702
710
|
|
|
703
711
|
/** 지금 띄워 둔 것. 상태줄·검사에서 본다. */
|
|
704
|
-
export const 미리보기상태 = () => (미리보기중
|
|
705
|
-
? { url: 미리보기중.서버.url, 폴더: 미리보기중.보인이름, 되살아나나: 미리보기중.서버.되살아나나 }
|
|
706
|
-
: null);
|
|
707
|
-
|
|
708
712
|
async function 미리보기(session, ctx, arg) {
|
|
709
713
|
const 말 = String(arg ?? '').trim();
|
|
710
714
|
|
|
@@ -986,6 +990,154 @@ function showThink(session, { 자세히 = false } = {}) {
|
|
|
986
990
|
say('');
|
|
987
991
|
}
|
|
988
992
|
|
|
993
|
+
/**
|
|
994
|
+
* 대화 갈래.
|
|
995
|
+
*
|
|
996
|
+
* 여기서 하는 일은 화면과 말뿐이다. 갈래를 들고 있는 것은 agent/threads.js 다.
|
|
997
|
+
*/
|
|
998
|
+
function 갈래명령(session, ctx, arg = '') {
|
|
999
|
+
const 갈래 = ctx?.갈래;
|
|
1000
|
+
say('');
|
|
1001
|
+
if (!갈래) {
|
|
1002
|
+
// 한 번 돌리고 끝내는 자리(-p)에는 갈래가 없다. 없는 것을 있는 척하지 않는다.
|
|
1003
|
+
say(` ${c.gray('이 자리에서는 갈래를 못 씁니다 — 대화 화면에서만 됩니다.')}`);
|
|
1004
|
+
say('');
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const 말 = String(arg ?? '').trim();
|
|
1009
|
+
const [머리, ...나머지] = 말.split(/\s+/);
|
|
1010
|
+
const 뒷말 = 나머지.join(' ');
|
|
1011
|
+
const 알림 = (g) => {
|
|
1012
|
+
say(` ${c.hcyan('⑂')} ${c.bold(g.이름)} ${c.gray('갈래로 왔습니다.')} `
|
|
1013
|
+
+ c.gray(session.messages.length ? `오간 말 ${session.messages.length}개` : '빈 대화입니다'));
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
if (/^(new|새|새로)$/i.test(머리 ?? '')) {
|
|
1017
|
+
알림(갈래.새로(뒷말));
|
|
1018
|
+
say(` ${c.gray('본줄기로 돌아가려면')} ${c.cyan('/thread 1')}`);
|
|
1019
|
+
say('');
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (/^(fork|갈라|분기)$/i.test(머리 ?? '')) {
|
|
1024
|
+
const g = 갈래.갈라내기(뒷말);
|
|
1025
|
+
say(` ${c.hcyan('⑂')} ${c.bold(g.이름)} ${c.gray('로 갈라 나왔습니다.')} `
|
|
1026
|
+
+ c.gray(`여기까지 오간 말 ${session.messages.length}개를 그대로 물려받았습니다.`));
|
|
1027
|
+
say(` ${c.gray('여기서 무엇을 하든 본줄기는 그대로입니다.')}`);
|
|
1028
|
+
say('');
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (/^(close|닫기|끝)$/i.test(머리 ?? '')) {
|
|
1033
|
+
const r = 갈래.닫기(뒷말);
|
|
1034
|
+
if (!r.ok) say(` ${c.red('못 닫았습니다')} ${c.gray(r.why)}`);
|
|
1035
|
+
else {
|
|
1036
|
+
say(` ${mark.ok} ${c.gray(`${r.닫은것.이름} 갈래를 닫았습니다. 적어 둔 것은 남아 있습니다 —`)} ${c.cyan('/sessions')}`);
|
|
1037
|
+
알림(r.지금);
|
|
1038
|
+
}
|
|
1039
|
+
say('');
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (말) {
|
|
1044
|
+
const g = 갈래.옮기기(말);
|
|
1045
|
+
if (!g) {
|
|
1046
|
+
say(` ${c.red('그런 갈래가 없습니다')} ${c.gray(말)}`);
|
|
1047
|
+
say(` ${c.gray('/thread 만 치면 목록이 나옵니다.')}`);
|
|
1048
|
+
} else 알림(g);
|
|
1049
|
+
say('');
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// 그냥 /thread — 목록.
|
|
1054
|
+
rule('대화 갈래', 70);
|
|
1055
|
+
for (const r of 갈래.목록()) {
|
|
1056
|
+
const 표 = r.지금 ? c.hcyan('▶') : c.gray(' ');
|
|
1057
|
+
const 이름 = r.지금 ? c.white(r.이름) : c.gray(r.이름);
|
|
1058
|
+
say(` ${표} ${c.gray(String(r.번호))} ${pad(이름, 24)} ${c.gray(`말 ${String(r.말수).padStart(3)}개`)}`
|
|
1059
|
+
+ (r.id ? ` ${c.gray(r.id)}` : ''));
|
|
1060
|
+
}
|
|
1061
|
+
say('');
|
|
1062
|
+
say(` ${c.gray('/thread new [이름]')} ${c.gray('빈 갈래로 나간다 — 곁가지 질문을 여기서')}`);
|
|
1063
|
+
say(` ${c.gray('/thread fork [이름]')} ${c.gray('지금까지를 물려받아 갈라 나간다')}`);
|
|
1064
|
+
say(` ${c.gray('/thread <번호>')} ${c.gray('그 갈래로 옮긴다')}`);
|
|
1065
|
+
say(` ${c.gray('/thread close')} ${c.gray('지금 갈래를 닫는다')}`);
|
|
1066
|
+
say('');
|
|
1067
|
+
say(` ${c.gray('연결·모델·도구·되돌리기는 갈래끼리 같이 씁니다. 오간 말과 토큰만 따로입니다.')}`);
|
|
1068
|
+
say('');
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* 쓰면서 저절로 알게 된 것.
|
|
1073
|
+
*
|
|
1074
|
+
* 보여 주는 것이 중요하다. 프롬프트에 몰래 들어가는 글이 있으면 사람은
|
|
1075
|
+
* 모델이 왜 그렇게 답했는지 알 수 없게 된다 — 여기서 통째로 볼 수 있어야
|
|
1076
|
+
* '자동으로 쌓인다' 가 무섭지 않은 말이 된다. 지우는 길도 같이 둔다.
|
|
1077
|
+
*/
|
|
1078
|
+
function 배움명령(session, ctx, arg = '') {
|
|
1079
|
+
const 배움 = ctx?.배움;
|
|
1080
|
+
say('');
|
|
1081
|
+
if (!배움) {
|
|
1082
|
+
say(` ${c.gray('이 자리에서는 못 봅니다 — 대화 화면에서만 됩니다.')}`);
|
|
1083
|
+
say('');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (/^(지우기|clear|forget|비우기)$/i.test(String(arg).trim())) {
|
|
1088
|
+
배움.지우기('전부');
|
|
1089
|
+
session.배움요약 = null;
|
|
1090
|
+
say(` ${mark.ok} ${c.gray('쌓아 둔 것을 비웠습니다. 다시 겪으면서 새로 쌓습니다.')}`);
|
|
1091
|
+
say('');
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const { 명령, 모델, 모델이름 } = 배움.현황(session.conn.model);
|
|
1096
|
+
rule('겪어 본 것', 70);
|
|
1097
|
+
|
|
1098
|
+
if (!명령.length && !모델) {
|
|
1099
|
+
say(` ${c.gray('아직 쌓인 것이 없습니다. 명령을 돌리고 대화를 나눌수록 여기가 찹니다.')}`);
|
|
1100
|
+
say('');
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (명령.length) {
|
|
1105
|
+
say(` ${c.bold('이 폴더에서 돌려 본 명령')}`);
|
|
1106
|
+
for (const r of 명령.slice(0, 12)) {
|
|
1107
|
+
const 표 = r.no === 0 ? c.green('✓') : r.ok === 0 ? c.red('✗') : c.yellow('~');
|
|
1108
|
+
say(` ${표} ${pad(r.이름, 24)} ${c.gray(`됨 ${r.ok} · 안 됨 ${r.no}`)}`);
|
|
1109
|
+
}
|
|
1110
|
+
say('');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (모델) {
|
|
1114
|
+
say(` ${c.bold('이 모델에 대해')} ${c.gray(모델이름)}`);
|
|
1115
|
+
const 걸음 = 모델.걸음 ?? 0;
|
|
1116
|
+
const 줄 = (이름, n) => {
|
|
1117
|
+
if (!n) return;
|
|
1118
|
+
const 비율 = 걸음 ? Math.round((n / 걸음) * 100) : 0;
|
|
1119
|
+
say(` ${c.gray(pad(이름, 24))} ${pad(String(n), 5, 'right')} ${c.gray(걸음 ? `(${비율}%)` : '')}`);
|
|
1120
|
+
};
|
|
1121
|
+
say(` ${c.gray(pad('같이 걸어 본 걸음', 24))} ${pad(String(걸음), 5, 'right')}`);
|
|
1122
|
+
줄('인자가 잘림', 모델.잘린인자);
|
|
1123
|
+
줄('빈 답', 모델.빈답);
|
|
1124
|
+
줄('편집이 빗나감', 모델.편집실패);
|
|
1125
|
+
if (모델.보정) say(` ${c.gray(pad('토큰 추정 보정', 24))} ${pad(`×${모델.보정.toFixed(2)}`, 5, 'right')}`);
|
|
1126
|
+
say('');
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const 실린것 = 배움.요약(session.conn.model);
|
|
1130
|
+
if (실린것) {
|
|
1131
|
+
say(` ${c.bold('이 중 프롬프트에 실리는 것')}`);
|
|
1132
|
+
for (const l of 실린것.split('\n').slice(1)) say(` ${c.gray(l)}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
say(` ${c.gray('아직 프롬프트에 실을 만큼 확실한 것은 없습니다 — 두 번 이상 겪어야 싣습니다.')}`);
|
|
1135
|
+
}
|
|
1136
|
+
say('');
|
|
1137
|
+
say(` ${c.gray('지우려면')} ${c.cyan('/learned 지우기')}`);
|
|
1138
|
+
say('');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
989
1141
|
function showContext(session) {
|
|
990
1142
|
const b = session.breakdown();
|
|
991
1143
|
say('');
|
|
@@ -1002,7 +1154,17 @@ function showContext(session) {
|
|
|
1002
1154
|
say(` ${c.gray(pad('남음', 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
|
|
1003
1155
|
say('');
|
|
1004
1156
|
say(` ${c.gray('/compact 대화 줄이기 /clear 통째로 비우기')}`);
|
|
1005
|
-
|
|
1157
|
+
/*
|
|
1158
|
+
* 추정이라고만 적어 두면 사람은 얼마나 믿어야 할지 모른다. 서버가 알려 준
|
|
1159
|
+
* 실제값에 맞춰 가고 있으면 그 사실을 적는다 — '추정' 과 '맞춰 본 추정' 은
|
|
1160
|
+
* 믿을 만한 정도가 다르다.
|
|
1161
|
+
*/
|
|
1162
|
+
if (b.보정잰것 > 0) {
|
|
1163
|
+
const 차이 = Math.round((b.보정 - 1) * 100);
|
|
1164
|
+
say(` ${c.gray(`서버가 알려 준 실제값에 맞춰 ${차이 >= 0 ? '+' : ''}${차이}% 보정했습니다 (${b.보정잰것}번 재봄).`)}`);
|
|
1165
|
+
} else {
|
|
1166
|
+
say(` ${c.gray('숫자는 추정입니다 — 정확한 토크나이저를 쓰지 않습니다.')}`);
|
|
1167
|
+
}
|
|
1006
1168
|
say('');
|
|
1007
1169
|
}
|
|
1008
1170
|
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 연결 프로필 저장/읽기. ~/.deel/config.json (프로젝트 폴더의 .deel/config.json 이 우선)
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join,
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
|
|
5
5
|
|
|
6
6
|
// 설정이 놓이는 자리.
|
|
@@ -19,6 +19,15 @@ function projectDir() {
|
|
|
19
19
|
return join(process.cwd(), '.deel');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 이 PC 의 설정 폴더 (`~/.deel` 또는 DEEL_HOME).
|
|
24
|
+
*
|
|
25
|
+
* configPath() 와 다르다 — 저쪽은 프로젝트에 설정이 있으면 그쪽을 준다.
|
|
26
|
+
* 여기는 언제나 **이 PC 것**이다. 모델에 대해 알아낸 것처럼 폴더를 옮겨도
|
|
27
|
+
* 따라와야 하는 것을 여기 둔다 (agent/evolve.js).
|
|
28
|
+
*/
|
|
29
|
+
export function homeDir() { return userDir(); }
|
|
30
|
+
|
|
22
31
|
export function configPath() {
|
|
23
32
|
const local = join(projectDir(), 'config.json');
|
|
24
33
|
if (existsSync(local)) return local;
|
package/src/oneshot.js
CHANGED
|
@@ -318,6 +318,10 @@ export async function runOnce(opts = {}) {
|
|
|
318
318
|
곁(` ${c.yellow('↻')} ${c.gray(`${ev.why} — 상한을 ${ev.from} → ${ev.to} 로 올려 다시 부릅니다`)}`);
|
|
319
319
|
break;
|
|
320
320
|
|
|
321
|
+
case 'folded':
|
|
322
|
+
곁(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 (${ev.아낀토큰.toLocaleString()} 토큰을 비움)`)}`);
|
|
323
|
+
break;
|
|
324
|
+
|
|
321
325
|
case 'compacted':
|
|
322
326
|
곁(` ${c.cyan('◱')} ${c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 (${ev.before.toLocaleString()} → ${ev.after.toLocaleString()} 토큰)`)}`);
|
|
323
327
|
break;
|
package/src/repl.js
CHANGED
|
@@ -10,14 +10,17 @@ import { handle, COMMANDS, 미리보기끄기 } from './commands.js';
|
|
|
10
10
|
import { next as nextWork, get as getWork, canWrite } from './agent/modes.js';
|
|
11
11
|
import { route } from './agent/route.js';
|
|
12
12
|
import { run } from './agent/loop.js';
|
|
13
|
-
import { Session } from './agent/session.js';
|
|
13
|
+
import { Session, repairToolPairs } from './agent/session.js';
|
|
14
14
|
import { makeScope } from './safety/guard.js';
|
|
15
15
|
import { History } from './safety/undo.js';
|
|
16
16
|
import { Audit } from './safety/audit.js';
|
|
17
|
-
import { activeProfile, load, resolveKey, save as saveCfg } from './config.js';
|
|
17
|
+
import { activeProfile, load, resolveKey, save as saveCfg, homeDir } from './config.js';
|
|
18
18
|
import { discover } from './skills/discover.js';
|
|
19
|
-
import { allowEndpoint, setOffline, isOffline
|
|
19
|
+
import { allowEndpoint, setOffline, isOffline } from './safety/network.js';
|
|
20
20
|
import { Store, latest, prune } from './agent/store.js';
|
|
21
|
+
import { Threads } from './agent/threads.js';
|
|
22
|
+
import { 배움 } from './agent/evolve.js';
|
|
23
|
+
import { 마크다운 } from './ui/md.js';
|
|
21
24
|
import { askHidden } from './ui/prompt.js';
|
|
22
25
|
import { explain, shows as levelShows } from './ui/level.js';
|
|
23
26
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
@@ -27,7 +30,7 @@ import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
|
27
30
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
28
31
|
import { expand as expandMentions } from './agent/mention.js';
|
|
29
32
|
import { 다붙이기 } from './backend/mcp.js';
|
|
30
|
-
import {
|
|
33
|
+
import { 읽기 as 기억읽기 } from './agent/memory.js';
|
|
31
34
|
import { 갈래고르기 } from './ui/working.js';
|
|
32
35
|
import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
|
|
33
36
|
|
|
@@ -179,11 +182,19 @@ export async function chatLoop(opts = {}) {
|
|
|
179
182
|
say(` ${c.gray('이어할 대화가 없습니다. 새로 시작합니다.')}`);
|
|
180
183
|
} else {
|
|
181
184
|
store = new Store(root, target);
|
|
182
|
-
const { messages } = store.load();
|
|
185
|
+
const { messages: 적힌것 } = store.load();
|
|
186
|
+
/*
|
|
187
|
+
* 도구가 도는 중에 죽었으면 호출만 적히고 결과가 없다. 그대로 보내면
|
|
188
|
+
* 규격 서버가 400 을 내서 이어받자마자 첫 마디에서 죽는다. 손봐서 받는다.
|
|
189
|
+
*/
|
|
190
|
+
const { messages, 고친것 } = repairToolPairs(적힌것);
|
|
183
191
|
if (messages.length) {
|
|
184
192
|
session.messages = messages;
|
|
185
193
|
say('');
|
|
186
194
|
say(` ${mark.ok} ${c.bold(target)} ${c.gray(`— 메시지 ${messages.length}개를 이어 받았습니다.`)}`);
|
|
195
|
+
if (고친것) {
|
|
196
|
+
say(` ${c.gray(`중단된 도구 호출 ${고친것}개를 걷어냈습니다 — 그때 하던 일은 다시 시켜 주세요.`)}`);
|
|
197
|
+
}
|
|
187
198
|
}
|
|
188
199
|
}
|
|
189
200
|
}
|
|
@@ -506,6 +517,26 @@ export async function chatLoop(opts = {}) {
|
|
|
506
517
|
},
|
|
507
518
|
};
|
|
508
519
|
|
|
520
|
+
/*
|
|
521
|
+
* 겪어 본 것 (agent/evolve.js). 쓸수록 이 PC 에 맞춰 나아지는 자리다.
|
|
522
|
+
*
|
|
523
|
+
* 켤 때 두 가지를 받아 온다.
|
|
524
|
+
* · 프롬프트에 실을 몇 줄 — 여기서 되는 명령, 이 모델의 버릇
|
|
525
|
+
* · 지난번에 알아낸 토큰 배수 — 첫 턴부터 제대로 셈한다
|
|
526
|
+
*
|
|
527
|
+
* 둘 다 없으면 아무 일도 안 일어난다. 처음 켠 PC 는 지금과 똑같이 돈다.
|
|
528
|
+
*/
|
|
529
|
+
ctx.배움 = new 배움(root, homeDir());
|
|
530
|
+
session.배움요약 = ctx.배움.요약(conn.model);
|
|
531
|
+
const 아는배수 = ctx.배움.아는보정(conn.model);
|
|
532
|
+
if (아는배수) { session.보정 = 아는배수; session.보정잰것 = 1; }
|
|
533
|
+
|
|
534
|
+
/*
|
|
535
|
+
* 대화 갈래. 연결·도구·되돌리기는 같이 쓰고 오간 말만 여러 벌 갖는다.
|
|
536
|
+
* 갈래마다 저장 파일을 따로 열어서, 나중에 `/sessions` 로 각각 찾아갈 수 있다.
|
|
537
|
+
*/
|
|
538
|
+
ctx.갈래 = new Threads(session, ctx, () => new Store(root).begin({ model: conn.model, base: conn.base, root }), store);
|
|
539
|
+
|
|
509
540
|
// ── 컨텍스트 길이를 모델에서 긁어온다 ─────────────────────────────────
|
|
510
541
|
//
|
|
511
542
|
// 켤 때마다 서버에 물어본다. 저장된 값을 그대로 믿지 않는다 —
|
|
@@ -750,7 +781,7 @@ export async function chatLoop(opts = {}) {
|
|
|
750
781
|
// 어디까지 적었는지. 도중에 죽어도 여기까지는 남아 있게 자주 흘려 보낸다.
|
|
751
782
|
let saved = session.messages.length;
|
|
752
783
|
const flush = () => {
|
|
753
|
-
for (const m of session.messages.slice(saved)) store.append(m);
|
|
784
|
+
for (const m of session.messages.slice(saved)) ctx.갈래.현재store().append(m);
|
|
754
785
|
saved = session.messages.length;
|
|
755
786
|
};
|
|
756
787
|
let tools = 0;
|
|
@@ -759,6 +790,18 @@ export async function chatLoop(opts = {}) {
|
|
|
759
790
|
let 턴탈났나 = false;
|
|
760
791
|
let thinkChars = 0;
|
|
761
792
|
let streamed = false;
|
|
793
|
+
/*
|
|
794
|
+
* 답을 그리는 자리. 턴마다 새로 만든다 — 앞 턴의 코드 울타리 상태가
|
|
795
|
+
* 다음 턴으로 새면 멀쩡한 답이 통째로 코드 블록으로 그려진다.
|
|
796
|
+
*/
|
|
797
|
+
const 답그림 = new 마크다운({ 폭: (process.stdout.columns || 80) - 6 });
|
|
798
|
+
// 답 흐름이 끊기는 자리마다 남은 반 줄을 비운다. 안 비우면 마지막 줄이 사라진다.
|
|
799
|
+
const 답비우기 = () => {
|
|
800
|
+
for (const 조각 of 답그림.끝()) {
|
|
801
|
+
if (typeof 조각 === 'string') 화면.붙임(` ${답표시} ${조각}\n`);
|
|
802
|
+
else 화면.붙임(조각.이어붙임 + (조각.끝났나 ? '\n' : ''));
|
|
803
|
+
}
|
|
804
|
+
};
|
|
762
805
|
let thinkingShown = false;
|
|
763
806
|
let stage = null;
|
|
764
807
|
// 접는 중 표시. 끝나거나 다른 글을 찍기 전에 반드시 멈춰야 한다.
|
|
@@ -830,15 +873,28 @@ export async function chatLoop(opts = {}) {
|
|
|
830
873
|
* 세로줄 하나면 된다. 칸을 나눠 그리는 화면으로 갈 이유가 없다 —
|
|
831
874
|
* 파이프로 넘기거나 기록으로 남길 때도 그대로 읽힌다.
|
|
832
875
|
*/
|
|
876
|
+
/*
|
|
877
|
+
* 답을 마크다운으로 그린다 (ui/md.js).
|
|
878
|
+
*
|
|
879
|
+
* 줄이 끝나야 그릴 수 있다 — `**굵` 까지 왔을 때는 그게 굵은 글씨가
|
|
880
|
+
* 될지 알 수 없고, 한 번 찍은 글자는 되돌릴 수 없다. 그래서 md 가
|
|
881
|
+
* 줄 단위로 모았다가 내놓는다. 줄이 화면 폭보다 길어지면 거기까지를
|
|
882
|
+
* 날것으로 흘려보낸다(md 가 알아서 한다) — 긴 문단에서 몇 초씩
|
|
883
|
+
* 아무것도 안 나오면 멈춘 것처럼 보이기 때문이다.
|
|
884
|
+
*/
|
|
833
885
|
case 'content':
|
|
834
886
|
clearThinking();
|
|
835
|
-
if (!streamed) { streamed = true; 화면.일바꿈('답');
|
|
836
|
-
|
|
887
|
+
if (!streamed) { streamed = true; 화면.일바꿈('답'); }
|
|
888
|
+
for (const 조각 of 답그림.넣기(ev.text)) {
|
|
889
|
+
if (typeof 조각 === 'string') { 화면.붙임(` ${답표시} ${조각}\n`); continue; }
|
|
890
|
+
// 아직 안 끝난 긴 줄. 그 줄이 처음 나가는 것이면 세로줄을 앞에 세운다.
|
|
891
|
+
화면.붙임((조각.첫조각 ? ` ${답표시} ` : '') + 조각.이어붙임 + (조각.끝났나 ? '\n' : ''));
|
|
892
|
+
}
|
|
837
893
|
break;
|
|
838
894
|
|
|
839
895
|
case 'tool_start':
|
|
840
896
|
clearThinking();
|
|
841
|
-
if (streamed) { say(''); streamed = false; }
|
|
897
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
842
898
|
// 문구를 지금 하는 일에 맞춘다. 아무 말이나 돌려 대면 두 번째부터
|
|
843
899
|
// 아무도 안 읽고, 그때부터는 화면이 조용한 것과 같아진다.
|
|
844
900
|
화면.일바꿈(갈래고르기(ev.name));
|
|
@@ -849,7 +905,7 @@ export async function chatLoop(opts = {}) {
|
|
|
849
905
|
// 여럿을 같이 돌린다 — 한 줄로 알리고, 이름은 결과와 붙여서 그린다.
|
|
850
906
|
case 'tools_start':
|
|
851
907
|
clearThinking();
|
|
852
|
-
if (streamed) { say(''); streamed = false; }
|
|
908
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
853
909
|
화면.일바꿈(갈래고르기(ev.names?.[0]));
|
|
854
910
|
say('');
|
|
855
911
|
say(` ${c.gray(`${ev.count}개를 함께 돌립니다`)} ${c.gray('·')} ${c.gray(ev.names.join(' '))}`);
|
|
@@ -861,7 +917,7 @@ export async function chatLoop(opts = {}) {
|
|
|
861
917
|
// '시작' 을 안 거쳤다. 그래서 '생각 중…' 줄이 안 지워진 채로 결과가
|
|
862
918
|
// 그 줄 뒤에 가서 붙고, 이름도 없이 "└ 인자가 잘렸습니다" 만 남는다.
|
|
863
919
|
// 무슨 도구가 왜 그랬는지 알 수 없는 화면이 된다.
|
|
864
|
-
if (ev.showLabel) { clearThinking(); if (streamed) { say(''); streamed = false; } say(''); }
|
|
920
|
+
if (ev.showLabel) { clearThinking(); if (streamed) { 답비우기(); say(''); streamed = false; } say(''); }
|
|
865
921
|
// 같이 돈 것은 이름을 다시 적어 준다. 안 그러면 어느 결과인지 모른다.
|
|
866
922
|
if (ev.parallel || ev.showLabel) say(` ${toolLabel(ev.name, ev.args)}`);
|
|
867
923
|
if (ev.name === 'TodoWrite' && ev.result?.todos) {
|
|
@@ -936,7 +992,7 @@ export async function chatLoop(opts = {}) {
|
|
|
936
992
|
*/
|
|
937
993
|
case 'task_start':
|
|
938
994
|
clearThinking();
|
|
939
|
-
if (streamed) { say(''); streamed = false; }
|
|
995
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
940
996
|
화면.일바꿈('하위', clip(ev.목적, 24));
|
|
941
997
|
say('');
|
|
942
998
|
say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(ev.목적, 60))}`
|
|
@@ -946,7 +1002,7 @@ export async function chatLoop(opts = {}) {
|
|
|
946
1002
|
|
|
947
1003
|
case 'task_done': {
|
|
948
1004
|
clearThinking();
|
|
949
|
-
if (streamed) { say(''); streamed = false; }
|
|
1005
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
950
1006
|
const 끝 = ev.끝 ?? {};
|
|
951
1007
|
const 잘됨 = 끝.type === 'done';
|
|
952
1008
|
const 왜 = { done: '끝냈습니다', limit: '걸음 수를 다 써서 멈췄습니다 — 다 못 했습니다',
|
|
@@ -981,6 +1037,16 @@ export async function chatLoop(opts = {}) {
|
|
|
981
1037
|
접는중 = true;
|
|
982
1038
|
break;
|
|
983
1039
|
|
|
1040
|
+
/*
|
|
1041
|
+
* 요약 압축보다 먼저 오는 것. 대화는 안 건드리고 옛 도구 결과만 접었다.
|
|
1042
|
+
* 조용히 하면 사람은 어느 파일 내용이 왜 사라졌는지 모른다 — 한 줄로 알린다.
|
|
1043
|
+
*/
|
|
1044
|
+
case 'folded':
|
|
1045
|
+
clearThinking();
|
|
1046
|
+
say(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 — `)}`
|
|
1047
|
+
+ `${c.white(ev.아낀토큰.toLocaleString())} ${c.gray('토큰을 비웠습니다. 대화는 그대로입니다.')}`);
|
|
1048
|
+
break;
|
|
1049
|
+
|
|
984
1050
|
case 'compacted': {
|
|
985
1051
|
접기멈춤();
|
|
986
1052
|
const 줄인 = ev.before - ev.after;
|
|
@@ -989,7 +1055,7 @@ export async function chatLoop(opts = {}) {
|
|
|
989
1055
|
`${c.green(`(${Math.round((줄인 / Math.max(1, ev.before)) * 100)}% 줄어듦)`)}`);
|
|
990
1056
|
if (ev.fallback) say(` ${c.yellow('요약을 못 받아 그냥 줄였습니다.')}`);
|
|
991
1057
|
// 접히면 이력이 통째로 바뀐다. 덧붙이기로는 못 맞추니 새로 적는다.
|
|
992
|
-
store.replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
|
|
1058
|
+
ctx.갈래.현재store().replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
|
|
993
1059
|
saved = session.messages.length;
|
|
994
1060
|
break;
|
|
995
1061
|
}
|
|
@@ -1033,7 +1099,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1033
1099
|
case 'stuck':
|
|
1034
1100
|
턴탈났나 = true;
|
|
1035
1101
|
clearThinking();
|
|
1036
|
-
if (streamed) { say(''); streamed = false; }
|
|
1102
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1037
1103
|
say('');
|
|
1038
1104
|
say(` ${c.yellow('⊘')} ${c.bold('같은 자리에서 헛돌고 있어 멈췄습니다.')}`);
|
|
1039
1105
|
say(` ${c.gray(ev.why)}`);
|
|
@@ -1055,7 +1121,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1055
1121
|
case 'aborted':
|
|
1056
1122
|
턴탈났나 = true;
|
|
1057
1123
|
clearThinking();
|
|
1058
|
-
if (streamed) { say(''); streamed = false; }
|
|
1124
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1059
1125
|
say('');
|
|
1060
1126
|
// 남았는지 아닌지를 **사실대로** 말한다. 전에는 무조건 '남아 있다' 고 했는데
|
|
1061
1127
|
// 실제로는 아무것도 안 남는 경우가 있었다. 그러면 "이어서 해줘" 라고 했을 때
|
|
@@ -1074,7 +1140,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1074
1140
|
|
|
1075
1141
|
case 'done':
|
|
1076
1142
|
clearThinking();
|
|
1077
|
-
if (streamed) say('');
|
|
1143
|
+
if (streamed) { 답비우기(); say(''); }
|
|
1078
1144
|
만든파일보이기(ev.files);
|
|
1079
1145
|
break;
|
|
1080
1146
|
}
|
package/src/safety/guard.js
CHANGED
|
@@ -72,12 +72,47 @@ const BLOCKED = [
|
|
|
72
72
|
{ re: /\brm\s+(-[a-z]*[rR][a-z]*f|-[a-z]*f[a-z]*[rR])\b[^|;&]*\s(\/|~|\$HOME)\s*$/i, why: '뿌리 폴더를 통째로 지우려 합니다' },
|
|
73
73
|
{ re: /\b(mkfs|fdisk|diskpart)\b/i, why: '디스크를 초기화하는 명령입니다' },
|
|
74
74
|
{ re: /\bformat\s+[a-z]:/i, why: '드라이브를 포맷하는 명령입니다' },
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// 윈도우 짝을 빠뜨리면 안 된다 — deel 은 윈도우에서 주로 돈다.
|
|
76
|
+
//
|
|
77
|
+
// 옛 규칙은 스위치가 **차례대로** 오고 끝에 `\*` 가 붙은 모양만 봤다.
|
|
78
|
+
// 그래서 `del /f /s /q C:\` 와 `rd /q /s C:\` 가 그대로 새어 나갔다.
|
|
79
|
+
// 스위치는 아무 차례로나 오므로 차례를 안 따진다.
|
|
80
|
+
{ re: /\b(rd|rmdir)\b[^|;&]*\s\/s\b/i, why: '폴더를 통째로 지웁니다 — 정션이 있으면 원본까지 딸려 갑니다' },
|
|
81
|
+
{ re: /\bdel\b[^|;&]*\s\/[sq]\b[^|;&]*(\\\*|\s[a-z]:\\?\s*$)/i, why: '하위 폴더까지 전부 지웁니다' },
|
|
82
|
+
// 파워셸도 같은 일을 한다. 드라이브 뿌리나 집 폴더를 통째로 미는 것만 본다 —
|
|
83
|
+
// 폴더 하나 지우는 평범한 Remove-Item 까지 막으면 도구를 못 쓴다.
|
|
84
|
+
{
|
|
85
|
+
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(-?[a-z]*\s*)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
|
|
86
|
+
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
87
|
+
},
|
|
77
88
|
{ re: /git\s+push\b[^|;&]*--force(?!-with-lease)/i, why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)' },
|
|
78
89
|
// 낱말만 보면 안 된다. `node scripts/shutdown.js` 나 `npm run reboot` 이
|
|
79
90
|
// '시스템을 끕니다' 로 막히고 감사기록에까지 남았다. 명령의 **첫 낱말**일 때만 본다.
|
|
80
91
|
{ re: /(^|[|;&]\s*)(sudo\s+)?(shutdown|reboot|halt|poweroff)\b/i, why: '시스템을 끕니다' },
|
|
92
|
+
// 포크 폭탄.
|
|
93
|
+
//
|
|
94
|
+
// 껐다 켜면 기계는 돌아오니 '되돌릴 수 없는 것' 은 아니다. 그런데 되돌아오지
|
|
95
|
+
// 않는 것이 하나 있다 — **아직 안 적은 것들.** 기계가 굳으면 열어 둔 것을
|
|
96
|
+
// 저장할 수도, 하던 대화를 끝낼 수도 없다. 그래서 목록에 넣는다.
|
|
97
|
+
//
|
|
98
|
+
// 이름은 아무거나 될 수 있으니(`:` 든 `bomb` 든) 이름 자체를 보지 않고 **모양**을
|
|
99
|
+
// 본다: 함수를 만들고, 그 안에서 **저를 불러 곧바로 파이프나 뒤로 보내기에**
|
|
100
|
+
// 물리고, 밖에서 그 이름을 부른다. 뒷갈이(\2)로 같은 이름인지까지 본다.
|
|
101
|
+
//
|
|
102
|
+
// '저를 부른 **바로 뒤**가 | 나 &' 인 것이 열쇠다. 그냥 '이름이 몸 안에 있고
|
|
103
|
+
// & 도 있다' 로 보면 `up(){ docker compose up -d & }; up` 같은 평범한 함수가
|
|
104
|
+
// 걸린다 — 거기서는 up 뒤에 `-d` 가 온다.
|
|
105
|
+
//
|
|
106
|
+
// 이름 앞뒤로 낱말 경계도 본다. 그래야 `tee -a out.log` 의 log 나
|
|
107
|
+
// `deploy_app` 의 deploy 를 저를 부르는 것으로 잘못 읽지 않는다.
|
|
108
|
+
// 앞자리에 따옴표와 여는 괄호도 넣는다. 안 넣으면 `bash -c ":(){ :|:& };:"` 처럼
|
|
109
|
+
// 한 겹 싸는 것만으로 그대로 새어 나간다.
|
|
110
|
+
{
|
|
111
|
+
re: /(^|[|;&\s"'(])([\w.:]{1,16})\s*\(\s*\)\s*\{[^}]*(?<![\w.])\2(?![\w.])\s*[|&][^}]*\}\s*;?\s*(?<![\w.])\2(?![\w.])/,
|
|
112
|
+
why: '스스로를 끝없이 불려 기계를 멈춰 세웁니다 (포크 폭탄)',
|
|
113
|
+
},
|
|
114
|
+
// 같은 것을 한 줄로 적는 오래된 방법. 이 모양으로 쓸 다른 일이 없다.
|
|
115
|
+
{ re: /\bfork\s+while\s+fork\b/i, why: '스스로를 끝없이 불려 기계를 멈춰 세웁니다 (포크 폭탄)' },
|
|
81
116
|
{ re: /curl[^|]*\|\s*(ba)?sh/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
82
117
|
{ re: /\biwr\b[^|]*\|\s*iex\b/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
83
118
|
];
|
package/src/safety/undo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync,
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { looksBinary } from '../tools/encoding.js';
|
|
6
6
|
|
|
7
7
|
// 되돌리기 이력은 파일 내용을 통째로 담는다. 이만큼 커지면 오래된 턴을 버린다.
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 첫 실행 마법사 + 진단 실행.
|
|
2
|
-
import { c, say, rule, mark
|
|
3
|
-
import { ask, pick
|
|
2
|
+
import { c, say, rule, mark } from './ui/ansi.js';
|
|
3
|
+
import { ask, pick } from './ui/prompt.js';
|
|
4
4
|
import { spin } from './ui/spinner.js';
|
|
5
5
|
import { detect } from './backend/detect.js';
|
|
6
6
|
import { probe } from './backend/probe.js';
|
package/src/skills/discover.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 플러그인도 없다 — 거기서는 방법론이 0개였고, 모델은 매번 제 나름대로 했다.
|
|
8
8
|
// 시킨 것만 겨우 하고 끝나는 얄팍한 결과가 거기서 나온다.
|
|
9
9
|
// 품고 다니는 것은 **가장 낮은 자리**에 둔다. 같은 이름을 사용자가 만들면 그쪽이 이긴다.
|
|
10
|
-
import { readdirSync, readFileSync, existsSync
|
|
10
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
11
11
|
import { join, basename, dirname } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { homedir } from 'node:os';
|
package/src/tools/edit-match.js
CHANGED
|
@@ -46,11 +46,22 @@ function findAll(text, needle, tier) {
|
|
|
46
46
|
let re;
|
|
47
47
|
try { re = new RegExp(p, 'g'); } catch { return []; }
|
|
48
48
|
const out = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// 만들 때 말고 **돌릴 때** 터지는 것이 있다.
|
|
50
|
+
//
|
|
51
|
+
// 모델은 큰 파일을 고칠 때 덩이를 통째로 old_string 에 담아 보낸다. 4만 자쯤
|
|
52
|
+
// 넘어가면 new RegExp 는 멀쩡히 만들어지는데(패턴만 훑는다) 실제로 돌릴 때
|
|
53
|
+
// 프로그램 크기 한도에 걸려 SyntaxError 가 난다. 위의 try 는 만들 때만 감싸므로
|
|
54
|
+
// 그 오류가 그대로 튀어나가서 "찾지 못했습니다" 대신 도구가 죽었다.
|
|
55
|
+
//
|
|
56
|
+
// 여기서는 못 찾은 것으로 친다. 정확히 일치하는 경우는 위에서 indexOf 로 이미
|
|
57
|
+
// 처리했으니, 느슨하게 맞춰 보는 이 길만 포기하는 것이다.
|
|
58
|
+
try {
|
|
59
|
+
for (const m of text.matchAll(re)) {
|
|
60
|
+
if (m[0].length === 0) continue;
|
|
61
|
+
out.push({ start: m.index, end: m.index + m[0].length });
|
|
62
|
+
if (out.length > 50) break; // 너무 많으면 어차피 모호하다
|
|
63
|
+
}
|
|
64
|
+
} catch { return []; }
|
|
54
65
|
return out;
|
|
55
66
|
}
|
|
56
67
|
|
package/src/tools/excel-com.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// 2) 엑셀은 바쁘면 호출을 거절한다(0x80010001, 0x800AC472). 잘못된 호출이
|
|
18
18
|
// 아니라 '지금은 말고' 라는 뜻이라, 쉬었다 다시 부르면 된다.
|
|
19
19
|
import { spawn } from 'node:child_process';
|
|
20
|
-
import { mkdtempSync, readFileSync,
|
|
20
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
21
21
|
import { tmpdir } from 'node:os';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { decode } from './encoding.js';
|
package/src/tools/excel.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// 부르는 쪽은 어느 갈래인지 몰라도 된다. 다만 암호가 필요할 때는 물어볼 수
|
|
7
7
|
// 있어야 하므로, 물어보는 방법을 받아 온다(askPassword).
|
|
8
|
-
import { readFileSync
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
9
|
import { extname } from 'node:path';
|
|
10
10
|
import { readXlsx, toCsv, looksXlsx, looksOle } from './xlsx.js';
|
|
11
11
|
import { excelToTables, canUseExcel } from './excel-com.js';
|
package/src/tools/fsutil.js
CHANGED
|
@@ -171,14 +171,6 @@ export function walk(root, { limit = 20000, skipDirs = SKIP_DIRS } = {}) {
|
|
|
171
171
|
return out;
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// 텍스트 파일인지 — 앞부분에 NUL 이 있으면 바이너리로 본다.
|
|
175
|
-
export function isText(path) {
|
|
176
|
-
try {
|
|
177
|
-
const fd = readFileSync(path);
|
|
178
|
-
return !fd.subarray(0, 8000).includes(0);
|
|
179
|
-
} catch { return false; }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
174
|
/**
|
|
183
175
|
* 글 파일을 읽는다. 무엇으로 쓰여 있든 알아보고 읽는다.
|
|
184
176
|
*
|
package/src/tools/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { writeFileSync, appendFileSync, readFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
|
-
import { globToRegex, walk, readText, readTextFull,
|
|
6
|
+
import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
|
|
7
7
|
import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
|
|
8
8
|
import { checkCommand, checkPaths, isMutating } from '../safety/guard.js';
|
|
9
9
|
import { 띄우기, JOBS_TOOL } from './jobs.js';
|