@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/README.md +599 -0
- package/index.ts +158 -0
- package/openclaw.plugin.json +20 -0
- package/package.json +38 -0
- package/src/commands/claude-bg.ts +52 -0
- package/src/commands/claude-fg.ts +71 -0
- package/src/commands/claude-kill.ts +42 -0
- package/src/commands/claude-respond.ts +92 -0
- package/src/commands/claude-resume.ts +114 -0
- package/src/commands/claude-sessions.ts +27 -0
- package/src/commands/claude-stats.ts +20 -0
- package/src/commands/claude.ts +61 -0
- package/src/gateway.ts +185 -0
- package/src/notifications.ts +405 -0
- package/src/session-manager.ts +481 -0
- package/src/session.ts +455 -0
- package/src/shared.ts +194 -0
- package/src/tools/claude-bg.ts +100 -0
- package/src/tools/claude-fg.ts +106 -0
- package/src/tools/claude-kill.ts +66 -0
- package/src/tools/claude-launch.ts +173 -0
- package/src/tools/claude-output.ts +80 -0
- package/src/tools/claude-respond.ts +113 -0
- package/src/tools/claude-sessions.ts +63 -0
- package/src/tools/claude-stats.ts +33 -0
- package/src/types.ts +77 -0
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
|
+
}
|