deel-local-cli 1.5.8 → 1.6.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 +23 -13
- package/README.md +22 -12
- package/bin/deel.js +10 -0
- package/package.json +2 -2
- package/src/acp/map.js +143 -0
- package/src/acp/serve.js +160 -13
- package/src/agent/commit.js +511 -0
- package/src/agent/compact.js +269 -226
- package/src/agent/loop.js +154 -12
- package/src/agent/mention.js +56 -10
- package/src/agent/review.js +192 -0
- package/src/agent/session.js +20 -2
- package/src/agent/threads.js +1 -1
- package/src/backend/adapter.js +342 -280
- package/src/backend/azure.js +151 -0
- package/src/backend/ctxsize.js +19 -0
- package/src/backend/detect.js +106 -0
- package/src/backend/http.js +350 -30
- package/src/backend/probe.js +48 -1
- package/src/backend/proxy.js +151 -0
- package/src/backend/quota.js +133 -0
- package/src/backend/retry.js +132 -0
- package/src/backend/vision.js +185 -0
- package/src/commands.js +341 -8
- package/src/completion.js +264 -0
- package/src/config.js +117 -1
- package/src/i18n/en.js +13 -1
- package/src/i18n/index.js +22 -3
- package/src/i18n/ja.js +266 -0
- package/src/i18n/ko.js +12 -0
- package/src/i18n/zh.js +266 -0
- package/src/oneshot.js +28 -3
- package/src/pack/sbom.js +25 -0
- package/src/pack/selfpack.js +9 -3
- package/src/plugins/manage.js +34 -7
- package/src/repl.js +78 -6
- package/src/report.js +26 -2
- package/src/safety/guard.js +13 -2
- package/src/safety/keystore.js +237 -0
- package/src/safety/network.js +76 -14
- package/src/safety/policy.js +209 -0
- package/src/safety/secrets.js +32 -0
- package/src/setup.js +40 -7
- package/src/tools/clipboard.js +178 -0
- package/src/tools/fastgrep.js +217 -0
- package/src/tools/fsutil.js +55 -12
- package/src/tools/ignore.js +192 -0
- package/src/tools/index.js +226 -36
- package/src/tools/jobs.js +4 -12
- package/src/tools/outline.js +3 -1
- package/src/tools/pdf.js +1326 -0
- package/src/tools/shell.js +123 -0
- package/src/tools/verify.js +20 -4
- package/src/tools/webfetch.js +18 -5
- package/src/ui/inputbox.js +2 -2
- package/src/ui/screen.js +4 -1
- package/src/ui/status.js +22 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deel completion <셸>` — 탭 눌러 완성하기.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 필요한가 ─────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 이 프로그램에는 명령이 열댓 개, 깃발이 스무 개쯤 있는데 화면 어디에도 다
|
|
7
|
+
* 안 나온다(`--help` 는 자주 쓰는 것만 보여 준다). 그래서 `deel sbom` 이나
|
|
8
|
+
* `--effort` 가 있다는 것을 아는 사람만 쓴다. 탭 완성은 도움말을 안 읽어도
|
|
9
|
+
* **있다는 사실**을 알려 준다.
|
|
10
|
+
*
|
|
11
|
+
* ── 무엇을 조심하나 ────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* **거짓말하지 않는 목록.** 완성이 알려 주는 명령이 실제로 없으면, 사람은
|
|
14
|
+
* 그것을 치고 「모르는 명령」을 본다. 도움말보다 나쁘다 — 도움말은 안 읽으면
|
|
15
|
+
* 그만이지만, 완성은 없는 것을 있다고 우긴다.
|
|
16
|
+
*
|
|
17
|
+
* 그래서 목록은 **여기 한 벌만** 두고, bin/deel.js 가 실제로 받는 명령과
|
|
18
|
+
* 어긋나지 않는지 검사가 지킨다(test/completion.test.js). 새 명령을 넣고
|
|
19
|
+
* 여기 안 적으면 검사가 빨개진다.
|
|
20
|
+
*
|
|
21
|
+
* 값이 정해진 깃발(`--mode` · `--think`)은 그 값도 실제 코드에서 가져온다.
|
|
22
|
+
* 손으로 베껴 적으면 언젠가 갈린다.
|
|
23
|
+
*/
|
|
24
|
+
import { MODES } from './agent/modes.js';
|
|
25
|
+
import { LEVELS, PROFILES } from './agent/effort.js';
|
|
26
|
+
|
|
27
|
+
/** deel 이 받는 명령. bin/deel.js 의 switch 와 같아야 한다(검사가 지킨다). */
|
|
28
|
+
export const 명령들 = [
|
|
29
|
+
{ 이름: 'chat', 뜻: '대화 시작 (그냥 deel 만 쳐도 같다)', en: 'start a conversation (bare deel does this too)' },
|
|
30
|
+
{ 이름: 'run', 뜻: '한 번만 돌고 끝낸다 (스크립트·배치)', en: 'run once and exit (scripts, batch jobs)' },
|
|
31
|
+
{ 이름: 'acp', 뜻: '에디터가 자식으로 띄우는 자리 — 사람이 칠 명령이 아니다', en: 'editor integration (ACP); not meant to be typed by hand' },
|
|
32
|
+
{ 이름: 'status', 뜻: '지금 무엇에 붙어 있나', en: 'what it is connected to right now' },
|
|
33
|
+
{ 이름: 'setup', 뜻: '처음 설정', en: 'first-time setup' },
|
|
34
|
+
{ 이름: 'diagnose', 뜻: '왜 안 되는지 짚어 본다', en: 'figure out why it is not working' },
|
|
35
|
+
{ 이름: 'doctor', 뜻: 'diagnose 와 같다', en: 'same as diagnose' },
|
|
36
|
+
{ 이름: 'pack', 뜻: '오프라인 반입용으로 묶는다', en: 'bundle for offline transfer' },
|
|
37
|
+
{ 이름: 'audit', 뜻: '무엇을 했는지 기록을 본다', en: 'show the record of what it did' },
|
|
38
|
+
{ 이름: 'sbom', 뜻: '무엇이 들었는지 목록을 낸다', en: 'list what is inside (SBOM)' },
|
|
39
|
+
{ 이름: 'scan', 뜻: '게이트웨이에 어떤 모델이 있나 훑는다', en: 'scan the gateway for available models' },
|
|
40
|
+
{ 이름: 'sessions', 뜻: '지난 대화 목록', en: 'list past conversations' },
|
|
41
|
+
{ 이름: 'ls', 뜻: 'sessions 와 같다', en: 'same as sessions' },
|
|
42
|
+
{ 이름: 'completion', 뜻: '탭 완성 스크립트를 낸다', en: 'print a tab-completion script' },
|
|
43
|
+
{ 이름: 'version', 뜻: '판 번호', en: 'version number' },
|
|
44
|
+
{ 이름: 'help', 뜻: '도움말', en: 'help' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** `-p` 는 명령이지만 깃발처럼 생겨서 완성 목록에는 안 올린다(쳐도 된다). */
|
|
48
|
+
export const 숨은명령 = ['-p'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 깃발.
|
|
52
|
+
*
|
|
53
|
+
* `값` 이 있으면 그 다음 낱말을 그 목록에서 고른다. `파일`·`폴더` 는 셸에게
|
|
54
|
+
* 맡긴다 — 우리가 흉내내는 것보다 셸이 훨씬 잘한다.
|
|
55
|
+
*/
|
|
56
|
+
export const 깃발들 = [
|
|
57
|
+
{ 이름: '--root', 뜻: '작업 폴더', en: 'working folder', 값: '폴더' },
|
|
58
|
+
{ 이름: '--work', 뜻: '살림 폴더 (.deel 자리)', en: 'state folder (where .deel lives)', 값: '폴더' },
|
|
59
|
+
{ 이름: '--mode', 뜻: '무엇을 할 수 있게 할까', en: 'what the agent is allowed to do', 값: Object.keys(MODES) },
|
|
60
|
+
{ 이름: '--level', 뜻: '화면을 얼마나 자세히', en: 'how much detail on screen', 값: null },
|
|
61
|
+
{ 이름: '--ctx', 뜻: '창 크기 (예: 32k)', en: 'context window size (e.g. 32k)', 값: null },
|
|
62
|
+
{ 이름: '--max-tokens', 뜻: '한 번에 뱉을 최대 토큰', en: 'max tokens per reply', 값: null },
|
|
63
|
+
{ 이름: '--think', 뜻: '생각의 양', en: 'how much thinking', 값: LEVELS },
|
|
64
|
+
{ 이름: '--effort', 뜻: '생각을 어디에 몰아줄까', en: 'where to spend the thinking budget', 값: Object.keys(PROFILES) },
|
|
65
|
+
{ 이름: '--offline', 뜻: '바깥으로 아예 안 나간다', en: 'never reach the network', 값: false },
|
|
66
|
+
{ 이름: '--yes', 뜻: '물어보지 않고 진행', en: 'do not ask, just proceed', 값: false },
|
|
67
|
+
{ 이름: '--json', 뜻: '결과를 JSON 으로', en: 'output JSON', 값: false },
|
|
68
|
+
{ 이름: '--quiet', 뜻: '군말 없이', en: 'no chatter', 값: false },
|
|
69
|
+
{ 이름: '--tui', 뜻: '입력 상자를 켠다', en: 'force the input box on', 값: false },
|
|
70
|
+
{ 이름: '--no-tui', 뜻: '입력 상자 없이 줄 화면으로', en: 'plain line output, no input box', 값: false },
|
|
71
|
+
{ 이름: '--continue', 뜻: '지난 대화 이어서', en: 'continue the last conversation', 값: null },
|
|
72
|
+
{ 이름: '--resume', 뜻: '고른 대화 이어서', en: 'resume a chosen conversation', 값: null },
|
|
73
|
+
{ 이름: '--version', 뜻: '판 번호', en: 'version number', 값: false },
|
|
74
|
+
{ 이름: '--help', 뜻: '도움말', en: 'help', 값: false },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export const 셸들 = ['bash', 'zsh', 'powershell', 'pwsh'];
|
|
78
|
+
|
|
79
|
+
const 이름만 = () => 명령들.map((x) => x.이름);
|
|
80
|
+
const 깃발이름만 = () => 깃발들.map((x) => x.이름);
|
|
81
|
+
|
|
82
|
+
/** 셸 문자열 안에 그대로 넣어도 되게. 우리 목록은 다 ASCII 라 넉넉하다. */
|
|
83
|
+
const 홑따옴표 = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
84
|
+
|
|
85
|
+
function bash판({ zsh = false } = {}) {
|
|
86
|
+
const 값깃발 = 깃발들.filter((x) => Array.isArray(x.값));
|
|
87
|
+
const 폴더깃발 = 깃발들.filter((x) => x.값 === '폴더').map((x) => x.이름);
|
|
88
|
+
const 값갈래 = 값깃발.map((x) => ` ${x.이름}) COMPREPLY=( $(compgen -W ${홑따옴표(x.값.join(' '))} -- "$cur") ); return 0 ;;`).join('\n');
|
|
89
|
+
const 폴더갈래 = 폴더깃발.length
|
|
90
|
+
? ` ${폴더깃발.join('|')}) COMPREPLY=( $(compgen -d -- "$cur") ); return 0 ;;`
|
|
91
|
+
: '';
|
|
92
|
+
|
|
93
|
+
return `# deel 탭 완성 (${zsh ? 'zsh' : 'bash'})
|
|
94
|
+
#
|
|
95
|
+
# 넣는 법:
|
|
96
|
+
# deel completion ${zsh ? 'zsh' : 'bash'} > ~/.deel-completion.${zsh ? 'zsh' : 'bash'}
|
|
97
|
+
# echo 'source ~/.deel-completion.${zsh ? 'zsh' : 'bash'}' >> ~/.${zsh ? 'zshrc' : 'bashrc'}
|
|
98
|
+
${zsh ? '\nautoload -U +X bashcompinit && bashcompinit\n' : ''}
|
|
99
|
+
_deel() {
|
|
100
|
+
local cur prev
|
|
101
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
102
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
103
|
+
|
|
104
|
+
# 값이 정해진 깃발 다음이면 그 값들만.
|
|
105
|
+
case "$prev" in
|
|
106
|
+
${값갈래}
|
|
107
|
+
${폴더갈래}
|
|
108
|
+
completion) COMPREPLY=( $(compgen -W ${홑따옴표(셸들.join(' '))} -- "$cur") ); return 0 ;;
|
|
109
|
+
esac
|
|
110
|
+
|
|
111
|
+
# 깃발을 치는 중이면 깃발만.
|
|
112
|
+
if [[ "$cur" == -* ]]; then
|
|
113
|
+
COMPREPLY=( $(compgen -W ${홑따옴표(깃발이름만().join(' '))} -- "$cur") )
|
|
114
|
+
return 0
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# 첫 낱말이면 명령. 그 뒤는 파일·폴더를 셸에게 맡긴다.
|
|
118
|
+
local i cmd=""
|
|
119
|
+
for (( i=1; i < COMP_CWORD; i++ )); do
|
|
120
|
+
case "\${COMP_WORDS[i]}" in
|
|
121
|
+
-*) ;;
|
|
122
|
+
*) cmd="\${COMP_WORDS[i]}"; break ;;
|
|
123
|
+
esac
|
|
124
|
+
done
|
|
125
|
+
if [[ -z "$cmd" ]]; then
|
|
126
|
+
COMPREPLY=( $(compgen -W ${홑따옴표(이름만().join(' '))} -- "$cur") )
|
|
127
|
+
else
|
|
128
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
complete -F _deel deel
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/*
|
|
136
|
+
* 파워셸 판.
|
|
137
|
+
*
|
|
138
|
+
* ── 왜 이 스크립트만 영어인가 ──────────────────────────────────────────
|
|
139
|
+
*
|
|
140
|
+
* 이 집의 규칙은 한글 이름·한글 주석이지만, **여기서 만들어 내는 글은
|
|
141
|
+
* 우리 코드가 아니라 파워셸이 읽을 글**이다. 그리고 윈도우에 기본으로 깔린
|
|
142
|
+
* Windows PowerShell 5.1 은 BOM 없는 .ps1 파일을 UTF-8 이 아니라 그 PC 의
|
|
143
|
+
* 옛 코드페이지(한국이면 CP949)로 읽는다.
|
|
144
|
+
*
|
|
145
|
+
* 그래서 `$명령` 같은 한글 변수 이름을 쓰면, 파이프로 바로 먹일 때는 되는데
|
|
146
|
+
* `deel completion powershell > profile.ps1` 로 저장한 순간 글자가 깨져
|
|
147
|
+
* **문법 오류**가 난다. 사람은 셸이 켜질 때마다 빨간 글을 보게 되고, 그게
|
|
148
|
+
* deel 때문인 줄도 모른다.
|
|
149
|
+
*
|
|
150
|
+
* 설명(툴팁)까지 영어인 것도 같은 까닭이다. 처음엔 따옴표 안이니 깨져도
|
|
151
|
+
* 문법은 안 무너지겠거니 했는데, 진짜 파워셸에 넣어 보니 **무너졌다.**
|
|
152
|
+
* CP949 는 두 바이트 인코딩이라, 한글의 UTF-8 바이트를 짝지어 먹다가
|
|
153
|
+
* **닫는 따옴표까지 뒷바이트로 삼켜 버린다.** 그러면 문자열이 안 닫혀서
|
|
154
|
+
* 그 줄이 통째로 문법 오류가 된다. 실제로 `'지금 무엇에 붙어 있나'` 가
|
|
155
|
+
* 그렇게 터졌다.
|
|
156
|
+
*
|
|
157
|
+
* 그래서 이 스크립트는 **처음부터 끝까지 ASCII** 다. 한글 화면을 쓰는
|
|
158
|
+
* 사람에게도 여기 툴팁만 영어인데, 안 뜨는 것보다는 낫다.
|
|
159
|
+
* (bash·zsh 완성은 설명을 아예 안 보여 주므로 잃는 것이 없다.)
|
|
160
|
+
*/
|
|
161
|
+
function 파워셸판() {
|
|
162
|
+
const 명령목록 = 명령들.map((x) => ` @{ name = ${홑따옴표(x.이름)}; help = ${홑따옴표(x.en)} }`).join('\n');
|
|
163
|
+
const 깃발목록 = 깃발들.map((x) => ` @{ name = ${홑따옴표(x.이름)}; help = ${홑따옴표(x.en)} }`).join('\n');
|
|
164
|
+
const 값목록 = 깃발들
|
|
165
|
+
.filter((x) => Array.isArray(x.값))
|
|
166
|
+
.map((x) => ` ${홑따옴표(x.이름)} = @(${x.값.map(홑따옴표).join(', ')})`)
|
|
167
|
+
.join('\n');
|
|
168
|
+
|
|
169
|
+
// 파워셸은 완성 후보에 설명을 같이 실을 수 있다. 탭을 누르면 뜻이 같이 뜬다.
|
|
170
|
+
return `# deel tab completion (PowerShell)
|
|
171
|
+
#
|
|
172
|
+
# Install (this session):
|
|
173
|
+
# deel completion powershell | Out-String | Invoke-Expression
|
|
174
|
+
#
|
|
175
|
+
# Install (every session):
|
|
176
|
+
# deel completion powershell | Out-File -Encoding utf8 -Append $PROFILE
|
|
177
|
+
#
|
|
178
|
+
# This whole script is ASCII on purpose. Windows PowerShell 5.1 reads a BOM-less
|
|
179
|
+
# .ps1 as the local ANSI codepage; there, a double-byte decode can swallow a
|
|
180
|
+
# closing quote and break the script outright.
|
|
181
|
+
|
|
182
|
+
Register-ArgumentCompleter -Native -CommandName deel -ScriptBlock {
|
|
183
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
184
|
+
|
|
185
|
+
$cmds = @(
|
|
186
|
+
${명령목록}
|
|
187
|
+
)
|
|
188
|
+
$flags = @(
|
|
189
|
+
${깃발목록}
|
|
190
|
+
)
|
|
191
|
+
$values = @{
|
|
192
|
+
${값목록}
|
|
193
|
+
'completion' = @(${셸들.map(홑따옴표).join(', ')})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Which word comes before the one being typed?
|
|
197
|
+
#
|
|
198
|
+
# This was wrong once: when the typed word is empty (the cursor sits after a
|
|
199
|
+
# space), the LAST element is the previous word -- but the code always looked
|
|
200
|
+
# one further back, so "--mode <TAB>" offered commands instead of modes.
|
|
201
|
+
# Only pressing TAB in a real PowerShell showed it.
|
|
202
|
+
$words = @($commandAst.CommandElements | ForEach-Object { $_.ToString() })
|
|
203
|
+
$prev = ''
|
|
204
|
+
if ($wordToComplete -eq '') {
|
|
205
|
+
if ($words.Count -ge 1) { $prev = $words[$words.Count - 1] }
|
|
206
|
+
} elseif ($words.Count -ge 2) {
|
|
207
|
+
$prev = $words[$words.Count - 2]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# After a flag that takes fixed values, offer only those.
|
|
211
|
+
if ($values.ContainsKey($prev)) {
|
|
212
|
+
return $values[$prev] |
|
|
213
|
+
Where-Object { $_ -like "$wordToComplete*" } |
|
|
214
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Typing a flag: offer flags only.
|
|
218
|
+
if ($wordToComplete -like '-*') {
|
|
219
|
+
return $flags |
|
|
220
|
+
Where-Object { $_.name -like "$wordToComplete*" } |
|
|
221
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.name, $_.name, 'ParameterName', $_.help) }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Otherwise: commands.
|
|
225
|
+
$cmds |
|
|
226
|
+
Where-Object { $_.name -like "$wordToComplete*" } |
|
|
227
|
+
ForEach-Object { [System.Management.Automation.CompletionResult]::new($_.name, $_.name, 'ParameterValue', $_.help) }
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 셸 하나짜리 완성 스크립트를 낸다.
|
|
234
|
+
*
|
|
235
|
+
* @returns {{ok:true, 글:string} | {ok:false, 왜:string}}
|
|
236
|
+
*/
|
|
237
|
+
export function 완성스크립트(셸) {
|
|
238
|
+
const 이름 = String(셸 ?? '').trim().toLowerCase();
|
|
239
|
+
if (!이름) {
|
|
240
|
+
return { ok: false, 왜: `어느 셸인지 알려 주세요: ${셸들.join(' · ')}\n 예: deel completion bash` };
|
|
241
|
+
}
|
|
242
|
+
if (이름 === 'bash') return { ok: true, 글: bash판() };
|
|
243
|
+
if (이름 === 'zsh') return { ok: true, 글: bash판({ zsh: true }) };
|
|
244
|
+
if (이름 === 'powershell' || 이름 === 'pwsh') return { ok: true, 글: 파워셸판() };
|
|
245
|
+
/*
|
|
246
|
+
* 모르는 셸에 아무 스크립트나 주지 않는다.
|
|
247
|
+
*
|
|
248
|
+
* fish 에 bash 스크립트를 주면 셸이 켜질 때마다 오류가 나는데, 사람은
|
|
249
|
+
* 그게 deel 때문인 줄 모른다. 안 되는 것은 안 된다고 하는 편이 낫다.
|
|
250
|
+
*/
|
|
251
|
+
return { ok: false, 왜: `${셸} 은 아직 없습니다. 있는 것: ${셸들.join(' · ')}` };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `deel completion <셸>` 이 부르는 자리. */
|
|
255
|
+
export function runCompletion(args) {
|
|
256
|
+
const r = 완성스크립트(args?.[0] ?? '');
|
|
257
|
+
if (!r.ok) {
|
|
258
|
+
process.stderr.write(`\n ${r.왜}\n\n`);
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
// 스크립트는 **표준출력으로만** 낸다 — 파이프로 바로 먹일 수 있어야 한다.
|
|
262
|
+
process.stdout.write(r.글);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
package/src/config.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs';
|
|
5
|
+
import { 프록시정하기 } from './backend/proxy.js';
|
|
6
|
+
import { 셸정하기 } from './tools/shell.js';
|
|
7
|
+
import { 애저정하기 } from './backend/azure.js';
|
|
8
|
+
import { 정책읽기 } from './safety/policy.js';
|
|
9
|
+
import { 잠그기, 풀기, 잠긴것인가, 쓸수있나, 보관방식 } from './safety/keystore.js';
|
|
5
10
|
|
|
6
11
|
// 설정이 놓이는 자리.
|
|
7
12
|
//
|
|
@@ -36,7 +41,99 @@ export function configPath() {
|
|
|
36
41
|
|
|
37
42
|
const EMPTY = { version: 1, active: null, profiles: [] };
|
|
38
43
|
|
|
44
|
+
/*
|
|
45
|
+
* 열쇠를 이 PC 의 잠금장치에 옮긴다 (safety/keystore.js).
|
|
46
|
+
*
|
|
47
|
+
* 예전 판이 평문으로 적어 둔 설정이 그대로 남아 있다. 사람더러 다시 넣으라고
|
|
48
|
+
* 하면 대부분 안 한다 — 지금 잘 되고 있으니까. 그래서 **처음 읽을 때 한 번**
|
|
49
|
+
* 조용히 옮기고, 옮겼다는 것만 한 줄로 알린다.
|
|
50
|
+
*
|
|
51
|
+
* 못 잠그면 파일을 아예 안 건드린다. 반쯤 잠근 파일을 남기면 그 뒤로는
|
|
52
|
+
* 무엇이 잠긴 것이고 무엇이 평문인지 아무도 모른다.
|
|
53
|
+
*
|
|
54
|
+
* DEEL_KEYSTORE=off 면 안 한다 — 잠금장치가 정책으로 막힌 곳에서 매번
|
|
55
|
+
* 파워셸을 두드리며 느려지는 것을 사람이 끌 수 있어야 한다.
|
|
56
|
+
*/
|
|
57
|
+
let 옮겨봤나 = false;
|
|
58
|
+
let 소식 = null;
|
|
59
|
+
|
|
60
|
+
/** 열쇠를 옮겼으면 그 한 줄을 준다. 한 번 읽으면 지워진다 (두 번 안 알린다). */
|
|
61
|
+
export function 잠금소식() { const s = 소식; 소식 = null; return s; }
|
|
62
|
+
|
|
63
|
+
/** 화면·심사서에 그대로 쓰는 '열쇠를 어디에 두나' 한 줄. */
|
|
64
|
+
export function 열쇠보관(cfg = null) {
|
|
65
|
+
const p = cfg ? activeProfile(cfg) : null;
|
|
66
|
+
return 보관방식(p?.apiKey ?? null);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function 잠금옮기기(cfg) {
|
|
70
|
+
if (옮겨봤나 || process.env.DEEL_KEYSTORE === 'off') return;
|
|
71
|
+
옮겨봤나 = true;
|
|
72
|
+
const 평문 = (cfg.profiles ?? []).filter((p) => p?.apiKey && !잠긴것인가(p.apiKey));
|
|
73
|
+
if (!평문.length) return;
|
|
74
|
+
if (!쓸수있나().되나) return;
|
|
75
|
+
|
|
76
|
+
let 옮김 = 0;
|
|
77
|
+
for (const p of 평문) {
|
|
78
|
+
const 잠근것 = 잠그기(p.apiKey);
|
|
79
|
+
if (잠근것) { p.apiKey = 잠근것; 옮김 += 1; }
|
|
80
|
+
}
|
|
81
|
+
if (!옮김) return;
|
|
82
|
+
try {
|
|
83
|
+
// 읽은 자리에 그대로 되쓴다. 프로젝트 설정을 홈으로 옮겨 버리면
|
|
84
|
+
// 이 폴더에서만 쓰던 열쇠가 온 컴퓨터의 기본값이 된다.
|
|
85
|
+
const 프로젝트것 = configPath() === join(projectDir(), 'config.json');
|
|
86
|
+
save(cfg, { toProject: 프로젝트것 });
|
|
87
|
+
const 방식 = 쓸수있나().방식 === 'keychain' ? '키체인' : 'DPAPI';
|
|
88
|
+
소식 = 옮김 === 1
|
|
89
|
+
? `게이트웨이 열쇠를 이 PC 계정에서만 풀리게 잠갔습니다 (${방식})`
|
|
90
|
+
: `게이트웨이 열쇠 ${옮김}개를 이 PC 계정에서만 풀리게 잠갔습니다 (${방식})`;
|
|
91
|
+
} catch { /* 못 쓰면 그냥 둔다 — 메모리에만 잠긴 채로 이번 판을 쓴다 */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
39
94
|
export function load() {
|
|
95
|
+
const cfg = 읽기();
|
|
96
|
+
잠금옮기기(cfg);
|
|
97
|
+
// 프록시는 설정을 읽는 자리에서 정한다. run · acp · setup · diagnose 어느 문으로
|
|
98
|
+
// 들어와도 한 번은 여기를 지나므로, 여기가 빠뜨리지 않는 유일한 자리다.
|
|
99
|
+
프록시정하기({ config: cfg });
|
|
100
|
+
// 명령을 돌릴 셸도 같은 까닭으로 여기서 정한다 (tools/shell.js).
|
|
101
|
+
셸정하기({ config: cfg });
|
|
102
|
+
// Azure 판 번호(api-version)도 같은 자리에서 정한다 (backend/azure.js).
|
|
103
|
+
애저정하기({ config: cfg });
|
|
104
|
+
// 관리 정책이 걸려 있으면 여기서 덮어쓴다 (safety/policy.js).
|
|
105
|
+
정책덮기(cfg);
|
|
106
|
+
return cfg;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* 관리 정책이 정한 것을 설정 위에 덮는다.
|
|
111
|
+
*
|
|
112
|
+
* 정책은 설정을 **이긴다.** 설정 파일은 쓰는 사람 것이라 지우고 고칠 수 있고,
|
|
113
|
+
* 그래서 "이번 배포 동안은 이 게이트웨이만" 같은 것을 적어 둘 자리가 아니다.
|
|
114
|
+
*
|
|
115
|
+
* 그렇다고 정책이 **풀어 주지는 못한다.** offline 을 켤 수는 있어도 끌 수는
|
|
116
|
+
* 없고, 금지를 더할 수는 있어도 사용자가 적어 둔 금지를 지우지는 못한다.
|
|
117
|
+
* 정책 파일 한 줄로 안전장치가 헐거워지는 길을 안 낸다.
|
|
118
|
+
*/
|
|
119
|
+
function 정책덮기(cfg) {
|
|
120
|
+
const 정책 = 정책읽기().값;
|
|
121
|
+
if (!정책 || typeof 정책 !== 'object') return cfg;
|
|
122
|
+
|
|
123
|
+
if (typeof 정책.baseUrl === 'string' && 정책.baseUrl.trim()) {
|
|
124
|
+
for (const 프로필 of cfg.profiles ?? []) 프로필.baseUrl = 정책.baseUrl.trim();
|
|
125
|
+
cfg.정책주소 = 정책.baseUrl.trim();
|
|
126
|
+
}
|
|
127
|
+
if (정책.offline === true) cfg.offline = true; // 끄지는 못한다 — 켜기만
|
|
128
|
+
if (Array.isArray(정책.permissions?.deny)) {
|
|
129
|
+
cfg.permissions = cfg.permissions ?? {};
|
|
130
|
+
const 있던것 = Array.isArray(cfg.permissions.deny) ? cfg.permissions.deny : [];
|
|
131
|
+
cfg.permissions.deny = [...new Set([...있던것, ...정책.permissions.deny])];
|
|
132
|
+
}
|
|
133
|
+
return cfg;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function 읽기() {
|
|
40
137
|
const p = configPath();
|
|
41
138
|
if (!existsSync(p)) return structuredClone(EMPTY);
|
|
42
139
|
try {
|
|
@@ -51,6 +148,16 @@ export function save(cfg, { toProject = false } = {}) {
|
|
|
51
148
|
const dir = toProject ? projectDir() : userDir();
|
|
52
149
|
const p = join(dir, 'config.json');
|
|
53
150
|
mkdirSync(dir, { recursive: true });
|
|
151
|
+
// 평문 열쇠는 여기서 잠근다. 설정을 쓰는 길이 여기 하나뿐이라, 여기만
|
|
152
|
+
// 지키면 평문이 새 파일로 나갈 자리가 없다. 못 잠그면 평문으로 두되
|
|
153
|
+
// (아무것도 못 하는 것보다 낫다) 화면과 심사서에 그렇게 적는다.
|
|
154
|
+
if (process.env.DEEL_KEYSTORE !== 'off') {
|
|
155
|
+
for (const 프로필 of cfg?.profiles ?? []) {
|
|
156
|
+
if (!프로필?.apiKey || 잠긴것인가(프로필.apiKey)) continue;
|
|
157
|
+
const 잠근것 = 잠그기(프로필.apiKey);
|
|
158
|
+
if (잠근것) 프로필.apiKey = 잠근것;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
54
161
|
writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
55
162
|
// 키가 들어 있는 파일이므로 가능한 환경에서는 본인만 읽게 잠근다.
|
|
56
163
|
try { chmodSync(p, 0o600); } catch {}
|
|
@@ -63,9 +170,18 @@ export function activeProfile(cfg = load()) {
|
|
|
63
170
|
}
|
|
64
171
|
|
|
65
172
|
// 환경변수가 있으면 파일보다 우선한다 — 사내망에서 키를 파일에 안 남기고 싶을 때 쓴다.
|
|
173
|
+
const 푼것 = new Map(); // 잠긴 값 → 푼 글. 한 판에 한 번만 풀면 된다.
|
|
174
|
+
|
|
66
175
|
export function resolveKey(profile) {
|
|
67
176
|
const byName = profile?.id ? process.env[`DEEL_KEY_${profile.id.toUpperCase()}`] : null;
|
|
68
|
-
|
|
177
|
+
const 값 = byName || process.env.DEEL_API_KEY || profile?.apiKey || '';
|
|
178
|
+
if (!잠긴것인가(값)) return 값;
|
|
179
|
+
// 푸는 데 파워셸을 한 번 부른다(0.2초쯤). 한마디마다 부르면 그게 다 사람이
|
|
180
|
+
// 기다리는 시간이라, 판에 한 번만 풀고 들고 있는다.
|
|
181
|
+
if (푼것.has(값)) return 푼것.get(값);
|
|
182
|
+
const r = 풀기(값);
|
|
183
|
+
푼것.set(값, r.ok ? r.text : '');
|
|
184
|
+
return r.ok ? r.text : '';
|
|
69
185
|
}
|
|
70
186
|
|
|
71
187
|
export function upsert(cfg, profile) {
|
package/src/i18n/en.js
CHANGED
|
@@ -72,6 +72,10 @@ export const en = {
|
|
|
72
72
|
'cmd.pin.arg': '[text|clear number|clear all]',
|
|
73
73
|
'cmd.evidence.desc': 'evidence — what changed and what proves it. Says what is unproven too',
|
|
74
74
|
'cmd.evidence.arg': '[file]',
|
|
75
|
+
'cmd.commit.desc': 'commit — stages only what this session changed; message from the diff and the evidence',
|
|
76
|
+
'cmd.review.desc': 'review the changes in a fresh context — reports findings only, changes nothing',
|
|
77
|
+
'cmd.paste.desc': 'attach the screenshot sitting in the clipboard (capture, then /paste)',
|
|
78
|
+
'cmd.commit.arg': '[all|preview|title]',
|
|
75
79
|
'cmd.sessions.desc': 'past sessions in this folder',
|
|
76
80
|
'cmd.recall.desc': 'search past sessions — by what was said, not by title',
|
|
77
81
|
'cmd.recall.arg': '<text>',
|
|
@@ -118,6 +122,8 @@ export const en = {
|
|
|
118
122
|
'head.inside': 'this machine',
|
|
119
123
|
'head.outside': 'outside',
|
|
120
124
|
'head.offlineLock': '[offline lock]',
|
|
125
|
+
'head.viaProxy': 'via proxy {프록시}',
|
|
126
|
+
'head.proxyBad': '[proxy setting unusable — see /status]',
|
|
121
127
|
'head.nowhereElse': '← and nowhere else',
|
|
122
128
|
'head.glyphHint': 'The {안} at the start of the status bar means this. It becomes {밖} when traffic leaves.',
|
|
123
129
|
'head.conn': 'link',
|
|
@@ -205,6 +211,8 @@ export const en = {
|
|
|
205
211
|
// ── Screen language ───────────────────────────────────────────────────
|
|
206
212
|
'lang.now': 'Screen language is {이름}.',
|
|
207
213
|
'lang.ko': '한국어 (Korean)',
|
|
214
|
+
'lang.ja': '日本語 (Japanese)',
|
|
215
|
+
'lang.zh': '中文 (Chinese)',
|
|
208
216
|
'lang.en': 'English',
|
|
209
217
|
'lang.howto': 'To change it',
|
|
210
218
|
'lang.changed': 'Screen language is now {이름}.',
|
|
@@ -214,7 +222,7 @@ export const en = {
|
|
|
214
222
|
'lang.codeStays': 'Unless you set it separately, what the model reads follows the screen. Code, tool names and argument names stay as they are.',
|
|
215
223
|
'lang.promptNow': 'The model is instructed in {이름}.',
|
|
216
224
|
'lang.follows': 'the same as the screen',
|
|
217
|
-
'lang.splitHowto': 'To be instructed in English
|
|
225
|
+
'lang.splitHowto': 'To be instructed in English while the screen stays in your language:',
|
|
218
226
|
'lang.promptChanged': 'Instructions in {이름}, answers in {답} — 23% off the fixed share on an 8k window.',
|
|
219
227
|
'lang.envHint': 'To start in English every time, set DEEL_LANG=en',
|
|
220
228
|
|
|
@@ -287,6 +295,10 @@ export const en = {
|
|
|
287
295
|
'ask.pickPrompt': '1-{끝}, or type your own',
|
|
288
296
|
'ask.freePrompt': 'Answer',
|
|
289
297
|
|
|
298
|
+
// ── When the server pushes back (backend/retry.js) ────────────────────
|
|
299
|
+
'loop.backoff': 'The server pushed back ({무엇}) — calling again in {초}s ({n}/{max})',
|
|
300
|
+
'cost.retries': 'retries',
|
|
301
|
+
|
|
290
302
|
// ── Common ────────────────────────────────────────────────────────────
|
|
291
303
|
'common.unknownCommand': 'Unknown command',
|
|
292
304
|
'common.seeHelp': 'Run /help for the list',
|
package/src/i18n/index.js
CHANGED
|
@@ -26,11 +26,22 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { ko } from './ko.js';
|
|
28
28
|
import { en } from './en.js';
|
|
29
|
+
import { ja } from './ja.js';
|
|
30
|
+
import { zh } from './zh.js';
|
|
29
31
|
|
|
30
|
-
export const 언어들 = ['ko', 'en'];
|
|
32
|
+
export const 언어들 = ['ko', 'en', 'ja', 'zh'];
|
|
31
33
|
export const 기본언어 = 'ko';
|
|
32
34
|
|
|
33
|
-
const 표 = { ko, en };
|
|
35
|
+
const 표 = { ko, en, ja, zh };
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* 못 찾았을 때 어디로 물러나나.
|
|
39
|
+
*
|
|
40
|
+
* 한국어로만 물러나면, 일본어를 고른 사람이 안 옮긴 자리에서 **한글**을 본다.
|
|
41
|
+
* 그 사람에게는 영어가 훨씬 읽힌다. 그래서 ko 가 아닌 말은 영어를 한 번
|
|
42
|
+
* 거쳐서 내려간다. 어느 쪽이든 빈칸은 안 낸다 — 빈칸은 고장으로 읽힌다.
|
|
43
|
+
*/
|
|
44
|
+
const 물러날곳 = { ko: [], en: ['ko'], ja: ['en', 'ko'], zh: ['en', 'ko'] };
|
|
34
45
|
|
|
35
46
|
let 지금 = 기본언어;
|
|
36
47
|
|
|
@@ -40,6 +51,8 @@ export function 언어고르기(값) {
|
|
|
40
51
|
if (!v) return null;
|
|
41
52
|
if (v === 'ko' || v === 'kr' || v === 'korean' || v === '한국어' || v === '한글') return 'ko';
|
|
42
53
|
if (v === 'en' || v === 'eng' || v === 'english' || v === '영어') return 'en';
|
|
54
|
+
if (v === 'ja' || v === 'jp' || v === 'jpn' || v === 'japanese' || v === '일본어' || v === '日本語') return 'ja';
|
|
55
|
+
if (v === 'zh' || v === 'cn' || v === 'chs' || v === 'chinese' || v === '중국어' || v === '中文' || v === '简体中文') return 'zh';
|
|
43
56
|
// ko_KR.UTF-8 · en-US 같은 것도 받는다.
|
|
44
57
|
const 앞 = v.split(/[-_.]/)[0];
|
|
45
58
|
return 언어들.includes(앞) ? 앞 : null;
|
|
@@ -159,7 +172,13 @@ export function 조사붙이기(값, 갈래) {
|
|
|
159
172
|
export function 말(열쇠, 채움 = null) {
|
|
160
173
|
const k = String(열쇠 ?? '');
|
|
161
174
|
// 있는 것 → 한국어 → 열쇠. 어느 자리에서도 빈칸이 안 나오게 세 겹으로 받친다.
|
|
162
|
-
|
|
175
|
+
let 글 = 표[지금]?.[k];
|
|
176
|
+
if (글 === undefined) {
|
|
177
|
+
for (const 뒤 of 물러날곳[지금] ?? ['ko']) {
|
|
178
|
+
if (표[뒤]?.[k] !== undefined) { 글 = 표[뒤][k]; break; }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (글 === undefined) 글 = ko[k] ?? k;
|
|
163
182
|
if (!채움) return 글;
|
|
164
183
|
/*
|
|
165
184
|
* 자리 이름에 \w 를 쓰면 안 된다.
|