@adhdev/daemon-core 0.5.8 → 0.5.17
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/dist/index.d.ts +460 -330
- package/dist/index.js +543 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/cli/claude-cli/provider.json +34 -24
- package/providers/_builtin/cli/gemini-cli/provider.json +24 -17
- package/providers/_builtin/ide/antigravity/provider.json +21 -0
- package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +19 -24
- package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +67 -5
- package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +19 -24
- package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +271 -32
- package/providers/_builtin/registry.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +96 -25
- package/src/commands/router.ts +132 -33
- package/src/daemon/dev-server.ts +285 -0
- package/src/daemon-core.ts +7 -6
- package/src/detection/ide-detector.ts +5 -5
- package/src/index.ts +24 -7
- package/src/launch.ts +2 -2
- package/src/providers/acp-provider-instance.ts +54 -56
- package/src/providers/cli-provider-instance.ts +32 -4
- package/src/providers/contracts.ts +5 -22
- package/src/providers/ide-provider-instance.ts +8 -10
- package/src/providers/provider-instance.ts +70 -33
- package/src/shared-types.ts +203 -0
- package/src/status/reporter.ts +31 -22
- package/src/types.ts +26 -110
package/package.json
CHANGED
|
@@ -61,40 +61,50 @@
|
|
|
61
61
|
"spawn": {
|
|
62
62
|
"command": "claude",
|
|
63
63
|
"args": [],
|
|
64
|
-
"shell":
|
|
64
|
+
"shell": true,
|
|
65
65
|
"env": {}
|
|
66
66
|
},
|
|
67
67
|
"patterns": {
|
|
68
68
|
"prompt": [
|
|
69
|
-
{},
|
|
70
|
-
{},
|
|
71
|
-
{},
|
|
72
|
-
{},
|
|
73
|
-
{},
|
|
74
|
-
{},
|
|
75
|
-
{}
|
|
69
|
+
{ "source": "Type your message", "flags": "i" },
|
|
70
|
+
{ "source": "^>\\s*$", "flags": "m" },
|
|
71
|
+
{ "source": "[›❯]\\s*[\\r\\n]", "flags": "" },
|
|
72
|
+
{ "source": "[›❯]\\s*$", "flags": "m" },
|
|
73
|
+
{ "source": "for\\s*shortcuts", "flags": "i" },
|
|
74
|
+
{ "source": "\\?\\s*for\\s*help", "flags": "i" },
|
|
75
|
+
{ "source": "Press enter", "flags": "i" }
|
|
76
76
|
],
|
|
77
77
|
"generating": [
|
|
78
|
-
{},
|
|
79
|
-
{},
|
|
80
|
-
{},
|
|
81
|
-
{},
|
|
82
|
-
{}
|
|
78
|
+
{ "source": "[\\u2800-\\u28ff]", "flags": "" },
|
|
79
|
+
{ "source": "esc to (cancel|interrupt|stop)", "flags": "i" },
|
|
80
|
+
{ "source": "generating\\.\\.\\.", "flags": "i" },
|
|
81
|
+
{ "source": "Claude is (?:thinking|processing|working)", "flags": "i" },
|
|
82
|
+
{ "source": "Flummoxing", "flags": "i" }
|
|
83
83
|
],
|
|
84
84
|
"approval": [
|
|
85
|
-
{},
|
|
86
|
-
{},
|
|
87
|
-
{},
|
|
88
|
-
{},
|
|
89
|
-
{},
|
|
90
|
-
{},
|
|
91
|
-
{},
|
|
92
|
-
{},
|
|
93
|
-
{}
|
|
85
|
+
{ "source": "Allow\\s*once", "flags": "i" },
|
|
86
|
+
{ "source": "Always\\s*allow", "flags": "i" },
|
|
87
|
+
{ "source": "\\(y/n\\)", "flags": "i" },
|
|
88
|
+
{ "source": "\\[Y/n\\]", "flags": "i" },
|
|
89
|
+
{ "source": "Run\\s*this\\s*command", "flags": "i" },
|
|
90
|
+
{ "source": "Allow\\s*tool", "flags": "i" },
|
|
91
|
+
{ "source": "Yes,?\\s*don'?t\\s*ask", "flags": "i" },
|
|
92
|
+
{ "source": "Deny", "flags": "i" },
|
|
93
|
+
{ "source": "Do you want to proceed", "flags": "i" }
|
|
94
94
|
],
|
|
95
95
|
"ready": [
|
|
96
|
-
{},
|
|
97
|
-
{}
|
|
96
|
+
{ "source": "for\\s*shortcuts", "flags": "i" },
|
|
97
|
+
{ "source": "[›❯]\\s*$", "flags": "m" }
|
|
98
|
+
],
|
|
99
|
+
"dialog": [
|
|
100
|
+
{ "source": "Quick safety check", "flags": "i" },
|
|
101
|
+
{ "source": "Is this a project", "flags": "i" },
|
|
102
|
+
{ "source": "Enter to confirm", "flags": "i" }
|
|
98
103
|
]
|
|
104
|
+
},
|
|
105
|
+
"approvalKeys": {
|
|
106
|
+
"0": "1",
|
|
107
|
+
"1": "2",
|
|
108
|
+
"2": "3"
|
|
99
109
|
}
|
|
100
110
|
}
|
|
@@ -65,29 +65,36 @@
|
|
|
65
65
|
},
|
|
66
66
|
"patterns": {
|
|
67
67
|
"prompt": [
|
|
68
|
-
{},
|
|
69
|
-
{},
|
|
70
|
-
{}
|
|
68
|
+
{ "source": "\\?\\s*for\\s*shortcuts", "flags": "i" },
|
|
69
|
+
{ "source": "^>\\s*$", "flags": "m" },
|
|
70
|
+
{ "source": "[›❯]\\s*$", "flags": "m" },
|
|
71
|
+
{ "source": "sandbox", "flags": "i" },
|
|
72
|
+
{ "source": "Type your message", "flags": "i" }
|
|
71
73
|
],
|
|
72
74
|
"generating": [
|
|
73
|
-
{},
|
|
74
|
-
{},
|
|
75
|
-
{},
|
|
76
|
-
{},
|
|
77
|
-
{}
|
|
78
|
-
{}
|
|
75
|
+
{ "source": "[\\u2800-\\u28ff]", "flags": "" },
|
|
76
|
+
{ "source": "[▀▄▌▐░▒▓█]", "flags": "" },
|
|
77
|
+
{ "source": "Thinking", "flags": "i" },
|
|
78
|
+
{ "source": "Generating", "flags": "i" },
|
|
79
|
+
{ "source": "esc to (cancel|interrupt|stop)", "flags": "i" }
|
|
79
80
|
],
|
|
80
81
|
"approval": [
|
|
81
|
-
{},
|
|
82
|
-
{},
|
|
83
|
-
{},
|
|
84
|
-
{},
|
|
85
|
-
{},
|
|
86
|
-
{}
|
|
82
|
+
{ "source": "Allow\\s*once", "flags": "i" },
|
|
83
|
+
{ "source": "Always\\s*allow", "flags": "i" },
|
|
84
|
+
{ "source": "\\(y/n\\)", "flags": "i" },
|
|
85
|
+
{ "source": "\\[Y/n\\]", "flags": "i" },
|
|
86
|
+
{ "source": "Run\\s*this\\s*command", "flags": "i" },
|
|
87
|
+
{ "source": "Deny", "flags": "i" },
|
|
88
|
+
{ "source": "auto-?approve", "flags": "i" }
|
|
87
89
|
],
|
|
88
90
|
"ready": [
|
|
89
|
-
{},
|
|
90
|
-
{}
|
|
91
|
+
{ "source": "\\?\\s*for\\s*shortcuts", "flags": "i" },
|
|
92
|
+
{ "source": "sandbox", "flags": "i" }
|
|
91
93
|
]
|
|
94
|
+
},
|
|
95
|
+
"approvalKeys": {
|
|
96
|
+
"0": "y",
|
|
97
|
+
"1": "a",
|
|
98
|
+
"2": "n"
|
|
92
99
|
}
|
|
93
100
|
}
|
|
@@ -63,6 +63,27 @@
|
|
|
63
63
|
"label": "Long Generation Threshold (sec)",
|
|
64
64
|
"min": 30,
|
|
65
65
|
"max": 600
|
|
66
|
+
},
|
|
67
|
+
"showThinking": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"default": true,
|
|
70
|
+
"public": true,
|
|
71
|
+
"label": "Show Thinking",
|
|
72
|
+
"description": "Display AI thinking/reasoning process in chat"
|
|
73
|
+
},
|
|
74
|
+
"showToolCalls": {
|
|
75
|
+
"type": "boolean",
|
|
76
|
+
"default": true,
|
|
77
|
+
"public": true,
|
|
78
|
+
"label": "Show Tool Calls",
|
|
79
|
+
"description": "Display tool call summaries (Searched, Analyzed, Edited, etc.)"
|
|
80
|
+
},
|
|
81
|
+
"showTerminal": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"default": true,
|
|
84
|
+
"public": true,
|
|
85
|
+
"label": "Show Terminal",
|
|
86
|
+
"description": "Display terminal command execution and output"
|
|
66
87
|
}
|
|
67
88
|
}
|
|
68
89
|
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Antigravity v1 — resolve_action
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 스크롤 최하단 → scrollIntoView → .click()
|
|
5
5
|
* 파라미터: ${BUTTON_TEXT}
|
|
6
|
-
*
|
|
7
|
-
* 핵심: viewport 안에 보이는 버튼 중 마지막(최신) 매칭 우선
|
|
8
6
|
*/
|
|
9
7
|
(() => {
|
|
10
8
|
const want = ${ BUTTON_TEXT };
|
|
11
9
|
const wantNorm = (want || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
12
|
-
|
|
10
|
+
|
|
13
11
|
function norm(t) { return (t || '').replace(/\s+/g, ' ').trim().toLowerCase(); }
|
|
14
|
-
|
|
12
|
+
|
|
15
13
|
function matches(el) {
|
|
16
14
|
const raw = (el.textContent || '').trim();
|
|
17
15
|
const t = norm(raw);
|
|
@@ -35,15 +33,16 @@
|
|
|
35
33
|
}
|
|
36
34
|
return false;
|
|
37
35
|
}
|
|
38
|
-
|
|
36
|
+
|
|
37
|
+
// 1. 채팅 패널 스크롤을 최하단으로
|
|
38
|
+
const conv = document.querySelector('.antigravity-agent-side-panel') || document.querySelector('#conversation');
|
|
39
|
+
const scrollEl = conv ? (conv.querySelector('.overflow-y-auto') || conv) : null;
|
|
40
|
+
if (scrollEl) scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
41
|
+
|
|
42
|
+
// 2. viewport 필터 없이 DOM에 있는 모든 visible 버튼 검색
|
|
39
43
|
const sel = 'button, [role="button"]';
|
|
40
|
-
const allBtns = [...document.querySelectorAll(sel)].filter(b =>
|
|
41
|
-
|
|
42
|
-
const rect = b.getBoundingClientRect();
|
|
43
|
-
// viewport 안에 보이는 것만 (y > 0, y < window.innerHeight)
|
|
44
|
-
return rect.y > 0 && rect.y < window.innerHeight;
|
|
45
|
-
});
|
|
46
|
-
|
|
44
|
+
const allBtns = [...document.querySelectorAll(sel)].filter(b => b.offsetWidth > 0 && b.offsetHeight > 0);
|
|
45
|
+
|
|
47
46
|
// 마지막(최신) 매칭 우선 — 역순 검색
|
|
48
47
|
let found = null;
|
|
49
48
|
for (let i = allBtns.length - 1; i >= 0; i--) {
|
|
@@ -52,17 +51,13 @@
|
|
|
52
51
|
break;
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
|
|
56
55
|
if (found) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
y: Math.round(rect.y + rect.height / 2),
|
|
63
|
-
w: Math.round(rect.width),
|
|
64
|
-
h: Math.round(rect.height)
|
|
65
|
-
});
|
|
56
|
+
const text = found.textContent?.trim()?.substring(0, 40);
|
|
57
|
+
// 버튼이 화면에 보이도록 스크롤 후 클릭
|
|
58
|
+
try { found.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
|
59
|
+
try { found.click(); } catch (_) {}
|
|
60
|
+
return JSON.stringify({ resolved: true, clicked: text });
|
|
66
61
|
}
|
|
67
62
|
return JSON.stringify({ found: false, want: wantNorm });
|
|
68
63
|
})()
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
const hash = 'user:' + text.slice(0, 200);
|
|
156
156
|
if (seenHashes.has(hash)) continue;
|
|
157
157
|
seenHashes.add(hash);
|
|
158
|
-
collected.push({ role: 'user', text, el });
|
|
158
|
+
collected.push({ role: 'user', text, el, kind: 'standard' });
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -164,6 +164,8 @@
|
|
|
164
164
|
for (const ab of assistantBlocks) {
|
|
165
165
|
if (ab.offsetHeight < 10) continue;
|
|
166
166
|
if (ab.closest('[class*="max-h-"][class*="overflow-y-auto"]')) continue;
|
|
167
|
+
// Thought 내부의 블록은 제외 (thinking으로 별도 수집)
|
|
168
|
+
if (ab.closest('.isolate')?.querySelector('button')?.textContent?.startsWith('Thought for')) continue;
|
|
167
169
|
|
|
168
170
|
let text = getCleanMd(ab);
|
|
169
171
|
if (!text || text.length < 2) continue;
|
|
@@ -172,7 +174,66 @@
|
|
|
172
174
|
const hash = 'assistant:' + text.slice(0, 200);
|
|
173
175
|
if (seenHashes.has(hash)) continue;
|
|
174
176
|
seenHashes.add(hash);
|
|
175
|
-
collected.push({ role: 'assistant', text, el: ab });
|
|
177
|
+
collected.push({ role: 'assistant', text, el: ab, kind: 'standard' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Thought 수집 (AI 사고 과정) ───
|
|
181
|
+
const thoughtBtns = scroll.querySelectorAll('button');
|
|
182
|
+
for (const btn of thoughtBtns) {
|
|
183
|
+
const label = (btn.textContent || '').trim();
|
|
184
|
+
if (!label.startsWith('Thought for')) continue;
|
|
185
|
+
const sibling = btn.nextElementSibling;
|
|
186
|
+
if (!sibling) continue;
|
|
187
|
+
// 실제 thinking 텍스트는 sibling 내부의 .leading-relaxed.select-text에 있음
|
|
188
|
+
const contentEl = sibling.querySelector('.leading-relaxed.select-text');
|
|
189
|
+
if (!contentEl) continue;
|
|
190
|
+
const clone = contentEl.cloneNode(true);
|
|
191
|
+
clone.querySelectorAll('button, svg, style, script, [role="button"]').forEach(n => n.remove());
|
|
192
|
+
const thinkText = (clone.innerText || clone.textContent || '').trim();
|
|
193
|
+
if (!thinkText || thinkText.length < 5) continue;
|
|
194
|
+
const hash = 'think:' + thinkText.slice(0, 200);
|
|
195
|
+
if (seenHashes.has(hash)) continue;
|
|
196
|
+
seenHashes.add(hash);
|
|
197
|
+
// 순수 텍스트만 전달 (마크다운 포맷은 프론트엔드에서 처리)
|
|
198
|
+
collected.push({ role: 'assistant', text: thinkText.slice(0, 3000), el: btn, kind: 'thought', meta: { label } });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Terminal/Tool Call 수집 (명령 실행 결과) ───
|
|
202
|
+
const termHeaders = scroll.querySelectorAll('div.mb-1.px-2.py-1');
|
|
203
|
+
for (const h of termHeaders) {
|
|
204
|
+
const spanEl = h.querySelector('span');
|
|
205
|
+
const label = (spanEl?.textContent || '').trim();
|
|
206
|
+
if (!label.match(/^(Ran|Running) command/i)) continue;
|
|
207
|
+
const parent = h.parentElement;
|
|
208
|
+
if (!parent) continue;
|
|
209
|
+
const pre = parent.querySelector('pre');
|
|
210
|
+
const cmdText = (pre?.textContent || '').trim();
|
|
211
|
+
if (!cmdText || cmdText.length < 2) continue;
|
|
212
|
+
const hash = 'term:' + cmdText.slice(0, 200);
|
|
213
|
+
if (seenHashes.has(hash)) continue;
|
|
214
|
+
seenHashes.add(hash);
|
|
215
|
+
// 순수 텍스트만 전달 (라벨/아이콘은 프론트엔드에서 처리)
|
|
216
|
+
const isRunning = label.startsWith('Running');
|
|
217
|
+
collected.push({ role: 'assistant', text: cmdText.slice(0, 3000), el: h, kind: 'terminal', meta: { label, isRunning } });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Tool Call 요약 수집 (Searched, Analyzed, Edited 등) ───
|
|
221
|
+
const toolDivs = scroll.querySelectorAll('div[data-tooltip-id]');
|
|
222
|
+
for (const td of toolDivs) {
|
|
223
|
+
const cls = (td.className || '').toString();
|
|
224
|
+
if (!cls.includes('cursor-pointer') || !cls.includes('text-sm')) continue;
|
|
225
|
+
// span 단위로 읽어서 공백으로 연결 (textContent는 "SearchedDevServer"처럼 붙음)
|
|
226
|
+
const spans = td.querySelectorAll('span');
|
|
227
|
+
const text = spans.length > 0
|
|
228
|
+
? Array.from(spans).map(s => (s.textContent || '').trim()).filter(Boolean).join(' ')
|
|
229
|
+
: (td.textContent || '').trim();
|
|
230
|
+
if (!text.match(/^(Searched|Analyzed|Edited|Read|Viewed|Created|Listed|Checked)/)) continue;
|
|
231
|
+
if (text.length > 120) continue;
|
|
232
|
+
const hash = 'tool:' + text;
|
|
233
|
+
if (seenHashes.has(hash)) continue;
|
|
234
|
+
seenHashes.add(hash);
|
|
235
|
+
// 간결하게 한 줄 요약
|
|
236
|
+
collected.push({ role: 'assistant', text: text, el: td, kind: 'tool' });
|
|
176
237
|
}
|
|
177
238
|
|
|
178
239
|
// 3. DOM 순서 정렬
|
|
@@ -183,15 +244,16 @@
|
|
|
183
244
|
return 0;
|
|
184
245
|
});
|
|
185
246
|
|
|
186
|
-
// 최신
|
|
187
|
-
const trimmed = collected.length >
|
|
247
|
+
// 최신 50개만 유지 (thinking/tool_use 포함으로 늘림)
|
|
248
|
+
const trimmed = collected.length > 50 ? collected.slice(-50) : collected;
|
|
188
249
|
|
|
189
250
|
const final = trimmed.map((m, i) => ({
|
|
190
251
|
id: 'msg_' + i,
|
|
191
252
|
role: m.role,
|
|
192
253
|
content: m.text.length > 6000 ? m.text.slice(0, 6000) + '\n[... truncated]' : m.text,
|
|
193
254
|
index: i,
|
|
194
|
-
kind: 'standard',
|
|
255
|
+
kind: m.kind || 'standard',
|
|
256
|
+
meta: m.meta || undefined,
|
|
195
257
|
vsc_history: true
|
|
196
258
|
}));
|
|
197
259
|
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Antigravity v1 — resolve_action
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 스크롤 최하단 → scrollIntoView → .click()
|
|
5
5
|
* 파라미터: ${BUTTON_TEXT}
|
|
6
|
-
*
|
|
7
|
-
* 핵심: viewport 안에 보이는 버튼 중 마지막(최신) 매칭 우선
|
|
8
6
|
*/
|
|
9
7
|
(() => {
|
|
10
8
|
const want = ${ BUTTON_TEXT };
|
|
11
9
|
const wantNorm = (want || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
12
|
-
|
|
10
|
+
|
|
13
11
|
function norm(t) { return (t || '').replace(/\s+/g, ' ').trim().toLowerCase(); }
|
|
14
|
-
|
|
12
|
+
|
|
15
13
|
function matches(el) {
|
|
16
14
|
const raw = (el.textContent || '').trim();
|
|
17
15
|
const t = norm(raw);
|
|
@@ -35,15 +33,16 @@
|
|
|
35
33
|
}
|
|
36
34
|
return false;
|
|
37
35
|
}
|
|
38
|
-
|
|
36
|
+
|
|
37
|
+
// 1. 채팅 패널 스크롤을 최하단으로
|
|
38
|
+
const conv = document.querySelector('.antigravity-agent-side-panel') || document.querySelector('#conversation');
|
|
39
|
+
const scrollEl = conv ? (conv.querySelector('.overflow-y-auto') || conv) : null;
|
|
40
|
+
if (scrollEl) scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
41
|
+
|
|
42
|
+
// 2. viewport 필터 없이 DOM에 있는 모든 visible 버튼 검색
|
|
39
43
|
const sel = 'button, [role="button"]';
|
|
40
|
-
const allBtns = [...document.querySelectorAll(sel)].filter(b =>
|
|
41
|
-
|
|
42
|
-
const rect = b.getBoundingClientRect();
|
|
43
|
-
// viewport 안에 보이는 것만 (y > 0, y < window.innerHeight)
|
|
44
|
-
return rect.y > 0 && rect.y < window.innerHeight;
|
|
45
|
-
});
|
|
46
|
-
|
|
44
|
+
const allBtns = [...document.querySelectorAll(sel)].filter(b => b.offsetWidth > 0 && b.offsetHeight > 0);
|
|
45
|
+
|
|
47
46
|
// 마지막(최신) 매칭 우선 — 역순 검색
|
|
48
47
|
let found = null;
|
|
49
48
|
for (let i = allBtns.length - 1; i >= 0; i--) {
|
|
@@ -52,17 +51,13 @@
|
|
|
52
51
|
break;
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
|
-
|
|
54
|
+
|
|
56
55
|
if (found) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
y: Math.round(rect.y + rect.height / 2),
|
|
63
|
-
w: Math.round(rect.width),
|
|
64
|
-
h: Math.round(rect.height)
|
|
65
|
-
});
|
|
56
|
+
const text = found.textContent?.trim()?.substring(0, 40);
|
|
57
|
+
// 버튼이 화면에 보이도록 스크롤 후 클릭
|
|
58
|
+
try { found.scrollIntoView({ block: 'nearest' }); } catch (_) {}
|
|
59
|
+
try { found.click(); } catch (_) {}
|
|
60
|
+
return JSON.stringify({ resolved: true, clicked: text });
|
|
66
61
|
}
|
|
67
62
|
return JSON.stringify({ found: false, want: wantNorm });
|
|
68
63
|
})()
|