devez-vibe 1.3.9 → 1.3.11
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/bridge/claude-agent-sdk-bridge.mjs +680 -184
- package/package.json +41 -41
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
4
5
|
import { createReadStream } from "node:fs";
|
|
5
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
8
9
|
import { createInterface } from "node:readline";
|
|
9
10
|
import {
|
|
10
11
|
deleteSession,
|
|
11
12
|
forkSession,
|
|
12
|
-
getSessionInfo,
|
|
13
|
-
getSessionMessages,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
getSessionInfo,
|
|
14
|
+
getSessionMessages,
|
|
15
|
+
filterEscalatingDefaultMode,
|
|
16
|
+
listSessions,
|
|
17
|
+
resolveSettings,
|
|
18
|
+
startup,
|
|
19
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
17
20
|
|
|
18
21
|
const VERSION = process.env.DEVEZ_VIBE_VERSION || "dev";
|
|
19
22
|
const sessions = new Map();
|
|
@@ -21,10 +24,10 @@ const sessions = new Map();
|
|
|
21
24
|
// transcript under, so a session can be renamed mid-flight. Old id → live id,
|
|
22
25
|
// which keeps ids the host already handed out (or wrote to disk) resolvable.
|
|
23
26
|
const sessionAliases = new Map();
|
|
24
|
-
const pendingHostRequests = new Map();
|
|
25
|
-
const modelCatalogs = new Map();
|
|
26
|
-
const CLAUDE_MODEL_ORDER = ["fable", "opus", "sonnet", "haiku"];
|
|
27
|
-
let nextHostRequest = 1;
|
|
27
|
+
const pendingHostRequests = new Map();
|
|
28
|
+
const modelCatalogs = new Map();
|
|
29
|
+
const CLAUDE_MODEL_ORDER = ["fable", "opus", "sonnet", "haiku"];
|
|
30
|
+
let nextHostRequest = 1;
|
|
28
31
|
|
|
29
32
|
class AsyncQueue {
|
|
30
33
|
constructor() {
|
|
@@ -56,9 +59,9 @@ class AsyncQueue {
|
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
function write(message) {
|
|
60
|
-
process.stdout.write(`${JSON.stringify(message)}\n`, "utf8");
|
|
61
|
-
}
|
|
62
|
+
function write(message) {
|
|
63
|
+
process.stdout.write(`${JSON.stringify(message)}\n`, "utf8");
|
|
64
|
+
}
|
|
62
65
|
|
|
63
66
|
function notify(method, params) {
|
|
64
67
|
write({ method, params });
|
|
@@ -71,34 +74,34 @@ function rpcError(error) {
|
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function hostRequest(method, params, signal) {
|
|
75
|
-
const id = `claude-host-${nextHostRequest++}`;
|
|
76
|
-
return new Promise((resolve, reject) => {
|
|
77
|
+
function hostRequest(method, params, signal) {
|
|
78
|
+
const id = `claude-host-${nextHostRequest++}`;
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
77
80
|
const abort = () => {
|
|
78
81
|
pendingHostRequests.delete(id);
|
|
79
82
|
reject(new Error("사용자 입력 요청이 취소되었습니다."));
|
|
80
83
|
};
|
|
81
84
|
if (signal?.aborted) return abort();
|
|
82
85
|
signal?.addEventListener("abort", abort, { once: true });
|
|
83
|
-
pendingHostRequests.set(id, {
|
|
84
|
-
resolve: (value) => {
|
|
85
|
-
signal?.removeEventListener("abort", abort);
|
|
86
|
-
resolve(value);
|
|
87
|
-
},
|
|
88
|
-
reject: (error) => {
|
|
89
|
-
signal?.removeEventListener("abort", abort);
|
|
90
|
-
reject(error);
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
try {
|
|
94
|
-
write({ id, method, params });
|
|
95
|
-
} catch (error) {
|
|
96
|
-
if (!pendingHostRequests.delete(id)) return;
|
|
97
|
-
signal?.removeEventListener("abort", abort);
|
|
98
|
-
reject(error);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
86
|
+
pendingHostRequests.set(id, {
|
|
87
|
+
resolve: (value) => {
|
|
88
|
+
signal?.removeEventListener("abort", abort);
|
|
89
|
+
resolve(value);
|
|
90
|
+
},
|
|
91
|
+
reject: (error) => {
|
|
92
|
+
signal?.removeEventListener("abort", abort);
|
|
93
|
+
reject(error);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
write({ id, method, params });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!pendingHostRequests.delete(id)) return;
|
|
100
|
+
signal?.removeEventListener("abort", abort);
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
102
105
|
|
|
103
106
|
function sanitizedEnvironment() {
|
|
104
107
|
const env = { ...process.env };
|
|
@@ -168,7 +171,7 @@ function compactClaudeModelName(model) {
|
|
|
168
171
|
return fallback || clean(model.displayName || model.resolvedModel || model.value);
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
function catalogEntry(model, defaultResolvedModel) {
|
|
174
|
+
function catalogEntry(model, defaultResolvedModel) {
|
|
172
175
|
const value = String(model.value || "");
|
|
173
176
|
const resolved = String(model.resolvedModel || value);
|
|
174
177
|
const efforts = model.supportsEffort && Array.isArray(model.supportedEffortLevels)
|
|
@@ -180,13 +183,14 @@ function catalogEntry(model, defaultResolvedModel) {
|
|
|
180
183
|
model: visibleModel(value),
|
|
181
184
|
displayName: compactClaudeModelName(model),
|
|
182
185
|
defaultReasoningEffort: efforts.includes("high") ? "high" : efforts.at(-1) || "",
|
|
183
|
-
supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort })),
|
|
184
|
-
|
|
186
|
+
supportedReasoningEfforts: efforts.map((reasoningEffort) => ({ reasoningEffort })),
|
|
187
|
+
supportsAutoMode: Boolean(model.supportsAutoMode),
|
|
188
|
+
isDefault: Boolean(defaultResolvedModel) && resolved === defaultResolvedModel,
|
|
185
189
|
...(contextWindow > 0 ? { contextWindow } : {}),
|
|
186
190
|
};
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
async function loadModelCatalog(params) {
|
|
193
|
+
async function loadModelCatalog(params) {
|
|
190
194
|
const cacheKey = `${params.claudePath || "claude"}\n${params.cwd || process.cwd()}`;
|
|
191
195
|
if (modelCatalogs.has(cacheKey)) return modelCatalogs.get(cacheKey);
|
|
192
196
|
const pending = (async () => {
|
|
@@ -207,18 +211,18 @@ async function loadModelCatalog(params) {
|
|
|
207
211
|
})();
|
|
208
212
|
try {
|
|
209
213
|
const models = await agentQuery.supportedModels();
|
|
210
|
-
const defaultResolvedModel = String(
|
|
211
|
-
models.find((model) => model.value === "default")?.resolvedModel || "",
|
|
212
|
-
);
|
|
213
|
-
models.sort((left, right) => {
|
|
214
|
-
const leftFamily = String(left.value || "").match(/(fable|opus|sonnet|haiku)/i)?.[1]?.toLowerCase();
|
|
215
|
-
const rightFamily = String(right.value || "").match(/(fable|opus|sonnet|haiku)/i)?.[1]?.toLowerCase();
|
|
216
|
-
const leftOrder = CLAUDE_MODEL_ORDER.indexOf(leftFamily);
|
|
217
|
-
const rightOrder = CLAUDE_MODEL_ORDER.indexOf(rightFamily);
|
|
218
|
-
return (leftOrder < 0 ? CLAUDE_MODEL_ORDER.length : leftOrder)
|
|
219
|
-
- (rightOrder < 0 ? CLAUDE_MODEL_ORDER.length : rightOrder);
|
|
220
|
-
});
|
|
221
|
-
return {
|
|
214
|
+
const defaultResolvedModel = String(
|
|
215
|
+
models.find((model) => model.value === "default")?.resolvedModel || "",
|
|
216
|
+
);
|
|
217
|
+
models.sort((left, right) => {
|
|
218
|
+
const leftFamily = String(left.value || "").match(/(fable|opus|sonnet|haiku)/i)?.[1]?.toLowerCase();
|
|
219
|
+
const rightFamily = String(right.value || "").match(/(fable|opus|sonnet|haiku)/i)?.[1]?.toLowerCase();
|
|
220
|
+
const leftOrder = CLAUDE_MODEL_ORDER.indexOf(leftFamily);
|
|
221
|
+
const rightOrder = CLAUDE_MODEL_ORDER.indexOf(rightFamily);
|
|
222
|
+
return (leftOrder < 0 ? CLAUDE_MODEL_ORDER.length : leftOrder)
|
|
223
|
+
- (rightOrder < 0 ? CLAUDE_MODEL_ORDER.length : rightOrder);
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
222
226
|
data: models
|
|
223
227
|
.filter((model) => model.value && model.value !== "default")
|
|
224
228
|
.map((model) => catalogEntry(model, defaultResolvedModel)),
|
|
@@ -294,7 +298,14 @@ function adoptSessionId(session, incoming) {
|
|
|
294
298
|
});
|
|
295
299
|
}
|
|
296
300
|
|
|
297
|
-
const PERMISSION_MODES = [
|
|
301
|
+
const PERMISSION_MODES = [
|
|
302
|
+
"default",
|
|
303
|
+
"acceptEdits",
|
|
304
|
+
"plan",
|
|
305
|
+
"auto",
|
|
306
|
+
"dontAsk",
|
|
307
|
+
"bypassPermissions",
|
|
308
|
+
];
|
|
298
309
|
|
|
299
310
|
function permissionMode(requested, fallback = "default") {
|
|
300
311
|
const mode = String(requested || "");
|
|
@@ -303,30 +314,173 @@ function permissionMode(requested, fallback = "default") {
|
|
|
303
314
|
|
|
304
315
|
// Moves a live session onto a mode the badge picked. A rejected mode — policy
|
|
305
316
|
// disables bypass, say — leaves the session on the one it already had.
|
|
306
|
-
async function applyPermissionMode(session, requested) {
|
|
307
|
-
const mode = permissionMode(requested, session.permissionMode || "default");
|
|
308
|
-
if (mode === session.permissionMode) return;
|
|
309
|
-
try {
|
|
310
|
-
await session.query.setPermissionMode(mode);
|
|
311
|
-
session.permissionMode = mode;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
async function applyPermissionMode(session, requested) {
|
|
318
|
+
const mode = permissionMode(requested, session.permissionMode || "default");
|
|
319
|
+
if (mode === session.permissionMode) return null;
|
|
320
|
+
try {
|
|
321
|
+
await session.query.setPermissionMode(mode);
|
|
322
|
+
session.permissionMode = mode;
|
|
323
|
+
return null;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return error?.message || String(error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function claudePermissionStatus(params) {
|
|
330
|
+
const resolved = await resolveSettings({
|
|
331
|
+
cwd: params.cwd || process.cwd(),
|
|
332
|
+
settingSources: ["user", "project", "local"],
|
|
333
|
+
});
|
|
334
|
+
const effective = filterEscalatingDefaultMode(resolved);
|
|
335
|
+
const permissions = effective.permissions || {};
|
|
336
|
+
const bypassAccepted = effective.skipDangerousModePermissionPrompt === true
|
|
337
|
+
|| effective.bypassPermissionsModeAccepted === true;
|
|
338
|
+
const bypassAvailable = permissions.disableBypassPermissionsMode !== "disable" && bypassAccepted;
|
|
339
|
+
const autoDisabled = effective.disableAutoMode === "disable";
|
|
340
|
+
let defaultMode = permissionMode(permissions.defaultMode);
|
|
341
|
+
if ((defaultMode === "bypassPermissions" && !bypassAvailable)
|
|
342
|
+
|| (defaultMode === "auto" && autoDisabled)) defaultMode = "default";
|
|
343
|
+
const rules = [];
|
|
344
|
+
const directories = [];
|
|
345
|
+
for (const source of resolved.sources) {
|
|
346
|
+
const sourcePermissions = source.settings?.permissions || {};
|
|
347
|
+
for (const behavior of ["allow", "ask", "deny"]) {
|
|
348
|
+
for (const rule of Array.isArray(sourcePermissions[behavior])
|
|
349
|
+
? sourcePermissions[behavior]
|
|
350
|
+
: []) {
|
|
351
|
+
rules.push({
|
|
352
|
+
behavior,
|
|
353
|
+
rule: String(rule),
|
|
354
|
+
source: source.source,
|
|
355
|
+
path: source.path || null,
|
|
356
|
+
mutable: ["user", "project", "local"].includes(source.source),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const directory of Array.isArray(sourcePermissions.additionalDirectories)
|
|
361
|
+
? sourcePermissions.additionalDirectories
|
|
362
|
+
: []) {
|
|
363
|
+
directories.push({
|
|
364
|
+
directory: String(directory),
|
|
365
|
+
source: source.source,
|
|
366
|
+
path: source.path || null,
|
|
367
|
+
mutable: ["user", "project", "local"].includes(source.source),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const denials = [...sessions.values()]
|
|
372
|
+
.filter((session) => session.cwd === (params.cwd || process.cwd()))
|
|
373
|
+
.flatMap((session) => session.permissionDenials || [])
|
|
374
|
+
.slice(-20)
|
|
375
|
+
.reverse();
|
|
376
|
+
return {
|
|
377
|
+
defaultMode,
|
|
378
|
+
bypassAvailable,
|
|
379
|
+
autoDisabled,
|
|
380
|
+
rulesLocked: effective.allowManagedPermissionRulesOnly === true,
|
|
381
|
+
rules,
|
|
382
|
+
directories,
|
|
383
|
+
denials,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function permissionSettingsPath(cwd, destination) {
|
|
388
|
+
if (destination === "user") {
|
|
389
|
+
return join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "settings.json");
|
|
390
|
+
}
|
|
391
|
+
if (destination === "project") return join(cwd, ".claude", "settings.json");
|
|
392
|
+
if (destination === "local") return join(cwd, ".claude", "settings.local.json");
|
|
393
|
+
throw new Error(`지원하지 않는 Claude 권한 설정 범위: ${destination}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function updateClaudePermission(params) {
|
|
397
|
+
const cwd = params.cwd || process.cwd();
|
|
398
|
+
const status = await claudePermissionStatus({ cwd });
|
|
399
|
+
if (status.rulesLocked) {
|
|
400
|
+
throw new Error("관리 정책에서 사용자·프로젝트 권한 규칙 변경을 비활성화했습니다.");
|
|
401
|
+
}
|
|
402
|
+
const destination = String(params.destination || "project");
|
|
403
|
+
const path = permissionSettingsPath(cwd, destination);
|
|
404
|
+
let settings = {};
|
|
405
|
+
try {
|
|
406
|
+
settings = JSON.parse(await readFile(path, "utf8"));
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error?.code !== "ENOENT") throw error;
|
|
409
|
+
}
|
|
410
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) settings = {};
|
|
411
|
+
if (!settings.permissions || typeof settings.permissions !== "object"
|
|
412
|
+
|| Array.isArray(settings.permissions)) settings.permissions = {};
|
|
413
|
+
|
|
414
|
+
const action = String(params.action || "");
|
|
415
|
+
const behavior = String(params.behavior || "");
|
|
416
|
+
const key = behavior === "directory" ? "additionalDirectories" : behavior;
|
|
417
|
+
if (!["allow", "ask", "deny", "additionalDirectories"].includes(key)) {
|
|
418
|
+
throw new Error(`지원하지 않는 Claude 권한 규칙 종류: ${behavior}`);
|
|
419
|
+
}
|
|
420
|
+
const value = String(params.value || "").trim();
|
|
421
|
+
if (!value) throw new Error("빈 Claude 권한 규칙은 저장할 수 없습니다.");
|
|
422
|
+
const values = Array.isArray(settings.permissions[key])
|
|
423
|
+
? settings.permissions[key].map(String)
|
|
424
|
+
: [];
|
|
425
|
+
if (action === "add" && !values.includes(value)) values.push(value);
|
|
426
|
+
else if (action === "remove") {
|
|
427
|
+
const index = values.indexOf(value);
|
|
428
|
+
if (index >= 0) values.splice(index, 1);
|
|
429
|
+
} else if (action !== "add") {
|
|
430
|
+
throw new Error(`지원하지 않는 Claude 권한 규칙 작업: ${action}`);
|
|
431
|
+
}
|
|
432
|
+
settings.permissions[key] = values;
|
|
433
|
+
await mkdir(dirname(path), { recursive: true });
|
|
434
|
+
await writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
435
|
+
return claudePermissionStatus({ cwd });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function setClaudeAutoModeDisabled(params) {
|
|
439
|
+
const cwd = params.cwd || process.cwd();
|
|
440
|
+
const path = permissionSettingsPath(cwd, "user");
|
|
441
|
+
let settings = {};
|
|
442
|
+
try {
|
|
443
|
+
settings = JSON.parse(await readFile(path, "utf8"));
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (error?.code !== "ENOENT") throw error;
|
|
446
|
+
}
|
|
447
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) settings = {};
|
|
448
|
+
if (params.disabled === false) delete settings.disableAutoMode;
|
|
449
|
+
else settings.disableAutoMode = "disable";
|
|
450
|
+
await mkdir(dirname(path), { recursive: true });
|
|
451
|
+
await writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
452
|
+
return claudePermissionStatus({ cwd });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function retryClaudePermission(params) {
|
|
456
|
+
const session = lookupSession(params.sessionId);
|
|
457
|
+
if (!session) throw new Error("재시도할 Claude 세션을 찾을 수 없습니다.");
|
|
458
|
+
const tool = String(params.tool || "tool");
|
|
459
|
+
const input = params.input && typeof params.input === "object" ? params.input : {};
|
|
460
|
+
const previousMode = session.permissionMode;
|
|
461
|
+
return startPrompt({
|
|
462
|
+
sessionId: session.id,
|
|
463
|
+
permissionMode: previousMode === "auto" ? "default" : previousMode,
|
|
464
|
+
restorePermissionMode: previousMode === "auto" ? "auto" : null,
|
|
465
|
+
input: [{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: [
|
|
468
|
+
`The user selected the denied ${tool} action in /permissions and asked to retry it.`,
|
|
469
|
+
"Retry that action now. A manual permission prompt will be shown before it runs.",
|
|
470
|
+
`Original tool input: ${JSON.stringify(input)}`,
|
|
471
|
+
].join("\n"),
|
|
472
|
+
}],
|
|
473
|
+
});
|
|
474
|
+
}
|
|
320
475
|
|
|
321
476
|
function makeOptions(params, sessionId, resume) {
|
|
322
477
|
const options = {
|
|
323
478
|
cwd: params.cwd || process.cwd(),
|
|
324
479
|
includePartialMessages: true,
|
|
325
|
-
permissionMode: permissionMode(params.permissionMode),
|
|
326
|
-
// Not a mode, a capability: the SDK refuses `bypassPermissions` outright
|
|
327
|
-
// unless the session was started with this.
|
|
328
|
-
//
|
|
329
|
-
// nothing the session did not already have.
|
|
480
|
+
permissionMode: permissionMode(params.permissionMode),
|
|
481
|
+
// Not a mode, a capability: the SDK refuses `bypassPermissions` outright
|
|
482
|
+
// unless the session was started with this. The UI exposes it only after
|
|
483
|
+
// Claude's resolved settings and acknowledgement make the mode available.
|
|
330
484
|
allowDangerouslySkipPermissions: true,
|
|
331
485
|
enableFileCheckpointing: true,
|
|
332
486
|
persistSession: true,
|
|
@@ -352,7 +506,7 @@ function makeOptions(params, sessionId, resume) {
|
|
|
352
506
|
return options;
|
|
353
507
|
}
|
|
354
508
|
|
|
355
|
-
async function startAgentQuery(prompt, options) {
|
|
509
|
+
async function startAgentQuery(prompt, options) {
|
|
356
510
|
const warm = await startup({ options });
|
|
357
511
|
try {
|
|
358
512
|
return warm.query(prompt);
|
|
@@ -360,7 +514,29 @@ async function startAgentQuery(prompt, options) {
|
|
|
360
514
|
warm.close();
|
|
361
515
|
throw error;
|
|
362
516
|
}
|
|
363
|
-
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function permissionSuggestionLabel(suggestions) {
|
|
520
|
+
if (!Array.isArray(suggestions) || !suggestions.length) return null;
|
|
521
|
+
const rules = suggestions.flatMap((suggestion) =>
|
|
522
|
+
Array.isArray(suggestion.rules)
|
|
523
|
+
? suggestion.rules.map((rule) => rule.ruleContent
|
|
524
|
+
? `${rule.toolName}(${rule.ruleContent})`
|
|
525
|
+
: String(rule.toolName || ""))
|
|
526
|
+
: []);
|
|
527
|
+
const directories = suggestions.flatMap((suggestion) =>
|
|
528
|
+
Array.isArray(suggestion.directories) ? suggestion.directories.map(String) : []);
|
|
529
|
+
const values = [...new Set([...rules, ...directories].filter(Boolean))];
|
|
530
|
+
const destinations = new Set(suggestions.map((suggestion) => suggestion.destination));
|
|
531
|
+
const scope = destinations.has("userSettings")
|
|
532
|
+
? "모든 프로젝트에서"
|
|
533
|
+
: destinations.has("projectSettings") || destinations.has("localSettings")
|
|
534
|
+
? "이 프로젝트에서"
|
|
535
|
+
: "이번 세션에서";
|
|
536
|
+
return values.length
|
|
537
|
+
? `${scope} 항상 허용: ${values.join(", ")}`
|
|
538
|
+
: `${scope} 다시 묻지 않기`;
|
|
539
|
+
}
|
|
364
540
|
|
|
365
541
|
async function requestToolPermission(toolName, input, permission) {
|
|
366
542
|
if (toolName === "AskUserQuestion") {
|
|
@@ -375,29 +551,29 @@ async function requestToolPermission(toolName, input, permission) {
|
|
|
375
551
|
isOther: true,
|
|
376
552
|
multiSelect: Boolean(question.multiSelect),
|
|
377
553
|
}));
|
|
378
|
-
const requestQuestions = () => hostRequest(
|
|
379
|
-
"item/tool/requestUserInput",
|
|
380
|
-
// Keep model-provided text out of the NDJSON envelope. This protects
|
|
381
|
-
// Korean text, backslashes, and malformed Unicode from a partial escape
|
|
382
|
-
// corrupting the request line before the host can open its dialog.
|
|
383
|
-
{
|
|
384
|
-
encoding: "base64-json",
|
|
385
|
-
payload: Buffer.from(JSON.stringify({ questions }), "utf8").toString("base64"),
|
|
386
|
-
},
|
|
387
|
-
permission.signal,
|
|
388
|
-
);
|
|
389
|
-
let response;
|
|
390
|
-
try {
|
|
391
|
-
response = await requestQuestions();
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (!isQuestionDeliveryError(error)) throw error;
|
|
394
|
-
try {
|
|
395
|
-
response = await requestQuestions();
|
|
396
|
-
} catch (retryError) {
|
|
397
|
-
if (!isQuestionDeliveryError(retryError)) throw retryError;
|
|
398
|
-
return { behavior: "deny", message: questionFallbackMessage(questions) };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
554
|
+
const requestQuestions = () => hostRequest(
|
|
555
|
+
"item/tool/requestUserInput",
|
|
556
|
+
// Keep model-provided text out of the NDJSON envelope. This protects
|
|
557
|
+
// Korean text, backslashes, and malformed Unicode from a partial escape
|
|
558
|
+
// corrupting the request line before the host can open its dialog.
|
|
559
|
+
{
|
|
560
|
+
encoding: "base64-json",
|
|
561
|
+
payload: Buffer.from(JSON.stringify({ questions }), "utf8").toString("base64"),
|
|
562
|
+
},
|
|
563
|
+
permission.signal,
|
|
564
|
+
);
|
|
565
|
+
let response;
|
|
566
|
+
try {
|
|
567
|
+
response = await requestQuestions();
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (!isQuestionDeliveryError(error)) throw error;
|
|
570
|
+
try {
|
|
571
|
+
response = await requestQuestions();
|
|
572
|
+
} catch (retryError) {
|
|
573
|
+
if (!isQuestionDeliveryError(retryError)) throw retryError;
|
|
574
|
+
return { behavior: "deny", message: questionFallbackMessage(questions) };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
401
577
|
const answers = {};
|
|
402
578
|
for (let index = 0; index < questions.length; index += 1) {
|
|
403
579
|
const selected = response?.answers?.[`q${index}`]?.answers;
|
|
@@ -410,38 +586,43 @@ async function requestToolPermission(toolName, input, permission) {
|
|
|
410
586
|
return { behavior: "allow", updatedInput: { ...input, answers } };
|
|
411
587
|
}
|
|
412
588
|
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
let
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
589
|
+
// Claude calls this callback only after its selected mode and permission rules
|
|
590
|
+
// decide that a human answer is needed. Forward every such request to the host;
|
|
591
|
+
// auto-allowing the ordinary cases here would silently defeat default/auto mode.
|
|
592
|
+
const planApproval = toolName === "ExitPlanMode";
|
|
593
|
+
const persistentApprovalLabel = permissionSuggestionLabel(permission.suggestions);
|
|
594
|
+
const common = {
|
|
595
|
+
claudePermission: true,
|
|
596
|
+
title: permission.title || permission.displayName,
|
|
597
|
+
persistentApprovalLabel,
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
let method = "item/permissions/requestApproval";
|
|
601
|
+
let params = {
|
|
602
|
+
...common,
|
|
603
|
+
reason: permission.decisionReason || permission.description || permission.title,
|
|
604
|
+
permissions: { tool: toolName, blockedPath: permission.blockedPath },
|
|
605
|
+
};
|
|
606
|
+
if (planApproval) {
|
|
607
|
+
params = {
|
|
608
|
+
...common,
|
|
609
|
+
reason: input.plan || permission.description || "계획대로 진행할까요?",
|
|
610
|
+
permissions: { tool: toolName },
|
|
611
|
+
};
|
|
612
|
+
} else if (toolName === "Bash") {
|
|
613
|
+
method = "item/commandExecution/requestApproval";
|
|
614
|
+
params = {
|
|
615
|
+
...common,
|
|
616
|
+
command: input.command || "command",
|
|
617
|
+
cwd: input.cwd,
|
|
618
|
+
reason: permission.decisionReason || permission.description,
|
|
439
619
|
};
|
|
440
620
|
} else if (["Edit", "Write", "NotebookEdit"].includes(toolName)) {
|
|
441
|
-
method = "item/fileChange/requestApproval";
|
|
442
|
-
params = {
|
|
443
|
-
|
|
444
|
-
|
|
621
|
+
method = "item/fileChange/requestApproval";
|
|
622
|
+
params = {
|
|
623
|
+
...common,
|
|
624
|
+
grantRoot: permission.blockedPath || input.file_path || input.notebook_path,
|
|
625
|
+
reason: permission.decisionReason || permission.description,
|
|
445
626
|
};
|
|
446
627
|
}
|
|
447
628
|
const response = await hostRequest(method, params, permission.signal);
|
|
@@ -455,28 +636,210 @@ async function requestToolPermission(toolName, input, permission) {
|
|
|
455
636
|
? { updatedPermissions: permission.suggestions }
|
|
456
637
|
: {}),
|
|
457
638
|
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
"
|
|
474
|
-
"
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function runClaudeCommand(params, args) {
|
|
642
|
+
const executable = String(params.claudePath || "claude");
|
|
643
|
+
return new Promise((resolve, reject) => {
|
|
644
|
+
const child = spawn(executable, args, {
|
|
645
|
+
cwd: params.cwd || process.cwd(),
|
|
646
|
+
env: sanitizedEnvironment(),
|
|
647
|
+
windowsHide: true,
|
|
648
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
649
|
+
});
|
|
650
|
+
const stdout = [];
|
|
651
|
+
const stderr = [];
|
|
652
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
653
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
654
|
+
child.once("error", reject);
|
|
655
|
+
child.once("close", (code) => {
|
|
656
|
+
const output = Buffer.concat(stdout).toString("utf8").trim();
|
|
657
|
+
const detail = Buffer.concat(stderr).toString("utf8").trim();
|
|
658
|
+
if (code === 0) resolve(output);
|
|
659
|
+
else reject(new Error(detail || output || `Claude 명령이 종료 코드 ${code}로 실패했습니다.`));
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function runClaudeJson(params, args) {
|
|
665
|
+
const output = await runClaudeCommand(params, args);
|
|
666
|
+
try { return output ? JSON.parse(output) : null; }
|
|
667
|
+
catch (error) {
|
|
668
|
+
throw new Error(`Claude 플러그인 응답을 해석하지 못했습니다: ${error.message}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function splitPluginId(id, fallbackMarketplace = "local") {
|
|
673
|
+
const value = String(id || "");
|
|
674
|
+
const separator = value.lastIndexOf("@");
|
|
675
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
676
|
+
return { name: value || "plugin", marketplace: fallbackMarketplace };
|
|
677
|
+
}
|
|
678
|
+
return { name: value.slice(0, separator), marketplace: value.slice(separator + 1) };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function claudePluginValue(available, installed) {
|
|
682
|
+
const id = String(available?.pluginId || installed?.id || "plugin");
|
|
683
|
+
const parts = splitPluginId(id, available?.marketplaceName);
|
|
684
|
+
const displayName = String(available?.name || parts.name);
|
|
685
|
+
const description = String(available?.description || "");
|
|
686
|
+
const mcpServers = installed?.mcpServers && typeof installed.mcpServers === "object"
|
|
687
|
+
? Object.keys(installed.mcpServers)
|
|
688
|
+
: [];
|
|
689
|
+
const capabilities = [];
|
|
690
|
+
if (mcpServers.length) capabilities.push(`${mcpServers.length} MCP servers`);
|
|
691
|
+
return {
|
|
692
|
+
id,
|
|
693
|
+
// Keep the marketplace-qualified id as the action target. The display name
|
|
694
|
+
// remains short, so typed searches still resolve the same way as Claude CLI.
|
|
695
|
+
name: id,
|
|
696
|
+
description,
|
|
697
|
+
installed: Boolean(installed),
|
|
698
|
+
enabled: Boolean(installed?.enabled),
|
|
699
|
+
availability: "AVAILABLE",
|
|
700
|
+
installPolicy: "USER_INSTALLABLE",
|
|
701
|
+
mustShowInstallationInterstitial: true,
|
|
702
|
+
interface: {
|
|
703
|
+
displayName,
|
|
704
|
+
shortDescription: description,
|
|
705
|
+
developerName: parts.marketplace,
|
|
706
|
+
capabilities,
|
|
707
|
+
},
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function buildClaudePluginCatalog(installed, available, marketplaces) {
|
|
712
|
+
const groups = new Map();
|
|
713
|
+
const ensureGroup = (name, path = null) => {
|
|
714
|
+
const key = String(name || "local");
|
|
715
|
+
if (!groups.has(key)) {
|
|
716
|
+
groups.set(key, {
|
|
717
|
+
name: key,
|
|
718
|
+
...(path ? { path } : {}),
|
|
719
|
+
interface: { displayName: key },
|
|
720
|
+
plugins: [],
|
|
721
|
+
});
|
|
722
|
+
} else if (path) {
|
|
723
|
+
groups.get(key).path = path;
|
|
724
|
+
}
|
|
725
|
+
return groups.get(key);
|
|
726
|
+
};
|
|
727
|
+
for (const marketplace of Array.isArray(marketplaces) ? marketplaces : []) {
|
|
728
|
+
ensureGroup(marketplace.name, marketplace.installLocation);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const installedById = new Map();
|
|
732
|
+
for (const plugin of Array.isArray(installed) ? installed : []) {
|
|
733
|
+
if (plugin?.id && !installedById.has(plugin.id)) installedById.set(plugin.id, plugin);
|
|
734
|
+
}
|
|
735
|
+
const seen = new Set();
|
|
736
|
+
for (const plugin of Array.isArray(available) ? available : []) {
|
|
737
|
+
const id = String(plugin?.pluginId || "");
|
|
738
|
+
if (!id || seen.has(id)) continue;
|
|
739
|
+
seen.add(id);
|
|
740
|
+
const parts = splitPluginId(id, plugin.marketplaceName);
|
|
741
|
+
ensureGroup(plugin.marketplaceName || parts.marketplace)
|
|
742
|
+
.plugins.push(claudePluginValue(plugin, installedById.get(id)));
|
|
743
|
+
}
|
|
744
|
+
for (const plugin of installedById.values()) {
|
|
745
|
+
if (seen.has(plugin.id)) continue;
|
|
746
|
+
const parts = splitPluginId(plugin.id);
|
|
747
|
+
ensureGroup(parts.marketplace).plugins.push(claudePluginValue(null, plugin));
|
|
748
|
+
}
|
|
749
|
+
for (const group of groups.values()) {
|
|
750
|
+
group.plugins.sort((left, right) => left.interface.displayName.localeCompare(right.interface.displayName));
|
|
751
|
+
}
|
|
752
|
+
return { marketplaces: [...groups.values()], marketplaceLoadErrors: [] };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function claudePluginCatalog(params, installedOnly = false) {
|
|
756
|
+
const pluginArgs = installedOnly
|
|
757
|
+
? ["plugin", "list", "--json"]
|
|
758
|
+
: ["plugin", "list", "--available", "--json"];
|
|
759
|
+
const [plugins, marketplaces] = await Promise.all([
|
|
760
|
+
runClaudeJson(params, pluginArgs),
|
|
761
|
+
runClaudeJson(params, ["plugin", "marketplace", "list", "--json"]),
|
|
762
|
+
]);
|
|
763
|
+
const installed = installedOnly ? plugins : plugins?.installed;
|
|
764
|
+
const available = installedOnly ? [] : plugins?.available;
|
|
765
|
+
return buildClaudePluginCatalog(installed, available, marketplaces);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function installedClaudePlugin(params, id) {
|
|
769
|
+
const plugins = await runClaudeJson(params, ["plugin", "list", "--json"]);
|
|
770
|
+
return (Array.isArray(plugins) ? plugins : []).find((plugin) => plugin?.id === id);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function claudePluginDetail(params) {
|
|
774
|
+
const requested = String(params.pluginName || params.pluginId || "");
|
|
775
|
+
const raw = await runClaudeJson(params, ["plugin", "list", "--available", "--json"]);
|
|
776
|
+
const installed = (raw?.installed || []).find((plugin) => plugin?.id === requested);
|
|
777
|
+
const available = (raw?.available || []).find((plugin) => plugin?.pluginId === requested);
|
|
778
|
+
const entry = claudePluginValue(available, installed);
|
|
779
|
+
const skills = [];
|
|
780
|
+
if (installed?.installPath) {
|
|
781
|
+
try {
|
|
782
|
+
for (const child of await readdir(join(installed.installPath, "skills"), { withFileTypes: true })) {
|
|
783
|
+
if (child.isDirectory()) skills.push({ name: child.name });
|
|
784
|
+
}
|
|
785
|
+
} catch { /* Plugins do not have to provide skills. */ }
|
|
786
|
+
}
|
|
787
|
+
const mcpServers = installed?.mcpServers && typeof installed.mcpServers === "object"
|
|
788
|
+
? Object.keys(installed.mcpServers).map((name) => ({ name }))
|
|
789
|
+
: [];
|
|
790
|
+
return {
|
|
791
|
+
plugin: {
|
|
792
|
+
summary: entry.interface.shortDescription,
|
|
793
|
+
description: entry.description,
|
|
794
|
+
skills,
|
|
795
|
+
mcpServers,
|
|
796
|
+
apps: [],
|
|
797
|
+
hooks: [],
|
|
798
|
+
scheduledTasks: [],
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function claudeSkills(params) {
|
|
804
|
+
const installed = await runClaudeJson(params, ["plugin", "list", "--json"]);
|
|
805
|
+
const skills = [];
|
|
806
|
+
for (const plugin of Array.isArray(installed) ? installed : []) {
|
|
807
|
+
if (!plugin?.enabled || !plugin.installPath) continue;
|
|
808
|
+
try {
|
|
809
|
+
for (const child of await readdir(join(plugin.installPath, "skills"), { withFileTypes: true })) {
|
|
810
|
+
if (!child.isDirectory()) continue;
|
|
811
|
+
skills.push({
|
|
812
|
+
name: child.name,
|
|
813
|
+
path: join(plugin.installPath, "skills", child.name, "SKILL.md"),
|
|
814
|
+
description: `${plugin.id} plugin skill`,
|
|
815
|
+
enabled: true,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
} catch { /* Plugins do not have to provide skills. */ }
|
|
819
|
+
}
|
|
820
|
+
return { data: [{ cwd: params.cwd || process.cwd(), skills }] };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function isQuestionDeliveryError(error) {
|
|
824
|
+
return error?.code === -32700
|
|
825
|
+
|| String(error?.message || error).includes("사용자 입력 화면에 전달하지 못했습니다");
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function questionFallbackMessage(questions) {
|
|
829
|
+
const text = questions.map((question, index) => {
|
|
830
|
+
const options = question.options
|
|
831
|
+
.map((option, optionIndex) => `${optionIndex + 1}. ${option.label}`)
|
|
832
|
+
.join(" / ");
|
|
833
|
+
return `${index + 1}. ${question.question}${options ? ` (${options})` : ""}`;
|
|
834
|
+
}).join("\n");
|
|
835
|
+
return [
|
|
836
|
+
"사용자 입력 창을 두 번 표시하지 못했습니다.",
|
|
837
|
+
"도구를 다시 호출하지 말고 다음 질문을 일반 텍스트로 사용자에게 제시한 뒤 답변을 기다리세요:",
|
|
838
|
+
text,
|
|
839
|
+
].join("\n");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function createSession(params, resumeId) {
|
|
480
843
|
const id = resumeId || randomUUID();
|
|
481
844
|
const queue = new AsyncQueue();
|
|
482
845
|
const session = {
|
|
@@ -494,7 +857,8 @@ async function createSession(params, resumeId) {
|
|
|
494
857
|
turnSequence: 1,
|
|
495
858
|
itemSequence: 1,
|
|
496
859
|
streamBlocks: new Map(),
|
|
497
|
-
tools: new Map(),
|
|
860
|
+
tools: new Map(),
|
|
861
|
+
permissionDenials: [],
|
|
498
862
|
tasks: new Map(),
|
|
499
863
|
planCreatePending: false,
|
|
500
864
|
subagents: new Map(),
|
|
@@ -1400,9 +1764,16 @@ function toolOutput(content, structured) {
|
|
|
1400
1764
|
return content == null ? "" : JSON.stringify(content, null, 2);
|
|
1401
1765
|
}
|
|
1402
1766
|
|
|
1403
|
-
async function processResult(session, message) {
|
|
1404
|
-
if (!session.turn) return;
|
|
1405
|
-
const
|
|
1767
|
+
async function processResult(session, message) {
|
|
1768
|
+
if (!session.turn) return;
|
|
1769
|
+
for (const denial of Array.isArray(message.permission_denials) ? message.permission_denials : []) {
|
|
1770
|
+
rememberPermissionDenial(session, {
|
|
1771
|
+
tool: denial.tool_name,
|
|
1772
|
+
toolUseId: denial.tool_use_id,
|
|
1773
|
+
input: denial.tool_input,
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
const interrupted = session.turn.interruptRequested === true;
|
|
1406
1777
|
const totals = [...Object.values(message.modelUsage || {})].reduce((sum, usage) => ({
|
|
1407
1778
|
inputTokens: sum.inputTokens + Number(usage.inputTokens || 0) + Number(usage.cacheReadInputTokens || 0) + Number(usage.cacheCreationInputTokens || 0),
|
|
1408
1779
|
cachedInputTokens: sum.cachedInputTokens + Number(usage.cacheReadInputTokens || 0),
|
|
@@ -1421,17 +1792,42 @@ async function processResult(session, message) {
|
|
|
1421
1792
|
modelContextWindow: totals.contextWindow || session.lastContextWindow || undefined,
|
|
1422
1793
|
},
|
|
1423
1794
|
});
|
|
1424
|
-
const error = message.is_error && !interrupted
|
|
1795
|
+
const error = message.is_error && !interrupted
|
|
1425
1796
|
? { message: message.errors?.join("\n") || message.stop_reason || "Claude 실행 실패" }
|
|
1426
1797
|
: null;
|
|
1427
|
-
finishTurn(session, error, message.duration_ms);
|
|
1798
|
+
finishTurn(session, error, message.duration_ms);
|
|
1799
|
+
if (session.restorePermissionMode) {
|
|
1800
|
+
const restore = session.restorePermissionMode;
|
|
1801
|
+
session.restorePermissionMode = null;
|
|
1802
|
+
await applyPermissionMode(session, restore);
|
|
1803
|
+
}
|
|
1428
1804
|
notify("claude/account/updated", {
|
|
1429
1805
|
threadId: session.id,
|
|
1430
1806
|
account: await safeAccount(session.query),
|
|
1431
1807
|
usage: await safeUsage(session.query),
|
|
1432
1808
|
});
|
|
1433
|
-
await runPendingPrompt(session);
|
|
1434
|
-
}
|
|
1809
|
+
await runPendingPrompt(session);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function rememberPermissionDenial(session, denial) {
|
|
1813
|
+
const toolUseId = String(denial.toolUseId || "");
|
|
1814
|
+
const existing = toolUseId
|
|
1815
|
+
? session.permissionDenials.find((candidate) => candidate.toolUseId === toolUseId)
|
|
1816
|
+
: null;
|
|
1817
|
+
if (existing) {
|
|
1818
|
+
existing.tool = String(denial.tool || existing.tool || "Tool");
|
|
1819
|
+
existing.reason = String(denial.reason || existing.reason || "");
|
|
1820
|
+
if (denial.input && typeof denial.input === "object") existing.input = denial.input;
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
session.permissionDenials.push({
|
|
1824
|
+
tool: String(denial.tool || "Tool"),
|
|
1825
|
+
toolUseId,
|
|
1826
|
+
reason: String(denial.reason || ""),
|
|
1827
|
+
input: denial.input && typeof denial.input === "object" ? denial.input : {},
|
|
1828
|
+
});
|
|
1829
|
+
if (session.permissionDenials.length > 20) session.permissionDenials.shift();
|
|
1830
|
+
}
|
|
1435
1831
|
|
|
1436
1832
|
// The turn that was waiting starts on its own, so the host sees it exactly like a
|
|
1437
1833
|
// prompt sent the moment the previous turn ended.
|
|
@@ -1474,10 +1870,16 @@ async function consume(session) {
|
|
|
1474
1870
|
await processStreamEvent(session, message);
|
|
1475
1871
|
} else if (message.type === "assistant") processAssistant(session, message);
|
|
1476
1872
|
else if (message.type === "user") processUser(session, message);
|
|
1477
|
-
else if (message.type === "result") await processResult(session, message);
|
|
1478
|
-
else if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
1479
|
-
notify("thread/compacted", { threadId: session.id });
|
|
1480
|
-
} else if (message.type === "
|
|
1873
|
+
else if (message.type === "result") await processResult(session, message);
|
|
1874
|
+
else if (message.type === "system" && message.subtype === "compact_boundary") {
|
|
1875
|
+
notify("thread/compacted", { threadId: session.id });
|
|
1876
|
+
} else if (message.type === "system" && message.subtype === "permission_denied") {
|
|
1877
|
+
rememberPermissionDenial(session, {
|
|
1878
|
+
tool: message.tool_name,
|
|
1879
|
+
toolUseId: message.tool_use_id,
|
|
1880
|
+
reason: message.decision_reason || message.decision_reason_type,
|
|
1881
|
+
});
|
|
1882
|
+
} else if (message.type === "rate_limit_event") {
|
|
1481
1883
|
notify("claude/account/updated", { threadId: session.id, rateLimitInfo: message.rate_limit_info });
|
|
1482
1884
|
} else if (message.type === "system" && message.subtype === "api_retry") {
|
|
1483
1885
|
notify("warning", { threadId: session.id, provider: "Claude", message: `Claude API 재시도 ${message.attempt}/${message.max_retries}` });
|
|
@@ -1553,8 +1955,19 @@ async function runPrompt(session, params) {
|
|
|
1553
1955
|
await session.query.applyFlagSettings({ effortLevel: effort });
|
|
1554
1956
|
}
|
|
1555
1957
|
session.effort = effort;
|
|
1556
|
-
await applyPermissionMode(session, params.permissionMode);
|
|
1557
|
-
|
|
1958
|
+
const permissionRejection = await applyPermissionMode(session, params.permissionMode);
|
|
1959
|
+
if (permissionRejection) {
|
|
1960
|
+
notify("claude/permissionMode/rejected", {
|
|
1961
|
+
threadId: visibleSession(session.id),
|
|
1962
|
+
permissionMode: params.permissionMode,
|
|
1963
|
+
effectivePermissionMode: session.permissionMode,
|
|
1964
|
+
message: permissionRejection,
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
if (params.restorePermissionMode) {
|
|
1968
|
+
session.restorePermissionMode = permissionMode(params.restorePermissionMode);
|
|
1969
|
+
}
|
|
1970
|
+
const content = await inputContent(params.input, params.handoffContext);
|
|
1558
1971
|
const turnId = beginTurn(session, params.input);
|
|
1559
1972
|
session.queue.push({
|
|
1560
1973
|
type: "user",
|
|
@@ -1825,13 +2238,74 @@ async function readableCwd(id, cwd) {
|
|
|
1825
2238
|
|
|
1826
2239
|
async function dispatch(method, params = {}) {
|
|
1827
2240
|
if (method === "model/list") return loadModelCatalog(params);
|
|
1828
|
-
if (method === "
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2241
|
+
if (method === "plugin/list") return claudePluginCatalog(params);
|
|
2242
|
+
if (method === "plugin/installed") return claudePluginCatalog(params, true);
|
|
2243
|
+
if (method === "plugin/read") return claudePluginDetail(params);
|
|
2244
|
+
if (method === "plugin/install") {
|
|
2245
|
+
const requested = String(params.pluginName || "");
|
|
2246
|
+
const id = requested.includes("@") || !params.remoteMarketplaceName
|
|
2247
|
+
? requested
|
|
2248
|
+
: `${requested}@${params.remoteMarketplaceName}`;
|
|
2249
|
+
if (!id) throw new Error("설치할 Claude 플러그인 이름이 없습니다.");
|
|
2250
|
+
await runClaudeCommand(params, ["plugin", "install", id, "--scope", "user"]);
|
|
2251
|
+
return { pluginId: id, appsNeedingAuth: [] };
|
|
2252
|
+
}
|
|
2253
|
+
if (method === "plugin/uninstall") {
|
|
2254
|
+
const id = String(params.pluginId || "");
|
|
2255
|
+
if (!id) throw new Error("제거할 Claude 플러그인 이름이 없습니다.");
|
|
2256
|
+
const installed = await installedClaudePlugin(params, id);
|
|
2257
|
+
await runClaudeCommand(params, [
|
|
2258
|
+
"plugin", "uninstall", id, "--scope", installed?.scope || "user",
|
|
2259
|
+
]);
|
|
2260
|
+
return {};
|
|
2261
|
+
}
|
|
2262
|
+
if (method === "plugin/set-enabled") {
|
|
2263
|
+
const id = String(params.pluginId || "");
|
|
2264
|
+
if (!id) throw new Error("변경할 Claude 플러그인 이름이 없습니다.");
|
|
2265
|
+
const installed = await installedClaudePlugin(params, id);
|
|
2266
|
+
await runClaudeCommand(params, [
|
|
2267
|
+
"plugin", params.enabled ? "enable" : "disable", id,
|
|
2268
|
+
"--scope", installed?.scope || "user",
|
|
2269
|
+
]);
|
|
2270
|
+
return { effectiveEnabled: Boolean(params.enabled) };
|
|
2271
|
+
}
|
|
2272
|
+
if (method === "marketplace/add") {
|
|
2273
|
+
const ref = params.refName ? `@${params.refName}` : "";
|
|
2274
|
+
const source = `${String(params.source || "")}${ref}`;
|
|
2275
|
+
if (!source) throw new Error("추가할 Claude 마켓플레이스 주소가 없습니다.");
|
|
2276
|
+
await runClaudeCommand(params, ["plugin", "marketplace", "add", source]);
|
|
2277
|
+
return { alreadyAdded: false, installedRoot: source };
|
|
2278
|
+
}
|
|
2279
|
+
if (method === "marketplace/remove") {
|
|
2280
|
+
const name = String(params.marketplaceName || "");
|
|
2281
|
+
if (!name) throw new Error("제거할 Claude 마켓플레이스 이름이 없습니다.");
|
|
2282
|
+
await runClaudeCommand(params, ["plugin", "marketplace", "remove", name]);
|
|
2283
|
+
return {};
|
|
2284
|
+
}
|
|
2285
|
+
if (method === "marketplace/upgrade") {
|
|
2286
|
+
const output = await runClaudeCommand(params, ["plugin", "marketplace", "update"]);
|
|
2287
|
+
return { message: output, consideredRoots: ["Claude marketplaces"], errors: [] };
|
|
1834
2288
|
}
|
|
2289
|
+
if (method === "skills/list") return claudeSkills(params);
|
|
2290
|
+
if (method === "app/list") return { data: [] };
|
|
2291
|
+
if (method === "plugin/reload") {
|
|
2292
|
+
return { message: "Claude 플러그인 설정을 다시 읽었습니다. 새 대화부터 적용됩니다." };
|
|
2293
|
+
}
|
|
2294
|
+
if (method === "permissions/status") return claudePermissionStatus(params);
|
|
2295
|
+
if (method === "permissions/update") return updateClaudePermission(params);
|
|
2296
|
+
if (method === "permissions/auto-mode") return setClaudeAutoModeDisabled(params);
|
|
2297
|
+
if (method === "permissions/retry") return retryClaudePermission(params);
|
|
2298
|
+
if (method === "session/permissionMode") {
|
|
2299
|
+
const session = lookupSession(params.sessionId);
|
|
2300
|
+
// A session that has not started yet picks the mode up from its first turn.
|
|
2301
|
+
if (!session) return { permissionMode: permissionMode(params.permissionMode) };
|
|
2302
|
+
session.restorePermissionMode = null;
|
|
2303
|
+
const rejection = await applyPermissionMode(session, params.permissionMode);
|
|
2304
|
+
return {
|
|
2305
|
+
permissionMode: session.permissionMode,
|
|
2306
|
+
...(rejection ? { rejection } : {}),
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
1835
2309
|
if (method === "session/start") {
|
|
1836
2310
|
const { session, account, usage } = await createSession(params);
|
|
1837
2311
|
return {
|
|
@@ -1984,7 +2458,29 @@ async function dispatch(method, params = {}) {
|
|
|
1984
2458
|
throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
|
|
1985
2459
|
}
|
|
1986
2460
|
|
|
1987
|
-
async function runSelfTest() {
|
|
2461
|
+
async function runSelfTest() {
|
|
2462
|
+
if (permissionMode("dontAsk") !== "dontAsk"
|
|
2463
|
+
|| permissionMode("not-a-mode", "auto") !== "auto") {
|
|
2464
|
+
throw new Error("Claude permission mode self-test failed");
|
|
2465
|
+
}
|
|
2466
|
+
const pluginCatalog = buildClaudePluginCatalog(
|
|
2467
|
+
[{ id: "cloudflare@official", enabled: true, scope: "user" }],
|
|
2468
|
+
[{
|
|
2469
|
+
pluginId: "cloudflare@official",
|
|
2470
|
+
name: "cloudflare",
|
|
2471
|
+
description: "Cloud tools",
|
|
2472
|
+
marketplaceName: "official",
|
|
2473
|
+
}],
|
|
2474
|
+
[{ name: "official", installLocation: "/tmp/official" }],
|
|
2475
|
+
);
|
|
2476
|
+
const plugin = pluginCatalog.marketplaces[0]?.plugins[0];
|
|
2477
|
+
if (plugin?.id !== "cloudflare@official"
|
|
2478
|
+
|| plugin?.name !== "cloudflare@official"
|
|
2479
|
+
|| plugin?.interface?.displayName !== "cloudflare"
|
|
2480
|
+
|| plugin?.installed !== true
|
|
2481
|
+
|| plugin?.enabled !== true) {
|
|
2482
|
+
throw new Error(`Claude plugin catalogue self-test failed: ${JSON.stringify(pluginCatalog)}`);
|
|
2483
|
+
}
|
|
1988
2484
|
const user = (uuid, text) => ({
|
|
1989
2485
|
type: "user",
|
|
1990
2486
|
uuid,
|
|
@@ -2476,20 +2972,20 @@ lines.on("line", async (line) => {
|
|
|
2476
2972
|
write({ method: "warning", params: { provider: "Claude", message: `Claude 브리지 JSON 해석 실패: ${error.message}` } });
|
|
2477
2973
|
return;
|
|
2478
2974
|
}
|
|
2479
|
-
if (typeof message.id === "string" && ("result" in message || "error" in message)) {
|
|
2480
|
-
const pending = pendingHostRequests.get(message.id);
|
|
2481
|
-
if (pending) {
|
|
2482
|
-
pendingHostRequests.delete(message.id);
|
|
2483
|
-
if (message.error) {
|
|
2484
|
-
const error = new Error(message.error.message || "호스트 요청이 거부되었습니다.");
|
|
2485
|
-
error.code = message.error.code;
|
|
2486
|
-
pending.reject(error);
|
|
2487
|
-
} else {
|
|
2488
|
-
pending.resolve(message.result ?? { decision: "decline" });
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2975
|
+
if (typeof message.id === "string" && ("result" in message || "error" in message)) {
|
|
2976
|
+
const pending = pendingHostRequests.get(message.id);
|
|
2977
|
+
if (pending) {
|
|
2978
|
+
pendingHostRequests.delete(message.id);
|
|
2979
|
+
if (message.error) {
|
|
2980
|
+
const error = new Error(message.error.message || "호스트 요청이 거부되었습니다.");
|
|
2981
|
+
error.code = message.error.code;
|
|
2982
|
+
pending.reject(error);
|
|
2983
|
+
} else {
|
|
2984
|
+
pending.resolve(message.result ?? { decision: "decline" });
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2493
2989
|
if (typeof message.id !== "number" || typeof message.method !== "string") return;
|
|
2494
2990
|
try { write({ id: message.id, result: await dispatch(message.method, message.params) }); }
|
|
2495
2991
|
catch (error) { write({ id: message.id, error: rpcError(error) }); }
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "devez-vibe",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"codex",
|
|
7
|
-
"cli",
|
|
8
|
-
"tui",
|
|
9
|
-
"terminal",
|
|
10
|
-
"app-server",
|
|
11
|
-
"claude-agent-sdk"
|
|
12
|
-
],
|
|
13
|
-
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
14
|
-
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
18
|
-
},
|
|
19
|
-
"license": "MIT",
|
|
20
|
-
"bin": {
|
|
21
|
-
"dvz": "bin/dvz.exe"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"bin/dvz.exe",
|
|
25
|
-
"bridge/claude-agent-sdk-bridge.mjs",
|
|
26
|
-
"README.md",
|
|
27
|
-
"LICENSE"
|
|
28
|
-
],
|
|
29
|
-
"os": [
|
|
30
|
-
"win32"
|
|
31
|
-
],
|
|
32
|
-
"cpu": [
|
|
33
|
-
"x64"
|
|
34
|
-
],
|
|
35
|
-
"engines": {
|
|
36
|
-
"node": ">=18"
|
|
37
|
-
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.223"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "devez-vibe",
|
|
3
|
+
"version": "1.3.11",
|
|
4
|
+
"description": "Stable terminal UI for Codex and Claude Agent SDK",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codex",
|
|
7
|
+
"cli",
|
|
8
|
+
"tui",
|
|
9
|
+
"terminal",
|
|
10
|
+
"app-server",
|
|
11
|
+
"claude-agent-sdk"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/MrHoje/Devez-vibe#readme",
|
|
14
|
+
"bugs": "https://github.com/MrHoje/Devez-vibe/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/MrHoje/Devez-vibe.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bin": {
|
|
21
|
+
"dvz": "bin/dvz.exe"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/dvz.exe",
|
|
25
|
+
"bridge/claude-agent-sdk-bridge.mjs",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"os": [
|
|
30
|
+
"win32"
|
|
31
|
+
],
|
|
32
|
+
"cpu": [
|
|
33
|
+
"x64"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.223"
|
|
40
|
+
}
|
|
41
|
+
}
|