@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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager, resolveOriginChannel } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeBgTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_bg",
|
|
8
|
+
description:
|
|
9
|
+
"Send a Claude Code session back to background (stop streaming). If no session specified, detaches whichever session is currently in foreground.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
session: Type.Optional(
|
|
12
|
+
Type.String({
|
|
13
|
+
description:
|
|
14
|
+
"Session name or ID to send to background. If omitted, detaches the current foreground session.",
|
|
15
|
+
}),
|
|
16
|
+
),
|
|
17
|
+
channel: Type.Optional(
|
|
18
|
+
Type.String({
|
|
19
|
+
description:
|
|
20
|
+
'Origin channel in "channel:target" format (e.g. "telegram:123456789"). Pass this when calling from an agent tool context.',
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
}),
|
|
24
|
+
async execute(_id: string, params: any) {
|
|
25
|
+
if (!sessionManager) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If a specific session is given, detach it
|
|
37
|
+
if (params.session) {
|
|
38
|
+
const session = sessionManager.resolve(params.session);
|
|
39
|
+
if (!session) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: `Error: Session "${params.session}" not found.`,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const channelId = resolveOriginChannel({ id: _id }, params.channel);
|
|
51
|
+
session.saveFgOutputOffset(channelId);
|
|
52
|
+
session.foregroundChannels.delete(channelId);
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: `Session ${session.name} [${session.id}] moved to background.`,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// No session specified — find any session that has this channel in foreground
|
|
64
|
+
const resolvedId = resolveOriginChannel({ id: _id }, params.channel);
|
|
65
|
+
const allSessions = sessionManager.list("all");
|
|
66
|
+
const fgSessions = allSessions.filter((s) =>
|
|
67
|
+
s.foregroundChannels.has(resolvedId),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (fgSessions.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: "No session is currently in foreground.",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const names: string[] = [];
|
|
82
|
+
for (const s of fgSessions) {
|
|
83
|
+
s.saveFgOutputOffset(resolvedId);
|
|
84
|
+
s.foregroundChannels.delete(resolvedId);
|
|
85
|
+
names.push(`${s.name} [${s.id}]`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: `Moved to background: ${names.join(", ")}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{ optional: false },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager, formatDuration, resolveOriginChannel } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeFgTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_fg",
|
|
8
|
+
description:
|
|
9
|
+
"Bring a Claude Code session to foreground (by name or ID). Shows buffered output and streams new output.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
session: Type.String({
|
|
12
|
+
description: "Session name or ID to bring to foreground",
|
|
13
|
+
}),
|
|
14
|
+
lines: Type.Optional(
|
|
15
|
+
Type.Number({
|
|
16
|
+
description: "Number of recent buffered lines to show (default 30)",
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
channel: Type.Optional(
|
|
20
|
+
Type.String({
|
|
21
|
+
description:
|
|
22
|
+
'Origin channel in "channel:target" format (e.g. "telegram:123456789"). Pass this when calling from an agent tool context.',
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
}),
|
|
26
|
+
async execute(_id: string, params: any) {
|
|
27
|
+
if (!sessionManager) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const session = sessionManager.resolve(params.session);
|
|
39
|
+
|
|
40
|
+
if (!session) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `Error: Session "${params.session}" not found.`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mark this conversation as a foreground channel
|
|
52
|
+
// _id is tool call ID; use explicit params.channel when available
|
|
53
|
+
const channelId = resolveOriginChannel({ id: _id }, params.channel);
|
|
54
|
+
|
|
55
|
+
// Get catchup output (produced while this channel was backgrounded)
|
|
56
|
+
const catchupLines = session.getCatchupOutput(channelId);
|
|
57
|
+
|
|
58
|
+
session.foregroundChannels.add(channelId);
|
|
59
|
+
|
|
60
|
+
const duration = formatDuration(session.duration);
|
|
61
|
+
|
|
62
|
+
const header = [
|
|
63
|
+
`Session ${session.name} [${session.id}] now in foreground.`,
|
|
64
|
+
`Status: ${session.status.toUpperCase()} | Duration: ${duration}`,
|
|
65
|
+
`${"─".repeat(60)}`,
|
|
66
|
+
].join("\n");
|
|
67
|
+
|
|
68
|
+
// Build catchup section if there's missed output
|
|
69
|
+
let catchupSection = "";
|
|
70
|
+
if (catchupLines.length > 0) {
|
|
71
|
+
catchupSection = [
|
|
72
|
+
`📋 Catchup (${catchupLines.length} missed output${catchupLines.length === 1 ? "" : "s"}):`,
|
|
73
|
+
catchupLines.join("\n"),
|
|
74
|
+
`${"─".repeat(60)}`,
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If no catchup, fall back to showing recent lines
|
|
79
|
+
const body =
|
|
80
|
+
catchupLines.length > 0
|
|
81
|
+
? catchupSection
|
|
82
|
+
: (session.getOutput(params.lines ?? 30).length > 0
|
|
83
|
+
? session.getOutput(params.lines ?? 30).join("\n")
|
|
84
|
+
: "(no output yet)");
|
|
85
|
+
|
|
86
|
+
const footer =
|
|
87
|
+
session.status === "running" || session.status === "starting"
|
|
88
|
+
? `\n${"─".repeat(60)}\nStreaming new output... Use claude_bg to detach.`
|
|
89
|
+
: `\n${"─".repeat(60)}\nSession is ${session.status}. No more output expected.`;
|
|
90
|
+
|
|
91
|
+
// Mark that this channel has now seen all output up to this point
|
|
92
|
+
session.markFgOutputSeen(channelId);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: `${header}\n${body}${footer}`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{ optional: false },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeKillTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_kill",
|
|
8
|
+
description: "Terminate a running Claude Code session by name or ID.",
|
|
9
|
+
parameters: Type.Object({
|
|
10
|
+
session: Type.String({ description: "Session name or ID to terminate" }),
|
|
11
|
+
}),
|
|
12
|
+
async execute(_id: string, params: any) {
|
|
13
|
+
if (!sessionManager) {
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const session = sessionManager.resolve(params.session);
|
|
25
|
+
|
|
26
|
+
if (!session) {
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `Error: Session "${params.session}" not found.`,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
session.status === "completed" ||
|
|
39
|
+
session.status === "failed" ||
|
|
40
|
+
session.status === "killed"
|
|
41
|
+
) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: `Session ${session.name} [${session.id}] is already ${session.status}. No action needed.`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sessionManager.kill(session.id);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `Session ${session.name} [${session.id}] has been terminated.`,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{ optional: false },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager, pluginConfig, resolveOriginChannel } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeLaunchTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_launch",
|
|
8
|
+
description:
|
|
9
|
+
"Launch a Claude Code session in background to execute a development task. Supports resuming previous sessions and multi-turn conversations. Returns a session ID and name for tracking.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
prompt: Type.String({ description: "The task prompt to execute" }),
|
|
12
|
+
name: Type.Optional(
|
|
13
|
+
Type.String({
|
|
14
|
+
description:
|
|
15
|
+
"Short human-readable name for the session (kebab-case, e.g. 'fix-auth'). Auto-generated from prompt if omitted.",
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
|
+
workdir: Type.Optional(
|
|
19
|
+
Type.String({ description: "Working directory (defaults to cwd)" }),
|
|
20
|
+
),
|
|
21
|
+
model: Type.Optional(
|
|
22
|
+
Type.String({ description: "Model name to use" }),
|
|
23
|
+
),
|
|
24
|
+
max_budget_usd: Type.Optional(
|
|
25
|
+
Type.Number({
|
|
26
|
+
description: "Maximum budget in USD (default 5)",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
system_prompt: Type.Optional(
|
|
30
|
+
Type.String({ description: "Additional system prompt" }),
|
|
31
|
+
),
|
|
32
|
+
allowed_tools: Type.Optional(
|
|
33
|
+
Type.Array(Type.String(), {
|
|
34
|
+
description: "List of allowed tools",
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
resume_session_id: Type.Optional(
|
|
38
|
+
Type.String({
|
|
39
|
+
description:
|
|
40
|
+
"Claude session ID to resume (from a previous session's claudeSessionId). Continues the conversation from where it left off.",
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
fork_session: Type.Optional(
|
|
44
|
+
Type.Boolean({
|
|
45
|
+
description:
|
|
46
|
+
"When resuming, fork to a new session instead of continuing the existing one. Use with resume_session_id.",
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
multi_turn: Type.Optional(
|
|
50
|
+
Type.Boolean({
|
|
51
|
+
description:
|
|
52
|
+
"Enable multi-turn mode. The session stays open for follow-up messages via claude_respond. Default: false.",
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
permission_mode: Type.Optional(
|
|
56
|
+
Type.Union(
|
|
57
|
+
[
|
|
58
|
+
Type.Literal("default"),
|
|
59
|
+
Type.Literal("plan"),
|
|
60
|
+
Type.Literal("acceptEdits"),
|
|
61
|
+
Type.Literal("bypassPermissions"),
|
|
62
|
+
],
|
|
63
|
+
{
|
|
64
|
+
description:
|
|
65
|
+
"Permission mode for the session. Defaults to plugin config or 'bypassPermissions'.",
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
channel: Type.Optional(
|
|
70
|
+
Type.String({
|
|
71
|
+
description:
|
|
72
|
+
'Origin channel for notifications, in "channel:target" format (e.g. "telegram:123456789"). Pass this when calling from an agent tool context so notifications reach the right channel.',
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
}),
|
|
76
|
+
async execute(_id: string, params: any) {
|
|
77
|
+
if (!sessionManager) {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const workdir = params.workdir || pluginConfig.defaultWorkdir || process.cwd();
|
|
89
|
+
const maxBudgetUsd = params.max_budget_usd ?? pluginConfig.defaultBudgetUsd ?? 5;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Resolve resume_session_id: accept name, internal ID, or Claude UUID
|
|
93
|
+
let resolvedResumeId = params.resume_session_id;
|
|
94
|
+
if (resolvedResumeId) {
|
|
95
|
+
const resolved = sessionManager.resolveClaudeSessionId(resolvedResumeId);
|
|
96
|
+
if (!resolved) {
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Error: Could not resolve resume_session_id "${resolvedResumeId}" to a Claude session ID. Use claude_sessions to list available sessions.`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
resolvedResumeId = resolved;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const session = sessionManager.spawn({
|
|
110
|
+
prompt: params.prompt,
|
|
111
|
+
name: params.name,
|
|
112
|
+
workdir,
|
|
113
|
+
model: params.model || pluginConfig.defaultModel,
|
|
114
|
+
maxBudgetUsd,
|
|
115
|
+
systemPrompt: params.system_prompt,
|
|
116
|
+
allowedTools: params.allowed_tools,
|
|
117
|
+
resumeSessionId: resolvedResumeId,
|
|
118
|
+
forkSession: params.fork_session,
|
|
119
|
+
multiTurn: params.multi_turn,
|
|
120
|
+
permissionMode: params.permission_mode,
|
|
121
|
+
// _id is the tool call ID (e.g. "toolu_xxx"), not a channel ID.
|
|
122
|
+
// When the agent passes params.channel explicitly, use that;
|
|
123
|
+
// otherwise resolveOriginChannel falls back to config.
|
|
124
|
+
originChannel: resolveOriginChannel({ id: _id }, params.channel),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const promptSummary =
|
|
128
|
+
params.prompt.length > 80
|
|
129
|
+
? params.prompt.slice(0, 80) + "..."
|
|
130
|
+
: params.prompt;
|
|
131
|
+
|
|
132
|
+
const details = [
|
|
133
|
+
`Session launched successfully.`,
|
|
134
|
+
` Name: ${session.name}`,
|
|
135
|
+
` ID: ${session.id}`,
|
|
136
|
+
` Dir: ${workdir}`,
|
|
137
|
+
` Model: ${session.model ?? "default"}`,
|
|
138
|
+
` Prompt: "${promptSummary}"`,
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
if (params.resume_session_id) {
|
|
142
|
+
details.push(` Resume: ${params.resume_session_id}${params.fork_session ? " (forked)" : ""}`);
|
|
143
|
+
}
|
|
144
|
+
if (params.multi_turn) {
|
|
145
|
+
details.push(` Mode: multi-turn (use claude_respond to send follow-up messages)`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
details.push(``);
|
|
149
|
+
details.push(`Use claude_sessions to check status, claude_output to see output.`);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: details.join("\n"),
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
} catch (err: any) {
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: `Error launching session: ${err.message}`,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{ optional: false },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager, formatDuration } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeOutputTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_output",
|
|
8
|
+
description: "Show recent output from a Claude Code session (by name or ID).",
|
|
9
|
+
parameters: Type.Object({
|
|
10
|
+
session: Type.String({ description: "Session name or ID to get output from" }),
|
|
11
|
+
lines: Type.Optional(
|
|
12
|
+
Type.Number({
|
|
13
|
+
description: "Number of recent lines to show (default 50)",
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
full: Type.Optional(
|
|
17
|
+
Type.Boolean({
|
|
18
|
+
description: "Show all available output",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
}),
|
|
22
|
+
async execute(_id: string, params: any) {
|
|
23
|
+
if (!sessionManager) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const session = sessionManager.resolve(params.session);
|
|
35
|
+
|
|
36
|
+
if (!session) {
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: `Error: Session "${params.session}" not found.`,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const outputLines = params.full
|
|
48
|
+
? session.getOutput()
|
|
49
|
+
: session.getOutput(params.lines ?? 50);
|
|
50
|
+
|
|
51
|
+
const duration = formatDuration(session.duration);
|
|
52
|
+
const header = [
|
|
53
|
+
`Session: ${session.name} [${session.id}] | Status: ${session.status.toUpperCase()} | Duration: ${duration}`,
|
|
54
|
+
`${"─".repeat(60)}`,
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
if (outputLines.length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `${header}\n(no output yet)`,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `${header}\n${outputLines.join("\n")}`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{ optional: false },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager, notificationRouter } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeRespondTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_respond",
|
|
8
|
+
description:
|
|
9
|
+
"Send a follow-up message to a running Claude Code session. The session must be running. Works with multi-turn sessions (launched with multi_turn: true) or any running session via SDK streamInput.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
session: Type.String({
|
|
12
|
+
description: "Session name or ID to respond to",
|
|
13
|
+
}),
|
|
14
|
+
message: Type.String({
|
|
15
|
+
description: "The message to send to the session",
|
|
16
|
+
}),
|
|
17
|
+
interrupt: Type.Optional(
|
|
18
|
+
Type.Boolean({
|
|
19
|
+
description:
|
|
20
|
+
"If true, interrupt the current turn before sending the message. Useful to redirect the session mid-response.",
|
|
21
|
+
}),
|
|
22
|
+
),
|
|
23
|
+
}),
|
|
24
|
+
async execute(_id: string, params: any) {
|
|
25
|
+
if (!sessionManager) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const session = sessionManager.resolve(params.session);
|
|
37
|
+
|
|
38
|
+
if (!session) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `Error: Session "${params.session}" not found.`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (session.status !== "running") {
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `Error: Session ${session.name} [${session.id}] is not running (status: ${session.status}). Cannot send a message to a non-running session.`,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Optionally interrupt the current turn
|
|
62
|
+
if (params.interrupt) {
|
|
63
|
+
await session.interrupt();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Send the message
|
|
67
|
+
await session.sendMessage(params.message);
|
|
68
|
+
|
|
69
|
+
// Display the response in the origin channel so the conversation is visible
|
|
70
|
+
if (notificationRouter && session.originChannel) {
|
|
71
|
+
const respondMsg = [
|
|
72
|
+
`↩️ [${session.name}] Responded:`,
|
|
73
|
+
params.message,
|
|
74
|
+
].join("\n");
|
|
75
|
+
notificationRouter.emitToChannel(session.originChannel, respondMsg);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const msgSummary =
|
|
79
|
+
params.message.length > 80
|
|
80
|
+
? params.message.slice(0, 80) + "..."
|
|
81
|
+
: params.message;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: [
|
|
88
|
+
`Message sent to session ${session.name} [${session.id}].`,
|
|
89
|
+
params.interrupt ? ` (interrupted current turn first)` : "",
|
|
90
|
+
` Message: "${msgSummary}"`,
|
|
91
|
+
``,
|
|
92
|
+
`Use claude_output to see the response.`,
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join("\n"),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `Error sending message: ${err.message}`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ optional: false },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { sessionManager, formatSessionListing } from "../shared";
|
|
3
|
+
|
|
4
|
+
export function registerClaudeSessionsTool(api: any): void {
|
|
5
|
+
api.registerTool(
|
|
6
|
+
{
|
|
7
|
+
name: "claude_sessions",
|
|
8
|
+
description:
|
|
9
|
+
"List all Claude Code sessions with their status and progress.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
status: Type.Optional(
|
|
12
|
+
Type.Union(
|
|
13
|
+
[
|
|
14
|
+
Type.Literal("all"),
|
|
15
|
+
Type.Literal("running"),
|
|
16
|
+
Type.Literal("completed"),
|
|
17
|
+
Type.Literal("failed"),
|
|
18
|
+
],
|
|
19
|
+
{ description: 'Filter by status (default "all")' },
|
|
20
|
+
),
|
|
21
|
+
),
|
|
22
|
+
}),
|
|
23
|
+
async execute(_id: string, params: any) {
|
|
24
|
+
if (!sessionManager) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: "Error: SessionManager not initialized. The claude-code service must be running.",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const filter = params.status || "all";
|
|
36
|
+
const sessions = sessionManager.list(filter);
|
|
37
|
+
|
|
38
|
+
if (sessions.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: "No sessions found.",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = sessions.map(formatSessionListing);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: lines.join("\n\n"),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{ optional: false },
|
|
62
|
+
);
|
|
63
|
+
}
|