mirror of
https://github.com/aljazceru/claude-code-viewer.git
synced 2025-12-18 22:04:19 +01:00
fix: disable tool approve for old claude code version
This commit is contained in:
15
biome.json
15
biome.json
@@ -25,7 +25,10 @@
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noProcessEnv": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
@@ -37,5 +40,13 @@
|
||||
"parser": {
|
||||
"allowComments": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["e2e/**"],
|
||||
"linter": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
||||
dependencies:
|
||||
'@anthropic-ai/claude-code':
|
||||
specifier: ^1.0.98
|
||||
version: 1.0.98
|
||||
version: 1.0.128
|
||||
'@hono/zod-validator':
|
||||
specifier: ^0.7.2
|
||||
version: 0.7.2(hono@4.9.5)(zod@4.1.5)
|
||||
@@ -151,8 +151,8 @@ packages:
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@anthropic-ai/claude-code@1.0.98':
|
||||
resolution: {integrity: sha512-IV193Eh8STdRcN3VkNcojPIlLnQPch+doBVrDSEV1rPPePISy7pzHFZL0Eg7zIPj9gHkHV1D2s0RMMwzVXJThA==}
|
||||
'@anthropic-ai/claude-code@1.0.128':
|
||||
resolution: {integrity: sha512-uUg5cFMJfeQetQzFw76Vpbro6DAXst2Lpu8aoZWRFSoQVYu5ZSAnbBoxaWmW/IgnHSqIIvtMwzCoqmcA9j9rNQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3338,7 +3338,7 @@ snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@anthropic-ai/claude-code@1.0.98':
|
||||
'@anthropic-ai/claude-code@1.0.128':
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.33.5
|
||||
'@img/sharp-darwin-x64': 0.33.5
|
||||
|
||||
@@ -33,7 +33,11 @@ export const useNewChatMutation = (
|
||||
onSuccess: async (response) => {
|
||||
onSuccess?.();
|
||||
router.push(
|
||||
`/projects/${projectId}/sessions/${response.sessionId}#message-${response.userMessageId}`,
|
||||
`/projects/${projectId}/sessions/${response.sessionId}` +
|
||||
response.userMessageId !==
|
||||
undefined
|
||||
? `#message-${response.userMessageId}`
|
||||
: "",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useServerEventListener } from "@/lib/sse/hook/useServerEventListener";
|
||||
import type {
|
||||
PermissionRequest,
|
||||
PermissionResponse,
|
||||
} from "@/types/permissions";
|
||||
import { honoClient } from "../lib/api/client";
|
||||
|
||||
export const usePermissionRequests = () => {
|
||||
const [currentPermissionRequest, setCurrentPermissionRequest] =
|
||||
@@ -23,12 +25,10 @@ export const usePermissionRequests = () => {
|
||||
const handlePermissionResponse = useCallback(
|
||||
async (response: PermissionResponse) => {
|
||||
try {
|
||||
const apiResponse = await fetch("/api/tasks/permission-response", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(response),
|
||||
const apiResponse = await honoClient.api.tasks[
|
||||
"permission-response"
|
||||
].$post({
|
||||
json: response,
|
||||
});
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
@@ -40,7 +40,7 @@ export const usePermissionRequests = () => {
|
||||
setCurrentPermissionRequest(null);
|
||||
} catch (error) {
|
||||
console.error("Error sending permission response:", error);
|
||||
// TODO: Show error toast to user
|
||||
toast.error("Failed to send permission response");
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { hc } from "hono/client";
|
||||
import type { RouteType } from "../../server/hono/route";
|
||||
import { env } from "../../server/lib/env";
|
||||
|
||||
export const honoClient = hc<RouteType>(
|
||||
typeof window === "undefined"
|
||||
? // biome-ignore lint/complexity/useLiteralKeys: TypeScript restriction
|
||||
`http://localhost:${process.env["PORT"] ?? 3000}/`
|
||||
: "/",
|
||||
typeof window === "undefined" ? `http://localhost:${env.get("PORT")}/` : "/",
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { setCookie } from "hono/cookie";
|
||||
import { streamSSE } from "hono/streaming";
|
||||
import { z } from "zod";
|
||||
import { type Config, configSchema } from "../config/config";
|
||||
import { env } from "../lib/env";
|
||||
import { ClaudeCodeTaskController } from "../service/claude-code/ClaudeCodeTaskController";
|
||||
import type { SerializableAliveTask } from "../service/claude-code/types";
|
||||
import { adaptInternalEventToSSE } from "../service/events/adaptInternalEventToSSE";
|
||||
@@ -39,8 +40,7 @@ export const routes = (app: HonoAppType) => {
|
||||
const fileWatcher = getFileWatcher();
|
||||
const eventBus = getEventBus();
|
||||
|
||||
// biome-ignore lint/complexity/useLiteralKeys: env var
|
||||
if (process.env["NEXT_PHASE"] !== "phase-production-build") {
|
||||
if (env.get("NEXT_PHASE") !== "phase-production-build") {
|
||||
fileWatcher.startWatching();
|
||||
|
||||
setInterval(() => {
|
||||
@@ -167,7 +167,7 @@ export const routes = (app: HonoAppType) => {
|
||||
"query",
|
||||
z.object({
|
||||
basePath: z.string().optional().default("/"),
|
||||
}),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
@@ -182,14 +182,14 @@ export const routes = (app: HonoAppType) => {
|
||||
try {
|
||||
const result = await getFileCompletion(
|
||||
project.meta.projectPath,
|
||||
basePath,
|
||||
basePath
|
||||
);
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
console.error("File completion error:", error);
|
||||
return c.json({ error: "Failed to get file completion" }, 500);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
.get("/projects/:projectId/claude-commands", async (c) => {
|
||||
@@ -202,18 +202,18 @@ export const routes = (app: HonoAppType) => {
|
||||
}).then((dirents) =>
|
||||
dirents
|
||||
.filter((d) => d.isFile() && d.name.endsWith(".md"))
|
||||
.map((d) => d.name.replace(/\.md$/, "")),
|
||||
.map((d) => d.name.replace(/\.md$/, ""))
|
||||
),
|
||||
project.meta.projectPath !== null
|
||||
? readdir(
|
||||
resolve(project.meta.projectPath, ".claude", "commands"),
|
||||
{
|
||||
withFileTypes: true,
|
||||
},
|
||||
}
|
||||
).then((dirents) =>
|
||||
dirents
|
||||
.filter((d) => d.isFile() && d.name.endsWith(".md"))
|
||||
.map((d) => d.name.replace(/\.md$/, "")),
|
||||
.map((d) => d.name.replace(/\.md$/, ""))
|
||||
)
|
||||
: [],
|
||||
]);
|
||||
@@ -274,7 +274,7 @@ export const routes = (app: HonoAppType) => {
|
||||
z.object({
|
||||
fromRef: z.string().min(1, "fromRef is required"),
|
||||
toRef: z.string().min(1, "toRef is required"),
|
||||
}),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
@@ -289,7 +289,7 @@ export const routes = (app: HonoAppType) => {
|
||||
const result = await getDiff(
|
||||
project.meta.projectPath,
|
||||
fromRef,
|
||||
toRef,
|
||||
toRef
|
||||
);
|
||||
return c.json(result);
|
||||
} catch (error) {
|
||||
@@ -299,7 +299,7 @@ export const routes = (app: HonoAppType) => {
|
||||
}
|
||||
return c.json({ error: "Failed to get diff" }, 500);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
.get("/mcp/list", async (c) => {
|
||||
@@ -313,7 +313,7 @@ export const routes = (app: HonoAppType) => {
|
||||
"json",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const { projectId } = c.req.param();
|
||||
@@ -325,13 +325,13 @@ export const routes = (app: HonoAppType) => {
|
||||
}
|
||||
|
||||
const task = await getTaskController(
|
||||
c.get("config"),
|
||||
c.get("config")
|
||||
).startOrContinueTask(
|
||||
{
|
||||
projectId,
|
||||
cwd: project.meta.projectPath,
|
||||
},
|
||||
message,
|
||||
message
|
||||
);
|
||||
|
||||
return c.json({
|
||||
@@ -339,7 +339,7 @@ export const routes = (app: HonoAppType) => {
|
||||
sessionId: task.sessionId,
|
||||
userMessageId: task.userMessageId,
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
.post(
|
||||
@@ -348,7 +348,7 @@ export const routes = (app: HonoAppType) => {
|
||||
"json",
|
||||
z.object({
|
||||
resumeMessage: z.string(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const { projectId, sessionId } = c.req.param();
|
||||
@@ -360,14 +360,14 @@ export const routes = (app: HonoAppType) => {
|
||||
}
|
||||
|
||||
const task = await getTaskController(
|
||||
c.get("config"),
|
||||
c.get("config")
|
||||
).startOrContinueTask(
|
||||
{
|
||||
projectId,
|
||||
sessionId,
|
||||
cwd: project.meta.projectPath,
|
||||
},
|
||||
resumeMessage,
|
||||
resumeMessage
|
||||
);
|
||||
|
||||
return c.json({
|
||||
@@ -375,7 +375,7 @@ export const routes = (app: HonoAppType) => {
|
||||
sessionId: task.sessionId,
|
||||
userMessageId: task.userMessageId,
|
||||
});
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
.get("/tasks/alive", async (c) => {
|
||||
@@ -386,7 +386,7 @@ export const routes = (app: HonoAppType) => {
|
||||
status: task.status,
|
||||
sessionId: task.sessionId,
|
||||
userMessageId: task.userMessageId,
|
||||
}),
|
||||
})
|
||||
),
|
||||
});
|
||||
})
|
||||
@@ -398,7 +398,7 @@ export const routes = (app: HonoAppType) => {
|
||||
const { sessionId } = c.req.valid("json");
|
||||
getTaskController(c.get("config")).abortTask(sessionId);
|
||||
return c.json({ message: "Task aborted" });
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
.post(
|
||||
@@ -408,15 +408,15 @@ export const routes = (app: HonoAppType) => {
|
||||
z.object({
|
||||
permissionRequestId: z.string(),
|
||||
decision: z.enum(["allow", "deny"]),
|
||||
}),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const permissionResponse = c.req.valid("json");
|
||||
getTaskController(c.get("config")).respondToPermissionRequest(
|
||||
permissionResponse,
|
||||
permissionResponse
|
||||
);
|
||||
return c.json({ message: "Permission response received" });
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
.get("/sse", async (c) => {
|
||||
@@ -426,7 +426,7 @@ export const routes = (app: HonoAppType) => {
|
||||
const stream = writeTypeSafeSSE(rawStream);
|
||||
|
||||
const onSessionListChanged = (
|
||||
event: InternalEventDeclaration["sessionListChanged"],
|
||||
event: InternalEventDeclaration["sessionListChanged"]
|
||||
) => {
|
||||
stream.writeSSE("sessionListChanged", {
|
||||
projectId: event.projectId,
|
||||
@@ -434,7 +434,7 @@ export const routes = (app: HonoAppType) => {
|
||||
};
|
||||
|
||||
const onSessionChanged = (
|
||||
event: InternalEventDeclaration["sessionChanged"],
|
||||
event: InternalEventDeclaration["sessionChanged"]
|
||||
) => {
|
||||
stream.writeSSE("sessionChanged", {
|
||||
projectId: event.projectId,
|
||||
@@ -443,7 +443,7 @@ export const routes = (app: HonoAppType) => {
|
||||
};
|
||||
|
||||
const onTaskChanged = (
|
||||
event: InternalEventDeclaration["taskChanged"],
|
||||
event: InternalEventDeclaration["taskChanged"]
|
||||
) => {
|
||||
stream.writeSSE("taskChanged", {
|
||||
aliveTasks: event.aliveTasks,
|
||||
@@ -467,7 +467,7 @@ export const routes = (app: HonoAppType) => {
|
||||
},
|
||||
async (err) => {
|
||||
console.error("Streaming error:", err);
|
||||
},
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
23
src/server/lib/env/index.ts
vendored
Normal file
23
src/server/lib/env/index.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type EnvSchema, envSchema } from "./schema";
|
||||
|
||||
const parseEnv = () => {
|
||||
// biome-ignore lint/style/noProcessEnv: allow only here
|
||||
const parsed = envSchema.safeParse(process.env);
|
||||
if (!parsed.success) {
|
||||
console.error(parsed.error);
|
||||
throw new Error(`Invalid environment variables: ${parsed.error.message}`);
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
export const env = (() => {
|
||||
let parsedEnv: EnvSchema | undefined;
|
||||
|
||||
return {
|
||||
get: <Key extends keyof EnvSchema>(key: Key): EnvSchema[Key] => {
|
||||
parsedEnv ??= parseEnv();
|
||||
return parsedEnv[key];
|
||||
},
|
||||
};
|
||||
})();
|
||||
18
src/server/lib/env/schema.ts
vendored
Normal file
18
src/server/lib/env/schema.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const envSchema = z.object({
|
||||
NODE_ENV: z
|
||||
.enum(["development", "production", "test"])
|
||||
.optional()
|
||||
.default("development"),
|
||||
CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH: z.string().optional(),
|
||||
GLOBAL_CLAUDE_DIR: z.string().optional(),
|
||||
NEXT_PHASE: z.string().optional(),
|
||||
PORT: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("3000")
|
||||
.transform((val) => parseInt(val, 10)),
|
||||
});
|
||||
|
||||
export type EnvSchema = z.infer<typeof envSchema>;
|
||||
51
src/server/service/claude-code/ClaudeCodeExecutor.ts
Normal file
51
src/server/service/claude-code/ClaudeCodeExecutor.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { query } from "@anthropic-ai/claude-code";
|
||||
import { env } from "../../lib/env";
|
||||
import { ClaudeCodeVersion } from "./ClaudeCodeVersion";
|
||||
|
||||
type CCQuery = typeof query;
|
||||
type CCQueryPrompt = Parameters<CCQuery>[0]["prompt"];
|
||||
type CCQueryOptions = NonNullable<Parameters<CCQuery>[0]["options"]>;
|
||||
|
||||
export class ClaudeCodeExecutor {
|
||||
private pathToClaudeCodeExecutable: string;
|
||||
private claudeCodeVersion: ClaudeCodeVersion | null;
|
||||
|
||||
constructor() {
|
||||
const executablePath = env.get("CLAUDE_CODE_VIEWER_CC_EXECUTABLE_PATH");
|
||||
this.pathToClaudeCodeExecutable =
|
||||
executablePath !== undefined
|
||||
? resolve(executablePath)
|
||||
: execSync("which claude", {}).toString().trim();
|
||||
this.claudeCodeVersion = ClaudeCodeVersion.fromCLIString(
|
||||
execSync(`${this.pathToClaudeCodeExecutable} --version`, {}).toString()
|
||||
);
|
||||
}
|
||||
|
||||
public get features() {
|
||||
return {
|
||||
enableToolApproval:
|
||||
this.claudeCodeVersion?.greaterThanOrEqual(
|
||||
new ClaudeCodeVersion({ major: 1, minor: 0, patch: 82 })
|
||||
) ?? false,
|
||||
extractUuidFromSDKMessage:
|
||||
this.claudeCodeVersion?.greaterThanOrEqual(
|
||||
new ClaudeCodeVersion({ major: 1, minor: 0, patch: 86 })
|
||||
) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public query(prompt: CCQueryPrompt, options: CCQueryOptions) {
|
||||
const { canUseTool, ...baseOptions } = options;
|
||||
|
||||
return query({
|
||||
prompt,
|
||||
options: {
|
||||
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
||||
...baseOptions,
|
||||
...(this.features.enableToolApproval ? { canUseTool } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { query } from "@anthropic-ai/claude-code";
|
||||
import prexit from "prexit";
|
||||
import { ulid } from "ulid";
|
||||
import type { Config } from "../../config/config";
|
||||
import { getEventBus, type IEventBus } from "../events/EventBus";
|
||||
import { ClaudeCodeExecutor } from "./ClaudeCodeExecutor";
|
||||
import { createMessageGenerator } from "./createMessageGenerator";
|
||||
import type {
|
||||
AliveClaudeCodeTask,
|
||||
@@ -15,7 +14,7 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
export class ClaudeCodeTaskController {
|
||||
private pathToClaudeCodeExecutable: string;
|
||||
private claudeCode: ClaudeCodeExecutor;
|
||||
private tasks: ClaudeCodeTask[] = [];
|
||||
private eventBus: IEventBus;
|
||||
private config: Config;
|
||||
@@ -23,9 +22,7 @@ export class ClaudeCodeTaskController {
|
||||
private permissionResponses: Map<string, PermissionResponse> = new Map();
|
||||
|
||||
constructor(config: Config) {
|
||||
this.pathToClaudeCodeExecutable = execSync("which claude", {})
|
||||
.toString()
|
||||
.trim();
|
||||
this.claudeCode = new ClaudeCodeExecutor();
|
||||
this.eventBus = getEventBus();
|
||||
this.config = config;
|
||||
|
||||
@@ -49,7 +46,7 @@ export class ClaudeCodeTaskController {
|
||||
return async (
|
||||
toolName: string,
|
||||
toolInput: Record<string, unknown>,
|
||||
_options: { signal: AbortSignal },
|
||||
_options: { signal: AbortSignal }
|
||||
) => {
|
||||
// If not in default mode, use the configured permission mode behavior
|
||||
if (this.config.permissionMode !== "default") {
|
||||
@@ -84,7 +81,7 @@ export class ClaudeCodeTaskController {
|
||||
// Store the request
|
||||
this.pendingPermissionRequests.set(
|
||||
permissionRequest.id,
|
||||
permissionRequest,
|
||||
permissionRequest
|
||||
);
|
||||
|
||||
// Emit event to notify UI
|
||||
@@ -95,7 +92,7 @@ export class ClaudeCodeTaskController {
|
||||
// Wait for user response with timeout
|
||||
const response = await this.waitForPermissionResponse(
|
||||
permissionRequest.id,
|
||||
60000,
|
||||
60000
|
||||
); // 60 second timeout
|
||||
|
||||
if (response) {
|
||||
@@ -123,7 +120,7 @@ export class ClaudeCodeTaskController {
|
||||
|
||||
private async waitForPermissionResponse(
|
||||
permissionRequestId: string,
|
||||
timeoutMs: number,
|
||||
timeoutMs: number
|
||||
): Promise<PermissionResponse | null> {
|
||||
return new Promise((resolve) => {
|
||||
const checkResponse = () => {
|
||||
@@ -156,7 +153,7 @@ export class ClaudeCodeTaskController {
|
||||
|
||||
public get aliveTasks() {
|
||||
return this.tasks.filter(
|
||||
(task) => task.status === "running" || task.status === "paused",
|
||||
(task) => task.status === "running" || task.status === "paused"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,16 +163,18 @@ export class ClaudeCodeTaskController {
|
||||
projectId: string;
|
||||
sessionId?: string;
|
||||
},
|
||||
message: string,
|
||||
message: string
|
||||
): Promise<AliveClaudeCodeTask> {
|
||||
const existingTask = this.aliveTasks.find(
|
||||
(task) => task.sessionId === currentSession.sessionId,
|
||||
(task) => task.sessionId === currentSession.sessionId
|
||||
);
|
||||
|
||||
if (existingTask) {
|
||||
return await this.continueTask(existingTask, message);
|
||||
const result = await this.continueTask(existingTask, message);
|
||||
return result;
|
||||
} else {
|
||||
return await this.startTask(currentSession, message);
|
||||
const result = await this.startTask(currentSession, message);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +190,7 @@ export class ClaudeCodeTaskController {
|
||||
projectId: string;
|
||||
sessionId?: string;
|
||||
},
|
||||
message: string,
|
||||
message: string
|
||||
) {
|
||||
const {
|
||||
generateMessages,
|
||||
@@ -222,7 +221,7 @@ export class ClaudeCodeTaskController {
|
||||
(resolve, reject) => {
|
||||
aliveTaskResolve = resolve;
|
||||
aliveTaskReject = reject;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
let resolved = false;
|
||||
@@ -233,35 +232,40 @@ export class ClaudeCodeTaskController {
|
||||
|
||||
let currentTask: AliveClaudeCodeTask | undefined;
|
||||
|
||||
for await (const message of query({
|
||||
prompt: task.generateMessages(),
|
||||
options: {
|
||||
for await (const message of this.claudeCode.query(
|
||||
task.generateMessages(),
|
||||
{
|
||||
resume: task.baseSessionId,
|
||||
cwd: task.cwd,
|
||||
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
||||
permissionMode: this.config.permissionMode,
|
||||
canUseTool: this.createCanUseToolCallback(
|
||||
task.id,
|
||||
task.baseSessionId,
|
||||
task.baseSessionId
|
||||
),
|
||||
abortController: abortController,
|
||||
},
|
||||
})) {
|
||||
}
|
||||
)) {
|
||||
currentTask ??= this.aliveTasks.find((t) => t.id === task.id);
|
||||
|
||||
if (currentTask !== undefined && currentTask.status === "paused") {
|
||||
this.updateExistingTask({
|
||||
this.upsertExistingTask({
|
||||
...currentTask,
|
||||
status: "running",
|
||||
});
|
||||
}
|
||||
|
||||
// 初回の system message だとまだ history ファイルが作成されていないので
|
||||
if (
|
||||
(message.type === "user" || message.type === "assistant") &&
|
||||
message.uuid !== undefined
|
||||
) {
|
||||
if (message.type === "user" || message.type === "assistant") {
|
||||
// 本来は message.uuid の存在チェックをしたいが、古いバージョンでは存在しないことがある
|
||||
console.log(
|
||||
"[DEBUG startTask] 9. Processing user/assistant message"
|
||||
);
|
||||
|
||||
if (!resolved) {
|
||||
console.log(
|
||||
"[DEBUG startTask] 10. Resolving task for first time"
|
||||
);
|
||||
|
||||
const runningTask: RunningClaudeCodeTask = {
|
||||
status: "running",
|
||||
id: task.id,
|
||||
@@ -278,8 +282,14 @@ export class ClaudeCodeTaskController {
|
||||
abortController: abortController,
|
||||
};
|
||||
this.tasks.push(runningTask);
|
||||
console.log(
|
||||
"[DEBUG startTask] 11. About to call aliveTaskResolve"
|
||||
);
|
||||
aliveTaskResolve(runningTask);
|
||||
resolved = true;
|
||||
console.log(
|
||||
"[DEBUG startTask] 12. aliveTaskResolve called, resolved=true"
|
||||
);
|
||||
}
|
||||
|
||||
resolveFirstMessage();
|
||||
@@ -288,11 +298,14 @@ export class ClaudeCodeTaskController {
|
||||
await Promise.all(
|
||||
task.onMessageHandlers.map(async (onMessageHandler) => {
|
||||
await onMessageHandler(message);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (currentTask !== undefined && message.type === "result") {
|
||||
this.updateExistingTask({
|
||||
console.log(
|
||||
"[DEBUG startTask] 15. Result message received, pausing task"
|
||||
);
|
||||
this.upsertExistingTask({
|
||||
...currentTask,
|
||||
status: "paused",
|
||||
});
|
||||
@@ -304,25 +317,38 @@ export class ClaudeCodeTaskController {
|
||||
const updatedTask = this.aliveTasks.find((t) => t.id === task.id);
|
||||
|
||||
if (updatedTask === undefined) {
|
||||
console.log(
|
||||
"[DEBUG startTask] 17. ERROR: Task not found in aliveTasks"
|
||||
);
|
||||
const error = new Error(
|
||||
`illegal state: task is not running, task: ${JSON.stringify(updatedTask)}`,
|
||||
`illegal state: task is not running, task: ${JSON.stringify(
|
||||
updatedTask
|
||||
)}`
|
||||
);
|
||||
aliveTaskReject(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.updateExistingTask({
|
||||
this.upsertExistingTask({
|
||||
...updatedTask,
|
||||
status: "completed",
|
||||
});
|
||||
} catch (error) {
|
||||
if (!resolved) {
|
||||
console.log(
|
||||
"[DEBUG startTask] 20. Rejecting task (not yet resolved)"
|
||||
);
|
||||
aliveTaskReject(error);
|
||||
resolved = true;
|
||||
}
|
||||
|
||||
console.error("Error resuming task", error);
|
||||
this.updateExistingTask({
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message, error.stack);
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
this.upsertExistingTask({
|
||||
...task,
|
||||
status: "failed",
|
||||
});
|
||||
@@ -342,7 +368,7 @@ export class ClaudeCodeTaskController {
|
||||
}
|
||||
|
||||
task.abortController.abort();
|
||||
this.updateExistingTask({
|
||||
this.upsertExistingTask({
|
||||
id: task.id,
|
||||
projectId: task.projectId,
|
||||
sessionId: task.sessionId,
|
||||
@@ -359,15 +385,16 @@ export class ClaudeCodeTaskController {
|
||||
});
|
||||
}
|
||||
|
||||
private updateExistingTask(task: ClaudeCodeTask) {
|
||||
private upsertExistingTask(task: ClaudeCodeTask) {
|
||||
const target = this.tasks.find((t) => t.id === task.id);
|
||||
|
||||
if (!target) {
|
||||
throw new Error("Task not found");
|
||||
console.error("Task not found", task);
|
||||
this.tasks.push(task);
|
||||
} else {
|
||||
Object.assign(target, task);
|
||||
}
|
||||
|
||||
Object.assign(target, task);
|
||||
|
||||
if (task.status === "paused" || task.status === "running") {
|
||||
this.eventBus.emit("taskChanged", {
|
||||
aliveTasks: this.aliveTasks,
|
||||
|
||||
67
src/server/service/claude-code/ClaudeCodeVersion.ts
Normal file
67
src/server/service/claude-code/ClaudeCodeVersion.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const versionRegex = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/;
|
||||
const versionSchema = z
|
||||
.object({
|
||||
major: z.string().transform((value) => Number.parseInt(value, 10)),
|
||||
minor: z.string().transform((value) => Number.parseInt(value, 10)),
|
||||
patch: z.string().transform((value) => Number.parseInt(value, 10)),
|
||||
})
|
||||
.refine((data) =>
|
||||
[data.major, data.minor, data.patch].every((value) => !Number.isNaN(value)),
|
||||
);
|
||||
|
||||
type ParsedVersion = z.infer<typeof versionSchema>;
|
||||
|
||||
export class ClaudeCodeVersion {
|
||||
public constructor(public readonly version: ParsedVersion) {}
|
||||
|
||||
public static fromCLIString(version: string) {
|
||||
const groups = version.trim().match(versionRegex)?.groups;
|
||||
|
||||
if (groups === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = versionSchema.safeParse(groups);
|
||||
if (!parsed.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ClaudeCodeVersion(parsed.data);
|
||||
}
|
||||
|
||||
public get major() {
|
||||
return this.version.major;
|
||||
}
|
||||
|
||||
public get minor() {
|
||||
return this.version.minor;
|
||||
}
|
||||
|
||||
public get patch() {
|
||||
return this.version.patch;
|
||||
}
|
||||
|
||||
public equals(other: ClaudeCodeVersion) {
|
||||
return (
|
||||
this.version.major === other.version.major &&
|
||||
this.version.minor === other.version.minor &&
|
||||
this.version.patch === other.version.patch
|
||||
);
|
||||
}
|
||||
|
||||
public greaterThan(other: ClaudeCodeVersion) {
|
||||
return (
|
||||
this.version.major > other.version.major ||
|
||||
(this.version.major === other.version.major &&
|
||||
(this.version.minor > other.version.minor ||
|
||||
(this.version.minor === other.version.minor &&
|
||||
this.version.patch > other.version.patch)))
|
||||
);
|
||||
}
|
||||
|
||||
public greaterThanOrEqual(other: ClaudeCodeVersion) {
|
||||
return this.equals(other) || this.greaterThan(other);
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,21 @@ export type PendingClaudeCodeTask = BaseClaudeCodeTask & {
|
||||
export type RunningClaudeCodeTask = BaseClaudeCodeTask & {
|
||||
status: "running";
|
||||
sessionId: string;
|
||||
userMessageId: string;
|
||||
userMessageId: string | undefined;
|
||||
abortController: AbortController;
|
||||
};
|
||||
|
||||
export type PausedClaudeCodeTask = BaseClaudeCodeTask & {
|
||||
status: "paused";
|
||||
sessionId: string;
|
||||
userMessageId: string;
|
||||
userMessageId: string | undefined;
|
||||
abortController: AbortController;
|
||||
};
|
||||
|
||||
type CompletedClaudeCodeTask = BaseClaudeCodeTask & {
|
||||
status: "completed";
|
||||
sessionId: string;
|
||||
userMessageId: string;
|
||||
userMessageId: string | undefined;
|
||||
abortController: AbortController;
|
||||
resolveFirstMessage: () => void;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ class EventBus {
|
||||
|
||||
public emit<EventName extends keyof InternalEventDeclaration>(
|
||||
event: EventName,
|
||||
data: InternalEventDeclaration[EventName],
|
||||
data: InternalEventDeclaration[EventName]
|
||||
): void {
|
||||
this.emitter.emit(event, {
|
||||
...data,
|
||||
@@ -20,8 +20,8 @@ class EventBus {
|
||||
public on<EventName extends keyof InternalEventDeclaration>(
|
||||
event: EventName,
|
||||
listener: (
|
||||
data: InternalEventDeclaration[EventName],
|
||||
) => void | Promise<void>,
|
||||
data: InternalEventDeclaration[EventName]
|
||||
) => void | Promise<void>
|
||||
): void {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
@@ -29,8 +29,8 @@ class EventBus {
|
||||
public off<EventName extends keyof InternalEventDeclaration>(
|
||||
event: EventName,
|
||||
listener: (
|
||||
data: InternalEventDeclaration[EventName],
|
||||
) => void | Promise<void>,
|
||||
data: InternalEventDeclaration[EventName]
|
||||
) => void | Promise<void>
|
||||
): void {
|
||||
this.emitter.off(event, listener);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { homedir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { env } from "../lib/env";
|
||||
|
||||
// biome-ignore lint/complexity/useLiteralKeys: typescript restriction
|
||||
const GLOBAL_CLAUDE_DIR = process.env["GLOBAL_CLAUDE_DIR"];
|
||||
const GLOBAL_CLAUDE_DIR = env.get("GLOBAL_CLAUDE_DIR");
|
||||
|
||||
export const globalClaudeDirectoryPath =
|
||||
GLOBAL_CLAUDE_DIR === undefined
|
||||
|
||||
Reference in New Issue
Block a user