devez-vibe 1.10.7 → 1.10.9
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/bin/dvz.exe +0 -0
- package/bin/dvz.js +35 -35
- package/bridge/claude-agent-sdk-bridge.mjs +71 -0
- package/package.json +50 -50
- package/prepare-search-deps.mjs +42 -42
package/bin/dvz.exe
CHANGED
|
Binary file
|
package/bin/dvz.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
|
|
8
|
-
const packageExecutable = join(dirname(fileURLToPath(import.meta.url)), "dvz.exe");
|
|
9
|
-
const pointer = process.env.LOCALAPPDATA
|
|
10
|
-
? join(process.env.LOCALAPPDATA, "DevezVibe", "current-executable.txt")
|
|
11
|
-
: "";
|
|
12
|
-
|
|
13
|
-
let executable = packageExecutable;
|
|
14
|
-
try {
|
|
15
|
-
const candidate = pointer ? readFileSync(pointer, "utf8").trim() : "";
|
|
16
|
-
if (isAbsolute(candidate) && existsSync(candidate)) executable = candidate;
|
|
17
|
-
} catch {
|
|
18
|
-
// A missing or incomplete pointer falls back to the npm-packaged executable.
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const child = spawn(executable, process.argv.slice(2), { stdio: "inherit" });
|
|
22
|
-
child.on("error", (error) => {
|
|
23
|
-
console.error(`Devez Vibe 실행 실패: ${error.message}`);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
});
|
|
26
|
-
child.on("exit", (code, signal) => {
|
|
27
|
-
if (signal) process.kill(process.pid, signal);
|
|
28
|
-
else process.exit(code ?? 1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
32
|
-
process.on(signal, () => {
|
|
33
|
-
if (!child.killed) child.kill(signal);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const packageExecutable = join(dirname(fileURLToPath(import.meta.url)), "dvz.exe");
|
|
9
|
+
const pointer = process.env.LOCALAPPDATA
|
|
10
|
+
? join(process.env.LOCALAPPDATA, "DevezVibe", "current-executable.txt")
|
|
11
|
+
: "";
|
|
12
|
+
|
|
13
|
+
let executable = packageExecutable;
|
|
14
|
+
try {
|
|
15
|
+
const candidate = pointer ? readFileSync(pointer, "utf8").trim() : "";
|
|
16
|
+
if (isAbsolute(candidate) && existsSync(candidate)) executable = candidate;
|
|
17
|
+
} catch {
|
|
18
|
+
// A missing or incomplete pointer falls back to the npm-packaged executable.
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const child = spawn(executable, process.argv.slice(2), { stdio: "inherit" });
|
|
22
|
+
child.on("error", (error) => {
|
|
23
|
+
console.error(`Devez Vibe 실행 실패: ${error.message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
child.on("exit", (code, signal) => {
|
|
27
|
+
if (signal) process.kill(process.pid, signal);
|
|
28
|
+
else process.exit(code ?? 1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
32
|
+
process.on(signal, () => {
|
|
33
|
+
if (!child.killed) child.kill(signal);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -3169,6 +3169,11 @@ function processSubagentSystemMessage(session, message) {
|
|
|
3169
3169
|
}
|
|
3170
3170
|
return true;
|
|
3171
3171
|
}
|
|
3172
|
+
// 워크플로 이름(meta.name)은 task_started에만 오고, 행을 만드는 스냅숏보다 먼저 올 수 있어 작업 id로 둔다.
|
|
3173
|
+
const workflowName = firstLine(message.workflow_name || "", 80);
|
|
3174
|
+
if (workflowName && message.task_id) {
|
|
3175
|
+
(session.workflowNames ||= new Map()).set(firstLine(message.task_id, 80), workflowName);
|
|
3176
|
+
}
|
|
3172
3177
|
const running = upsertStructuredSubagent(session, message);
|
|
3173
3178
|
if (!running) return false;
|
|
3174
3179
|
emitSubagents(session);
|
|
@@ -3177,6 +3182,8 @@ function processSubagentSystemMessage(session, message) {
|
|
|
3177
3182
|
if (message.subtype === "task_progress") {
|
|
3178
3183
|
const running = upsertStructuredSubagent(session, message);
|
|
3179
3184
|
if (!running) return false;
|
|
3185
|
+
// 진행만 바뀐 묶음에는 workflow_progress가 빠지므로 앞서 받은 값을 둔다.
|
|
3186
|
+
if (Array.isArray(message.workflow_progress)) running.workflow = workflowProgress(message.workflow_progress);
|
|
3180
3187
|
const tool = firstLine(message.last_tool_name || "", 80);
|
|
3181
3188
|
if (tool) running.tool = tool;
|
|
3182
3189
|
const summary = firstLine(message.summary || "", SUBAGENT_DESCRIPTION_LIMIT);
|
|
@@ -3319,6 +3326,28 @@ function firstLine(value, limit) {
|
|
|
3319
3326
|
return String(value ?? "").split("\n")[0].trim().slice(0, limit);
|
|
3320
3327
|
}
|
|
3321
3328
|
|
|
3329
|
+
// 워크플로 task_progress는 공개 타입에 없는 workflow_progress에 단계(workflow_phase)와
|
|
3330
|
+
// 에이전트(workflow_agent)를 싣는다(CLI 2.1.302 실측). 순정 상세 창처럼 대기·실행·완료·실패로 나눈다.
|
|
3331
|
+
function workflowProgress(entries) {
|
|
3332
|
+
const state = (entry) => {
|
|
3333
|
+
if (entry.state === "done") return "done";
|
|
3334
|
+
if (entry.state === "error") return "failed";
|
|
3335
|
+
return entry.startedAt === undefined && entry.queuedAt !== undefined ? "queued" : "running";
|
|
3336
|
+
};
|
|
3337
|
+
return {
|
|
3338
|
+
phases: entries
|
|
3339
|
+
.filter((entry) => entry?.type === "workflow_phase")
|
|
3340
|
+
.map((entry) => ({ index: Number(entry.index) || 0, title: firstLine(entry.title || "", 80) })),
|
|
3341
|
+
agents: entries
|
|
3342
|
+
.filter((entry) => entry?.type === "workflow_agent")
|
|
3343
|
+
.map((entry) => ({
|
|
3344
|
+
label: firstLine(entry.label || "", 80),
|
|
3345
|
+
phase: Number(entry.phaseIndex) || 0,
|
|
3346
|
+
state: state(entry),
|
|
3347
|
+
})),
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3322
3351
|
// SDK 0.3.292부터 하위 에이전트가 띄운 에이전트·명령에 parent_task_id가 붙는다.
|
|
3323
3352
|
// 자식 행을 부모 행 바로 아래로 모아 깊이와 함께 보내면 호스트가 들여 그린다.
|
|
3324
3353
|
// 부모가 이미 끝났거나 모르는 id면 최상위에 둔다.
|
|
@@ -3381,6 +3410,13 @@ function emitSubagents(session) {
|
|
|
3381
3410
|
tool: agent.tool,
|
|
3382
3411
|
elapsedMs: Date.now() - agent.startedAt,
|
|
3383
3412
|
depth,
|
|
3413
|
+
workflow: agent.backgroundTask === true && agent.name === "Workflow"
|
|
3414
|
+
? {
|
|
3415
|
+
name: session.workflowNames?.get(agent.taskId) || "",
|
|
3416
|
+
phases: agent.workflow?.phases || [],
|
|
3417
|
+
agents: agent.workflow?.agents || [],
|
|
3418
|
+
}
|
|
3419
|
+
: undefined,
|
|
3384
3420
|
})),
|
|
3385
3421
|
});
|
|
3386
3422
|
}
|
|
@@ -7282,6 +7318,41 @@ async function runSelfTest() {
|
|
|
7282
7318
|
throw new Error(`Claude ${tool} row outlived its snapshot`);
|
|
7283
7319
|
}
|
|
7284
7320
|
}
|
|
7321
|
+
// 워크플로 행은 스냅숏보다 먼저 온 task_started의 이름과 task_progress의 단계·에이전트를 싣고,
|
|
7322
|
+
// workflow_progress가 빠진 진행 묶음에서는 앞 값을 유지한다.
|
|
7323
|
+
for (const message of [
|
|
7324
|
+
{ subtype: "task_started", task_id: "wf-2", tool_use_id: "toolu_wf-2", task_type: "local_workflow", description: "조사", workflow_name: "probe" },
|
|
7325
|
+
{ subtype: "background_tasks_changed", tasks: [{ task_id: "wf-2", task_type: "local_workflow", description: "조사" }] },
|
|
7326
|
+
{
|
|
7327
|
+
subtype: "task_progress", task_id: "wf-2", tool_use_id: "toolu_wf-2", description: "A: one", summary: "조사",
|
|
7328
|
+
workflow_progress: [
|
|
7329
|
+
{ type: "workflow_phase", index: 1, title: "A" },
|
|
7330
|
+
{ type: "workflow_agent", index: 1, label: "one", phaseIndex: 1, state: "done", startedAt: 1, queuedAt: 1 },
|
|
7331
|
+
{ type: "workflow_agent", index: 2, label: "two", phaseIndex: 1, state: "start", startedAt: 2, queuedAt: 1 },
|
|
7332
|
+
{ type: "workflow_agent", index: 3, label: "three", phaseIndex: 1, state: "start", queuedAt: 1 },
|
|
7333
|
+
{ type: "workflow_agent", index: 4, label: "four", phaseIndex: 1, state: "error", startedAt: 2, queuedAt: 1 },
|
|
7334
|
+
],
|
|
7335
|
+
},
|
|
7336
|
+
{ subtype: "task_progress", task_id: "wf-2", tool_use_id: "toolu_wf-2", description: "A: two", summary: "조사" },
|
|
7337
|
+
]) processSubagentSystemMessage(commandSession, { type: "system", ...message });
|
|
7338
|
+
const workflowRow = captured.join("").trim().split("\n").filter(Boolean)
|
|
7339
|
+
.map((line) => JSON.parse(line))
|
|
7340
|
+
.filter((line) => line.method === "turn/subagents/updated" && line.params.threadId === "command-self-test")
|
|
7341
|
+
.at(-1)?.params.subagents.find((row) => row.taskId === "wf-2");
|
|
7342
|
+
const expectedWorkflow = {
|
|
7343
|
+
name: "probe",
|
|
7344
|
+
phases: [{ index: 1, title: "A" }],
|
|
7345
|
+
agents: [
|
|
7346
|
+
{ label: "one", phase: 1, state: "done" },
|
|
7347
|
+
{ label: "two", phase: 1, state: "running" },
|
|
7348
|
+
{ label: "three", phase: 1, state: "queued" },
|
|
7349
|
+
{ label: "four", phase: 1, state: "failed" },
|
|
7350
|
+
],
|
|
7351
|
+
};
|
|
7352
|
+
if (JSON.stringify(workflowRow?.workflow) !== JSON.stringify(expectedWorkflow)) {
|
|
7353
|
+
throw new Error(`Claude workflow progress row self-test failed: ${JSON.stringify(workflowRow)}`);
|
|
7354
|
+
}
|
|
7355
|
+
processSubagentSystemMessage(commandSession, { type: "system", subtype: "background_tasks_changed", tasks: [] });
|
|
7285
7356
|
// 포그라운드로 끝난 작업은 여전히 그 도구 결과가 행을 거둔다.
|
|
7286
7357
|
commandSession.tools.set("toolu_skill", { name: "Skill", input: {}, item: { id: "toolu_skill" } });
|
|
7287
7358
|
processSubagentSystemMessage(commandSession, {
|
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "devez-vibe",
|
|
3
|
-
"version": "1.10.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"codex",
|
|
8
|
-
"cli",
|
|
9
|
-
"tui",
|
|
10
|
-
"terminal",
|
|
11
|
-
"app-server",
|
|
12
|
-
"claude-agent-sdk"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
15
|
-
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
16
|
-
"repository": {
|
|
17
|
-
"type": "git",
|
|
18
|
-
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
19
|
-
},
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"bin": {
|
|
22
|
-
"dvz": "bin/dvz.js"
|
|
23
|
-
},
|
|
24
|
-
"files": [
|
|
25
|
-
"bin/dvz.js",
|
|
26
|
-
"bin/dvz.exe",
|
|
27
|
-
"bridge/claude-agent-sdk-bridge.mjs",
|
|
28
|
-
"install-skills.mjs",
|
|
29
|
-
"prepare-search-deps.mjs",
|
|
30
|
-
"skills/luna-loop/",
|
|
31
|
-
"skills/insane-search/",
|
|
32
|
-
"README.md",
|
|
33
|
-
"LICENSE"
|
|
34
|
-
],
|
|
35
|
-
"os": [
|
|
36
|
-
"win32"
|
|
37
|
-
],
|
|
38
|
-
"cpu": [
|
|
39
|
-
"x64"
|
|
40
|
-
],
|
|
41
|
-
"engines": {
|
|
42
|
-
"node": ">=18"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"postinstall": "node install-skills.mjs && node prepare-search-deps.mjs"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.302"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "devez-vibe",
|
|
3
|
+
"version": "1.10.9",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"codex",
|
|
8
|
+
"cli",
|
|
9
|
+
"tui",
|
|
10
|
+
"terminal",
|
|
11
|
+
"app-server",
|
|
12
|
+
"claude-agent-sdk"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
15
|
+
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bin": {
|
|
22
|
+
"dvz": "bin/dvz.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"bin/dvz.js",
|
|
26
|
+
"bin/dvz.exe",
|
|
27
|
+
"bridge/claude-agent-sdk-bridge.mjs",
|
|
28
|
+
"install-skills.mjs",
|
|
29
|
+
"prepare-search-deps.mjs",
|
|
30
|
+
"skills/luna-loop/",
|
|
31
|
+
"skills/insane-search/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"os": [
|
|
36
|
+
"win32"
|
|
37
|
+
],
|
|
38
|
+
"cpu": [
|
|
39
|
+
"x64"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"postinstall": "node install-skills.mjs && node prepare-search-deps.mjs"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.302"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/prepare-search-deps.mjs
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
// insane-search 스킬이 첫 호출에서 받는 파이썬 패키지를 설치 시점에 미리 깔아 둔다.
|
|
2
|
-
// 준비는 어디까지나 선택적이다. 파이썬이 없거나 설치가 실패해도 안내만 남기고 정상 종료해서
|
|
3
|
-
// devezVibe 설치 자체가 깨지지 않게 한다. 스킬은 첫 사용 때 다시 설치를 시도하므로 손해가 없다.
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
// SKILL.md의 의존성 가드와 같은 조합이다. 한쪽만 바뀌면 첫 호출에서 다시 설치가 돈다.
|
|
7
|
-
const guard = 'import curl_cffi,bs4,yaml,pypdf,markdownify; v=curl_cffi.__version__.split("."); assert (int(v[0]),int(v[1]))>=(0,15)';
|
|
8
|
-
const packages = ["curl_cffi>=0.15.0", "beautifulsoup4", "pyyaml", "pypdf", "markdownify"];
|
|
9
|
-
|
|
10
|
-
function run(command, args) {
|
|
11
|
-
return spawnSync(command, args, { encoding: "utf8", windowsHide: true, timeout: 300000 });
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Windows에는 python3가 없는 대신 실행하면 스토어를 여는 별칭이 잡혀 있을 수 있다.
|
|
15
|
-
// 버전 출력을 실제로 확인해서 쓸 수 있는 실행기만 고른다.
|
|
16
|
-
function findPython() {
|
|
17
|
-
for (const candidate of ["python3", "python", "py"]) {
|
|
18
|
-
const probe = run(candidate, ["-c", "import sys; print(sys.version_info[0])"]);
|
|
19
|
-
if (probe.status === 0 && probe.stdout.trim() === "3") return candidate;
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const python = findPython();
|
|
25
|
-
if (!python) {
|
|
26
|
-
console.log("insane-search 준비 건너뜀: 파이썬 3을 찾지 못했습니다. 설치하면 첫 사용 때 자동으로 준비됩니다.");
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (run(python, ["-c", guard]).status === 0) {
|
|
31
|
-
console.log("insane-search 의존성 준비 완료: 이미 설치되어 있습니다.");
|
|
32
|
-
process.exit(0);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// --user는 관리자 권한 없이도 설치되게 하고, 가상환경에서는 pip가 알아서 무시한다.
|
|
36
|
-
const install = run(python, ["-m", "pip", "install", "-U", "--user", "-q", ...packages]);
|
|
37
|
-
if (install.status === 0 && run(python, ["-c", guard]).status === 0) {
|
|
38
|
-
console.log("insane-search 의존성 준비 완료");
|
|
39
|
-
} else {
|
|
40
|
-
const detail = (install.stderr || install.stdout || "").trim().split("\n").pop() || "원인 미상";
|
|
41
|
-
console.log(`insane-search 준비 건너뜀 (${detail}). 첫 사용 때 다시 시도합니다.`);
|
|
42
|
-
}
|
|
1
|
+
// insane-search 스킬이 첫 호출에서 받는 파이썬 패키지를 설치 시점에 미리 깔아 둔다.
|
|
2
|
+
// 준비는 어디까지나 선택적이다. 파이썬이 없거나 설치가 실패해도 안내만 남기고 정상 종료해서
|
|
3
|
+
// devezVibe 설치 자체가 깨지지 않게 한다. 스킬은 첫 사용 때 다시 설치를 시도하므로 손해가 없다.
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
// SKILL.md의 의존성 가드와 같은 조합이다. 한쪽만 바뀌면 첫 호출에서 다시 설치가 돈다.
|
|
7
|
+
const guard = 'import curl_cffi,bs4,yaml,pypdf,markdownify; v=curl_cffi.__version__.split("."); assert (int(v[0]),int(v[1]))>=(0,15)';
|
|
8
|
+
const packages = ["curl_cffi>=0.15.0", "beautifulsoup4", "pyyaml", "pypdf", "markdownify"];
|
|
9
|
+
|
|
10
|
+
function run(command, args) {
|
|
11
|
+
return spawnSync(command, args, { encoding: "utf8", windowsHide: true, timeout: 300000 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Windows에는 python3가 없는 대신 실행하면 스토어를 여는 별칭이 잡혀 있을 수 있다.
|
|
15
|
+
// 버전 출력을 실제로 확인해서 쓸 수 있는 실행기만 고른다.
|
|
16
|
+
function findPython() {
|
|
17
|
+
for (const candidate of ["python3", "python", "py"]) {
|
|
18
|
+
const probe = run(candidate, ["-c", "import sys; print(sys.version_info[0])"]);
|
|
19
|
+
if (probe.status === 0 && probe.stdout.trim() === "3") return candidate;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const python = findPython();
|
|
25
|
+
if (!python) {
|
|
26
|
+
console.log("insane-search 준비 건너뜀: 파이썬 3을 찾지 못했습니다. 설치하면 첫 사용 때 자동으로 준비됩니다.");
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (run(python, ["-c", guard]).status === 0) {
|
|
31
|
+
console.log("insane-search 의존성 준비 완료: 이미 설치되어 있습니다.");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --user는 관리자 권한 없이도 설치되게 하고, 가상환경에서는 pip가 알아서 무시한다.
|
|
36
|
+
const install = run(python, ["-m", "pip", "install", "-U", "--user", "-q", ...packages]);
|
|
37
|
+
if (install.status === 0 && run(python, ["-c", guard]).status === 0) {
|
|
38
|
+
console.log("insane-search 의존성 준비 완료");
|
|
39
|
+
} else {
|
|
40
|
+
const detail = (install.stderr || install.stdout || "").trim().split("\n").pop() || "원인 미상";
|
|
41
|
+
console.log(`insane-search 준비 건너뜀 (${detail}). 첫 사용 때 다시 시도합니다.`);
|
|
42
|
+
}
|