s-app-scoder-agent 0.8.20 → 0.9.2
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.md +50 -6
- package/dist/agent.js +754 -79
- package/dist/agents-guide.js +201 -0
- package/dist/agents.js +76 -3
- package/dist/compaction.js +12 -4
- package/dist/config.js +16 -2
- package/dist/context.js +37 -11
- package/dist/diagnostics.js +188 -0
- package/dist/index.js +277 -6
- package/dist/mcp-import.js +204 -0
- package/dist/mcp.js +553 -54
- package/dist/memory.js +140 -0
- package/dist/permissions.js +18 -3
- package/dist/prompt.js +204 -84
- package/dist/repo-map.js +177 -0
- package/dist/sandbox.js +6 -5
- package/dist/session.js +29 -8
- package/dist/skills.js +211 -62
- package/dist/snapshot.js +5 -0
- package/dist/steering.js +4 -0
- package/dist/tools.js +374 -57
- package/dist/tui/app.js +79 -28
- package/dist/tui/ask-user-modal.js +25 -0
- package/dist/tui/chat-views.js +15 -8
- package/dist/tui/command-registry.js +297 -9
- package/dist/tui/commands.js +224 -36
- package/dist/tui/markdown.js +170 -12
- package/dist/tui/sidebar.js +36 -13
- package/dist/tui/state.js +101 -2
- package/dist/tui/theme.js +4 -0
- package/dist/tui/transcript.js +145 -0
- package/dist/watch.js +8 -0
- package/dist/webfetch.js +5 -2
- package/package.json +1 -1
package/dist/tui/state.js
CHANGED
|
@@ -20,6 +20,14 @@ export function initialState() {
|
|
|
20
20
|
phases: [],
|
|
21
21
|
mcpServers: {},
|
|
22
22
|
streamingOutputChars: 0,
|
|
23
|
+
streamStartedAt: null,
|
|
24
|
+
llmCallStartedAt: null,
|
|
25
|
+
lastLlmTokPerSec: null,
|
|
26
|
+
agent: "build",
|
|
27
|
+
context: null,
|
|
28
|
+
queue: { pending: 0, steering: 0 },
|
|
29
|
+
verification: null,
|
|
30
|
+
subagents: [],
|
|
23
31
|
};
|
|
24
32
|
}
|
|
25
33
|
const MAX_ITEMS = 400;
|
|
@@ -41,6 +49,8 @@ export function estimateItemRows(item) {
|
|
|
41
49
|
return 1 + Math.ceil((item.text?.length ?? 0) / 80) + (item.text ? item.text.split("\n").length : 0);
|
|
42
50
|
case "tool":
|
|
43
51
|
return 1 + 2; // title + preview 2 lines
|
|
52
|
+
case "compaction":
|
|
53
|
+
return 1;
|
|
44
54
|
case "diff":
|
|
45
55
|
return 3 + Math.min(24, (item.text?.split("\n").length ?? 0)); // header + ≤24 lines + border
|
|
46
56
|
case "todo":
|
|
@@ -111,12 +121,18 @@ function capTail(text, max = MAX_OUTPUT_CHARS) {
|
|
|
111
121
|
export function toolPhase(name) {
|
|
112
122
|
if (name === "write_todos")
|
|
113
123
|
return "plan";
|
|
114
|
-
if (name === "list_files" || name === "read_file" || name === "web_fetch" || name === "load_skill")
|
|
124
|
+
if (name === "list_files" || name === "read_file" || name === "grep" || name === "glob" || name === "web_fetch" || name === "load_skill" || name === "repo_map")
|
|
115
125
|
return "explore";
|
|
116
|
-
if (name === "write_file" || name === "edit_file")
|
|
126
|
+
if (name === "write_file" || name === "edit_file" || name === "apply_patch")
|
|
117
127
|
return "edit";
|
|
118
128
|
if (name === "run_command")
|
|
119
129
|
return "build";
|
|
130
|
+
if (name === "task")
|
|
131
|
+
return "delegate";
|
|
132
|
+
if (name === "ask_user")
|
|
133
|
+
return "clear";
|
|
134
|
+
if (name.startsWith("mcp__"))
|
|
135
|
+
return "mcp";
|
|
120
136
|
return null;
|
|
121
137
|
}
|
|
122
138
|
/** Guardrail event: đếm + giữ decision gần nhất (ngắn, tự giải thích cho header). */
|
|
@@ -156,6 +172,11 @@ export function applyEvent(state, ev) {
|
|
|
156
172
|
state.buildCmd = ev.buildCmd;
|
|
157
173
|
state.buildLamp = "none";
|
|
158
174
|
state.permission = ev.permission;
|
|
175
|
+
state.agent = ev.agent ?? "build";
|
|
176
|
+
state.context = null;
|
|
177
|
+
state.queue = { pending: 0, steering: 0 };
|
|
178
|
+
state.verification = null;
|
|
179
|
+
state.subagents = [];
|
|
159
180
|
state.startedAt = Date.now();
|
|
160
181
|
state.endedAt = null;
|
|
161
182
|
state.status = "running";
|
|
@@ -167,6 +188,9 @@ export function applyEvent(state, ev) {
|
|
|
167
188
|
state.lastGuardrail = null;
|
|
168
189
|
state.phases = [];
|
|
169
190
|
state.streamingOutputChars = 0;
|
|
191
|
+
state.streamStartedAt = null;
|
|
192
|
+
state.llmCallStartedAt = null;
|
|
193
|
+
state.lastLlmTokPerSec = null;
|
|
170
194
|
// The TUI pushes the user's message itself when submitting; avoid a
|
|
171
195
|
// duplicate when run_start carries the same task.
|
|
172
196
|
const last = lastItem(state);
|
|
@@ -194,6 +218,10 @@ export function applyEvent(state, ev) {
|
|
|
194
218
|
case "step": {
|
|
195
219
|
state.step = ev.step;
|
|
196
220
|
state.maxSteps = ev.maxSteps;
|
|
221
|
+
// New LLM turn: reset per-stream tracking.
|
|
222
|
+
state.streamingOutputChars = 0;
|
|
223
|
+
state.streamStartedAt = null;
|
|
224
|
+
state.llmCallStartedAt = Date.now();
|
|
197
225
|
// A new step starts: close any still-"streaming" assistant message.
|
|
198
226
|
const last = lastItem(state);
|
|
199
227
|
if (last && last.kind === "assistant" && last.streaming)
|
|
@@ -203,6 +231,9 @@ export function applyEvent(state, ev) {
|
|
|
203
231
|
break;
|
|
204
232
|
}
|
|
205
233
|
case "llm_delta": {
|
|
234
|
+
if (state.streamStartedAt == null) {
|
|
235
|
+
state.streamStartedAt = state.llmCallStartedAt ?? Date.now();
|
|
236
|
+
}
|
|
206
237
|
state.streamingOutputChars += ev.delta.length;
|
|
207
238
|
const last = lastItem(state);
|
|
208
239
|
if (last && last.kind === "assistant" && last.streaming) {
|
|
@@ -226,6 +257,17 @@ export function applyEvent(state, ev) {
|
|
|
226
257
|
state.usage.inputTokens += ev.usage.input_tokens ?? 0;
|
|
227
258
|
state.usage.outputTokens += ev.usage.output_tokens ?? 0;
|
|
228
259
|
state.usage.cachedTokens += ev.usage.cached_tokens ?? 0;
|
|
260
|
+
// Per-call speed from the actual call duration, not the whole run.
|
|
261
|
+
const start = state.streamStartedAt ?? state.llmCallStartedAt;
|
|
262
|
+
if (start != null) {
|
|
263
|
+
const elapsedMs = Math.max(1, Date.now() - start);
|
|
264
|
+
const out = ev.usage.output_tokens ?? 0;
|
|
265
|
+
if (out > 0) {
|
|
266
|
+
state.lastLlmTokPerSec = Math.round(out / (elapsedMs / 1000));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
state.streamingOutputChars = 0;
|
|
270
|
+
state.streamStartedAt = null;
|
|
229
271
|
break;
|
|
230
272
|
}
|
|
231
273
|
case "tool_call": {
|
|
@@ -320,10 +362,67 @@ export function applyEvent(state, ev) {
|
|
|
320
362
|
appendItem(state, { kind: "notice", text: ev.line, level: ev.line.includes("unavailable") ? "warn" : "info" });
|
|
321
363
|
break;
|
|
322
364
|
}
|
|
365
|
+
case "steer_queued": {
|
|
366
|
+
// Pending depth tracked via the absolute `queue` event; nothing else.
|
|
367
|
+
state.queue = { ...state.queue, pending: state.queue.pending + 1, steering: state.queue.steering + (ev.mode === "steer" ? 1 : 0) };
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case "steer_delivered": {
|
|
371
|
+
state.queue = { pending: 0, steering: 0 };
|
|
372
|
+
appendItem(state, { kind: "notice", text: `steer: ${ev.count} message(s) delivered`, level: "info" });
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "queue": {
|
|
376
|
+
state.queue = { pending: ev.pending, steering: ev.steering };
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "compaction": {
|
|
380
|
+
appendItem(state, { kind: "compaction", tokensBefore: ev.tokensBefore, tokensAfter: ev.tokensAfter, manual: ev.manual });
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case "context": {
|
|
384
|
+
state.context = { tokens: ev.tokens, window: ev.window, pct: ev.pct };
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "agent_start": {
|
|
388
|
+
// Track in the tree list; rendering lands in the sidebar (Phase 7).
|
|
389
|
+
const rest = state.subagents.filter((s) => s.id !== ev.id);
|
|
390
|
+
rest.push({ id: ev.id, name: ev.name, description: ev.description, status: "running" });
|
|
391
|
+
state.subagents = rest.slice(-20);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case "agent_end": {
|
|
395
|
+
state.subagents = state.subagents.map((s) => s.id === ev.id ? { ...s, status: ev.ok ? "ok" : "failed", steps: ev.steps, summary: ev.summary } : s);
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case "tool_progress": {
|
|
399
|
+
// Attach latest progress message to the running tool card (if any).
|
|
400
|
+
for (let i = state.items.length - 1; i >= 0; i--) {
|
|
401
|
+
const it = state.items[i];
|
|
402
|
+
if (it.kind === "tool" && it.ok === null && (it.callId === ev.id || it.name === ev.name)) {
|
|
403
|
+
if (ev.message)
|
|
404
|
+
it.output = capTail(`${it.output}\n… ${ev.message}`.trim());
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
if (it.kind === "tool")
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
case "question_request": {
|
|
413
|
+
// Surfaced via the dedicated ask_user modal (Phase 3/7); queue a notice
|
|
414
|
+
// so the transcript isn't silent while waiting for user input.
|
|
415
|
+
appendItem(state, { kind: "notice", text: `❓ ${ev.question}`, level: "info" });
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
323
418
|
case "finish": {
|
|
324
419
|
state.status = ev.success ? "success" : /interrupted|aborted/i.test(ev.summary) ? "aborted" : "failed";
|
|
325
420
|
state.summary = ev.summary;
|
|
326
421
|
state.endedAt = Date.now();
|
|
422
|
+
if (ev.usage)
|
|
423
|
+
state.usage = { ...ev.usage };
|
|
424
|
+
if (ev.verification)
|
|
425
|
+
state.verification = { ...ev.verification };
|
|
327
426
|
// Close ALL dangling streaming assistant messages — not just the last
|
|
328
427
|
// item: a notice/tool item appended after the assistant text would
|
|
329
428
|
// otherwise leave it stuck in raw streaming mode (invisible text).
|
package/dist/tui/theme.js
CHANGED
|
@@ -37,6 +37,10 @@ export const THEMES = {
|
|
|
37
37
|
light: LIGHT, "high-contrast": HIGH_CONTRAST, solarized: SOLARIZED,
|
|
38
38
|
};
|
|
39
39
|
export function defaultThemeName(env = process.env) {
|
|
40
|
+
if (env.SCODER_THEME && THEMES[env.SCODER_THEME])
|
|
41
|
+
return env.SCODER_THEME;
|
|
42
|
+
if (env.SCODER_PLAIN === "1" || env.NO_COLOR === "1" || process.env.NO_COLOR === "1")
|
|
43
|
+
return "mono";
|
|
40
44
|
if (env.TERM === "dumb" || !env.TERM)
|
|
41
45
|
return "mono";
|
|
42
46
|
if (env.COLOR_COUNT && Number(env.COLOR_COUNT) < 16)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
2
|
+
/** Render a ChatItem to its plain-text representation for export. */
|
|
3
|
+
export function itemToText(item) {
|
|
4
|
+
switch (item.kind) {
|
|
5
|
+
case "user": return `## you\n\n${item.text}`;
|
|
6
|
+
case "assistant": return `## agent\n\n${item.text}`;
|
|
7
|
+
case "thinking": return `## thinking\n\n${item.text}`;
|
|
8
|
+
case "tool": {
|
|
9
|
+
const args = Object.entries(item.args).map(([k, v]) => ` ${k}=${JSON.stringify(v)}`).join("\n");
|
|
10
|
+
const elapsed = item.durationMs != null ? ` (${item.durationMs}ms)` : "";
|
|
11
|
+
const status = item.ok === null ? "running" : item.ok ? "ok" : "FAILED";
|
|
12
|
+
return `## tool: ${item.name}${elapsed}\n\n${status}${args ? "\n" + args : ""}\n\n${item.output}`;
|
|
13
|
+
}
|
|
14
|
+
case "diff":
|
|
15
|
+
return `## diff: ${item.path} (+${item.added}/-${item.removed})\n\n${item.text}`;
|
|
16
|
+
case "build":
|
|
17
|
+
return `## build: ${item.command}\n\n${item.success ? "✓ passed" : "✗ FAILED"}`;
|
|
18
|
+
case "todo": {
|
|
19
|
+
const rows = item.items.map((t) => `- [${t.status === "completed" ? "x" : t.status === "in_progress" ? ">" : " "}] ${t.content}`);
|
|
20
|
+
return `## plan\n\n${rows.join("\n")}`;
|
|
21
|
+
}
|
|
22
|
+
case "compaction":
|
|
23
|
+
return `## compacted (${item.tokensBefore} → ${item.tokensAfter} tokens)${item.manual ? " [manual]" : ""}`;
|
|
24
|
+
case "notice":
|
|
25
|
+
return `${item.level === "error" ? "✗" : item.level === "warn" ? "⚠" : ""} ${item.text}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Format a chat item as Markdown (GitHub-flavored). */
|
|
29
|
+
export function itemToMarkdown(item) {
|
|
30
|
+
const ts = item.kind === "tool" ? `[${item.step.toString().padStart(2, "0")}]` : "";
|
|
31
|
+
switch (item.kind) {
|
|
32
|
+
case "user":
|
|
33
|
+
return `## you\n\n> ${item.text}`;
|
|
34
|
+
case "assistant":
|
|
35
|
+
return `## agent\n\n${item.text}`;
|
|
36
|
+
case "thinking":
|
|
37
|
+
return `> ⊙ ${item.text.split("\n").slice(-4).join(" | ")}`;
|
|
38
|
+
case "tool": {
|
|
39
|
+
const elapsed = item.durationMs != null ? ` — ${(item.durationMs / 1000).toFixed(1)}s` : "";
|
|
40
|
+
const ok = item.ok === null ? "⏳ running" : item.ok ? "✅" : "❌";
|
|
41
|
+
const args = Object.keys(item.args).length > 0
|
|
42
|
+
? `\`${item.name}(${Object.entries(item.args).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ")})\``
|
|
43
|
+
: `\`${item.name}\``;
|
|
44
|
+
return `${ts} ${ok} **${args}**${elapsed}\n\n\`\`\`\n${item.output}\n\`\`\``;
|
|
45
|
+
}
|
|
46
|
+
case "diff":
|
|
47
|
+
return `${ts} ✎ \`${item.path}\` \`+${item.added}/-${item.removed}\`\n\`\`\`diff\n${item.text}\n\`\`\``;
|
|
48
|
+
case "build":
|
|
49
|
+
return `${item.success ? "✅" : "❌"} **build**: \`${item.command}\``;
|
|
50
|
+
case "todo": {
|
|
51
|
+
const rows = item.items.map((t) => `- [${t.status === "completed" ? "x" : t.status === "in_progress" ? ">" : " "}] ${t.content}`);
|
|
52
|
+
return `## plan\n\n${rows.join("\n")}`;
|
|
53
|
+
}
|
|
54
|
+
case "compaction":
|
|
55
|
+
return `\n---\n*compacted: ${item.tokensBefore} → ${item.tokensAfter} tokens*${item.manual ? " (manual)" : ""}\n---\n`;
|
|
56
|
+
case "notice":
|
|
57
|
+
return `${item.level === "error" ? "> ❌" : item.level === "warn" ? "> ⚠" : ">"} ${item.text}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Export transcript to the requested format. */
|
|
61
|
+
export function exportTranscript(items, format = "md") {
|
|
62
|
+
if (format === "json") {
|
|
63
|
+
return JSON.stringify(items, null, 2);
|
|
64
|
+
}
|
|
65
|
+
return items.map(itemToMarkdown).join("\n\n");
|
|
66
|
+
}
|
|
67
|
+
/** Case-insensitive search over transcript items.
|
|
68
|
+
* Returns all items whose text representation matches `query`.
|
|
69
|
+
* Searches: assistant/thinking text, tool name+output, diff path+text, notice text.
|
|
70
|
+
*/
|
|
71
|
+
export function findInTranscript(items, query) {
|
|
72
|
+
if (!query.trim())
|
|
73
|
+
return [];
|
|
74
|
+
const lc = query.toLowerCase();
|
|
75
|
+
const results = [];
|
|
76
|
+
for (let i = 0; i < items.length; i++) {
|
|
77
|
+
const item = items[i];
|
|
78
|
+
const matched = (text) => text.toLowerCase().includes(lc);
|
|
79
|
+
let hit = false;
|
|
80
|
+
let snippet = "";
|
|
81
|
+
switch (item.kind) {
|
|
82
|
+
case "assistant":
|
|
83
|
+
case "thinking":
|
|
84
|
+
if (matched(item.text)) {
|
|
85
|
+
hit = true;
|
|
86
|
+
snippet = snippetOf(item.text, query);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case "tool":
|
|
90
|
+
if (matched(item.name) || matched(item.output) || matched(JSON.stringify(item.args))) {
|
|
91
|
+
hit = true;
|
|
92
|
+
snippet = snippetOf(`${item.name}: ${item.output}`, query);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "diff":
|
|
96
|
+
if (matched(item.path) || matched(item.text)) {
|
|
97
|
+
hit = true;
|
|
98
|
+
snippet = snippetOf(`${item.path} · ${item.text}`, query);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case "user":
|
|
102
|
+
if (matched(item.text)) {
|
|
103
|
+
hit = true;
|
|
104
|
+
snippet = snippetOf(item.text, query);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
case "notice":
|
|
108
|
+
if (matched(item.text)) {
|
|
109
|
+
hit = true;
|
|
110
|
+
snippet = snippetOf(item.text, query);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case "build":
|
|
114
|
+
if (matched(item.command)) {
|
|
115
|
+
hit = true;
|
|
116
|
+
snippet = snippetOf(item.command, query);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (hit)
|
|
123
|
+
results.push({ index: i, kind: item.kind, snippet });
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
|
127
|
+
/** Return a ~120-char snippet around the first occurrence of `query` in `text`. */
|
|
128
|
+
function snippetOf(text, query) {
|
|
129
|
+
const pos = text.toLowerCase().indexOf(query.toLowerCase());
|
|
130
|
+
if (pos === -1)
|
|
131
|
+
return text.slice(0, 120);
|
|
132
|
+
const start = Math.max(0, pos - 40);
|
|
133
|
+
const end = Math.min(text.length, pos + query.length + 80);
|
|
134
|
+
return (start > 0 ? "…" : "") + text.slice(start, end) + (end < text.length ? "…" : "");
|
|
135
|
+
}
|
|
136
|
+
// ── Copy ─────────────────────────────────────────────────────────────────────
|
|
137
|
+
/** Return the copyable text for a chat item (for clipboard). */
|
|
138
|
+
export function copyTextForItem(item) {
|
|
139
|
+
switch (item.kind) {
|
|
140
|
+
case "diff": return item.text;
|
|
141
|
+
case "tool": return item.output;
|
|
142
|
+
case "assistant": return item.text;
|
|
143
|
+
default: return itemToText(item);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/watch.js
CHANGED
|
@@ -29,6 +29,14 @@ export function watchWorkdir(workdir, onChange) {
|
|
|
29
29
|
}
|
|
30
30
|
catch { /* race — file removed between existsSync and watch */ }
|
|
31
31
|
}
|
|
32
|
+
// Watch the parent too so a skills directory created after startup is seen.
|
|
33
|
+
const scoderDir = join(workdir, ".scoder");
|
|
34
|
+
if (existsSync(scoderDir)) {
|
|
35
|
+
try {
|
|
36
|
+
watchers.push(watch(scoderDir, () => fire("skills")));
|
|
37
|
+
}
|
|
38
|
+
catch { /* ignore */ }
|
|
39
|
+
}
|
|
32
40
|
// .scoder/skills/ — skill files added/removed/edited.
|
|
33
41
|
const skillsDir = join(workdir, ".scoder", "skills");
|
|
34
42
|
if (existsSync(skillsDir)) {
|
package/dist/webfetch.js
CHANGED
|
@@ -39,8 +39,8 @@ function isPrivateAddress(ip) {
|
|
|
39
39
|
const lower = ip.toLowerCase();
|
|
40
40
|
if (lower === "::1" || lower === "::")
|
|
41
41
|
return true;
|
|
42
|
-
if (lower
|
|
43
|
-
return true; // link-local
|
|
42
|
+
if (/^fe[89ab][0-9a-f]{2}:/.test(lower))
|
|
43
|
+
return true; // link-local fe80::/10
|
|
44
44
|
if (/^f[cd][0-9a-f]{2}:/.test(lower))
|
|
45
45
|
return true; // unique local fc00::/7
|
|
46
46
|
// IPv4-mapped ::ffff:10.0.0.1
|
|
@@ -166,6 +166,9 @@ export async function toolWebFetch(ctx, args) {
|
|
|
166
166
|
if (signal.aborted) {
|
|
167
167
|
return { ok: false, output: `ERROR: fetch aborted (${signal.reason?.message ?? "aborted"})` };
|
|
168
168
|
}
|
|
169
|
+
if (current.protocol !== "http:" && current.protocol !== "https:") {
|
|
170
|
+
return { ok: false, output: `ERROR: redirect target uses unsupported protocol ${current.protocol}` };
|
|
171
|
+
}
|
|
169
172
|
await assertPublicHost(current.hostname);
|
|
170
173
|
const res = await fetch(current, {
|
|
171
174
|
method: "GET",
|
package/package.json
CHANGED