@betrue/openclaw-claude-code-plugin 1.0.0

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/index.ts ADDED
@@ -0,0 +1,158 @@
1
+ import { registerClaudeLaunchTool } from "./src/tools/claude-launch";
2
+ import { registerClaudeSessionsTool } from "./src/tools/claude-sessions";
3
+ import { registerClaudeKillTool } from "./src/tools/claude-kill";
4
+ import { registerClaudeOutputTool } from "./src/tools/claude-output";
5
+ import { registerClaudeFgTool } from "./src/tools/claude-fg";
6
+ import { registerClaudeBgTool } from "./src/tools/claude-bg";
7
+ import { registerClaudeRespondTool } from "./src/tools/claude-respond";
8
+ import { registerClaudeStatsTool } from "./src/tools/claude-stats";
9
+ import { registerClaudeCommand } from "./src/commands/claude";
10
+ import { registerClaudeSessionsCommand } from "./src/commands/claude-sessions";
11
+ import { registerClaudeKillCommand } from "./src/commands/claude-kill";
12
+ import { registerClaudeFgCommand } from "./src/commands/claude-fg";
13
+ import { registerClaudeBgCommand } from "./src/commands/claude-bg";
14
+ import { registerClaudeResumeCommand } from "./src/commands/claude-resume";
15
+ import { registerClaudeRespondCommand } from "./src/commands/claude-respond";
16
+ import { registerClaudeStatsCommand } from "./src/commands/claude-stats";
17
+ import { registerGatewayMethods } from "./src/gateway";
18
+ import { SessionManager } from "./src/session-manager";
19
+ import { NotificationRouter } from "./src/notifications";
20
+ import { setSessionManager, setNotificationRouter, setPluginConfig, pluginConfig } from "./src/shared";
21
+ import { execFile } from "child_process";
22
+
23
+ // Plugin register function - called by OpenClaw when loading the plugin
24
+ export function register(api: any) {
25
+ // Local references for service lifecycle
26
+ let sm: SessionManager | null = null;
27
+ let nr: NotificationRouter | null = null;
28
+ let cleanupInterval: ReturnType<typeof setInterval> | null = null;
29
+
30
+ // Tools
31
+ registerClaudeLaunchTool(api);
32
+ registerClaudeSessionsTool(api);
33
+ registerClaudeKillTool(api);
34
+ registerClaudeOutputTool(api);
35
+ registerClaudeFgTool(api);
36
+ registerClaudeBgTool(api);
37
+ registerClaudeRespondTool(api);
38
+ registerClaudeStatsTool(api);
39
+
40
+ // Commands
41
+ registerClaudeCommand(api);
42
+ registerClaudeSessionsCommand(api);
43
+ registerClaudeKillCommand(api);
44
+ registerClaudeFgCommand(api);
45
+ registerClaudeBgCommand(api);
46
+ registerClaudeResumeCommand(api);
47
+ registerClaudeRespondCommand(api);
48
+ registerClaudeStatsCommand(api);
49
+
50
+ // Gateway RPC methods (Task 17)
51
+ registerGatewayMethods(api);
52
+
53
+ // Service
54
+ api.registerService({
55
+ id: "claude-code",
56
+ start: () => {
57
+ const config = api.getConfig?.() ?? {};
58
+
59
+ // Store config globally for all modules (Task 20)
60
+ setPluginConfig(config);
61
+
62
+ // Create SessionManager — uses config for maxSessions and maxPersistedSessions
63
+ sm = new SessionManager(
64
+ pluginConfig.maxSessions,
65
+ pluginConfig.maxPersistedSessions,
66
+ );
67
+ setSessionManager(sm);
68
+
69
+ // Create NotificationRouter with OpenClaw's outbound message pipeline.
70
+ // Uses `openclaw message send` CLI since the plugin API does not expose
71
+ // a runtime.sendMessage method for proactive outbound messages.
72
+ //
73
+ // channelId format: "telegram:<chatId>" or just a bare chat ID.
74
+ // Fallback channel comes from config.fallbackChannel (e.g. "telegram:123456789").
75
+
76
+ const sendMessage = (channelId: string, text: string) => {
77
+ // Resolve channel and target from channelId
78
+ // Expected formats:
79
+ // "telegram:123456789" → channel=telegram, target=123456789
80
+ // "discord:987654321" → channel=discord, target=987654321
81
+ // "123456789" → channel=telegram (fallback), target=123456789
82
+ // "toolu_xxxx" → not a real channel, use fallback
83
+
84
+ // Parse fallbackChannel from config (e.g. "telegram:123456789")
85
+ let fallbackChannel = "telegram";
86
+ let fallbackTarget = "";
87
+ if (pluginConfig.fallbackChannel?.includes(":")) {
88
+ const [fc, ft] = pluginConfig.fallbackChannel.split(":", 2);
89
+ if (fc && ft) { fallbackChannel = fc; fallbackTarget = ft; }
90
+ }
91
+
92
+ let channel = fallbackChannel;
93
+ let target = fallbackTarget;
94
+
95
+ if (channelId === "unknown" || !channelId) {
96
+ // Tool-launched sessions have originChannel="unknown" — always use fallback
97
+ if (fallbackTarget) {
98
+ console.log(`[claude-code] sendMessage: channelId="${channelId}", using fallback ${fallbackChannel}:${fallbackTarget}`);
99
+ } else {
100
+ console.warn(`[claude-code] sendMessage: channelId="${channelId}" and no fallbackChannel configured — message will not be sent`);
101
+ return;
102
+ }
103
+ } else if (channelId.includes(":")) {
104
+ const [ch, tgt] = channelId.split(":", 2);
105
+ if (ch && tgt) {
106
+ channel = ch;
107
+ target = tgt;
108
+ }
109
+ } else if (/^-?\d+$/.test(channelId)) {
110
+ // Bare numeric ID — assume Telegram chat ID
111
+ channel = "telegram";
112
+ target = channelId;
113
+ } else if (fallbackTarget) {
114
+ // Non-numeric, non-structured ID (e.g. "toolu_xxx", "command")
115
+ // Use fallback from config
116
+ console.log(`[claude-code] sendMessage: unrecognized channelId="${channelId}", using fallback ${fallbackChannel}:${fallbackTarget}`);
117
+ } else {
118
+ console.warn(`[claude-code] sendMessage: unrecognized channelId="${channelId}" and no fallbackChannel configured — message will not be sent`);
119
+ return;
120
+ }
121
+
122
+ console.log(`[claude-code] sendMessage -> channel=${channel}, target=${target}, textLen=${text.length}`);
123
+
124
+ execFile("openclaw", ["message", "send", "--channel", channel, "--target", target, "-m", text], { timeout: 15_000 }, (err, stdout, stderr) => {
125
+ if (err) {
126
+ console.error(`[claude-code] sendMessage CLI ERROR: ${err.message}`);
127
+ if (stderr) console.error(`[claude-code] sendMessage CLI STDERR: ${stderr}`);
128
+ } else {
129
+ console.log(`[claude-code] sendMessage CLI OK -> channel=${channel}, target=${target}`);
130
+ if (stdout.trim()) console.log(`[claude-code] sendMessage CLI STDOUT: ${stdout.trim()}`);
131
+ }
132
+ });
133
+ };
134
+
135
+ nr = new NotificationRouter(sendMessage);
136
+ setNotificationRouter(nr);
137
+
138
+ // Wire NotificationRouter into SessionManager
139
+ sm.notificationRouter = nr;
140
+
141
+ // Start the long-running session reminder check
142
+ nr.startReminderCheck(() => sm?.list("running") ?? []);
143
+
144
+ // GC interval
145
+ cleanupInterval = setInterval(() => sm!.cleanup(), 5 * 60 * 1000);
146
+ },
147
+ stop: () => {
148
+ if (nr) nr.stop();
149
+ if (sm) sm.killAll();
150
+ if (cleanupInterval) clearInterval(cleanupInterval);
151
+ cleanupInterval = null;
152
+ sm = null;
153
+ nr = null;
154
+ setSessionManager(null);
155
+ setNotificationRouter(null);
156
+ },
157
+ });
158
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "id": "claude-code",
3
+ "name": "Claude Code Plugin",
4
+ "description": "Orchestrate Claude Code sessions from OpenClaw",
5
+ "version": "1.0.0",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "maxSessions": { "type": "number", "default": 5 },
11
+ "defaultBudgetUsd": { "type": "number", "default": 5 },
12
+ "defaultModel": { "type": "string", "description": "Default model for new sessions (e.g. 'sonnet', 'opus')" },
13
+ "defaultWorkdir": { "type": "string", "description": "Default working directory for new sessions" },
14
+ "idleTimeoutMinutes": { "type": "number", "default": 30, "description": "Idle timeout in minutes for multi-turn sessions before auto-kill" },
15
+ "maxPersistedSessions": { "type": "number", "default": 50, "description": "Maximum number of completed sessions to keep in memory for resume" },
16
+ "fallbackChannel": { "type": "string", "description": "Fallback notification channel (e.g. 'telegram:123456789')" },
17
+ "permissionMode": { "type": "string", "default": "bypassPermissions", "enum": ["default", "plan", "acceptEdits", "bypassPermissions"], "description": "Default permission mode for Claude Code sessions" }
18
+ }
19
+ }
20
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@betrue/openclaw-claude-code-plugin",
3
+ "version": "1.0.0",
4
+ "type": "commonjs",
5
+ "dependencies": {
6
+ "@anthropic-ai/claude-agent-sdk": "0.2.37",
7
+ "@sinclair/typebox": "^0.34.48",
8
+ "nanoid": "^3.3.7"
9
+ },
10
+ "main": "index.ts",
11
+ "files": [
12
+ "index.ts",
13
+ "src/",
14
+ "openclaw.plugin.json",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "openclaw",
19
+ "plugin",
20
+ "claude",
21
+ "claude-code",
22
+ "coding-agent",
23
+ "ai"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/alizarion/openclaw-claude-code-plugin.git"
28
+ },
29
+ "homepage": "https://github.com/alizarion/openclaw-claude-code-plugin#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/alizarion/openclaw-claude-code-plugin/issues"
32
+ },
33
+ "license": "MIT",
34
+ "author": "Betrue <hello@betrue.fr>",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
@@ -0,0 +1,52 @@
1
+ import { sessionManager, resolveOriginChannel } from "../shared";
2
+
3
+ export function registerClaudeBgCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_bg",
6
+ description: "Send the current foreground session back to background",
7
+ acceptsArgs: true,
8
+ requireAuth: true,
9
+ handler: (ctx: any) => {
10
+ if (!sessionManager) {
11
+ return {
12
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
13
+ };
14
+ }
15
+
16
+ const channelId = resolveOriginChannel(ctx);
17
+
18
+ // If a specific session is given, detach it
19
+ const ref = ctx.args?.trim();
20
+ if (ref) {
21
+ const session = sessionManager.resolve(ref);
22
+ if (!session) {
23
+ return { text: `Error: Session "${ref}" not found.` };
24
+ }
25
+ session.saveFgOutputOffset(channelId);
26
+ session.foregroundChannels.delete(channelId);
27
+ return {
28
+ text: `Session ${session.name} [${session.id}] moved to background.`,
29
+ };
30
+ }
31
+
32
+ // No argument — detach all foreground sessions for this channel
33
+ const allSessions = sessionManager.list("all");
34
+ const fgSessions = allSessions.filter((s) =>
35
+ s.foregroundChannels.has(channelId),
36
+ );
37
+
38
+ if (fgSessions.length === 0) {
39
+ return { text: "No session is currently in foreground." };
40
+ }
41
+
42
+ const names: string[] = [];
43
+ for (const s of fgSessions) {
44
+ s.saveFgOutputOffset(channelId);
45
+ s.foregroundChannels.delete(channelId);
46
+ names.push(`${s.name} [${s.id}]`);
47
+ }
48
+
49
+ return { text: `Moved to background: ${names.join(", ")}` };
50
+ },
51
+ });
52
+ }
@@ -0,0 +1,71 @@
1
+ import { sessionManager, formatDuration, resolveOriginChannel } from "../shared";
2
+
3
+ export function registerClaudeFgCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_fg",
6
+ description: "Bring a Claude Code session to foreground by name or ID",
7
+ acceptsArgs: true,
8
+ requireAuth: true,
9
+ handler: (ctx: any) => {
10
+ if (!sessionManager) {
11
+ return {
12
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
13
+ };
14
+ }
15
+
16
+ const ref = ctx.args?.trim();
17
+ if (!ref) {
18
+ return { text: "Usage: /claude_fg <name-or-id>" };
19
+ }
20
+
21
+ const session = sessionManager.resolve(ref);
22
+ if (!session) {
23
+ return { text: `Error: Session "${ref}" not found.` };
24
+ }
25
+
26
+ // Mark as foreground using the resolved channel (e.g. "telegram:123456")
27
+ const channelId = resolveOriginChannel(ctx);
28
+
29
+ // Get catchup output (produced while this channel was backgrounded)
30
+ const catchupLines = session.getCatchupOutput(channelId);
31
+
32
+ session.foregroundChannels.add(channelId);
33
+
34
+ const duration = formatDuration(session.duration);
35
+
36
+ const header = [
37
+ `Session ${session.name} [${session.id}] now in foreground.`,
38
+ `Status: ${session.status.toUpperCase()} | Duration: ${duration}`,
39
+ `${"─".repeat(60)}`,
40
+ ].join("\n");
41
+
42
+ // Build catchup section if there's missed output
43
+ let catchupSection = "";
44
+ if (catchupLines.length > 0) {
45
+ catchupSection = [
46
+ `📋 Catchup (${catchupLines.length} missed output${catchupLines.length === 1 ? "" : "s"}):`,
47
+ catchupLines.join("\n"),
48
+ `${"─".repeat(60)}`,
49
+ ].join("\n");
50
+ }
51
+
52
+ // If no catchup, fall back to showing recent lines
53
+ const body =
54
+ catchupLines.length > 0
55
+ ? catchupSection
56
+ : (session.getOutput(30).length > 0
57
+ ? session.getOutput(30).join("\n")
58
+ : "(no output yet)");
59
+
60
+ const footer =
61
+ session.status === "running" || session.status === "starting"
62
+ ? `\n${"─".repeat(60)}\nStreaming... Use /claude_bg to detach.`
63
+ : `\n${"─".repeat(60)}\nSession is ${session.status}.`;
64
+
65
+ // Mark that this channel has now seen all output up to this point
66
+ session.markFgOutputSeen(channelId);
67
+
68
+ return { text: `${header}\n${body}${footer}` };
69
+ },
70
+ });
71
+ }
@@ -0,0 +1,42 @@
1
+ import { sessionManager } from "../shared";
2
+
3
+ export function registerClaudeKillCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_kill",
6
+ description: "Kill a Claude Code session by name or ID",
7
+ acceptsArgs: true,
8
+ requireAuth: true,
9
+ handler: (ctx: any) => {
10
+ if (!sessionManager) {
11
+ return {
12
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
13
+ };
14
+ }
15
+
16
+ const ref = ctx.args?.trim();
17
+ if (!ref) {
18
+ return { text: "Usage: /claude_kill <name-or-id>" };
19
+ }
20
+
21
+ const session = sessionManager.resolve(ref);
22
+
23
+ if (!session) {
24
+ return { text: `Error: Session "${ref}" not found.` };
25
+ }
26
+
27
+ if (
28
+ session.status === "completed" ||
29
+ session.status === "failed" ||
30
+ session.status === "killed"
31
+ ) {
32
+ return {
33
+ text: `Session ${session.name} [${session.id}] is already ${session.status}. No action needed.`,
34
+ };
35
+ }
36
+
37
+ sessionManager.kill(session.id);
38
+
39
+ return { text: `Session ${session.name} [${session.id}] has been terminated.` };
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,92 @@
1
+ import { sessionManager, notificationRouter } from "../shared";
2
+
3
+ export function registerClaudeRespondCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_respond",
6
+ description:
7
+ "Send a follow-up message to a running Claude Code session. Usage: /claude_respond <id-or-name> <message>",
8
+ acceptsArgs: true,
9
+ requireAuth: true,
10
+ handler: async (ctx: any) => {
11
+ if (!sessionManager) {
12
+ return {
13
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
14
+ };
15
+ }
16
+
17
+ const args = (ctx.args ?? "").trim();
18
+ if (!args) {
19
+ return {
20
+ text: "Usage: /claude_respond <id-or-name> <message>\n /claude_respond --interrupt <id-or-name> <message>",
21
+ };
22
+ }
23
+
24
+ // Parse optional --interrupt flag
25
+ let interrupt = false;
26
+ let remaining = args;
27
+ if (remaining.startsWith("--interrupt ")) {
28
+ interrupt = true;
29
+ remaining = remaining.slice("--interrupt ".length).trim();
30
+ }
31
+
32
+ // Parse: first word is the session ref, rest is the message
33
+ const spaceIdx = remaining.indexOf(" ");
34
+ if (spaceIdx === -1) {
35
+ return {
36
+ text: "Error: Missing message. Usage: /claude_respond <id-or-name> <message>",
37
+ };
38
+ }
39
+
40
+ const ref = remaining.slice(0, spaceIdx);
41
+ const message = remaining.slice(spaceIdx + 1).trim();
42
+
43
+ if (!message) {
44
+ return {
45
+ text: "Error: Empty message. Usage: /claude_respond <id-or-name> <message>",
46
+ };
47
+ }
48
+
49
+ const session = sessionManager.resolve(ref);
50
+ if (!session) {
51
+ return { text: `Error: Session "${ref}" not found.` };
52
+ }
53
+
54
+ if (session.status !== "running") {
55
+ return {
56
+ text: `Error: Session ${session.name} [${session.id}] is not running (status: ${session.status}).`,
57
+ };
58
+ }
59
+
60
+ try {
61
+ if (interrupt) {
62
+ await session.interrupt();
63
+ }
64
+ await session.sendMessage(message);
65
+
66
+ // Display the response in the origin channel so the conversation is visible
67
+ if (notificationRouter && session.originChannel) {
68
+ const respondMsg = [
69
+ `↩️ [${session.name}] Responded:`,
70
+ message,
71
+ ].join("\n");
72
+ notificationRouter.emitToChannel(session.originChannel, respondMsg);
73
+ }
74
+
75
+ const msgSummary =
76
+ message.length > 80 ? message.slice(0, 80) + "..." : message;
77
+
78
+ return {
79
+ text: [
80
+ `Message sent to ${session.name} [${session.id}].`,
81
+ interrupt ? ` (interrupted current turn)` : "",
82
+ ` "${msgSummary}"`,
83
+ ]
84
+ .filter(Boolean)
85
+ .join("\n"),
86
+ };
87
+ } catch (err: any) {
88
+ return { text: `Error: ${err.message}` };
89
+ }
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,114 @@
1
+ import { sessionManager, resolveOriginChannel, formatDuration } from "../shared";
2
+
3
+ export function registerClaudeResumeCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_resume",
6
+ description:
7
+ "Resume a previous Claude Code session. Usage: /claude_resume <id-or-name> [prompt] or /claude_resume --list to see resumable sessions.",
8
+ acceptsArgs: true,
9
+ requireAuth: true,
10
+ handler: (ctx: any) => {
11
+ if (!sessionManager) {
12
+ return {
13
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
14
+ };
15
+ }
16
+
17
+ let args = (ctx.args ?? "").trim();
18
+ if (!args) {
19
+ return {
20
+ text: "Usage: /claude_resume <id-or-name> [prompt]\n /claude_resume --list — list resumable sessions\n /claude_resume --fork <id-or-name> [prompt] — fork instead of continuing",
21
+ };
22
+ }
23
+
24
+ // Handle --list flag
25
+ if (args === "--list") {
26
+ const persisted = sessionManager.listPersistedSessions();
27
+ if (persisted.length === 0) {
28
+ return { text: "No resumable sessions found. Sessions are persisted after completion." };
29
+ }
30
+
31
+ const lines = persisted.map((info) => {
32
+ const promptSummary =
33
+ info.prompt.length > 60
34
+ ? info.prompt.slice(0, 60) + "..."
35
+ : info.prompt;
36
+ const completedStr = info.completedAt
37
+ ? `completed ${formatDuration(Date.now() - info.completedAt)} ago`
38
+ : info.status;
39
+ return [
40
+ ` ${info.name} — ${completedStr}`,
41
+ ` Claude ID: ${info.claudeSessionId}`,
42
+ ` 📁 ${info.workdir}`,
43
+ ` 📝 "${promptSummary}"`,
44
+ ].join("\n");
45
+ });
46
+
47
+ return {
48
+ text: `Resumable sessions:\n\n${lines.join("\n\n")}`,
49
+ };
50
+ }
51
+
52
+ // Parse --fork flag
53
+ let fork = false;
54
+ if (args.startsWith("--fork ")) {
55
+ fork = true;
56
+ args = args.slice("--fork ".length).trim();
57
+ }
58
+
59
+ // Parse: first word is the session ref, rest is the prompt
60
+ const spaceIdx = args.indexOf(" ");
61
+ let ref: string;
62
+ let prompt: string;
63
+ if (spaceIdx === -1) {
64
+ ref = args;
65
+ prompt = "Continue where you left off.";
66
+ } else {
67
+ ref = args.slice(0, spaceIdx);
68
+ prompt = args.slice(spaceIdx + 1).trim() || "Continue where you left off.";
69
+ }
70
+
71
+ // Resolve the Claude session ID
72
+ const claudeSessionId = sessionManager.resolveClaudeSessionId(ref);
73
+ if (!claudeSessionId) {
74
+ return {
75
+ text: `Error: Could not find a Claude session ID for "${ref}".\nUse /claude_resume --list to see available sessions.`,
76
+ };
77
+ }
78
+
79
+ const config = ctx.config ?? {};
80
+
81
+ // Look up persisted info for workdir
82
+ const persisted = sessionManager.getPersistedSession(ref);
83
+ const workdir = persisted?.workdir ?? process.cwd();
84
+
85
+ try {
86
+ const session = sessionManager.spawn({
87
+ prompt,
88
+ workdir,
89
+ model: persisted?.model ?? config.defaultModel,
90
+ maxBudgetUsd: config.defaultBudgetUsd ?? 5,
91
+ resumeSessionId: claudeSessionId,
92
+ forkSession: fork,
93
+ originChannel: resolveOriginChannel(ctx),
94
+ });
95
+
96
+ const promptSummary =
97
+ prompt.length > 80 ? prompt.slice(0, 80) + "..." : prompt;
98
+
99
+ return {
100
+ text: [
101
+ `Session resumed${fork ? " (forked)" : ""}.`,
102
+ ` Name: ${session.name}`,
103
+ ` ID: ${session.id}`,
104
+ ` Resume from: ${claudeSessionId}`,
105
+ ` Dir: ${workdir}`,
106
+ ` Prompt: "${promptSummary}"`,
107
+ ].join("\n"),
108
+ };
109
+ } catch (err: any) {
110
+ return { text: `Error: ${err.message}` };
111
+ }
112
+ },
113
+ });
114
+ }
@@ -0,0 +1,27 @@
1
+ import { sessionManager, formatSessionListing } from "../shared";
2
+
3
+ export function registerClaudeSessionsCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_sessions",
6
+ description: "List all Claude Code sessions",
7
+ acceptsArgs: false,
8
+ requireAuth: true,
9
+ handler: () => {
10
+ if (!sessionManager) {
11
+ return {
12
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
13
+ };
14
+ }
15
+
16
+ const sessions = sessionManager.list("all");
17
+
18
+ if (sessions.length === 0) {
19
+ return { text: "No sessions found." };
20
+ }
21
+
22
+ const lines = sessions.map(formatSessionListing);
23
+
24
+ return { text: lines.join("\n\n") };
25
+ },
26
+ });
27
+ }
@@ -0,0 +1,20 @@
1
+ import { sessionManager, formatStats } from "../shared";
2
+
3
+ export function registerClaudeStatsCommand(api: any): void {
4
+ api.registerCommand({
5
+ name: "claude_stats",
6
+ description: "Show Claude Code Plugin usage metrics",
7
+ acceptsArgs: false,
8
+ requireAuth: true,
9
+ handler: () => {
10
+ if (!sessionManager) {
11
+ return {
12
+ text: "Error: SessionManager not initialized. The claude-code service must be running.",
13
+ };
14
+ }
15
+
16
+ const metrics = sessionManager.getMetrics();
17
+ return { text: formatStats(metrics) };
18
+ },
19
+ });
20
+ }