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 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
- listSessions,
15
- startup,
16
- } from "@anthropic-ai/claude-agent-sdk";
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
- isDefault: Boolean(defaultResolvedModel) && resolved === defaultResolvedModel,
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 = ["default", "acceptEdits", "plan", "auto", "bypassPermissions"];
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
- } catch (error) {
313
- notify("claude/permissionMode/rejected", {
314
- threadId: visibleSession(session.id),
315
- permissionMode: mode,
316
- message: error?.message || String(error),
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. Devez Vibe already auto-allows
328
- // tools through `canUseTool`, so allowing the mode to be *reachable* grants
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
- // Plan mode only means anything if leaving it is the user's call — the CLI
414
- // shows the plan and waits. The blanket allow below would answer for them.
415
- const planApproval = toolName === "ExitPlanMode";
416
-
417
- // Devez Vibe runs in its existing full-access profile. Claude's callback is
418
- // still kept so AskUserQuestion and explicit user-authored ask rules reach UI.
419
- if (!permission.matchedAskRule && !planApproval) {
420
- return { behavior: "allow", updatedInput: input };
421
- }
422
-
423
- let method = "item/permissions/requestApproval";
424
- let params = {
425
- reason: permission.decisionReason || permission.description || permission.title,
426
- permissions: { tool: toolName, blockedPath: permission.blockedPath },
427
- };
428
- if (planApproval) {
429
- params = {
430
- reason: input.plan || permission.description || "계획대로 진행할까요?",
431
- permissions: { tool: toolName },
432
- };
433
- } else if (toolName === "Bash") {
434
- method = "item/commandExecution/requestApproval";
435
- params = {
436
- command: input.command || "command",
437
- cwd: input.cwd,
438
- reason: permission.decisionReason || permission.description,
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
- grantRoot: permission.blockedPath || input.file_path || input.notebook_path,
444
- reason: permission.decisionReason || permission.description,
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 isQuestionDeliveryError(error) {
461
- return error?.code === -32700
462
- || String(error?.message || error).includes("사용자 입력 화면에 전달하지 못했습니다");
463
- }
464
-
465
- function questionFallbackMessage(questions) {
466
- const text = questions.map((question, index) => {
467
- const options = question.options
468
- .map((option, optionIndex) => `${optionIndex + 1}. ${option.label}`)
469
- .join(" / ");
470
- return `${index + 1}. ${question.question}${options ? ` (${options})` : ""}`;
471
- }).join("\n");
472
- return [
473
- "사용자 입력 창을 두 번 표시하지 못했습니다.",
474
- "도구를 다시 호출하지 말고 다음 질문을 일반 텍스트로 사용자에게 제시한 뒤 답변을 기다리세요:",
475
- text,
476
- ].join("\n");
477
- }
478
-
479
- async function createSession(params, resumeId) {
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 interrupted = session.turn.interruptRequested === true;
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 === "rate_limit_event") {
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
- const content = await inputContent(params.input, params.handoffContext);
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 === "session/permissionMode") {
1829
- const session = lookupSession(params.sessionId);
1830
- // A session that has not started yet picks the mode up from its first turn.
1831
- if (!session) return { permissionMode: permissionMode(params.permissionMode) };
1832
- await applyPermissionMode(session, params.permissionMode);
1833
- return { permissionMode: session.permissionMode };
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.9",
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
+ }