deel-local-cli 1.1.1 → 1.4.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.
Files changed (60) hide show
  1. package/README.en.md +826 -19
  2. package/README.md +689 -14
  3. package/bin/deel.js +73 -4
  4. package/package.json +7 -3
  5. package/src/acp/jsonrpc.js +230 -0
  6. package/src/acp/map.js +219 -0
  7. package/src/acp/serve.js +556 -0
  8. package/src/agent/budget.js +0 -14
  9. package/src/agent/card.js +110 -0
  10. package/src/agent/compact.js +95 -0
  11. package/src/agent/evidence.js +186 -0
  12. package/src/agent/evolve.js +213 -0
  13. package/src/agent/grade.js +20 -0
  14. package/src/agent/loop.js +172 -14
  15. package/src/agent/models.js +169 -0
  16. package/src/agent/modes.js +49 -1
  17. package/src/agent/pins.js +140 -0
  18. package/src/agent/project.js +10 -4
  19. package/src/agent/session.js +359 -11
  20. package/src/agent/store.js +31 -0
  21. package/src/agent/threads.js +154 -0
  22. package/src/commands.js +751 -55
  23. package/src/config.js +10 -1
  24. package/src/i18n/en.js +244 -0
  25. package/src/i18n/index.js +126 -0
  26. package/src/i18n/ko.js +231 -0
  27. package/src/lsp/client.js +459 -0
  28. package/src/lsp/diag.js +112 -0
  29. package/src/lsp/rpc.js +84 -0
  30. package/src/lsp/servers.js +218 -0
  31. package/src/oneshot.js +23 -0
  32. package/src/pack/sbom.js +218 -0
  33. package/src/pack/selfpack.js +19 -3
  34. package/src/repl.js +273 -22
  35. package/src/safety/guard.js +37 -2
  36. package/src/safety/secrets.js +205 -0
  37. package/src/safety/undo.js +11 -4
  38. package/src/setup.js +2 -2
  39. package/src/skills/discover.js +1 -1
  40. package/src/tools/desc.en.js +219 -0
  41. package/src/tools/edit-match.js +16 -5
  42. package/src/tools/excel-com.js +1 -1
  43. package/src/tools/excel.js +1 -1
  44. package/src/tools/fsutil.js +0 -8
  45. package/src/tools/index.js +149 -6
  46. package/src/tools/lsp.js +327 -0
  47. package/src/tools/task.js +30 -2
  48. package/src/tools/todo.js +19 -3
  49. package/src/ui/ansi.js +42 -2
  50. package/src/ui/approve.js +25 -21
  51. package/src/ui/inputbox.js +69 -13
  52. package/src/ui/intro.js +174 -0
  53. package/src/ui/level.js +11 -5
  54. package/src/ui/md.js +227 -0
  55. package/src/ui/notify.js +101 -0
  56. package/src/ui/prompt.js +1 -1
  57. package/src/ui/screen.js +1 -1
  58. package/src/ui/status.js +190 -34
  59. package/src/ui/working.js +0 -3
  60. package/src/ui/wrap.js +1 -8
@@ -98,12 +98,43 @@ export class Store {
98
98
  this.#write({ t: 'msg', m: msg });
99
99
  }
100
100
 
101
+ /**
102
+ * 못 박은 것을 적어 둔다. 통째로 한 줄 — 마지막 것이 이긴다.
103
+ *
104
+ * 메시지와 따로 두는 이유는 pins.js 에 적은 것과 같다. 접기·요약은 메시지를
105
+ * 손보는데, 못 박은 것은 그 손질에 닿으면 안 된다.
106
+ */
107
+ 못박기목록(목록) {
108
+ this.#open();
109
+ this.#write({ t: 'pins', at: new Date().toISOString(), 목록: 목록 ?? [] });
110
+ }
111
+
112
+ /** 마지막으로 적힌 못 박은 것. 없으면 빈 배열. */
113
+ 못박은것읽기() {
114
+ if (!existsSync(this.file)) return [];
115
+ let 마지막 = [];
116
+ try {
117
+ for (const line of readFileSync(this.file, 'utf8').split('\n')) {
118
+ if (!line.includes('"t":"pins"')) continue;
119
+ try {
120
+ const j = JSON.parse(line);
121
+ if (j.t === 'pins' && Array.isArray(j.목록)) 마지막 = j.목록;
122
+ } catch { /* 깨진 줄은 건너뛴다 */ }
123
+ }
124
+ } catch { return []; }
125
+ return 마지막;
126
+ }
127
+
101
128
  // 압축이 일어나면 이력이 통째로 바뀐다. 그때는 새로 적는다.
102
129
  replace(messages, note = '압축') {
103
130
  this.#open();
104
131
  const meta = this.readMeta() ?? {};
132
+ // 여기가 놓치기 쉬운 자리다. 파일을 새로 쓰면서 못 박은 것을 안 옮기면,
133
+ // '요약해도 안 지워진다' 는 말이 바로 그 요약에서 거짓이 된다.
134
+ const 못박은것 = this.못박은것읽기();
105
135
  const lines = [JSON.stringify({ t: 'meta', at: new Date().toISOString(), ...meta })];
106
136
  lines.push(JSON.stringify({ t: 'note', at: new Date().toISOString(), note }));
137
+ if (못박은것.length) lines.push(JSON.stringify({ t: 'pins', at: new Date().toISOString(), 목록: 못박은것 }));
107
138
  for (const m of messages) lines.push(JSON.stringify({ t: 'msg', m }));
108
139
  try { writeFileSync(this.file, lines.join('\n') + '\n', 'utf8'); } catch {}
109
140
  }
@@ -0,0 +1,154 @@
1
+ // 한 창에서 대화를 여러 갈래로 굴린다.
2
+ //
3
+ // ── 왜 만드나 ───────────────────────────────────────────────────────────
4
+ //
5
+ // 일하다 보면 곁가지가 생긴다. "이 함수 왜 이래?" "이 오류 뭐야?" 같은 것들이다.
6
+ // 지금은 그걸 하던 대화에 그냥 던진다. 그러면 두 가지가 같이 나빠진다.
7
+ //
8
+ // 1) 곁가지가 읽어 온 파일·오류·시행착오가 **본 줄기의 자리를 먹는다.**
9
+ // 한 번 물어본 것 때문에 하던 일이 접히는 자리까지 밀려간다.
10
+ // 2) 모델이 방금 본 곁가지에 끌려간다. 하던 일로 돌아오라고 다시 말해야 한다.
11
+ //
12
+ // 창을 하나 더 띄우면 되지 않느냐 — 안 된다. 새 창은 연결도, 스킬도, 폴더 지문도,
13
+ // 되돌리기 이력도 처음부터 다시 잡는다. 켜는 데만 몇 초씩 들고, `/undo` 는 창마다
14
+ // 따로 놀아서 어느 창이 뭘 되돌리는지 알 수 없게 된다.
15
+ //
16
+ // 그래서 **대화만** 여러 벌 갖는다.
17
+ //
18
+ // 같이 쓰는 것 연결·모델·도구·스킬·작업 범위·되돌리기·감사 기록
19
+ // 갈래마다 따로 오간 말·토큰 셈·할 일 목록·저장 파일
20
+ //
21
+ // Task 도구와는 다른 것이다. Task 는 모델이 스스로 떼어 내는 것이고 결과만 돌아온다.
22
+ // 갈래는 **사람이** 오가는 것이고, 오간 말이 그대로 남아서 나중에 되돌아올 수 있다.
23
+ //
24
+ // ── 조심한 것 ───────────────────────────────────────────────────────────
25
+ //
26
+ // 갈래마다 저장 파일을 따로 준다. 한 파일에 섞어 적으면 이어하기가 두 갈래를
27
+ // 한 줄기로 읽어서, 서로 상관없는 대화가 한 덩어리가 된다.
28
+
29
+ /** 이름이 없을 때 붙여 줄 이름. 사람이 목록에서 알아볼 수 있으면 된다. */
30
+ function 기본이름(n) { return `갈래 ${n}`; }
31
+
32
+ export class Threads {
33
+ /**
34
+ * @param {object} session 지금 도는 대화. 첫 갈래가 된다.
35
+ * @param {object} ctx 할 일 목록이 여기 붙어 있다
36
+ * @param {function} 새store () => Store — 갈래마다 저장 파일을 새로 연다
37
+ * @param {object} 첫store 지금 대화가 쓰고 있던 저장 파일
38
+ */
39
+ constructor(session, ctx, 새store, 첫store) {
40
+ this.session = session;
41
+ this.ctx = ctx;
42
+ this.새store = 새store;
43
+ this.갈래들 = [{ 이름: '본줄기', messages: session.messages, usage: session.usage, todos: ctx?.todos ?? null, store: 첫store }];
44
+ this.자리 = 0;
45
+ this.센것 = 1;
46
+ }
47
+
48
+ 현재() { return this.갈래들[this.자리]; }
49
+ 현재store() { return this.현재().store; }
50
+ 개수() { return this.갈래들.length; }
51
+
52
+ /** 지금 화면에서 오가던 것을 갈래에 넣어 둔다. 옮기기 전에 반드시 부른다. */
53
+ #담아두기() {
54
+ const g = this.현재();
55
+ g.messages = this.session.messages;
56
+ g.usage = this.session.usage;
57
+ g.todos = this.ctx?.todos ?? null;
58
+ }
59
+
60
+ /** 갈래 하나를 화면으로 꺼낸다. */
61
+ #꺼내기(i) {
62
+ this.자리 = i;
63
+ const g = this.현재();
64
+ this.session.messages = g.messages;
65
+ this.session.usage = g.usage;
66
+ if (this.ctx) this.ctx.todos = g.todos;
67
+ // 상태줄이 지금 어느 갈래인지 보여 줄 수 있게 남긴다.
68
+ // 갈래가 하나뿐이면 안 보인다 — 안 쓰는 사람 화면은 그대로여야 한다.
69
+ this.session.갈래표 = this.갈래들.length > 1 ? g.이름 : null;
70
+ return g;
71
+ }
72
+
73
+ /**
74
+ * 새 갈래. 빈 대화로 시작한다.
75
+ * @param {string} 이름
76
+ * @param {object[]} 물려줄것 갈라내기면 지금 대화의 사본
77
+ */
78
+ 새로(이름 = '', 물려줄것 = null) {
79
+ this.#담아두기();
80
+ this.센것++;
81
+ const store = this.새store();
82
+ const g = {
83
+ 이름: String(이름 ?? '').trim() || 기본이름(this.센것),
84
+ messages: 물려줄것 ? [...물려줄것] : [],
85
+ usage: { in: 0, out: 0, calls: 0, ms: 0 },
86
+ todos: null,
87
+ store,
88
+ };
89
+ this.갈래들.push(g);
90
+ // 갈라낸 것은 지금까지 오간 말을 새 파일에도 적어 둔다. 안 적으면 그 갈래를
91
+ // 나중에 이어할 때 앞부분이 없어서 무슨 얘기였는지 알 수 없다.
92
+ if (물려줄것?.length) { for (const m of 물려줄것) store.append(m); }
93
+ return this.#꺼내기(this.갈래들.length - 1);
94
+ }
95
+
96
+ /** 지금 대화를 그대로 복사해 새 갈래로 나간다. */
97
+ 갈라내기(이름 = '') {
98
+ return this.새로(이름, this.session.messages);
99
+ }
100
+
101
+ /**
102
+ * 갈래를 찾는다. 번호(1부터)도 되고 이름 일부도 된다.
103
+ * @returns {number} 못 찾으면 -1
104
+ */
105
+ 찾기(말) {
106
+ const s = String(말 ?? '').trim();
107
+ if (!s) return -1;
108
+ if (/^\d+$/.test(s)) {
109
+ const i = Number(s) - 1;
110
+ return i >= 0 && i < this.갈래들.length ? i : -1;
111
+ }
112
+ const 낮 = s.toLowerCase();
113
+ const 딱 = this.갈래들.findIndex((g) => g.이름.toLowerCase() === 낮);
114
+ if (딱 >= 0) return 딱;
115
+ return this.갈래들.findIndex((g) => g.이름.toLowerCase().includes(낮));
116
+ }
117
+
118
+ 옮기기(말) {
119
+ const i = this.찾기(말);
120
+ if (i < 0) return null;
121
+ if (i === this.자리) return this.현재();
122
+ this.#담아두기();
123
+ return this.#꺼내기(i);
124
+ }
125
+
126
+ /**
127
+ * 갈래를 닫는다. 마지막 하나는 못 닫는다 — 닫으면 대화가 없어진다.
128
+ * 저장 파일은 안 지운다. `/sessions` 로 다시 찾아갈 수 있어야 한다.
129
+ */
130
+ 닫기(말 = '') {
131
+ if (this.갈래들.length <= 1) return { ok: false, why: '갈래가 하나뿐입니다.' };
132
+ const i = 말 ? this.찾기(말) : this.자리;
133
+ if (i < 0) return { ok: false, why: '그런 갈래가 없습니다.' };
134
+ if (i === this.자리) this.#담아두기();
135
+ const [닫은것] = this.갈래들.splice(i, 1);
136
+ // 닫힌 자리보다 뒤에 있었으면 번호가 하나 당겨진다.
137
+ const 다음 = i === this.자리 ? Math.max(0, i - 1) : (this.자리 > i ? this.자리 - 1 : this.자리);
138
+ this.자리 = 다음;
139
+ this.#꺼내기(다음);
140
+ return { ok: true, 닫은것, 지금: this.현재() };
141
+ }
142
+
143
+ /** 화면에 뿌릴 목록. */
144
+ 목록() {
145
+ return this.갈래들.map((g, i) => ({
146
+ 번호: i + 1,
147
+ 이름: g.이름,
148
+ 지금: i === this.자리,
149
+ 말수: (i === this.자리 ? this.session.messages : g.messages).length,
150
+ 토큰: (i === this.자리 ? this.session.usage : g.usage)?.in ?? 0,
151
+ id: g.store?.id ?? null,
152
+ }));
153
+ }
154
+ }