deel-local-cli 1.5.8 → 1.6.1

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.
Files changed (59) hide show
  1. package/README.en.md +30 -14
  2. package/README.md +28 -12
  3. package/bin/deel.js +10 -0
  4. package/package.json +2 -2
  5. package/src/acp/map.js +143 -0
  6. package/src/acp/serve.js +166 -13
  7. package/src/agent/commit.js +511 -0
  8. package/src/agent/compact.js +269 -226
  9. package/src/agent/loop.js +196 -12
  10. package/src/agent/mention.js +56 -10
  11. package/src/agent/review.js +192 -0
  12. package/src/agent/session.js +22 -2
  13. package/src/agent/threads.js +1 -1
  14. package/src/backend/adapter.js +342 -280
  15. package/src/backend/azure.js +151 -0
  16. package/src/backend/ctxsize.js +19 -0
  17. package/src/backend/detect.js +106 -0
  18. package/src/backend/http.js +350 -30
  19. package/src/backend/probe.js +48 -1
  20. package/src/backend/proxy.js +151 -0
  21. package/src/backend/quota.js +133 -0
  22. package/src/backend/retry.js +132 -0
  23. package/src/backend/vision.js +185 -0
  24. package/src/commands.js +341 -8
  25. package/src/completion.js +264 -0
  26. package/src/config.js +117 -1
  27. package/src/i18n/en.js +13 -1
  28. package/src/i18n/index.js +22 -3
  29. package/src/i18n/ja.js +266 -0
  30. package/src/i18n/ko.js +12 -0
  31. package/src/i18n/zh.js +266 -0
  32. package/src/oneshot.js +28 -3
  33. package/src/pack/sbom.js +25 -0
  34. package/src/pack/selfpack.js +9 -3
  35. package/src/plugins/manage.js +34 -7
  36. package/src/repl.js +92 -6
  37. package/src/report.js +26 -2
  38. package/src/safety/guard.js +74 -2
  39. package/src/safety/keystore.js +237 -0
  40. package/src/safety/network.js +76 -14
  41. package/src/safety/policy.js +209 -0
  42. package/src/safety/secrets.js +32 -0
  43. package/src/setup.js +40 -7
  44. package/src/tools/clipboard.js +178 -0
  45. package/src/tools/convert.js +287 -0
  46. package/src/tools/docs.js +72 -1
  47. package/src/tools/fastgrep.js +217 -0
  48. package/src/tools/fsutil.js +55 -12
  49. package/src/tools/ignore.js +192 -0
  50. package/src/tools/index.js +391 -41
  51. package/src/tools/jobs.js +4 -12
  52. package/src/tools/outline.js +3 -1
  53. package/src/tools/pdf.js +1368 -0
  54. package/src/tools/shell.js +123 -0
  55. package/src/tools/verify.js +20 -4
  56. package/src/tools/webfetch.js +18 -5
  57. package/src/ui/inputbox.js +2 -2
  58. package/src/ui/screen.js +4 -1
  59. package/src/ui/status.js +22 -0
@@ -0,0 +1,151 @@
1
+ // Azure OpenAI — 주소 모양이 다르다.
2
+ //
3
+ // ── 왜 따로 다루나 ─────────────────────────────────────────────────────
4
+ //
5
+ // 사내에서 OpenAI 를 쓰는 한국 회사는 대개 Azure 를 거친다. 그런데 Azure 는
6
+ // OpenAI 호환이라면서 **주소 모양만 다르다.**
7
+ //
8
+ // 보통 https://gw.example.com/v1/chat/completions
9
+ // Azure https://<이름>.openai.azure.com/openai/deployments/<배포>/chat/completions?api-version=2024-10-21
10
+ //
11
+ // 다른 점이 네 군데다.
12
+ //
13
+ // 1) 모델 이름이 주소 안에 있다 — 몸통의 `model` 이 아니라 **배포 이름**이다.
14
+ // 2) `?api-version=` 이 없으면 400 이다. 이건 옵션이 아니라 필수다.
15
+ // 3) 모델 목록이 `/models` 가 아니라 `/openai/deployments` 에 있다.
16
+ // 4) 열쇠는 `Authorization: Bearer` 가 아니라 `api-key` 헤더다.
17
+ //
18
+ // 그래서 사람이 포털에서 복사한 주소를 그대로 넣으면 지금까지는
19
+ // `.../openai/deployments/gpt-4o/v1/models` 를 두드리다 "연결 실패" 로 끝났다.
20
+ // 주소는 맞는데 우리가 모양을 몰랐던 것이라, 사람이 고칠 방법도 없었다.
21
+ //
22
+ // ── 판 번호를 왜 우리가 정하나 ─────────────────────────────────────────
23
+ //
24
+ // `api-version` 은 Azure 가 정하는 날짜다. 우리가 아무 값이나 쓰면 안 되고,
25
+ // 사람이 모르면 넣을 수도 없다. 그래서 GA 판 하나를 기본으로 두고, 주소에
26
+ // 이미 적혀 있으면 **그쪽을 따른다** — 사내 정책으로 판을 고정해 둔 곳이 있다.
27
+ // 설정(`apiVersion`)과 환경변수로도 바꾼다.
28
+ const 기본판 = '2024-10-21';
29
+
30
+ let 정한판 = 기본판;
31
+
32
+ /** 설정·환경변수에서 판 번호를 정한다. config.load() 가 부른다. */
33
+ export function 애저정하기({ env = process.env, config = null } = {}) {
34
+ const 값 = String(env.DEEL_AZURE_API_VERSION ?? config?.apiVersion ?? '').trim();
35
+ 정한판 = 값 || 기본판;
36
+ return 정한판;
37
+ }
38
+
39
+ /** 지금 쓰는 판 번호. */
40
+ export function 지금판() { return 정한판; }
41
+
42
+ /** 검사가 원래대로 돌려놓을 때. */
43
+ export function 판되돌리기() { 정한판 = 기본판; return 정한판; }
44
+
45
+ // Azure 가 쓰는 호스트들. 포털이 주는 이름이 서비스마다 다르다.
46
+ const 애저호스트 = /\.(openai\.azure\.com|cognitiveservices\.azure\.com|services\.ai\.azure\.com)$/i;
47
+
48
+ /*
49
+ * 이 주소가 Azure 인가. 호스트로 보고, 아니면 경로 모양으로 본다.
50
+ *
51
+ * 단, 주소가 이미 `/v1` 로 끝나면 **Azure 로 다루지 않는다.**
52
+ * Azure 자원에도 OpenAI 규격 그대로인 `/openai/v1` 창구가 있고, 그 주소를
53
+ * 넣는 사람이 있다. 그것까지 배포 주소로 바꾸려 들면 되던 것이 안 된다 —
54
+ * 사람은 어제까지 되던 주소가 왜 갑자기 안 되는지 알 길이 없다.
55
+ */
56
+ export function 애저인가(input) {
57
+ const s = String(input ?? '').trim();
58
+ if (!s) return false;
59
+ const u = 뜯기(s);
60
+ if (!u) return /\/openai\/deployments(\/|\?|$)/i.test(s);
61
+ if (/\/v\d+\/?$/.test(u.pathname)) return false;
62
+ return 애저호스트.test(u.hostname) || /\/openai\/deployments(\/|$)/i.test(u.pathname);
63
+ }
64
+
65
+ function 뜯기(s) {
66
+ try { return new URL(/^https?:\/\//i.test(s) ? s : `https://${s}`); }
67
+ catch { return null; }
68
+ }
69
+
70
+ /**
71
+ * 사람이 복사해 온 주소를 우리가 쓸 모양으로 푼다.
72
+ *
73
+ * 포털·문서·curl 예시가 저마다 다른 자리까지 준다. 다 받는다.
74
+ *
75
+ * https://<이름>.openai.azure.com
76
+ * https://<이름>.openai.azure.com/
77
+ * https://<이름>.openai.azure.com/openai/deployments/gpt-4o
78
+ * https://<이름>.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-06-01
79
+ *
80
+ * @returns {{origin:string, 배포:string|null, 판:string, base:string|null, 목록주소:string}|null}
81
+ */
82
+ export function 애저풀기(input) {
83
+ const u = 뜯기(String(input ?? '').trim());
84
+ if (!u) return null;
85
+
86
+ // 주소에 판이 적혀 있으면 그 말을 따른다 — 사내에서 판을 고정해 둔 곳이 있다.
87
+ const 판 = (u.searchParams.get('api-version') || '').trim() || 정한판;
88
+
89
+ const 조각 = u.pathname.split('/').filter(Boolean);
90
+ const i = 조각.findIndex((x) => x.toLowerCase() === 'deployments');
91
+ // `/openai/deployments/<배포>` 뒤에 붙은 `chat/completions` 같은 것은 버린다 —
92
+ // 우리가 다시 붙일 것이라, 두 번 붙으면 404 가 된다.
93
+ const 배포 = i >= 0 && 조각[i + 1] ? 풀어보기(조각[i + 1]) : null;
94
+
95
+ /*
96
+ * `openai/deployments` **앞에 붙은 길은 지킨다.**
97
+ *
98
+ * 사내에서 Azure 를 그대로 열어 주는 곳은 드물다. APIM 같은 앞단을 하나 두고
99
+ * `https://apim.사내/azure-openai/openai/deployments/...` 처럼 한 겹 아래에
100
+ * 매단다. 그 앞머리를 버리고 호스트 바로 밑에 다시 붙이면, 사람이 정확히
101
+ * 옮겨 적은 주소가 우리 손에서 틀린 주소가 된다. 그러고는 404 만 돌아온다.
102
+ */
103
+ const 앞머리 = i >= 1 && 조각[i - 1].toLowerCase() === 'openai' ? 조각.slice(0, i - 1) : [];
104
+ const 앞길 = 앞머리.length ? `/${앞머리.join('/')}` : '';
105
+
106
+ const origin = `${u.protocol}//${u.host}`;
107
+ return {
108
+ origin,
109
+ 앞길,
110
+ 배포,
111
+ 판,
112
+ base: 배포 ? 애저base(origin, 배포, 판, 앞길) : null,
113
+ 목록주소: `${origin}${앞길}/openai/deployments?api-version=${encodeURIComponent(판)}`,
114
+ };
115
+ }
116
+
117
+ /*
118
+ * 퍼센트 인코딩이 망가진 조각을 만나도 안 죽는다.
119
+ *
120
+ * `decodeURIComponent('100%')` 는 URIError 를 던진다. 그 자리가 프로그램의
121
+ * 제일 바깥이라, 사람은 `오류 URI malformed` 한 줄만 보고 무엇을 잘못
122
+ * 넣었는지 알 수 없다. 못 풀면 적힌 그대로 쓴다.
123
+ */
124
+ function 풀어보기(s) {
125
+ try { return decodeURIComponent(s); }
126
+ catch { return s; }
127
+ }
128
+
129
+ /** 배포 이름 하나로 쓸 주소를 만든다. 앞단에 매달려 있으면 그 앞길도 지킨다. */
130
+ export function 애저base(origin, 배포, 판 = 정한판, 앞길 = '') {
131
+ return `${String(origin).replace(/\/+$/, '')}${앞길}/openai/deployments/${encodeURIComponent(배포)}`
132
+ + `?api-version=${encodeURIComponent(판)}`;
133
+ }
134
+
135
+ /**
136
+ * 배포 목록 응답을 모델 목록으로.
137
+ *
138
+ * Azure 는 `{data:[{id, model, status}]}` 로 준다. 사람에게 보여 줄 이름은
139
+ * **배포 이름**(id)이다 — 주소에 들어가는 것이 그것이고, 밑에 깔린 모델
140
+ * 이름(gpt-4o)은 같은 것이 여러 배포에 붙을 수 있어 고를 때 도움이 안 된다.
141
+ */
142
+ export function 배포목록(json) {
143
+ const 것들 = json?.data ?? json?.value ?? [];
144
+ if (!Array.isArray(것들)) return [];
145
+ return 것들
146
+ .map((d) => ({
147
+ id: d.id ?? d.name ?? d.deploymentId ?? null,
148
+ note: [d.model, d.status && d.status !== 'succeeded' ? d.status : null].filter(Boolean).join(' · '),
149
+ }))
150
+ .filter((m) => m.id);
151
+ }
@@ -27,6 +27,7 @@
27
27
  // 서버가 거절한다. 그래서 실제로 쓸 값은 '올린 길이' 로 잡고, '모델 최대' 는
28
28
  // 따로 알려 준다 — 사용자가 서버에서 더 올린 다음 /ctx 로 맞출 수 있게.
29
29
  import { req, headersFor } from './http.js';
30
+ import { 애저인가 } from './azure.js';
30
31
 
31
32
  // 이 이름들 중 하나면 '모델이 낼 수 있는 최대 컨텍스트' 다. 순서가 곧 우선순위다.
32
33
  const 최대이름 = [
@@ -161,6 +162,24 @@ export async function probeCtx(conn, { timeout = 6000 } = {}) {
161
162
  const model = conn.model;
162
163
  const tried = [];
163
164
 
165
+ /*
166
+ * Azure 는 컨텍스트 길이를 API 로 안 알려 준다.
167
+ *
168
+ * 그런데 여기서 두드리는 여섯 자리(`/models`, `/props`, `/info` …)는 Azure
169
+ * 배포 주소 밑에 아예 없다. 그대로 두면 켤 때마다 여섯 번 404 를 받으며
170
+ * 기다리고, 진단 화면에는 "아무 응답도 못 받았습니다" 가 뜬다 — 서버가
171
+ * 멀쩡한데 우리가 없는 문을 두드린 것뿐이다.
172
+ */
173
+ // 'Azure 인가' 를 여기서 따로 정하지 않는다. 글자 조각으로 보면 detect 와
174
+ // 답이 갈리고, 갈리는 순간 한쪽만 맞는 자리가 생긴다 (backend/azure.js).
175
+ if (애저인가(conn.base)) {
176
+ return {
177
+ value: null, max: null, loaded: null, out: null, source: null, outSource: null,
178
+ maxKey: null, loadedKey: null, tried: [],
179
+ why: 'Azure 는 컨텍스트 길이를 알려주지 않습니다 — 배포한 모델에 맞게 설정에서 직접 넣으세요',
180
+ };
181
+ }
182
+
164
183
  const 본다 = async (label, url, opts = {}) => {
165
184
  try {
166
185
  const r = await req(url, { headers: H(), timeout, ...opts });
@@ -1,5 +1,6 @@
1
1
  // 주소만 받아서 "이 서버가 무슨 규격이고 인증을 어떻게 받는지" 알아낸다.
2
2
  import { req, headersFor, AUTH_STYLES, serverMessage } from './http.js';
3
+ import { 애저인가, 애저풀기, 애저base, 배포목록 } from './azure.js';
3
4
 
4
5
  // 사람이 대충 적은 주소를 시도해볼 후보들로 넓힌다.
5
6
  export function candidates(input) {
@@ -14,6 +15,96 @@ export function candidates(input) {
14
15
  return out;
15
16
  }
16
17
 
18
+ /*
19
+ * Azure 확인.
20
+ *
21
+ * 배포 목록(`/openai/deployments?api-version=`)을 먼저 묻는다. 여기서 오는
22
+ * 이름이 곧 주소에 들어갈 이름이라, 목록을 받으면 사람은 고르기만 하면 된다.
23
+ *
24
+ * 목록을 막아 둔 테넌트가 많다(권한이 따로다). 그때도 **연결 실패로 치지
25
+ * 않는다** — 주소에 배포 이름이 이미 있으면 그것으로 그냥 간다. 목록을 못 본
26
+ * 것과 못 쓰는 것은 다르고, 여기서 실패로 처리하면 정작 잘 되는 설정이
27
+ * 설치 화면을 못 넘어간다.
28
+ *
29
+ * ── 다만 두 가지는 성공이 아니다 ──────────────────────────────────────
30
+ *
31
+ * 1) **아무 바이트도 안 왔을 때.** 포트가 닫혀 있거나, VPN 이 안 올라왔거나,
32
+ * 자원 이름을 잘못 적었을 때가 그렇다. 이걸 "목록만 못 봤다" 로 넘기면
33
+ * 설치 화면이 초록색 `연결됨` 을 띄우고, 사람은 권한 문제인 줄 알고
34
+ * 엉뚱한 데를 뒤진다. 서버에 닿지도 못했으면 닿지 못했다고 해야 한다.
35
+ *
36
+ * 2) **첫 번째 401 로 인증 방식을 정해 버리는 것.** Azure 앞단을 Entra ID 로
37
+ * 감싼 곳은 `api-key` 에 401 을 주고 `Bearer` 를 받는다. 첫 401 에서
38
+ * 멈추면 그 곳은 영영 못 붙는다 — 화면은 초록색인데 첫 한마디가 401 이다.
39
+ * 그래서 방식은 **다 해 보고** 고른다.
40
+ */
41
+ async function tryAzure(input, key) {
42
+ const 푼것 = 애저풀기(input);
43
+ if (!푼것) return null;
44
+ // Azure 는 api-key 헤더가 제 방식이다. 그것부터 본다.
45
+ const 차례 = ['api-key', 'bearer', 'none'].map((id) => AUTH_STYLES.find((s) => s.id === id));
46
+
47
+ let 닿음 = false; // 서버가 HTTP 로 대답을 하기는 했나
48
+ const 막힌것 = []; // 거절당한 방식들 — 다 해 보고 고른다
49
+ let 마지막오류 = null;
50
+
51
+ for (const style of 차례) {
52
+ if (style.id !== 'none' && !key) continue;
53
+ const r = await req(푼것.목록주소, { headers: headersFor(style.id, key), timeout: 12000 });
54
+ if (r.status) 닿음 = true;
55
+ else 마지막오류 = r.error ?? 마지막오류;
56
+
57
+ if (r.ok && r.json) {
58
+ const models = 배포목록(r.json);
59
+ const base = 푼것.base ?? (models[0] ? 애저base(푼것.origin, models[0].id, 푼것.판, 푼것.앞길) : null);
60
+ if (base) return { kind: 'openai', base, auth: style.id, models, ms: r.ms, azure: true };
61
+ // 200 인데 배포가 하나도 없다. 인증은 통했으니 더 두드려 봐야 같은 답이다.
62
+ return {
63
+ kind: null,
64
+ ms: r.ms,
65
+ why: '배포 목록은 받았는데 비어 있습니다 — 이 자원에 배포된 모델이 없습니다.'
66
+ + ' Azure 포털에서 모델을 배포한 뒤 다시 해 보세요.',
67
+ };
68
+ }
69
+ if ([401, 403, 404].includes(r.status)) 막힌것.push({ auth: style.id, status: r.status, ms: r.ms });
70
+ }
71
+
72
+ // 아무도 못 닿았다. 이건 목록 권한 문제가 아니라 연결 문제다.
73
+ if (!닿음) return { kind: null, why: 마지막오류 ?? '주소에 닿지 못했습니다' };
74
+
75
+ if (푼것.base && 막힌것.length) {
76
+ /*
77
+ * 어느 방식으로 적어 둘까.
78
+ *
79
+ * 401·403 은 "인증이 틀렸다" 는 말이고, 404 는 "인증은 됐는데 그 자리가
80
+ * 없다"에 가깝다. 그러니 404 를 받은 방식이 있으면 그쪽이 맞을 확률이
81
+ * 높다. 아무것도 안 통했으면 Azure 의 제 방식(api-key)으로 적어 두되,
82
+ * **열쇠를 줬는데 '인증 없음' 으로 적지는 않는다** — 그렇게 적으면 그
83
+ * 뒤의 모든 요청이 열쇠 없이 나간다.
84
+ */
85
+ // 열쇠를 줬으면 '인증 없음' 은 아예 후보에서 뺀다. 그것으로 적어 두면
86
+ // 그 뒤의 모든 요청이 맨몸으로 나간다 — 붙은 것처럼 보이는데 다 401 이다.
87
+ const 볼것 = key ? 막힌것.filter((x) => x.auth !== 'none') : 막힌것;
88
+ const 쓸것 = 볼것.length ? 볼것 : [{ auth: key ? 'api-key' : 'none', status: 막힌것[0].status, ms: 막힌것[0].ms }];
89
+ const 나은것 = 쓸것.find((x) => x.status === 404) ?? null;
90
+ const 고른것 = 나은것 ?? 쓸것[0];
91
+ const 다막힘 = !나은것;
92
+ return {
93
+ kind: 'openai',
94
+ base: 푼것.base,
95
+ auth: 고른것.auth,
96
+ models: [],
97
+ ms: 고른것.ms,
98
+ azure: true,
99
+ warn: 다막힘 && key
100
+ ? `배포 목록도 인증도 확인 못 했습니다 (해 본 방식마다 HTTP ${막힌것.map((x) => x.status).join('·')})`
101
+ + ' — 주소에 적힌 배포로 그냥 씁니다. 첫 한마디에서 걸리면 /model 로 인증 방식을 바꾸세요.'
102
+ : `배포 목록을 못 봤습니다 (HTTP ${고른것.status}) — 주소에 적힌 배포로 그냥 씁니다.`,
103
+ };
104
+ }
105
+ return null;
106
+ }
107
+
17
108
  // OpenAI 호환인지 확인 (GET {base}/models)
18
109
  async function tryOpenAI(base, key) {
19
110
  for (const style of AUTH_STYLES) {
@@ -71,6 +162,21 @@ export async function detect(input, key) {
71
162
  const tried = [];
72
163
  const origin = String(input).trim().replace(/\/+$/, '').replace(/\/v\d+$/, '');
73
164
 
165
+ // Azure 는 모양이 아예 다르다. 여기서 갈라 놓지 않으면 아래 후보들이
166
+ // 엉뚱한 자리를 두드리다 끝난다.
167
+ if (애저인가(input)) {
168
+ const 푼것 = 애저풀기(input);
169
+ tried.push(푼것?.목록주소 ?? String(input));
170
+ const hit = await tryAzure(input, key);
171
+ if (hit?.kind) return { ...hit, tried };
172
+ // 못 붙은 이유를 tryAzure 가 알고 있으면 그 말을 쓴다. 우리가 아는 것보다 정확하다.
173
+ return {
174
+ kind: null,
175
+ tried,
176
+ why: hit?.why ?? 'Azure 주소로 보이는데 배포를 못 찾았습니다 — 주소에 /openai/deployments/<배포이름> 까지 넣어 보세요.',
177
+ };
178
+ }
179
+
74
180
  // Ollama 를 먼저 본다 — 로컬이면 대개 이쪽이고 확인이 빠르다.
75
181
  const oll = await tryOllama(/^https?:\/\//i.test(origin) ? origin : 'http://' + origin);
76
182
  if (oll) return { ...oll, tried };