deel-local-cli 1.7.0 → 1.10.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 +1166 -0
- package/README.md +1223 -1107
- package/bin/deel.js +78 -12
- package/package.json +4 -4
- package/src/acp/map.js +1 -1
- package/src/acp/serve.js +40 -3
- package/src/agent/compact.js +314 -296
- package/src/agent/effort.js +27 -7
- package/src/agent/evidence.js +2 -0
- package/src/agent/filemem.js +141 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +218 -40
- package/src/agent/session.js +136 -14
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +272 -23
- package/src/backend/learn.js +24 -0
- package/src/backend/mcp.js +96 -5
- package/src/backend/probe.js +131 -61
- package/src/backend/quota.js +29 -5
- package/src/backend/retry.js +3 -0
- package/src/backend/scanui.js +1 -1
- package/src/backend/toolfit.js +325 -0
- package/src/commands.js +170 -61
- package/src/completion.js +20 -2
- package/src/config.js +47 -2
- package/src/i18n/en.js +210 -3
- package/src/i18n/index.js +40 -0
- package/src/i18n/ja.js +204 -3
- package/src/i18n/ko.js +270 -3
- package/src/i18n/zh.js +204 -3
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +92 -5
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +26 -10
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/repl.js +344 -61
- package/src/reset.js +397 -0
- package/src/safety/audit.js +95 -8
- package/src/safety/authcmd.js +316 -0
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +114 -38
- package/src/safety/undo.js +25 -6
- package/src/tools/desc.en.js +19 -14
- package/src/tools/index.js +349 -75
- package/src/tools/jobs.js +175 -42
- package/src/tools/lsp.js +5 -4
- package/src/tools/outline.js +7 -3
- package/src/tools/task.js +10 -6
- package/src/tools/todo.js +9 -2
- package/src/tools/verify.js +42 -7
- package/src/tools/webfetch.js +94 -10
- package/src/ui/export.js +1 -1
- package/src/ui/md.js +201 -5
- package/src/ui/motion.js +0 -1
- package/src/ui/pastechip.js +50 -3
- package/src/ui/pick.js +115 -0
- package/src/ui/screen.js +23 -3
- package/README.en.md +0 -1117
package/src/commands.js
CHANGED
|
@@ -12,7 +12,8 @@ import { allowEndpoint } from './safety/network.js';
|
|
|
12
12
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
13
13
|
import { 주소가리기 } from './safety/secrets.js';
|
|
14
14
|
import { pick, confirm } from './ui/prompt.js';
|
|
15
|
-
import { load, save, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
|
|
15
|
+
import { load, save, 저장시도, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
|
|
16
|
+
import { 지금상태 as 지금열쇠상태 } from './safety/authcmd.js';
|
|
16
17
|
import { 제공자고르기 } from './providers/index.js';
|
|
17
18
|
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
18
19
|
import { 말, 언어, 언어들, 언어정하기, 언어고르기, 옮긴만큼, 지시말, 지시말정하기, 지시말따로정했나 } from './i18n/index.js';
|
|
@@ -22,7 +23,7 @@ import { chat, 규격이름 } from './backend/adapter.js';
|
|
|
22
23
|
import { 알림채움 } from './backend/retry.js';
|
|
23
24
|
import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
|
|
24
25
|
import { 정한셸 } from './tools/shell.js';
|
|
25
|
-
import { TOOLS } from './tools/index.js';
|
|
26
|
+
import { TOOLS, 영어설명 } from './tools/index.js';
|
|
26
27
|
import { 둘러보기, 프로젝트갈래 } from './lsp/servers.js';
|
|
27
28
|
import { 지금것들 } from './lsp/client.js';
|
|
28
29
|
import { 보고서적기 } from './ui/export.js';
|
|
@@ -170,6 +171,23 @@ function 경로처럼보이나(line) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
// 반환: { handled, exit? } handled=false 면 모델에게 보낸다.
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 설정을 남기고, 못 남겼으면 화면에 한 줄 (config.js 의 저장시도).
|
|
177
|
+
*
|
|
178
|
+
* 여태 아홉 자리가 전부 `try { save(cfg) } catch {}` 였다. 못 남겨도 이번 판에는
|
|
179
|
+
* 먹으니 대화는 계속되는데, 바로 다음 줄에서 화면은 `✓ 바꿨습니다` 를 찍는다.
|
|
180
|
+
* 사람은 정해진 줄 알고 창을 닫았다가 다음에 옛 값을 보고, 그때는 무엇 때문인지
|
|
181
|
+
* 알 길이 없다 — 홈이 읽기 전용인지, 디스크가 찼는지.
|
|
182
|
+
*
|
|
183
|
+
* @returns {boolean} 남겼나
|
|
184
|
+
*/
|
|
185
|
+
function 설정남기기(cfg, 옵션 = {}) {
|
|
186
|
+
const r = 저장시도(cfg, 옵션);
|
|
187
|
+
if (!r.ok) say(` ${mark.warn} ${c.yellow(말('common.cfgSaveFailed', { 왜: clip(r.왜, 70) }))}`);
|
|
188
|
+
return r.ok;
|
|
189
|
+
}
|
|
190
|
+
|
|
173
191
|
/** /mode 아래에 규칙을 늘어놓는다. 아무것도 안 걸려 있으면 아무 말도 안 한다. */
|
|
174
192
|
function 규칙보이기(규칙들) {
|
|
175
193
|
if (!규칙들) return;
|
|
@@ -410,7 +428,7 @@ export async function handle(line, session, ctx) {
|
|
|
410
428
|
// 「화면 말을 따라간다」 가 아니라 옛 값이 되살아난다.
|
|
411
429
|
if (지시말따로정했나()) cfg.promptLang = 지시말();
|
|
412
430
|
else delete cfg.promptLang;
|
|
413
|
-
|
|
431
|
+
설정남기기(cfg);
|
|
414
432
|
const p = 옮긴만큼();
|
|
415
433
|
say('');
|
|
416
434
|
say(` ${mark.ok} ${말('lang.changed', { 이름: 이름(언어()) })}`);
|
|
@@ -541,7 +559,7 @@ export async function handle(line, session, ctx) {
|
|
|
541
559
|
return { handled: true };
|
|
542
560
|
}
|
|
543
561
|
cfg.bell = 켜는말.includes(값);
|
|
544
|
-
|
|
562
|
+
설정남기기(cfg);
|
|
545
563
|
say('');
|
|
546
564
|
say(` ${mark.ok} ${말(cfg.bell ? 'bell.turnedOn' : 'bell.turnedOff')}`);
|
|
547
565
|
// 켤 때는 한 번 울려 준다. "켰다는데 소리가 나나?" 를 그 자리에서 확인하게.
|
|
@@ -605,7 +623,7 @@ export async function handle(line, session, ctx) {
|
|
|
605
623
|
}
|
|
606
624
|
|
|
607
625
|
cfg.motion = 고른것.값;
|
|
608
|
-
|
|
626
|
+
설정남기기(cfg);
|
|
609
627
|
적용하기(cfg.motion);
|
|
610
628
|
|
|
611
629
|
say('');
|
|
@@ -821,18 +839,32 @@ export async function handle(line, session, ctx) {
|
|
|
821
839
|
case 'undo': {
|
|
822
840
|
const n = Math.max(1, parseInt(arg, 10) || 1);
|
|
823
841
|
const r = ctx.history.undo(n);
|
|
824
|
-
|
|
842
|
+
/*
|
|
843
|
+
* 세는 수는 **진짜로 되돌아간 것**이다.
|
|
844
|
+
*
|
|
845
|
+
* 전에는 restored.length 를 그대로 썼다. 그 배열에는 못 되돌린 것과
|
|
846
|
+
* 손대지 않은 것(바이너리처럼 내용을 못 떠 둔 파일)까지 들어 있어서,
|
|
847
|
+
* 하나도 못 되돌린 판에도 「파일 2개를 되돌렸습니다」 가 나올 수 있었다.
|
|
848
|
+
* 감사기록에도 그 부풀린 수가 그대로 남았다.
|
|
849
|
+
*/
|
|
850
|
+
const 되돌린수 = r.되돌린수 ?? r.restored.length;
|
|
851
|
+
ctx.audit.undo({ turns: r.turns, files: 되돌린수 });
|
|
825
852
|
if (!r.restored.length) {
|
|
826
853
|
say(` ${c.gray(말('undo.nothing'))}`);
|
|
827
854
|
say('');
|
|
828
855
|
return { handled: true };
|
|
829
856
|
}
|
|
830
|
-
say(` ${mark.ok} ${말('undo.done', { turns: r.turns, files:
|
|
857
|
+
say(` ${mark.ok} ${말('undo.done', { turns: r.turns, files: 되돌린수 })}`);
|
|
831
858
|
for (const f of r.restored) say(` ${c.gray(ctx.scope.show(f.path))} ${c.gray(f.how)}`);
|
|
859
|
+
// 못 되돌린 것이 있으면 그것만 따로 못 박는다. 위 목록에 섞여 있으면 안 읽힌다.
|
|
860
|
+
if (r.못한것?.length) {
|
|
861
|
+
say(` ${mark.warn} ${말('undo.failed', { n: r.못한것.length })}`);
|
|
862
|
+
}
|
|
832
863
|
|
|
833
864
|
// /diff 가 세던 것에서도 뺀다. 되돌린 파일이 '바뀐 파일' 로 남아 있으면
|
|
834
|
-
// 사람도 되돌아간 줄 모른다.
|
|
835
|
-
|
|
865
|
+
// 사람도 되돌아간 줄 모른다. **되돌아간 것만** 뺀다 — 못 되돌린 파일을
|
|
866
|
+
// 여기서 빼면 진짜로 바뀌어 있는 파일이 /diff 에서도 사라진다.
|
|
867
|
+
for (const f of r.restored) if (f.ok === true) session.changes.delete(f.path);
|
|
836
868
|
// 상태줄의 '되돌릴 턴' 도 여기서 줄여 준다. 안 줄이면 방금 되돌린 것을
|
|
837
869
|
// 아직 되돌릴 수 있는 것처럼 세고 있게 된다.
|
|
838
870
|
try { session.되돌릴턴 = ctx.history.turns().length; } catch { /* 못 세면 그냥 둔다 */ }
|
|
@@ -863,9 +895,20 @@ export async function handle(line, session, ctx) {
|
|
|
863
895
|
case 'serve': return await 미리보기(session, ctx, arg), { handled: true };
|
|
864
896
|
|
|
865
897
|
case 'tools': {
|
|
866
|
-
rule('
|
|
898
|
+
rule(말('scr.tools'), 70);
|
|
899
|
+
/*
|
|
900
|
+
* 원본 schema.description 을 그대로 찍으면 안 된다.
|
|
901
|
+
*
|
|
902
|
+
* 그 글은 한국어다. 영어 표(tools/desc.en.js)는 **모델에게 줄 때만**
|
|
903
|
+
* 갈아 끼우고 있어서, 영어로 켠 사람의 `/tools` 는 스물한 줄이 통째로
|
|
904
|
+
* 한국어로 나왔다. `/lang` 은 그 사이에도 100% 라고 답했다 — 이 글들은
|
|
905
|
+
* 애초에 말 표에 들어간 적이 없어서 셈에 안 잡혔기 때문이다.
|
|
906
|
+
*
|
|
907
|
+
* 여기서는 **화면 말**(언어())로 고른다. 모델에게 주는 글의 말과는
|
|
908
|
+
* 다른 축이다 — 영어로 시키고 한국어로 보는 조합이 실제로 쓰인다.
|
|
909
|
+
*/
|
|
867
910
|
for (const [n, t] of Object.entries(TOOLS)) {
|
|
868
|
-
say(` ${c.cyan(pad(n, 8))} ${c.gray(t.schema.description)}`);
|
|
911
|
+
say(` ${c.cyan(pad(n, 8))} ${c.gray(영어설명(t.schema, n, 언어()).description)}`);
|
|
869
912
|
}
|
|
870
913
|
say('');
|
|
871
914
|
return { handled: true };
|
|
@@ -873,14 +916,14 @@ export async function handle(line, session, ctx) {
|
|
|
873
916
|
|
|
874
917
|
case 'cost': {
|
|
875
918
|
const mins = ((Date.now() - session.startedAt) / 60000).toFixed(1);
|
|
876
|
-
rule('
|
|
877
|
-
say(` ${c.gray(pad('
|
|
878
|
-
say(` ${c.gray(pad('
|
|
879
|
-
say(` ${c.gray(pad('
|
|
880
|
-
say(` ${c.gray(pad('
|
|
919
|
+
rule(말('scr.session'), 70);
|
|
920
|
+
say(` ${c.gray(pad(말('cost.calls'), 14))} ${말('unit.calls', { n: session.usage.calls })}`);
|
|
921
|
+
say(` ${c.gray(pad(말('cost.tokIn'), 14))} ${session.usage.in.toLocaleString()}`);
|
|
922
|
+
say(` ${c.gray(pad(말('cost.tokOut'), 14))} ${session.usage.out.toLocaleString()}`);
|
|
923
|
+
say(` ${c.gray(pad(말('cost.toolMs'), 14))} ${말('unit.sec', { n: (session.usage.ms / 1000).toFixed(1) })}`);
|
|
881
924
|
// 서버가 잠깐 막아 다시 부른 횟수. 0 이면 안 적는다 — 없는 일을 줄로 남기면 표만 길어진다.
|
|
882
|
-
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${session.usage.retries}
|
|
883
|
-
say(` ${c.gray(pad('
|
|
925
|
+
if (session.usage.retries) say(` ${c.gray(pad(말('cost.retries'), 14))} ${말('unit.calls', { n: session.usage.retries })}`);
|
|
926
|
+
say(` ${c.gray(pad(말('cost.elapsed'), 14))} ${말('unit.min', { n: mins })}`);
|
|
884
927
|
/*
|
|
885
928
|
* 돈.
|
|
886
929
|
*
|
|
@@ -897,7 +940,7 @@ export async function handle(line, session, ctx) {
|
|
|
897
940
|
const 요금값 = 세션요금(session);
|
|
898
941
|
const 쓴돈 = 돈셈(session.usage, 요금값);
|
|
899
942
|
if (쓴돈) {
|
|
900
|
-
say(` ${c.gray(pad('
|
|
943
|
+
say(` ${c.gray(pad(말('cost.money'), 14))} ${c.bold(돈말(쓴돈.달러))}`
|
|
901
944
|
+ ` ${c.gray(`(${어디서온값(요금값)})`)}`);
|
|
902
945
|
} else if (바깥인가(session.conn.base) && (session.usage.in || session.usage.out)) {
|
|
903
946
|
const 붙은곳 = session.제공자 ? 제공자고르기(session.제공자) : null;
|
|
@@ -917,8 +960,8 @@ export async function handle(line, session, ctx) {
|
|
|
917
960
|
const 남은것 = 마지막할당량();
|
|
918
961
|
if (남은것) {
|
|
919
962
|
const 말줄 = 할당량말(남은것);
|
|
920
|
-
say(` ${c.gray(pad('
|
|
921
|
-
+ ` ${c.gray(`(${Math.round((Date.now() - 남은것.때) / 1000)}
|
|
963
|
+
say(` ${c.gray(pad(말('cost.quota'), 14))} ${아슬아슬한가(남은것) ? c.yellow(말줄) : 말줄}`
|
|
964
|
+
+ ` ${c.gray(`(${말('cost.quotaAge', { 초: Math.round((Date.now() - 남은것.때) / 1000) })})`)}`);
|
|
922
965
|
}
|
|
923
966
|
say('');
|
|
924
967
|
return { handled: true };
|
|
@@ -926,29 +969,53 @@ export async function handle(line, session, ctx) {
|
|
|
926
969
|
|
|
927
970
|
case 'status': {
|
|
928
971
|
const k = session.conn;
|
|
929
|
-
rule('
|
|
930
|
-
say(` ${c.gray(pad('
|
|
931
|
-
say(` ${c.gray(pad('
|
|
972
|
+
rule(말('scr.conn'), 70);
|
|
973
|
+
say(` ${c.gray(pad(말('status.kind'), 10))} ${규격이름(k.kind)}`);
|
|
974
|
+
say(` ${c.gray(pad(말('status.url'), 10))} ${k.base}`);
|
|
932
975
|
// 프록시를 거치면 어느 것을, 어디서 읽었는지(env · config)까지. 안 거치면 줄 자체가 없다.
|
|
933
976
|
// 적어 놨는데 못 쓰는 것(socks5 등)이면 그 까닭을 — 조용히 직접 가면 사람은 프록시를 탄 줄 안다.
|
|
934
977
|
const 프록시 = 프록시고르기(k.base);
|
|
935
|
-
if (프록시) say(` ${c.gray(pad('
|
|
936
|
-
else if (프록시설정().탈) say(` ${c.gray(pad('
|
|
937
|
-
say(` ${c.gray(pad('
|
|
938
|
-
say(` ${c.gray(pad('
|
|
978
|
+
if (프록시) say(` ${c.gray(pad(말('status.proxy'), 10))} ${프록시.url} ${c.gray(`(${프록시.출처})`)}`);
|
|
979
|
+
else if (프록시설정().탈) say(` ${c.gray(pad(말('status.proxy'), 10))} ${c.yellow(말('status.proxyOff'))} ${c.gray(`— ${프록시설정().탈}`)}`);
|
|
980
|
+
say(` ${c.gray(pad(말('status.model'), 10))} ${k.model}`);
|
|
981
|
+
say(` ${c.gray(pad(말('status.root'), 10))} ${session.root}`);
|
|
939
982
|
// 명령이 어느 셸에서 도는지. "ls 가 왜 안 되지" 의 답이 이 줄에 있다.
|
|
940
|
-
say(` ${c.gray(pad('
|
|
983
|
+
say(` ${c.gray(pad(말('status.shell'), 10))} ${정한셸().표시}`);
|
|
941
984
|
// 열쇠를 어디에 두고 있나. 사내 심사에서 제일 먼저 묻는 것이라
|
|
942
985
|
// 「어딘가 잠겨 있겠지」 로 두지 않고 지금 상태를 그대로 적는다.
|
|
943
|
-
|
|
944
|
-
|
|
986
|
+
/*
|
|
987
|
+
* 열쇠를 어디에 두고 있나 — 또는 **어디서 받아 오나.**
|
|
988
|
+
*
|
|
989
|
+
* 받아 오는 연결에서는 「보관」 을 적으면 거짓말이 된다. 우리는 그
|
|
990
|
+
* 열쇠를 갖고 있지 않다. 사내 심사에서 제일 먼저 묻는 자리라
|
|
991
|
+
* 있는 그대로 적는다 (safety/authcmd.js).
|
|
992
|
+
*/
|
|
993
|
+
if (session.conn.열쇠받기) {
|
|
994
|
+
const 상태 = 지금열쇠상태();
|
|
995
|
+
const 남은말 = 상태
|
|
996
|
+
? 말('auth.leftMin', { 분: Math.round(상태.남은초 / 60) })
|
|
997
|
+
: 말('auth.notYet');
|
|
998
|
+
say(` ${c.gray(pad(말('status.keyStore'), 10))} `
|
|
999
|
+
+ `${말(session.conn.열쇠받기.곳 === '정책' ? 'auth.fromPolicy' : 'auth.from')} ${c.gray(`· ${남은말}`)}`);
|
|
1000
|
+
say(` ${c.gray(pad('', 10))} ${c.gray(clip(session.conn.열쇠받기.명령, 56))}`);
|
|
1001
|
+
} else if (k.auth !== 'none') {
|
|
1002
|
+
say(` ${c.gray(pad(말('status.keyStore'), 10))} ${열쇠보관(load())}`);
|
|
1003
|
+
}
|
|
1004
|
+
say(` ${c.gray(pad(말('status.rules'), 10))} ${session.rules ? session.rules.name : 말('status.noRules')}`);
|
|
1005
|
+
// 파일이 있는데 못 읽은 것을 '없음' 으로 적으면, 규칙을 적어 둔 사람은
|
|
1006
|
+
// 걸려 있다고 믿는다. 안 걸린 채로 도는 것이 여기서 제일 나쁜 모양이다.
|
|
1007
|
+
if (session.규칙못읽음) {
|
|
1008
|
+
say(` ${c.gray(pad('', 10))} ${mark.warn} ${c.yellow(말('status.rulesUnread', {
|
|
1009
|
+
이름: session.규칙못읽음.이름, 까닭: session.규칙못읽음.까닭,
|
|
1010
|
+
}))}`);
|
|
1011
|
+
}
|
|
945
1012
|
const caps = [
|
|
946
|
-
k.tools ? c.green('
|
|
947
|
-
k.streaming ? c.green('
|
|
948
|
-
k.json ? c.green('
|
|
949
|
-
k.think ? c.green('
|
|
1013
|
+
k.tools ? c.green(말('status.capTools')) : c.red(말('status.capTools')),
|
|
1014
|
+
k.streaming ? c.green(말('status.capStream')) : c.gray(말('status.capStream')),
|
|
1015
|
+
k.json ? c.green(말('status.capSchema')) : c.gray(말('status.capSchema')),
|
|
1016
|
+
k.think ? c.green(말('status.capThink')) : c.gray(말('status.capThink')),
|
|
950
1017
|
].join(c.gray(' · '));
|
|
951
|
-
say(` ${c.gray(pad('
|
|
1018
|
+
say(` ${c.gray(pad(말('status.caps'), 10))} ${caps}`);
|
|
952
1019
|
say('');
|
|
953
1020
|
return { handled: true };
|
|
954
1021
|
}
|
|
@@ -1176,6 +1243,10 @@ export async function handle(line, session, ctx) {
|
|
|
1176
1243
|
say(` ${c.gray('모델에게는')} ${c.white('mcp__<서버>__<도구>')} ${c.gray('라는 이름으로 보입니다.')}`);
|
|
1177
1244
|
say(` ${c.yellow('※')} ${c.gray('이 도구들은 남의 프로그램이 돌립니다 —')} ${c.white('작업 범위(' + session.root + ') 를 안 지킵니다.')}`);
|
|
1178
1245
|
say(` ${c.gray(' 무엇을 불렀는지는')} ${c.white('.deel/audit.jsonl')} ${c.gray('에 남습니다.')}`);
|
|
1246
|
+
// 바로 위 줄이 약속이다. 못 지키고 있으면 여기서 말해야 한다.
|
|
1247
|
+
if (ctx?.audit?.못쓴것?.()) {
|
|
1248
|
+
say(` ${mark.warn} ${c.yellow('지금 그 파일에 못 적고 있습니다')} ${c.gray('— /증거 에서 까닭을 보세요.')}`);
|
|
1249
|
+
}
|
|
1179
1250
|
say('');
|
|
1180
1251
|
return { handled: true };
|
|
1181
1252
|
}
|
|
@@ -1225,7 +1296,19 @@ export async function handle(line, session, ctx) {
|
|
|
1225
1296
|
}
|
|
1226
1297
|
say('');
|
|
1227
1298
|
say(` ${c.gray('이어하려면 나갔다가')} ${c.cyan('deel --continue')} ${c.gray('또는')} ${c.cyan(`deel --resume ${rows[0].id}`)}`);
|
|
1228
|
-
|
|
1299
|
+
/*
|
|
1300
|
+
* 바로 아래 줄이 약속이다. 못 지키고 있으면 약속을 되풀이하면 안 된다.
|
|
1301
|
+
*
|
|
1302
|
+
* /mcp 화면에서 감사기록을 두고 한 것과 같은 자리다 — 「남습니다」 라고
|
|
1303
|
+
* 적어 놓고 실제로는 안 남는 것이 제일 나쁘다. 이 화면은 이어하기를
|
|
1304
|
+
* 보러 온 화면이라, 여기서 안 말하면 사람이 알 자리가 없다.
|
|
1305
|
+
*/
|
|
1306
|
+
const 저장샘 = ctx?.갈래?.현재store?.()?.못쓴것?.();
|
|
1307
|
+
if (저장샘) {
|
|
1308
|
+
say(` ${mark.warn} ${c.yellow(말('store.notSaving', { n: 저장샘.수, 까닭: 저장샘.까닭 }))}`);
|
|
1309
|
+
} else {
|
|
1310
|
+
say(` ${c.gray('지금 대화는 나가지 않아도 계속 저장되고 있습니다.')}`);
|
|
1311
|
+
}
|
|
1229
1312
|
say('');
|
|
1230
1313
|
return { handled: true };
|
|
1231
1314
|
}
|
|
@@ -1630,13 +1713,26 @@ function showThink(session, { 자세히 = false } = {}) {
|
|
|
1630
1713
|
const 개발자 = session.level === '개발자';
|
|
1631
1714
|
const 단계 = t.rows.map((r) => `${r.label} ${r.level}`).join(c.gray(' · '));
|
|
1632
1715
|
|
|
1716
|
+
/*
|
|
1717
|
+
* 이 화면은 여태 통째로 한국어였다.
|
|
1718
|
+
*
|
|
1719
|
+
* `/lang en` 으로 켠 사람에게 「추론 강도 medium (첫 판단 medium · …)」 이
|
|
1720
|
+
* 뜨면 아무것도 안 읽힌다. 명령 이름만 영어고 화면이 한국어인 자리는
|
|
1721
|
+
* 「덜 옮겨졌다」 가 아니라 「고장 났다」 로 읽힌다 — i18n/index.js 머리말.
|
|
1722
|
+
*
|
|
1723
|
+
* 예로 드는 명령도 말을 따라간다. `/think 배분 절약` 은 영어로 켠 사람이
|
|
1724
|
+
* 칠 수 없는 글자다. 두 이름 다 진짜로 받는다(위 배분말·자세히 갈래).
|
|
1725
|
+
*/
|
|
1726
|
+
const 한국어 = 언어() === 'ko';
|
|
1727
|
+
const 배분예 = 한국어 ? '/think 배분 절약' : '/think profile save';
|
|
1728
|
+
const 자세히예 = 한국어 ? '/think 자세히' : '/think detail';
|
|
1633
1729
|
say('');
|
|
1634
|
-
say(` ${c.gray('
|
|
1635
|
-
if (개발자 || 자세히) say(` ${c.gray('
|
|
1730
|
+
say(` ${c.gray(말('think.effort'))} ${c.bold(session.think)} ${c.gray(`(${단계})`)}`);
|
|
1731
|
+
if (개발자 || 자세히) say(` ${c.gray(pad(말('think.profile'), 한국어 ? 8 : 10))}${c.bold(t.name)} ${c.gray(t.desc)}`);
|
|
1636
1732
|
|
|
1637
1733
|
if (자세히) {
|
|
1638
1734
|
say('');
|
|
1639
|
-
say(` ${c.gray(pad('
|
|
1735
|
+
say(` ${c.gray(pad(말('think.col.stage'), 12) + pad(말('think.col.level'), 10) + pad(말('think.col.cap'), 10) + 말('think.col.when'))}`);
|
|
1640
1736
|
for (const r of t.rows) {
|
|
1641
1737
|
const 화살 = r.moved > 0 ? c.yellow('↑') : r.moved < 0 ? c.cyan('↓') : c.gray('·');
|
|
1642
1738
|
say(` ${pad(r.label, 12)}${화살} ${pad(r.level, 8)}${pad(r.cap.toLocaleString(), 10, 'right')} ${c.gray(r.why)}`);
|
|
@@ -1644,21 +1740,21 @@ function showThink(session, { 자세히 = false } = {}) {
|
|
|
1644
1740
|
say('');
|
|
1645
1741
|
// 상한이 어디서 왔는지 밝힌다. 세 줄이 같은 값일 때 그게 고장인지 아닌지
|
|
1646
1742
|
// 이 한 줄로 갈린다 — 아는 상한이 낮으면 셋이 같아지는 것이 맞다.
|
|
1647
|
-
|
|
1648
|
-
session.conn.
|
|
1649
|
-
|
|
1650
|
-
say(` ${c.gray('
|
|
1743
|
+
const 어디서 = session.conn.maxTokens ? 말('think.cap.you')
|
|
1744
|
+
: session.conn.maxOut ? 말('think.cap.server') : 말('think.cap.guess');
|
|
1745
|
+
say(` ${c.gray(말('think.cap.head'))} ${c.white((아는상한 ?? 16384).toLocaleString())} ${c.gray(어디서)}${c.gray(말('think.cap.tail'))} ${c.cyan('/out')}`);
|
|
1746
|
+
say(` ${c.gray(말('think.ctx'))} ${c.white(t.ctx.toLocaleString())}${c.gray(말('think.ctx.used'))}${c.white(t.used.toLocaleString())}`);
|
|
1651
1747
|
say('');
|
|
1652
|
-
say(` ${c.gray('
|
|
1653
|
-
say(` ${c.gray('
|
|
1748
|
+
say(` ${c.gray(말('think.effort'))} ${THINK_LEVELS.join(' · ')} ${c.gray(말('think.eg'))} ${c.cyan('/think high')}`);
|
|
1749
|
+
say(` ${c.gray(말('think.profile'))} ${Object.entries(PROFILES).map(([k, v]) => { const n = 한국어 ? v.name : (v.en ?? v.name); return n === k ? k : `${k}(${n})`; }).join(' · ')} ${c.gray(말('think.eg'))} ${c.cyan(배분예)}`);
|
|
1654
1750
|
} else if (개발자) {
|
|
1655
|
-
say(` ${c.gray('
|
|
1751
|
+
say(` ${c.gray(말('think.effort'))} ${c.cyan('/think high')} ${c.gray(말('think.profile'))} ${c.cyan(배분예)} ${c.gray(말('think.detail'))} ${c.cyan(자세히예)}`);
|
|
1656
1752
|
} else {
|
|
1657
|
-
say(` ${c.gray('
|
|
1753
|
+
say(` ${c.gray(말('think.harder'))} ${c.cyan('/think high')} ${c.gray(말('think.faster'))} ${c.cyan('/think low')}`);
|
|
1658
1754
|
}
|
|
1659
1755
|
|
|
1660
1756
|
if (!session.conn.think && session.think !== 'off') {
|
|
1661
|
-
say(` ${c.yellow('
|
|
1757
|
+
say(` ${c.yellow(말('think.nomodel'))} ${c.gray(말('think.nomodel.note'))}`);
|
|
1662
1758
|
}
|
|
1663
1759
|
say('');
|
|
1664
1760
|
}
|
|
@@ -1760,6 +1856,19 @@ function 증거명령(session, ctx, arg = '') {
|
|
|
1760
1856
|
say('');
|
|
1761
1857
|
rule('작업 증거', 70);
|
|
1762
1858
|
|
|
1859
|
+
/*
|
|
1860
|
+
* 기록이 새고 있으면 **목록보다 먼저** 말한다.
|
|
1861
|
+
*
|
|
1862
|
+
* 이 화면은 감사기록을 읽어서 만든다. 기록이 안 적히면 목록이 짧아지는
|
|
1863
|
+
* 게 아니라 「아무것도 안 했다」로 보인다 — 아래 이른 반환이 그 자리다.
|
|
1864
|
+
* 안 한 것을 말하라고 만든 화면이 안 한 것처럼 보이게 하면 안 된다.
|
|
1865
|
+
*/
|
|
1866
|
+
if (e.기록못씀) {
|
|
1867
|
+
say(` ${mark.warn} ${c.yellow(`감사기록 ${e.기록못씀.수}건이 안 적혔습니다`)} ${c.gray(`— ${clip(e.기록못씀.까닭, 60)}`)}`);
|
|
1868
|
+
say(` ${c.gray('아래는 실제로 한 것보다 짧습니다.')}`);
|
|
1869
|
+
say('');
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1763
1872
|
if (!e.바꾼것.length && !e.돌린것.length) {
|
|
1764
1873
|
say(` ${c.gray('이번 대화에서 아직 바꾸거나 돌린 것이 없습니다.')}`);
|
|
1765
1874
|
say('');
|
|
@@ -2219,8 +2328,8 @@ function 배움명령(session, ctx, arg = '') {
|
|
|
2219
2328
|
function showContext(session) {
|
|
2220
2329
|
const b = session.breakdown();
|
|
2221
2330
|
say('');
|
|
2222
|
-
rule('
|
|
2223
|
-
say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${b.total.toLocaleString()}
|
|
2331
|
+
rule(말('scr.context'), 70);
|
|
2332
|
+
say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${말('unit.tokens', { n: b.total.toLocaleString() })}`);
|
|
2224
2333
|
say('');
|
|
2225
2334
|
say(` ${bar(b.used, b.total, 32)} ${b.used.toLocaleString()} / ${b.total.toLocaleString()} ${c.gray(`${Math.round((b.used / b.total) * 100)}%`)}`);
|
|
2226
2335
|
say('');
|
|
@@ -2229,9 +2338,9 @@ function showContext(session) {
|
|
|
2229
2338
|
say(` ${c.gray(pad(r.label, 26))} ${pad(r.n.toLocaleString(), 8, 'right')}`);
|
|
2230
2339
|
}
|
|
2231
2340
|
say(` ${c.gray('─'.repeat(35))}`);
|
|
2232
|
-
say(` ${c.gray(pad('
|
|
2341
|
+
say(` ${c.gray(pad(말('ctx.left'), 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
|
|
2233
2342
|
say('');
|
|
2234
|
-
say(` ${c.gray(
|
|
2343
|
+
say(` ${c.gray(`/compact ${말('ctx.compactHint')} /clear ${말('ctx.clearHint')}`)}`);
|
|
2235
2344
|
/*
|
|
2236
2345
|
* 추정이라고만 적어 두면 사람은 얼마나 믿어야 할지 모른다. 서버가 알려 준
|
|
2237
2346
|
* 실제값에 맞춰 가고 있으면 그 사실을 적는다 — '추정' 과 '맞춰 본 추정' 은
|
|
@@ -2239,9 +2348,9 @@ function showContext(session) {
|
|
|
2239
2348
|
*/
|
|
2240
2349
|
if (b.보정잰것 > 0) {
|
|
2241
2350
|
const 차이 = Math.round((b.보정 - 1) * 100);
|
|
2242
|
-
say(` ${c.gray(
|
|
2351
|
+
say(` ${c.gray(말('ctx.calibrated', { 부호: 차이 >= 0 ? '+' : '', 퍼센트: 차이, 번: b.보정잰것 }))}`);
|
|
2243
2352
|
} else {
|
|
2244
|
-
say(` ${c.gray('
|
|
2353
|
+
say(` ${c.gray(말('ctx.estimate'))}`);
|
|
2245
2354
|
}
|
|
2246
2355
|
say('');
|
|
2247
2356
|
}
|
|
@@ -2348,7 +2457,7 @@ async function 출력상한(session, arg = '') {
|
|
|
2348
2457
|
|
|
2349
2458
|
if (말 === 'auto' || 말 === '자동') {
|
|
2350
2459
|
session.conn.maxTokens = null;
|
|
2351
|
-
if (prof) { delete prof.maxTokens;
|
|
2460
|
+
if (prof) { delete prof.maxTokens; 설정남기기(cfg); }
|
|
2352
2461
|
say(` ${mark.ok} 직접 정한 값을 지웠습니다.`);
|
|
2353
2462
|
say(` ${c.gray(알아낸것 ? `서버에서 알아낸 ${알아낸것.toLocaleString()} 토큰을 씁니다.` : '모르는 값이라 16,384 토큰으로 갑니다.')}`);
|
|
2354
2463
|
say('');
|
|
@@ -2364,7 +2473,7 @@ async function 출력상한(session, arg = '') {
|
|
|
2364
2473
|
return;
|
|
2365
2474
|
}
|
|
2366
2475
|
session.conn.maxTokens = 값;
|
|
2367
|
-
if (prof) { prof.maxTokens = 값;
|
|
2476
|
+
if (prof) { prof.maxTokens = 값; 설정남기기(cfg); }
|
|
2368
2477
|
say(` ${mark.ok} 답 길이 상한 ${c.bold(값.toLocaleString())} 토큰`);
|
|
2369
2478
|
say(` ${c.gray('모델이 못 내는 값을 넣으면 서버가 거절합니다. 거절당하면')} ${c.cyan('/out auto')} ${c.gray('로 되돌리세요.')}`);
|
|
2370
2479
|
say('');
|
|
@@ -2736,7 +2845,7 @@ async function 나가도되나묻기(session, p) {
|
|
|
2736
2845
|
async function 골라적용(session, cfg, p) {
|
|
2737
2846
|
if (!await 나가도되나묻기(session, p)) return;
|
|
2738
2847
|
cfg.active = p.id;
|
|
2739
|
-
|
|
2848
|
+
설정남기기(cfg);
|
|
2740
2849
|
연결적용(session, p);
|
|
2741
2850
|
say(` ${mark.ok} ${c.bold(p.name)} ${c.gray(p.model)} 로 바꿨습니다. 대화는 이어집니다.`);
|
|
2742
2851
|
say('');
|
|
@@ -2765,8 +2874,8 @@ async function 모델만바꾸기(session, cfg, 모델) {
|
|
|
2765
2874
|
// 서버는 그대로지만 문은 같이 지난다. 등록 안 된 서버로 옮겨 가는 길도
|
|
2766
2875
|
// 여기라서, 여기를 열어 두면 자물쇠에 구멍이 하나 남는다.
|
|
2767
2876
|
if (!await 나가도되나묻기(session, p)) return;
|
|
2768
|
-
if (!이미)
|
|
2769
|
-
else { cfg.active = p.id;
|
|
2877
|
+
if (!이미) 설정남기기(upsert(cfg, p));
|
|
2878
|
+
else { cfg.active = p.id; 설정남기기(cfg); }
|
|
2770
2879
|
연결적용(session, p);
|
|
2771
2880
|
say(` ${mark.ok} 모델을 ${c.bold(모델)} 로 바꿨습니다. ${c.gray('서버는 그대로입니다.')}`);
|
|
2772
2881
|
await 길이맞추기(session, cfg, p);
|
|
@@ -2788,7 +2897,7 @@ async function 길이맞추기(session, cfg, prof) {
|
|
|
2788
2897
|
session.conn.ctx = 값;
|
|
2789
2898
|
if (prof) {
|
|
2790
2899
|
prof.ctx = 값;
|
|
2791
|
-
|
|
2900
|
+
설정남기기(cfg);
|
|
2792
2901
|
}
|
|
2793
2902
|
say(` ${c.gray('컨텍스트')} ${c.white(값.toLocaleString())} ${c.gray('토큰 (' + fmtSize(값) + ') — ' + (r?.source ? r.source + '에서 읽음' : '서버가 안 알려줘 기본값'))}`);
|
|
2794
2903
|
if (r?.max && r?.loaded && r.max > r.loaded) {
|
package/src/completion.js
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { MODES } from './agent/modes.js';
|
|
25
25
|
import { LEVELS, PROFILES } from './agent/effort.js';
|
|
26
|
+
// 승인 모드(auto·confirm·strict). agent/modes.js 의 MODES 와 헷갈리기 쉬운데,
|
|
27
|
+
// 저쪽은 `--work` 가 받는 '무슨 일을 하는 중인가' 다. 실제로 한 번 뒤바뀌어
|
|
28
|
+
// 있었다 — `--mode` 를 누르면 code·plan 이 나오고 `--work` 는 폴더를 냈다.
|
|
29
|
+
import { 차례 as 승인모드 } from './ui/approve.js';
|
|
26
30
|
|
|
27
31
|
/** deel 이 받는 명령. bin/deel.js 의 switch 와 같아야 한다(검사가 지킨다). */
|
|
28
32
|
export const 명령들 = [
|
|
@@ -39,6 +43,7 @@ export const 명령들 = [
|
|
|
39
43
|
{ 이름: 'scan', 뜻: '게이트웨이에 어떤 모델이 있나 훑는다', en: 'scan the gateway for available models' },
|
|
40
44
|
{ 이름: 'sessions', 뜻: '지난 대화 목록', en: 'list past conversations' },
|
|
41
45
|
{ 이름: 'ls', 뜻: 'sessions 와 같다', en: 'same as sessions' },
|
|
46
|
+
{ 이름: 'reset', 뜻: '설정·기억·기록을 지운다 (그냥 치면 보여만 준다)', en: 'wipe settings, memory and records (bare: just shows)' },
|
|
42
47
|
{ 이름: 'completion', 뜻: '탭 완성 스크립트를 낸다', en: 'print a tab-completion script' },
|
|
43
48
|
{ 이름: 'version', 뜻: '판 번호', en: 'version number' },
|
|
44
49
|
{ 이름: 'help', 뜻: '도움말', en: 'help' },
|
|
@@ -55,8 +60,20 @@ export const 숨은명령 = ['-p'];
|
|
|
55
60
|
*/
|
|
56
61
|
export const 깃발들 = [
|
|
57
62
|
{ 이름: '--root', 뜻: '작업 폴더', en: 'working folder', 값: '폴더' },
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
/*
|
|
64
|
+
* 이 둘은 한 번 뒤바뀌어 있었다.
|
|
65
|
+
*
|
|
66
|
+
* `--mode` 는 **승인 모드**(auto·confirm·strict)인데 작업 모드 목록이
|
|
67
|
+
* 붙어 있었고, `--work` 는 **작업 모드**인데 「살림 폴더」 라고 적혀 폴더를
|
|
68
|
+
* 완성해 주고 있었다. 그래서 `deel --mode <탭>` 을 누르면 받지도 않는
|
|
69
|
+
* code·plan 이 나왔다.
|
|
70
|
+
*
|
|
71
|
+
* 검사도 이걸 못 잡았다 — 완성 목록이 agent/modes.js 에서 온 것이 맞나만
|
|
72
|
+
* 봤기 때문이다. 베낀 자리가 맞는지는 봤는데, 베껴 온 곳이 맞는지는 안 봤다.
|
|
73
|
+
* 지금은 도움말에 적힌 값과 맞춰 본다 (test/completion.test.js).
|
|
74
|
+
*/
|
|
75
|
+
{ 이름: '--work', 뜻: '무슨 일을 하는 중인가', en: 'what kind of work this is', 값: Object.keys(MODES) },
|
|
76
|
+
{ 이름: '--mode', 뜻: '무엇까지 물어보지 않고 할까', en: 'how much it may do without asking', 값: 승인모드 },
|
|
60
77
|
{ 이름: '--level', 뜻: '화면을 얼마나 자세히', en: 'how much detail on screen', 값: null },
|
|
61
78
|
{ 이름: '--ctx', 뜻: '창 크기 (예: 32k)', en: 'context window size (e.g. 32k)', 값: null },
|
|
62
79
|
{ 이름: '--max-tokens', 뜻: '한 번에 뱉을 최대 토큰', en: 'max tokens per reply', 값: null },
|
|
@@ -65,6 +82,7 @@ export const 깃발들 = [
|
|
|
65
82
|
{ 이름: '--online', 뜻: '묻지 않고 바깥으로 나간다', en: 'go outside without asking', 값: false },
|
|
66
83
|
{ 이름: '--offline', 뜻: '바깥으로 아예 안 나간다', en: 'never reach the network', 값: false },
|
|
67
84
|
{ 이름: '--yes', 뜻: '물어보지 않고 진행', en: 'do not ask, just proceed', 값: false },
|
|
85
|
+
{ 이름: '--hard', 뜻: 'reset all 에서 되돌리기·감사기록까지', en: 'with reset all: undo snapshots and the audit log too', 값: false },
|
|
68
86
|
{ 이름: '--json', 뜻: '결과를 JSON 으로', en: 'output JSON', 값: false },
|
|
69
87
|
{ 이름: '--quiet', 뜻: '군말 없이', en: 'no chatter', 값: false },
|
|
70
88
|
{ 이름: '--tui', 뜻: '입력 상자를 켠다', en: 'force the input box on', 값: false },
|
package/src/config.js
CHANGED
|
@@ -164,6 +164,23 @@ export function save(cfg, { toProject = false } = {}) {
|
|
|
164
164
|
return p;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* 설정을 남긴다. 못 남겨도 던지지 않고, **왜 못 남겼는지를 돌려준다.**
|
|
169
|
+
*
|
|
170
|
+
* 부르는 자리가 아홉 군데인데 전부 `try { save(cfg) } catch {}` 였다. 「못
|
|
171
|
+
* 남겨도 이번 세션에는 먹는다」 는 맞는 말이지만, 그 뒤에 화면은 `✓` 를
|
|
172
|
+
* 찍는다 — 사람은 정해진 줄 알고 다음에 창을 열었다가 옛 값을 본다. 그리고
|
|
173
|
+
* 그때는 무엇 때문인지 알 길이 없다(홈이 읽기 전용인지, 디스크가 찼는지).
|
|
174
|
+
*
|
|
175
|
+
* 던지는 것은 안 바꾼다. 설정 하나 못 적었다고 대화가 끊기면 본말이 뒤집힌다.
|
|
176
|
+
*
|
|
177
|
+
* @returns {{ok: true, 자리: string} | {ok: false, 왜: string}}
|
|
178
|
+
*/
|
|
179
|
+
export function 저장시도(cfg, 옵션 = {}) {
|
|
180
|
+
try { return { ok: true, 자리: save(cfg, 옵션) }; }
|
|
181
|
+
catch (err) { return { ok: false, 왜: err?.message ?? String(err) }; }
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
export function activeProfile(cfg = load()) {
|
|
168
185
|
if (!cfg.profiles.length) return null;
|
|
169
186
|
return cfg.profiles.find((x) => x.id === cfg.active) ?? cfg.profiles[0];
|
|
@@ -172,6 +189,23 @@ export function activeProfile(cfg = load()) {
|
|
|
172
189
|
// 환경변수가 있으면 파일보다 우선한다 — 사내망에서 키를 파일에 안 남기고 싶을 때 쓴다.
|
|
173
190
|
const 푼것 = new Map(); // 잠긴 값 → 푼 글. 한 판에 한 번만 풀면 된다.
|
|
174
191
|
|
|
192
|
+
/**
|
|
193
|
+
* 잠근 열쇠를 못 푼 까닭. 화면이 한 번 읽어 가면 지워진다.
|
|
194
|
+
*
|
|
195
|
+
* ── 왜 있어야 하나 ──────────────────────────────────────────────────────
|
|
196
|
+
*
|
|
197
|
+
* keystore.js 의 풀기() 는 왜 못 풀었는지를 성실하게 만들어 돌려준다 —
|
|
198
|
+
* 「이 PC 의 이 계정에서 잠근 것만 풀립니다」 같은 말이다. 그 함수 머리말이
|
|
199
|
+
* 그렇게 하는 까닭까지 적어 뒀다: **빈 글자만 돌려주면 사람은 401 만 보고
|
|
200
|
+
* 게이트웨이를 의심한다.**
|
|
201
|
+
*
|
|
202
|
+
* 그런데 부르는 쪽이 그 말을 버리고 있었다. 빈 열쇠가 나가면 http.js 는
|
|
203
|
+
* Authorization 머리말을 아예 안 붙이고, 게이트웨이는 401 을 준다. 사람은
|
|
204
|
+
* 방화벽·주소·계정을 뒤진다. 진짜 까닭은 「설정 파일을 다른 PC 로 옮겼다」 인데.
|
|
205
|
+
*/
|
|
206
|
+
let 열쇠탈 = null;
|
|
207
|
+
export function 열쇠탈소식() { const s = 열쇠탈; 열쇠탈 = null; return s; }
|
|
208
|
+
|
|
175
209
|
export function resolveKey(profile) {
|
|
176
210
|
const byName = profile?.id ? process.env[`DEEL_KEY_${profile.id.toUpperCase()}`] : null;
|
|
177
211
|
const 값 = byName || process.env.DEEL_API_KEY || profile?.apiKey || '';
|
|
@@ -180,8 +214,19 @@ export function resolveKey(profile) {
|
|
|
180
214
|
// 기다리는 시간이라, 판에 한 번만 풀고 들고 있는다.
|
|
181
215
|
if (푼것.has(값)) return 푼것.get(값);
|
|
182
216
|
const r = 풀기(값);
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
if (!r.ok) {
|
|
218
|
+
/*
|
|
219
|
+
* **실패는 안 담아 둔다.**
|
|
220
|
+
*
|
|
221
|
+
* 담아 두면 그 판 내내 다시 시도조차 안 한다. 사람이 그 사이에
|
|
222
|
+
* `deel setup` 으로 열쇠를 다시 넣어도 이 판에서는 영영 안 풀린다.
|
|
223
|
+
* 푸는 값이 0.2초라, 안 되는 것을 아껴서 얻을 것이 없다.
|
|
224
|
+
*/
|
|
225
|
+
열쇠탈 = r.why || '잠근 열쇠를 못 풀었습니다';
|
|
226
|
+
return '';
|
|
227
|
+
}
|
|
228
|
+
푼것.set(값, r.text);
|
|
229
|
+
return r.text;
|
|
185
230
|
}
|
|
186
231
|
|
|
187
232
|
export function upsert(cfg, profile) {
|