[[🦉Toki]]に `toki claude` コマンドとして実装。実行権限つけてこのスクリプトを実行すればOK。ベースは[[Claude Code]]に作成してもらいつつも、型定義追加したり全体をリファクタリングしたりは自分で行った。今後のことも考えると自分でやったほうがいいなと感じる。
> [!thinking] [[シバン]]で[[Deno]]コマンドとpermission指定するのは新しい発見だった...
```ts
#!/usr/bin/env -S deno run --allow-read --allow-env
import { join } from "https://deno.land/
[email protected]/path/mod.ts";
interface UserLog {
type: "user";
message: {
role: "user";
content: string | Content[];
};
}
interface AssistantLog {
type: "assistant";
message: {
type: "message";
content: Content[];
};
}
interface SummaryLog {
type: "summary";
}
type Log = UserLog | AssistantLog | SummaryLog;
interface TextContent {
type: "text";
text: string;
}
interface ToolUserContent {
type: "tool_use";
id: string;
name: string;
input: {
command: string;
description: string;
};
}
interface ToolResultContent {
type: "tool_result";
too_use_id: string;
content: string;
is_error: boolean;
}
type Content = TextContent | ToolUserContent | ToolResultContent;
function main() {
const specifiedFile = Deno.args[0];
let latestLog: string;
const claudeBaseDir = join(Deno.env.get("HOME") || "", ".claude/projects");
try {
Deno.statSync(claudeBaseDir);
} catch {
console.error(
`Claude Code設定ディレクトリが見つかりません: ${claudeBaseDir}`,
);
Deno.exit(1);
}
const currentPath = Deno.cwd()
.replace(/^\//, "-")
.replaceAll("/", "-")
.replaceAll(".", "-");
const projectDir = join(claudeBaseDir, currentPath);
if (specifiedFile) {
latestLog = `${projectDir}/${specifiedFile}`;
try {
Deno.statSync(latestLog);
} catch {
console.error(`指定されたファイルが見つかりません: ${latestLog}`);
Deno.exit(1);
}
} else {
try {
const entries = Array.from(Deno.readDirSync(projectDir))
.filter((entry) => entry.name.endsWith(".jsonl"))
.map((entry) => {
const stat = Deno.statSync(join(projectDir, entry.name));
return { name: entry.name, mtime: stat.mtime };
})
.sort((a, b) => (b.mtime?.getTime() || 0) - (a.mtime?.getTime() || 0));
if (entries.length === 0) {
console.error("会話ログファイルが見つかりません");
Deno.exit(1);
}
latestLog = join(projectDir, entries[0].name);
} catch {
console.error("会話ログファイルが見つかりません");
Deno.exit(1);
}
}
const contents = Deno.readTextFileSync(latestLog)
.trim()
.split("\n")
.map((line) => {
const log = JSON.parse(line) as Log;
if (log.type === "summary") {
return null;
}
const texts = typeof log.message.content === "string"
? [log.message.content]
: log.message.content
.filter((content) => content.type === "text")
.map((x) => x.text)
.map((x) =>
x === "[Request interrupted by user for tool use]"
? "**作業中の割り込み指示:**\nちょっと待ってください。一度中断してください。"
: x
);
if (texts.length === 0) {
return null;
}
const header = log.type === "user"
? `[!right-bubble] ![[minerva-face-right.webp]]`
: `[!left-bubble] ![[claude-san-face.webp]]`;
const messages = texts
.map((m) => `> ${m}`.replaceAll(/\n/g, "\n> "))
.join("\n");
return `> ${header}
${messages}`;
})
.filter((x) => x !== null);
console.log(contents.join("\n\n"));
}
if (import.meta.main) {
main();
}
```