vibe-coding-master 0.8.1 → 0.8.3

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.
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { isVcmRoleName } from "../../shared/constants.js";
2
3
  import { resolveVcmDataDir } from "../vcm-data-dir.js";
3
4
  const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
4
5
  const DEFAULT_GATEWAY_CHANNEL = "weixin-ilink";
@@ -41,6 +42,7 @@ export function createGatewaySettingsService(deps) {
41
42
  translationEnabled: input.translationEnabled ?? current.translationEnabled,
42
43
  currentProjectId: input.currentProjectId !== undefined ? normalizeNullableString(input.currentProjectId) : current.currentProjectId,
43
44
  currentTaskSlug: input.currentTaskSlug !== undefined ? normalizeNullableString(input.currentTaskSlug) : current.currentTaskSlug,
45
+ targetRole: input.targetRole ?? current.targetRole,
44
46
  binding: {
45
47
  ...current.binding,
46
48
  baseUrl: input.baseUrl !== undefined
@@ -71,7 +73,7 @@ export function createGatewaySettingsService(deps) {
71
73
  recentInboundMessageIds: []
72
74
  },
73
75
  pushCursors: {},
74
- latestPmReplies: current.latestPmReplies,
76
+ latestRoleReplies: current.latestRoleReplies,
75
77
  lastPollStatus: {
76
78
  state: "idle"
77
79
  },
@@ -89,6 +91,8 @@ export function createGatewaySettingsService(deps) {
89
91
  translationEnabled: settings.translationEnabled,
90
92
  currentProjectId: settings.currentProjectId,
91
93
  currentTaskSlug: settings.currentTaskSlug,
94
+ targetRole: settings.targetRole,
95
+ targetRoleSessionStatus: null,
92
96
  binding: {
93
97
  accountId: settings.binding.accountId,
94
98
  baseUrl: settings.binding.baseUrl,
@@ -128,9 +132,12 @@ export function normalizeSettings(input, timestamp, options = {
128
132
  ? input.pendingConfirmations
129
133
  : {};
130
134
  const pushCursors = isObject(input.pushCursors) ? input.pushCursors : {};
131
- const latestPmReplies = isObject(input.latestPmReplies)
132
- ? input.latestPmReplies
133
- : {};
135
+ const legacyInput = input;
136
+ const latestRoleReplies = isObject(input.latestRoleReplies)
137
+ ? input.latestRoleReplies
138
+ : isObject(legacyInput.latestPmReplies)
139
+ ? legacyInput.latestPmReplies
140
+ : {};
134
141
  return {
135
142
  version: 1,
136
143
  enabled: input.enabled === true,
@@ -138,6 +145,7 @@ export function normalizeSettings(input, timestamp, options = {
138
145
  translationEnabled: input.translationEnabled !== false,
139
146
  currentProjectId: normalizeNullableString(input.currentProjectId),
140
147
  currentTaskSlug: normalizeNullableString(input.currentTaskSlug),
148
+ targetRole: normalizeTargetRole(input.targetRole),
141
149
  binding: {
142
150
  ...createDefaultBinding(options.defaultBaseUrl),
143
151
  accountId: normalizeNullableString(bindingInput.accountId),
@@ -169,7 +177,7 @@ export function normalizeSettings(input, timestamp, options = {
169
177
  closeTask: normalizeCloseTaskConfirmation(pendingInput.closeTask)
170
178
  },
171
179
  pushCursors: normalizePushCursors(pushCursors),
172
- latestPmReplies: normalizeLatestPmReplies(latestPmReplies),
180
+ latestRoleReplies: normalizeLatestRoleReplies(latestRoleReplies),
173
181
  lastPollStatus: normalizePollStatus(input.lastPollStatus),
174
182
  lastMessageStatus: normalizeMessageStatus(input.lastMessageStatus),
175
183
  updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : timestamp
@@ -200,6 +208,9 @@ function normalizeGatewayChannel(input, fallback) {
200
208
  function normalizeLarkDomain(input) {
201
209
  return input === "lark" || input === "feishu" ? input : null;
202
210
  }
211
+ function normalizeTargetRole(input) {
212
+ return typeof input === "string" && isVcmRoleName(input) ? input : "project-manager";
213
+ }
203
214
  function normalizeCloseTaskConfirmation(input) {
204
215
  if (!isObject(input)) {
205
216
  return null;
@@ -225,14 +236,15 @@ function normalizePushCursors(input) {
225
236
  }
226
237
  return out;
227
238
  }
228
- function normalizeLatestPmReplies(input) {
239
+ function normalizeLatestRoleReplies(input) {
229
240
  const out = {};
230
- for (const [key, value] of Object.entries(input)) {
241
+ for (const value of Object.values(input)) {
231
242
  if (!isObject(value)) {
232
243
  continue;
233
244
  }
234
245
  const repoRoot = normalizeNullableString(value.repoRoot);
235
246
  const taskSlug = normalizeNullableString(value.taskSlug);
247
+ const role = normalizeTargetRole("role" in value ? value.role : undefined);
236
248
  const sessionId = normalizeNullableString(value.sessionId);
237
249
  const claudeSessionId = normalizeNullableString(value.claudeSessionId);
238
250
  const capturedAt = normalizeNullableString(value.capturedAt);
@@ -240,9 +252,10 @@ function normalizeLatestPmReplies(input) {
240
252
  if (!repoRoot || !taskSlug || !sessionId || !claudeSessionId || !capturedAt || !text) {
241
253
  continue;
242
254
  }
243
- out[key] = {
255
+ out[JSON.stringify([repoRoot, taskSlug, role])] = {
244
256
  repoRoot,
245
257
  taskSlug,
258
+ role,
246
259
  sessionId,
247
260
  claudeSessionId,
248
261
  transcriptEventId: normalizeNullableString(value.transcriptEventId),
@@ -14,7 +14,6 @@ import { createCodexBridgeIntegrationService } from "./services/codex-bridge-int
14
14
  import { createAutoMemoryService } from "./services/auto-memory-service.js";
15
15
  import { createArchitectRestartService } from "./services/architect-restart-service.js";
16
16
  import { createRoleContextRestartService } from "./services/role-context-restart-service.js";
17
- import { createRoleStallDetectorService } from "./services/role-stall-detector-service.js";
18
17
  import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
19
18
  import { createGateReviewService } from "./services/gate-review-service.js";
20
19
  import { createHarnessFeedbackService } from "./services/harness-feedback-service.js";
@@ -137,7 +136,6 @@ export async function createServer(deps, options = {}) {
137
136
  roundService: deps.roundService,
138
137
  taskWorkflowService: deps.taskWorkflowService,
139
138
  architectRestartService: deps.architectRestartService,
140
- roleStallDetector: deps.roleStallDetector,
141
139
  translationService: deps.translationService
142
140
  });
143
141
  registerSessionRoutes(app, {
@@ -351,10 +349,6 @@ export function createDefaultServerDeps(options = {}) {
351
349
  sessionService
352
350
  });
353
351
  const transcripts = createClaudeTranscriptService();
354
- const roleStallDetector = createRoleStallDetectorService({
355
- sessionService,
356
- roundService
357
- });
358
352
  const translationService = createTranslationService({
359
353
  runtime,
360
354
  sessionRegistry: registry,
@@ -432,7 +426,6 @@ export function createDefaultServerDeps(options = {}) {
432
426
  translationWorkerService,
433
427
  architectRestartService,
434
428
  roleContextRestartService,
435
- roleStallDetector,
436
429
  workflowControlService
437
430
  });
438
431
  const runtimeCoordinator = createRuntimeCoordinatorService({
@@ -445,7 +438,6 @@ export function createDefaultServerDeps(options = {}) {
445
438
  harnessFeedbackService,
446
439
  autoMemoryService,
447
440
  roundService,
448
- roleStallDetector,
449
441
  gatewayService,
450
442
  async getStateRoot(repoRoot) {
451
443
  return (await projectService.loadConfig(repoRoot)).stateRoot;
@@ -487,7 +479,6 @@ export function createDefaultServerDeps(options = {}) {
487
479
  autoMemoryService,
488
480
  commandDispatcher,
489
481
  claudeHookService,
490
- roleStallDetector,
491
482
  messageService,
492
483
  taskLaunchService,
493
484
  gateReviewService,
@@ -257,12 +257,6 @@ export function createClaudeHookService(deps) {
257
257
  role: input.role,
258
258
  eventName
259
259
  });
260
- await deps.roleStallDetector?.recordHook({
261
- ...createStallContext(context),
262
- role: input.role,
263
- eventName,
264
- event: input.event
265
- });
266
260
  }
267
261
  if (session) {
268
262
  await deps.translationService.recordConversationBoundary({
@@ -360,12 +354,6 @@ export function createClaudeHookService(deps) {
360
354
  }
361
355
  }
362
356
  }
363
- await deps.roleStallDetector?.recordHook({
364
- ...createStallContext(context),
365
- role: input.role,
366
- eventName,
367
- event: input.event
368
- });
369
357
  if (pmAwaitingUser) {
370
358
  return recordTurnEnd(input, context, eventName, {
371
359
  dispatchRouteFiles: false,
@@ -417,12 +405,6 @@ export function createClaudeHookService(deps) {
417
405
  if (memoryResult) {
418
406
  return memoryResult;
419
407
  }
420
- await deps.roleStallDetector?.recordHook({
421
- ...createStallContext(context),
422
- role: input.role,
423
- eventName,
424
- event: input.event
425
- });
426
408
  const routeDispatchInput = createRouteDispatchInput(input, context);
427
409
  const pending = await deps.messageService.listPendingRouteFiles(routeDispatchInput);
428
410
  const hasCompletionEvidence = pending.some((routeFile) => routeFile.fromRole === input.role);
@@ -496,14 +478,6 @@ export function createClaudeHookService(deps) {
496
478
  runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
497
479
  runtimeSessionToken: input.runtimeSessionToken
498
480
  });
499
- if (session) {
500
- await deps.roleStallDetector?.recordHook({
501
- ...createStallContext(context),
502
- role: input.role,
503
- eventName,
504
- event: input.event
505
- });
506
- }
507
481
  return {
508
482
  ok: true,
509
483
  eventName,
@@ -568,8 +542,8 @@ export function createClaudeHookService(deps) {
568
542
  if (eventName === "Stop" && input.role === "architect" && session) {
569
543
  await deps.architectRestartService?.recordArchitectStop(context.project.repoRoot, context.taskSlug, session.id);
570
544
  }
571
- if (options.notifyGateway && session && input.role === "project-manager") {
572
- void deps.gatewayService?.handlePmStop({
545
+ if (options.notifyGateway && session && isVcmRoleName(input.role)) {
546
+ void deps.gatewayService?.handleRoleStop({
573
547
  repoRoot: context.project.repoRoot,
574
548
  taskSlug: context.taskSlug,
575
549
  session
@@ -637,14 +611,6 @@ export function createClaudeHookService(deps) {
637
611
  eventName
638
612
  });
639
613
  }
640
- if (boundToTask) {
641
- await deps.roleStallDetector?.recordHook({
642
- ...createStallContext(context),
643
- role: input.role,
644
- eventName,
645
- event: input.event
646
- });
647
- }
648
614
  await deps.autoMemoryService.handleRoleHook({
649
615
  baseRepoRoot: context.project.repoRoot,
650
616
  taskRepoRoot: context.taskRepoRoot,
@@ -869,31 +835,6 @@ export function createClaudeHookService(deps) {
869
835
  taskSlug: context.taskSlug
870
836
  };
871
837
  }
872
- function createStallContext(context) {
873
- return {
874
- repoRoot: context.project.repoRoot,
875
- taskRepoRoot: context.taskRepoRoot,
876
- stateRoot: context.config.stateRoot,
877
- taskSlug: context.taskSlug
878
- };
879
- }
880
- async function processProgressHook(input, eventName) {
881
- const role = input.role;
882
- if (!isVcmRoleName(role)) {
883
- return completedHookResult(input, eventName);
884
- }
885
- const context = await getHookContext(input);
886
- if (!(await isCurrentRoleHook(context, input, eventName))) {
887
- return completedHookResult(input, eventName);
888
- }
889
- await deps.roleStallDetector?.recordHook({
890
- ...createStallContext(context),
891
- role,
892
- eventName,
893
- event: input.event
894
- });
895
- return completedHookResult(input, eventName);
896
- }
897
838
  function renderStopFailureRecoveryPrompt() {
898
839
  return [
899
840
  "[VCM Recovery]",
@@ -936,17 +877,6 @@ export function createClaudeHookService(deps) {
936
877
  });
937
878
  }
938
879
  const preferences = await deps.appSettings.getPreferences();
939
- if (deps.roleStallDetector) {
940
- const context = await getHookContext(input);
941
- if (await isCurrentRoleHook(context, input, "PermissionRequest")) {
942
- await deps.roleStallDetector.recordHook({
943
- ...createStallContext(context),
944
- role: input.role,
945
- eventName: "PermissionRequest",
946
- event: input.event
947
- });
948
- }
949
- }
950
880
  if (preferences.permissionRequestMode !== "allowAll") {
951
881
  return undefined;
952
882
  }
@@ -963,9 +893,6 @@ export function createClaudeHookService(deps) {
963
893
  async handleHook(input) {
964
894
  return withRoleHookLock(input, async () => {
965
895
  const eventName = parseHookEvent(input.event.hook_event_name);
966
- if (isProgressHook(eventName)) {
967
- return processProgressHook(input, eventName);
968
- }
969
896
  if (isTranslatorToolRoleName(input.role)) {
970
897
  return processTranslatorHook(input);
971
898
  }
@@ -1004,13 +931,6 @@ function parseHookEvent(value) {
1004
931
  if (value === "UserPromptSubmit"
1005
932
  || value === "Stop"
1006
933
  || value === "StopFailure"
1007
- || value === "PreToolUse"
1008
- || value === "PostToolUse"
1009
- || value === "PostToolUseFailure"
1010
- || value === "PostToolBatch"
1011
- || value === "SubagentStart"
1012
- || value === "SubagentStop"
1013
- || value === "PreCompact"
1014
934
  || value === "PostCompact") {
1015
935
  return value;
1016
936
  }
@@ -1021,15 +941,6 @@ function parseHookEvent(value) {
1021
941
  hint: "Use a Claude Code event installed by the VCM Harness."
1022
942
  });
1023
943
  }
1024
- function isProgressHook(eventName) {
1025
- return eventName === "PreToolUse"
1026
- || eventName === "PostToolUse"
1027
- || eventName === "PostToolUseFailure"
1028
- || eventName === "PostToolBatch"
1029
- || eventName === "SubagentStart"
1030
- || eventName === "SubagentStop"
1031
- || eventName === "PreCompact";
1032
- }
1033
944
  function parseBusinessHookEvent(value) {
1034
945
  const eventName = parseHookEvent(value);
1035
946
  if (eventName === "UserPromptSubmit"
@@ -63,15 +63,8 @@ const VCM_HOOK_DEFINITIONS = [
63
63
  { eventName: "PreToolUse", matcher: "Write|Edit", command: VCM_BASH_GUARD_HOOK_COMMAND, timeout: 10 },
64
64
  { eventName: "PreToolUse", matcher: "Agent", command: VCM_SUBAGENT_GUARD_HOOK_COMMAND, timeout: 10 },
65
65
  { eventName: "UserPromptSubmit", command: VCM_HOOK_COMMAND, timeout: 5 },
66
- { eventName: "PreToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
67
- { eventName: "PostToolUse", command: VCM_HOOK_COMMAND, timeout: 5 },
68
- { eventName: "PostToolUseFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
69
- { eventName: "PostToolBatch", command: VCM_HOOK_COMMAND, timeout: 5 },
70
- { eventName: "SubagentStart", command: VCM_HOOK_COMMAND, timeout: 5 },
71
- { eventName: "SubagentStop", command: VCM_HOOK_COMMAND, timeout: 5 },
72
66
  { eventName: "Stop", command: VCM_STOP_HOOK_COMMAND, timeout: 10 },
73
67
  { eventName: "StopFailure", command: VCM_HOOK_COMMAND, timeout: 5 },
74
- { eventName: "PreCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
75
68
  { eventName: "PostCompact", command: VCM_HOOK_COMMAND, timeout: 5 },
76
69
  { eventName: "PermissionRequest", command: VCM_PERMISSION_REQUEST_HOOK_COMMAND, timeout: 5 }
77
70
  ];
@@ -42,17 +42,9 @@ export function createRuntimeCoordinatorService(deps) {
42
42
  ]);
43
43
  const preferences = await deps.appSettings.getPreferences();
44
44
  if (!activeTask) {
45
- deps.roleStallDetector.clearProject(repoRoot);
46
45
  return { activeTask: null, gatewayStatus };
47
46
  }
48
47
  const taskRepoRoot = getTaskRuntimeRepoRoot(activeTask);
49
- const stateRoot = await deps.getStateRoot(repoRoot);
50
- await deps.roleStallDetector.reconcileTask({
51
- repoRoot,
52
- taskRepoRoot,
53
- stateRoot,
54
- taskSlug: activeTask.taskSlug
55
- });
56
48
  const harnessInitialized = await deps.harnessService.getHarnessStatus(taskRepoRoot)
57
49
  .then((status) => status.initialized)
58
50
  .catch(() => false);
@@ -86,7 +78,6 @@ export function createRuntimeCoordinatorService(deps) {
86
78
  void reconcileCurrentProject().catch(() => undefined);
87
79
  },
88
80
  stop() {
89
- deps.roleStallDetector.stop();
90
81
  if (reconcileTimer === undefined) {
91
82
  return;
92
83
  }
@@ -98,7 +89,6 @@ export function createRuntimeCoordinatorService(deps) {
98
89
  async function reconcileCurrentProject() {
99
90
  const project = await deps.projectService.getCurrentProject();
100
91
  if (!project) {
101
- deps.roleStallDetector.stop();
102
92
  return;
103
93
  }
104
94
  await reconcileProject(project.repoRoot);
@@ -721,8 +721,7 @@ export function createSessionService(deps) {
721
721
  return undefined;
722
722
  }
723
723
  const timestamp = now();
724
- const turnWasRunning = current.activityStatus === "running"
725
- && current.expectedRuntimeExitReason !== "role-stall-recovery";
724
+ const turnWasRunning = current.activityStatus === "running";
726
725
  const updated = {
727
726
  ...current,
728
727
  status: input.status,
@@ -738,71 +737,6 @@ export function createSessionService(deps) {
738
737
  await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, updated);
739
738
  return { record: updated, turnWasRunning };
740
739
  }
741
- async function persistTaskRoleRecord(repoRoot, taskSlug, record) {
742
- deps.registry.upsert(record);
743
- const config = await deps.projectService.loadConfig(repoRoot);
744
- const task = await deps.taskService.loadTask(repoRoot, taskSlug);
745
- await persistRoleSessionRecord(deps.fs, repoRoot, getTaskRuntimeRepoRoot(task), config.stateRoot, record);
746
- return record;
747
- }
748
- async function recoverRoleSession(repoRoot, taskSlug, input) {
749
- const current = await getTaskRoleSessionView(repoRoot, taskSlug, input.role);
750
- if (!current
751
- || current.id !== input.expectedSessionId
752
- || current.runtimeSessionToken !== input.expectedRuntimeSessionToken
753
- || current.status !== "running"
754
- || current.activityStatus !== "running") {
755
- throw new VcmError({
756
- code: "ROLE_STALL_SESSION_CHANGED",
757
- message: `${input.role} session changed before recovery could begin.`,
758
- statusCode: 409
759
- });
760
- }
761
- if (!current.claudeSessionId) {
762
- throw new VcmError({
763
- code: "CLAUDE_SESSION_MISSING",
764
- message: `${input.role} does not have a confirmed Claude session id to resume.`,
765
- statusCode: 409
766
- });
767
- }
768
- const prepared = await persistTaskRoleRecord(repoRoot, taskSlug, {
769
- ...current,
770
- expectedRuntimeExitReason: "role-stall-recovery",
771
- updatedAt: now()
772
- });
773
- if (deps.runtime.getSession(prepared.id)) {
774
- await deps.runtime.stop(prepared.id);
775
- }
776
- await persistTaskRoleRecord(repoRoot, taskSlug, {
777
- ...prepared,
778
- status: "exited",
779
- activityStatus: "idle",
780
- pid: undefined,
781
- expectedRuntimeExitReason: undefined,
782
- updatedAt: now()
783
- });
784
- const resumed = await launchRoleSession(repoRoot, taskSlug, input.role, {
785
- permissionMode: prepared.permissionMode,
786
- model: prepared.model,
787
- effort: prepared.effort
788
- }, "resume");
789
- if ((await waitForSessionInputReady(resumed.id)) === "exited") {
790
- throw new VcmError({
791
- code: "ROLE_STALL_RESUME_FAILED",
792
- message: `${input.role} session exited before recovery input could be submitted.`,
793
- statusCode: 409
794
- });
795
- }
796
- await submitTerminalInput(deps.runtime, resumed.id, input.recoveryPrompt);
797
- return persistTaskRoleRecord(repoRoot, taskSlug, {
798
- ...resumed,
799
- activityStatus: "running",
800
- lastTurnStartedAt: now(),
801
- lastHookEventAt: now(),
802
- expectedRuntimeExitReason: undefined,
803
- updatedAt: now()
804
- });
805
- }
806
740
  return {
807
741
  async assertModelLaunchReady(model = "default") {
808
742
  await getModelLaunchEnvironment(normalizeClaudeModel(model));
@@ -1307,8 +1241,7 @@ export function createSessionService(deps) {
1307
1241
  : undefined;
1308
1242
  }
1309
1243
  return markTaskRoleActivityIdle(repoRoot, taskSlug, role);
1310
- },
1311
- recoverRoleSession
1244
+ }
1312
1245
  };
1313
1246
  }
1314
1247
  function toRoleSessionRecordView(record, runtime) {
@@ -1398,7 +1331,7 @@ function isSessionMissingError(error) {
1398
1331
  error.code === "SESSION_MISSING";
1399
1332
  }
1400
1333
  function withoutRuntimeOnlySessionFields(session) {
1401
- const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, expectedRuntimeExitReason: _expectedRuntimeExitReason, ...persisted } = session;
1334
+ const { pid: _pid, runtimeSessionToken: _runtimeSessionToken, ...persisted } = session;
1402
1335
  return persisted;
1403
1336
  }
1404
1337
  function defaultIsProcessAlive(pid) {
@@ -1675,7 +1608,8 @@ function normalizeClaudePermissionMode(value) {
1675
1608
  return "default";
1676
1609
  }
1677
1610
  function normalizeClaudeModel(value) {
1678
- if (value === "opus"
1611
+ if (value === "claude-fable-5-1"
1612
+ || value === "opus"
1679
1613
  || value === "claude-opus-4-8"
1680
1614
  || value === "sonnet"
1681
1615
  || value === "fable"
@@ -1096,7 +1096,7 @@ export function createTranslationService(deps) {
1096
1096
  code: "GATEWAY_TRANSLATION_RESULT_MISSING",
1097
1097
  message: "Gateway output translation is not available in the current translation panel.",
1098
1098
  statusCode: 409,
1099
- hint: "Wait for the translation panel to finish translating the PM final reply, then retry from Gateway."
1099
+ hint: "Wait for the translation panel to finish translating the selected role's Round Final Reply, then retry from Gateway."
1100
1100
  });
1101
1101
  },
1102
1102
  async shouldDelayFlowPauseNotification(input) {
@@ -23,10 +23,17 @@ export const CLAUDE_MODEL_OPTIONS = [
23
23
  source: "claude",
24
24
  available: true
25
25
  },
26
+ {
27
+ value: "claude-fable-5-1",
28
+ label: "Fable 5.1",
29
+ description: "Pinned Claude Fable 5.1",
30
+ source: "claude",
31
+ available: true
32
+ },
26
33
  {
27
34
  value: "fable",
28
- label: "Fable",
29
- description: "Claude Fable",
35
+ label: "Fable Alias",
36
+ description: "Claude Code provider-selected Fable",
30
37
  source: "claude",
31
38
  available: true
32
39
  },