forked from laurentenhoor/devclaw
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchannel-unlink.ts
More file actions
144 lines (130 loc) · 5.14 KB
/
channel-unlink.ts
File metadata and controls
144 lines (130 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* channel_unlink — Remove a channel from a project.
*
* Unlinks a channel from a project. Validates that the channel
* exists and prevents removing the last channel from a project (projects must
* have at least one notification endpoint).
*/
import { jsonResult } from "../../json-result.js";
import type { PluginContext } from "../../context.js";
import type { ToolContext } from "../../types.js";
import { readProjects, writeProjects, channelIdsMatch } from "../../projects/index.js";
import { log as auditLog } from "../../audit.js";
import { requireWorkspaceDir } from "../helpers.js";
export function createChannelUnlinkTool(_ctx: PluginContext) {
return (toolCtx: ToolContext) => ({
name: "channel_unlink",
label: "Channel Unlink",
description:
"Remove a channel from a project. Validates that the channel exists and prevents " +
"removing the last channel (projects must have at least one notification endpoint).",
parameters: {
type: "object",
required: ["channelId", "project"],
properties: {
channelId: {
type: "string",
description: "Channel ID to remove (e.g., Telegram group ID)",
},
project: {
type: "string",
description: "Project name or slug to unlink the channel from",
},
confirm: {
type: "boolean",
description: "Set to true to confirm the removal. Defaults to false (dry-run).",
},
messageThreadId: {
type: "number",
description: "Optional Telegram forum topic ID. When provided, only unlinks the matching topic binding for this channel instead of all bindings for the chat.",
},
},
},
async execute(_id: string, params: Record<string, unknown>) {
const channelId = params.channelId as string;
const projectRef = params.project as string;
const confirm = params.confirm as boolean | undefined;
const messageThreadId = params.messageThreadId as number | undefined;
const workspaceDir = requireWorkspaceDir(toolCtx);
if (!channelId) throw new Error("channelId is required.");
if (!projectRef) throw new Error("project is required.");
const data = await readProjects(workspaceDir);
// Resolve target project by slug or name
const slug = projectRef.toLowerCase().replace(/\s+/g, "-");
const target =
data.projects[slug] ??
Object.values(data.projects).find(
(p) => p.name.toLowerCase() === projectRef.toLowerCase(),
);
if (!target) {
const available = Object.values(data.projects)
.map((p) => p.name)
.join(", ");
throw new Error(
`Project "${projectRef}" not found. Available projects: ${available || "none"}.`,
);
}
// Find the channel (optionally scoped by messageThreadId)
const idx = target.channels.findIndex((ch) =>
channelIdsMatch(ch.channelId, channelId) &&
(messageThreadId == null || ch.messageThreadId === messageThreadId)
);
if (idx === -1) {
throw new Error(
`Channel ${channelId} not found in project "${target.name}".`,
);
}
// Prevent removing the last channel
if (target.channels.length === 1) {
throw new Error(
`Cannot remove the last channel from project "${target.name}". Projects must have at least one channel.`,
);
}
const channel = target.channels[idx];
// Dry-run mode: show what would be removed
if (!confirm) {
return jsonResult({
success: false,
dryRun: true,
project: target.name,
projectSlug: target.slug,
channelId,
channelName: channel.name,
channelType: channel.channel,
messageThreadId: channel.messageThreadId ?? null,
remainingChannels: target.channels.length - 1,
announcement:
`DRY-RUN: Would remove channel "${channel.name}" (${channelId}${
channel.messageThreadId != null ? ` topic ${channel.messageThreadId}` : ""
}) from project "${target.name}". ` +
`${target.channels.length - 1} channel(s) would remain. Set confirm=true to proceed.`,
});
}
// Remove the channel
target.channels.splice(idx, 1);
await writeProjects(workspaceDir, data);
await auditLog(workspaceDir, "channel_unlink", {
project: target.name,
projectSlug: target.slug,
channelId,
channelName: channel.name,
channelType: channel.channel,
messageThreadId: channel.messageThreadId ?? null,
});
return jsonResult({
success: true,
project: target.name,
projectSlug: target.slug,
channelId,
channelName: channel.name,
channelType: channel.channel,
remainingChannels: target.channels.length,
announcement:
`Channel "${channel.name}" (${channelId}${
channel.messageThreadId != null ? ` topic ${channel.messageThreadId}` : ""
}) unlinked from project "${target.name}". ` +
`${target.channels.length} channel(s) remaining.`,
});
},
});
}