@openacp/cli 0.5.3 → 0.6.1
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 +51 -16
- package/dist/action-detect-6M5GCGAU.js +15 -0
- package/dist/admin-IKPS5PFC.js +16 -0
- package/dist/agents-55NX3DHM.js +14 -0
- package/dist/{api-client-UN7BXQOQ.js → api-client-BH2JFHQW.js} +4 -2
- package/dist/{autostart-K73RQZVV.js → autostart-A7JRU4WJ.js} +6 -2
- package/dist/{chunk-ECBD5I5R.js → chunk-2KJC3ILH.js} +123 -16
- package/dist/chunk-2KJC3ILH.js.map +1 -0
- package/dist/{chunk-2Z2XPUD5.js → chunk-4LFDEW22.js} +53 -5
- package/dist/chunk-4LFDEW22.js.map +1 -0
- package/dist/{chunk-Z46LGZ7R.js → chunk-4TR5Y3MP.js} +18 -1
- package/dist/chunk-4TR5Y3MP.js.map +1 -0
- package/dist/chunk-7G5QKLLF.js +105 -0
- package/dist/chunk-7G5QKLLF.js.map +1 -0
- package/dist/{chunk-IURZ4QHG.js → chunk-7QJS2XBD.js} +2 -1
- package/dist/chunk-7QJS2XBD.js.map +1 -0
- package/dist/chunk-AKIU4JBF.js +145 -0
- package/dist/chunk-AKIU4JBF.js.map +1 -0
- package/dist/{chunk-KSIQZC3J.js → chunk-EVFJW45N.js} +1 -1
- package/dist/chunk-EVFJW45N.js.map +1 -0
- package/dist/chunk-GINCOFNW.js +134 -0
- package/dist/chunk-GINCOFNW.js.map +1 -0
- package/dist/chunk-H7ZMPBZC.js +203 -0
- package/dist/chunk-H7ZMPBZC.js.map +1 -0
- package/dist/chunk-I7WC6E5S.js +71 -0
- package/dist/chunk-I7WC6E5S.js.map +1 -0
- package/dist/{chunk-6DAZSKE5.js → chunk-IMILOCR5.js} +2 -2
- package/dist/chunk-LGQYTK55.js +442 -0
- package/dist/chunk-LGQYTK55.js.map +1 -0
- package/dist/{chunk-X6LLG7XN.js → chunk-PMGNLNSH.js} +15 -6
- package/dist/chunk-PMGNLNSH.js.map +1 -0
- package/dist/{chunk-LCJIPE5S.js → chunk-R3UJUOXI.js} +889 -591
- package/dist/chunk-R3UJUOXI.js.map +1 -0
- package/dist/chunk-SM3G6UAX.js +122 -0
- package/dist/chunk-SM3G6UAX.js.map +1 -0
- package/dist/chunk-T22OLSET.js +265 -0
- package/dist/chunk-T22OLSET.js.map +1 -0
- package/dist/chunk-THBR6OXH.js +62 -0
- package/dist/chunk-THBR6OXH.js.map +1 -0
- package/dist/{chunk-5KYLXEG3.js → chunk-TOZQ3JFN.js} +52 -9
- package/dist/chunk-TOZQ3JFN.js.map +1 -0
- package/dist/{chunk-IQIPQTQT.js → chunk-UB7XUO7C.js} +171 -26
- package/dist/chunk-UB7XUO7C.js.map +1 -0
- package/dist/{chunk-OORPX73T.js → chunk-W3EYKZNQ.js} +17 -2
- package/dist/chunk-W3EYKZNQ.js.map +1 -0
- package/dist/{chunk-K53OZH5Y.js → chunk-ZCHNAM3B.js} +76 -2
- package/dist/chunk-ZCHNAM3B.js.map +1 -0
- package/dist/cli.js +30 -29
- package/dist/cli.js.map +1 -1
- package/dist/{config-OH26EIWN.js → config-AK2W3E67.js} +2 -2
- package/dist/config-editor-VIA7A72X.js +12 -0
- package/dist/{config-registry-SNKA2EH2.js → config-registry-QQOJ2GQP.js} +2 -2
- package/dist/{daemon-VKCONJUY.js → daemon-G27YZUWB.js} +3 -3
- package/dist/discord-2DKRH45T.js +2044 -0
- package/dist/discord-2DKRH45T.js.map +1 -0
- package/dist/doctor-AN6AZ3PF.js +9 -0
- package/dist/doctor-CHCYUTV5.js +14 -0
- package/dist/doctor-CHCYUTV5.js.map +1 -0
- package/dist/index.d.ts +331 -6
- package/dist/index.js +21 -11
- package/dist/{main-NEYPQHB4.js → main-56SPFYW4.js} +32 -24
- package/dist/main-56SPFYW4.js.map +1 -0
- package/dist/{menu-J5YVH665.js → menu-XR2GET2B.js} +2 -2
- package/dist/menu-XR2GET2B.js.map +1 -0
- package/dist/new-session-DRRP2J7E.js +16 -0
- package/dist/new-session-DRRP2J7E.js.map +1 -0
- package/dist/session-FVFLBREJ.js +19 -0
- package/dist/session-FVFLBREJ.js.map +1 -0
- package/dist/settings-LPOLJ6SA.js +12 -0
- package/dist/settings-LPOLJ6SA.js.map +1 -0
- package/dist/{setup-ZCWGOEAH.js → setup-IPWJCIJM.js} +9 -5
- package/dist/setup-IPWJCIJM.js.map +1 -0
- package/dist/{version-VC5CPXBX.js → version-ALWGGVKM.js} +2 -2
- package/dist/version-ALWGGVKM.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-2Z2XPUD5.js.map +0 -1
- package/dist/chunk-5KYLXEG3.js.map +0 -1
- package/dist/chunk-ECBD5I5R.js.map +0 -1
- package/dist/chunk-IQIPQTQT.js.map +0 -1
- package/dist/chunk-IURZ4QHG.js.map +0 -1
- package/dist/chunk-K53OZH5Y.js.map +0 -1
- package/dist/chunk-KSIQZC3J.js.map +0 -1
- package/dist/chunk-LCJIPE5S.js.map +0 -1
- package/dist/chunk-OORPX73T.js.map +0 -1
- package/dist/chunk-X6LLG7XN.js.map +0 -1
- package/dist/chunk-Z46LGZ7R.js.map +0 -1
- package/dist/config-editor-5TICUK3K.js +0 -12
- package/dist/doctor-X6UCE7GQ.js +0 -9
- package/dist/main-NEYPQHB4.js.map +0 -1
- /package/dist/{api-client-UN7BXQOQ.js.map → action-detect-6M5GCGAU.js.map} +0 -0
- /package/dist/{autostart-K73RQZVV.js.map → admin-IKPS5PFC.js.map} +0 -0
- /package/dist/{config-OH26EIWN.js.map → agents-55NX3DHM.js.map} +0 -0
- /package/dist/{config-editor-5TICUK3K.js.map → api-client-BH2JFHQW.js.map} +0 -0
- /package/dist/{config-registry-SNKA2EH2.js.map → autostart-A7JRU4WJ.js.map} +0 -0
- /package/dist/{chunk-6DAZSKE5.js.map → chunk-IMILOCR5.js.map} +0 -0
- /package/dist/{daemon-VKCONJUY.js.map → config-AK2W3E67.js.map} +0 -0
- /package/dist/{doctor-X6UCE7GQ.js.map → config-editor-VIA7A72X.js.map} +0 -0
- /package/dist/{menu-J5YVH665.js.map → config-registry-QQOJ2GQP.js.map} +0 -0
- /package/dist/{setup-ZCWGOEAH.js.map → daemon-G27YZUWB.js.map} +0 -0
- /package/dist/{version-VC5CPXBX.js.map → doctor-AN6AZ3PF.js.map} +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-ESOPMQAY.js";
|
|
4
|
+
|
|
5
|
+
// src/adapters/discord/forums.ts
|
|
6
|
+
import { ChannelType } from "discord.js";
|
|
7
|
+
async function ensureForums(guild, config, saveConfig) {
|
|
8
|
+
let forumChannelId = config.forumChannelId;
|
|
9
|
+
let notificationChannelId = config.notificationChannelId;
|
|
10
|
+
let forumChannel = null;
|
|
11
|
+
if (forumChannelId) {
|
|
12
|
+
try {
|
|
13
|
+
const ch = guild.channels.cache.get(forumChannelId) ?? await guild.channels.fetch(forumChannelId);
|
|
14
|
+
if (ch && (ch.type === ChannelType.GuildForum || ch.type === ChannelType.GuildText)) {
|
|
15
|
+
forumChannel = ch;
|
|
16
|
+
log.info({ forumChannelId, type: ch.type }, "[forums] Reusing existing sessions channel");
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
log.warn({ forumChannelId }, "[forums] Saved sessions channel not found, recreating...");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!forumChannel) {
|
|
23
|
+
if (guild.features.includes("COMMUNITY")) {
|
|
24
|
+
const channel = await guild.channels.create({
|
|
25
|
+
name: "openacp-sessions",
|
|
26
|
+
type: ChannelType.GuildForum
|
|
27
|
+
});
|
|
28
|
+
forumChannel = channel;
|
|
29
|
+
log.info({ forumChannelId: channel.id }, "[forums] Created forum channel");
|
|
30
|
+
} else {
|
|
31
|
+
const channel = await guild.channels.create({
|
|
32
|
+
name: "openacp-sessions",
|
|
33
|
+
type: ChannelType.GuildText
|
|
34
|
+
});
|
|
35
|
+
forumChannel = channel;
|
|
36
|
+
log.info({ forumChannelId: channel.id }, "[forums] Created text channel (Community mode not enabled, using threads fallback)");
|
|
37
|
+
}
|
|
38
|
+
await saveConfig({ channels: { discord: { forumChannelId: forumChannel.id } } });
|
|
39
|
+
}
|
|
40
|
+
let notificationChannel = null;
|
|
41
|
+
if (notificationChannelId) {
|
|
42
|
+
try {
|
|
43
|
+
const ch = guild.channels.cache.get(notificationChannelId) ?? await guild.channels.fetch(notificationChannelId);
|
|
44
|
+
if (ch && ch.type === ChannelType.GuildText) {
|
|
45
|
+
notificationChannel = ch;
|
|
46
|
+
log.info({ notificationChannelId }, "[forums] Reusing existing notification channel");
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
log.warn({ notificationChannelId }, "[forums] Saved notification channel not found, recreating...");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!notificationChannel) {
|
|
53
|
+
const channel = await guild.channels.create({
|
|
54
|
+
name: "openacp-notifications",
|
|
55
|
+
type: ChannelType.GuildText
|
|
56
|
+
});
|
|
57
|
+
notificationChannel = channel;
|
|
58
|
+
await saveConfig({ channels: { discord: { notificationChannelId: channel.id } } });
|
|
59
|
+
log.info({ notificationChannelId: channel.id }, "[forums] Created notification channel");
|
|
60
|
+
}
|
|
61
|
+
return { forumChannel, notificationChannel };
|
|
62
|
+
}
|
|
63
|
+
async function createSessionThread(forumChannel, name) {
|
|
64
|
+
if (forumChannel.type === ChannelType.GuildForum) {
|
|
65
|
+
const thread2 = await forumChannel.threads.create({
|
|
66
|
+
name,
|
|
67
|
+
message: { content: "\u23F3 Setting up..." }
|
|
68
|
+
});
|
|
69
|
+
return thread2;
|
|
70
|
+
}
|
|
71
|
+
const textChannel = forumChannel;
|
|
72
|
+
const msg = await textChannel.send({ content: `\u{1F4C2} **${name}** \u2014 \u23F3 Setting up...` });
|
|
73
|
+
const thread = await msg.startThread({ name });
|
|
74
|
+
return thread;
|
|
75
|
+
}
|
|
76
|
+
async function renameSessionThread(guild, threadId, newName) {
|
|
77
|
+
try {
|
|
78
|
+
const channel = guild.channels.cache.get(threadId) ?? await guild.channels.fetch(threadId);
|
|
79
|
+
if (channel && "setName" in channel) {
|
|
80
|
+
await channel.setName(newName);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function deleteSessionThread(guild, threadId) {
|
|
86
|
+
try {
|
|
87
|
+
const channel = guild.channels.cache.get(threadId) ?? await guild.channels.fetch(threadId);
|
|
88
|
+
if (channel && channel.isThread()) {
|
|
89
|
+
const thread = channel;
|
|
90
|
+
if (!thread.archived) {
|
|
91
|
+
await thread.setArchived(true);
|
|
92
|
+
}
|
|
93
|
+
if (!thread.locked) {
|
|
94
|
+
await thread.setLocked(true);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function ensureUnarchived(thread) {
|
|
101
|
+
if (thread.archived) {
|
|
102
|
+
try {
|
|
103
|
+
await thread.setArchived(false);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log.warn({ err, threadId: thread.id }, "[forums] Failed to unarchive thread");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function buildDeepLink(guildId, channelId, messageId) {
|
|
110
|
+
const base = `https://discord.com/channels/${guildId}/${channelId}`;
|
|
111
|
+
return messageId ? `${base}/${messageId}` : base;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
ensureForums,
|
|
116
|
+
createSessionThread,
|
|
117
|
+
renameSessionThread,
|
|
118
|
+
deleteSessionThread,
|
|
119
|
+
ensureUnarchived,
|
|
120
|
+
buildDeepLink
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=chunk-SM3G6UAX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/discord/forums.ts"],"sourcesContent":["import { ChannelType } from 'discord.js'\nimport type { ForumChannel, ThreadChannel, Guild, TextChannel } from 'discord.js'\nimport { log } from '../../core/log.js'\n\n// ─── ensureForums ─────────────────────────────────────────────────────────────\n\n/**\n * Ensures both the forum channel and notification channel exist.\n * Creates them if their IDs are null, then persists the IDs via saveConfig.\n *\n * saveConfig uses nested object path: { channels: { discord: { forumChannelId: ... } } }\n */\nexport async function ensureForums(\n guild: Guild,\n config: {\n forumChannelId: string | null\n notificationChannelId: string | null\n },\n saveConfig: (updates: Record<string, unknown>) => Promise<void>,\n): Promise<{ forumChannel: ForumChannel | TextChannel; notificationChannel: TextChannel }> {\n let forumChannelId = config.forumChannelId\n let notificationChannelId = config.notificationChannelId\n\n // Ensure forum/sessions channel exists — fetch existing or create new\n let forumChannel: ForumChannel | TextChannel | null = null\n if (forumChannelId) {\n try {\n const ch = guild.channels.cache.get(forumChannelId)\n ?? await guild.channels.fetch(forumChannelId)\n if (ch && (ch.type === ChannelType.GuildForum || ch.type === ChannelType.GuildText)) {\n forumChannel = ch as ForumChannel | TextChannel\n log.info({ forumChannelId, type: ch.type }, '[forums] Reusing existing sessions channel')\n }\n } catch {\n log.warn({ forumChannelId }, '[forums] Saved sessions channel not found, recreating...')\n }\n }\n if (!forumChannel) {\n // Prefer Forum Channel (requires Community mode), fallback to Text Channel with threads\n if (guild.features.includes('COMMUNITY')) {\n const channel = await guild.channels.create({\n name: 'openacp-sessions',\n type: ChannelType.GuildForum,\n })\n forumChannel = channel as ForumChannel\n log.info({ forumChannelId: channel.id }, '[forums] Created forum channel')\n } else {\n const channel = await guild.channels.create({\n name: 'openacp-sessions',\n type: ChannelType.GuildText,\n })\n forumChannel = channel as TextChannel\n log.info({ forumChannelId: channel.id }, '[forums] Created text channel (Community mode not enabled, using threads fallback)')\n }\n await saveConfig({ channels: { discord: { forumChannelId: forumChannel.id } } })\n }\n\n // Ensure notification channel exists — fetch existing or create new\n let notificationChannel: TextChannel | null = null\n if (notificationChannelId) {\n try {\n const ch = guild.channels.cache.get(notificationChannelId)\n ?? await guild.channels.fetch(notificationChannelId)\n if (ch && ch.type === ChannelType.GuildText) {\n notificationChannel = ch as TextChannel\n log.info({ notificationChannelId }, '[forums] Reusing existing notification channel')\n }\n } catch {\n log.warn({ notificationChannelId }, '[forums] Saved notification channel not found, recreating...')\n }\n }\n if (!notificationChannel) {\n const channel = await guild.channels.create({\n name: 'openacp-notifications',\n type: ChannelType.GuildText,\n })\n notificationChannel = channel as TextChannel\n await saveConfig({ channels: { discord: { notificationChannelId: channel.id } } })\n log.info({ notificationChannelId: channel.id }, '[forums] Created notification channel')\n }\n\n return { forumChannel, notificationChannel }\n}\n\n// ─── createSessionThread ──────────────────────────────────────────────────────\n\n/**\n * Creates a new thread for a session.\n * - Forum Channel: creates a forum post (thread with initial message)\n * - Text Channel: creates a public thread\n */\nexport async function createSessionThread(\n forumChannel: ForumChannel | TextChannel,\n name: string,\n): Promise<ThreadChannel> {\n if (forumChannel.type === ChannelType.GuildForum) {\n // Forum channel: create a post (thread with initial message)\n const thread = await (forumChannel as ForumChannel).threads.create({\n name,\n message: { content: '⏳ Setting up...' },\n })\n return thread\n }\n\n // Text channel fallback: send a message first, then create a thread on it\n const textChannel = forumChannel as TextChannel\n const msg = await textChannel.send({ content: `📂 **${name}** — ⏳ Setting up...` })\n const thread = await msg.startThread({ name })\n return thread\n}\n\n// ─── renameSessionThread ──────────────────────────────────────────────────────\n\n/**\n * Fetches and renames a thread. Ignores all errors (thread may be deleted/archived).\n */\nexport async function renameSessionThread(\n guild: Guild,\n threadId: string,\n newName: string,\n): Promise<void> {\n try {\n const channel = guild.channels.cache.get(threadId)\n ?? await guild.channels.fetch(threadId)\n if (channel && 'setName' in channel) {\n await (channel as ThreadChannel).setName(newName)\n }\n } catch {\n // Ignore — thread may be deleted or archived\n }\n}\n\n// ─── deleteSessionThread ──────────────────────────────────────────────────────\n\n/**\n * Archives and locks a thread instead of permanently deleting it.\n * Unlike Telegram (which just closes a topic), Discord delete is permanent\n * and destroys all messages. Archiving preserves the conversation history.\n */\nexport async function deleteSessionThread(\n guild: Guild,\n threadId: string,\n): Promise<void> {\n try {\n const channel = guild.channels.cache.get(threadId)\n ?? await guild.channels.fetch(threadId)\n if (channel && channel.isThread()) {\n const thread = channel as ThreadChannel\n if (!thread.archived) {\n await thread.setArchived(true)\n }\n if (!thread.locked) {\n await thread.setLocked(true)\n }\n }\n } catch {\n // Ignore — thread may already be deleted or inaccessible\n }\n}\n\n// ─── ensureUnarchived ─────────────────────────────────────────────────────────\n\n/**\n * If the thread is archived, unarchives it.\n */\nexport async function ensureUnarchived(thread: ThreadChannel): Promise<void> {\n if (thread.archived) {\n try {\n await thread.setArchived(false)\n } catch (err) {\n log.warn({ err, threadId: thread.id }, '[forums] Failed to unarchive thread')\n }\n }\n}\n\n// ─── buildDeepLink ────────────────────────────────────────────────────────────\n\n/**\n * Builds a Discord deep link URL to a channel/thread, optionally to a specific message.\n */\nexport function buildDeepLink(\n guildId: string,\n channelId: string,\n messageId?: string,\n): string {\n const base = `https://discord.com/channels/${guildId}/${channelId}`\n return messageId ? `${base}/${messageId}` : base\n}\n"],"mappings":"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","names":["thread"]}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
deleteSessionThread
|
|
3
|
+
} from "./chunk-SM3G6UAX.js";
|
|
4
|
+
import {
|
|
5
|
+
log
|
|
6
|
+
} from "./chunk-ESOPMQAY.js";
|
|
7
|
+
|
|
8
|
+
// src/adapters/discord/commands/session.ts
|
|
9
|
+
import {
|
|
10
|
+
ActionRowBuilder,
|
|
11
|
+
ButtonBuilder,
|
|
12
|
+
ButtonStyle
|
|
13
|
+
} from "discord.js";
|
|
14
|
+
var STATUS_EMOJI = {
|
|
15
|
+
active: "\u{1F7E2}",
|
|
16
|
+
initializing: "\u{1F7E1}",
|
|
17
|
+
finished: "\u2705",
|
|
18
|
+
error: "\u274C",
|
|
19
|
+
cancelled: "\u26D4"
|
|
20
|
+
};
|
|
21
|
+
var STATUS_ORDER = {
|
|
22
|
+
active: 0,
|
|
23
|
+
initializing: 1,
|
|
24
|
+
error: 2,
|
|
25
|
+
finished: 3,
|
|
26
|
+
cancelled: 4
|
|
27
|
+
};
|
|
28
|
+
async function handleCancel(interaction, adapter) {
|
|
29
|
+
await interaction.deferReply({ ephemeral: true });
|
|
30
|
+
const channelId = interaction.channelId;
|
|
31
|
+
const session = adapter.core.sessionManager.getSessionByThread("discord", channelId);
|
|
32
|
+
if (session) {
|
|
33
|
+
log.info({ sessionId: session.id }, "[discord-session] Cancel command");
|
|
34
|
+
await session.abortPrompt();
|
|
35
|
+
await interaction.editReply("\u26D4 Session cancelled.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const record = adapter.core.sessionManager.getRecordByThread("discord", channelId);
|
|
39
|
+
if (record && record.status !== "cancelled" && record.status !== "error") {
|
|
40
|
+
log.info({ sessionId: record.sessionId }, "[discord-session] Cancel command (from store)");
|
|
41
|
+
await adapter.core.sessionManager.cancelSession(record.sessionId);
|
|
42
|
+
await interaction.editReply("\u26D4 Session cancelled.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await interaction.editReply("No active session in this channel.");
|
|
46
|
+
}
|
|
47
|
+
async function handleStatus(interaction, adapter) {
|
|
48
|
+
await interaction.deferReply({ ephemeral: true });
|
|
49
|
+
const channelId = interaction.channelId;
|
|
50
|
+
const session = adapter.core.sessionManager.getSessionByThread("discord", channelId);
|
|
51
|
+
if (session) {
|
|
52
|
+
await interaction.editReply(
|
|
53
|
+
`**Session:** ${session.name || session.id}
|
|
54
|
+
**Agent:** ${session.agentName}
|
|
55
|
+
**Status:** ${session.status}
|
|
56
|
+
**Workspace:** \`${session.workingDirectory}\`
|
|
57
|
+
**Queue:** ${session.queueDepth} pending`
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const record = adapter.core.sessionManager.getRecordByThread("discord", channelId);
|
|
62
|
+
if (record) {
|
|
63
|
+
await interaction.editReply(
|
|
64
|
+
`**Session:** ${record.name || record.sessionId}
|
|
65
|
+
**Agent:** ${record.agentName}
|
|
66
|
+
**Status:** ${record.status} (not loaded)
|
|
67
|
+
**Workspace:** \`${record.workingDir}\``
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const sessions = adapter.core.sessionManager.listSessions("discord");
|
|
72
|
+
const active = sessions.filter(
|
|
73
|
+
(s) => s.status === "active" || s.status === "initializing"
|
|
74
|
+
);
|
|
75
|
+
await interaction.editReply(
|
|
76
|
+
`**OpenACP Status**
|
|
77
|
+
Active sessions: ${active.length}
|
|
78
|
+
Total sessions: ${sessions.length}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
async function handleSessions(interaction, adapter) {
|
|
82
|
+
await interaction.deferReply({ ephemeral: true });
|
|
83
|
+
try {
|
|
84
|
+
const allRecords = adapter.core.sessionManager.listRecords();
|
|
85
|
+
const records = allRecords.filter((r) => {
|
|
86
|
+
const platform = r.platform;
|
|
87
|
+
return !!platform?.topicId;
|
|
88
|
+
});
|
|
89
|
+
const headlessCount = allRecords.length - records.length;
|
|
90
|
+
if (records.length === 0) {
|
|
91
|
+
const extra = headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "";
|
|
92
|
+
await interaction.editReply(`No sessions found.${extra}`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
records.sort(
|
|
96
|
+
(a, b) => (STATUS_ORDER[a.status] ?? 5) - (STATUS_ORDER[b.status] ?? 5)
|
|
97
|
+
);
|
|
98
|
+
const MAX_DISPLAY = 25;
|
|
99
|
+
const displayed = records.slice(0, MAX_DISPLAY);
|
|
100
|
+
const lines = displayed.map((r) => {
|
|
101
|
+
const emoji = STATUS_EMOJI[r.status] || "\u26AA";
|
|
102
|
+
const name = r.name?.trim() || `${r.agentName} session`;
|
|
103
|
+
return `${emoji} **${name}** \`[${r.status}]\``;
|
|
104
|
+
});
|
|
105
|
+
const header = `**Sessions: ${records.length}**` + (headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "");
|
|
106
|
+
const truncated = records.length > MAX_DISPLAY ? `
|
|
107
|
+
|
|
108
|
+
*...and ${records.length - MAX_DISPLAY} more*` : "";
|
|
109
|
+
const finishedCount = allRecords.filter((r) => r.status === "finished").length;
|
|
110
|
+
const errorCount = allRecords.filter(
|
|
111
|
+
(r) => r.status === "error" || r.status === "cancelled"
|
|
112
|
+
).length;
|
|
113
|
+
const rows = [];
|
|
114
|
+
if (finishedCount + errorCount > 0) {
|
|
115
|
+
const cleanupRow = new ActionRowBuilder();
|
|
116
|
+
if (finishedCount > 0) {
|
|
117
|
+
cleanupRow.addComponents(
|
|
118
|
+
new ButtonBuilder().setCustomId("m:cleanup:finished").setLabel(`Cleanup finished (${finishedCount})`).setStyle(ButtonStyle.Secondary)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (errorCount > 0) {
|
|
122
|
+
cleanupRow.addComponents(
|
|
123
|
+
new ButtonBuilder().setCustomId("m:cleanup:errors").setLabel(`Cleanup errors (${errorCount})`).setStyle(ButtonStyle.Secondary)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
rows.push(cleanupRow);
|
|
127
|
+
const cleanupAllRow = new ActionRowBuilder();
|
|
128
|
+
cleanupAllRow.addComponents(
|
|
129
|
+
new ButtonBuilder().setCustomId("m:cleanup:all").setLabel(`Cleanup all non-active (${finishedCount + errorCount})`).setStyle(ButtonStyle.Secondary)
|
|
130
|
+
);
|
|
131
|
+
rows.push(cleanupAllRow);
|
|
132
|
+
}
|
|
133
|
+
await interaction.editReply({
|
|
134
|
+
content: `${header}
|
|
135
|
+
|
|
136
|
+
${lines.join("\n")}${truncated}`,
|
|
137
|
+
components: rows
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log.error({ err }, "[discord-session] handleSessions error");
|
|
141
|
+
await interaction.editReply("\u274C Failed to list sessions.").catch(() => {
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function handleHandoff(interaction, adapter) {
|
|
146
|
+
await interaction.deferReply({ ephemeral: true });
|
|
147
|
+
const channelId = interaction.channelId;
|
|
148
|
+
const session = adapter.core.sessionManager.getSessionByThread("discord", channelId);
|
|
149
|
+
if (!session) {
|
|
150
|
+
const record = adapter.core.sessionManager.getRecordByThread("discord", channelId);
|
|
151
|
+
if (!record) {
|
|
152
|
+
await interaction.editReply("No session found in this channel.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const cmd2 = `openacp agents run ${record.agentName} --resume ${record.agentSessionId} -- --continue`;
|
|
156
|
+
await interaction.editReply(
|
|
157
|
+
`**Resume in terminal:**
|
|
158
|
+
\`\`\`
|
|
159
|
+
${cmd2}
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
*Run this from your project directory:* \`${record.workingDir}\``
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const cmd = `openacp agents run ${session.agentName} --resume ${session.agentSessionId} -- --continue`;
|
|
167
|
+
await interaction.editReply(
|
|
168
|
+
`**Resume in terminal:**
|
|
169
|
+
\`\`\`
|
|
170
|
+
${cmd}
|
|
171
|
+
\`\`\`
|
|
172
|
+
|
|
173
|
+
*Run this from your project directory:* \`${session.workingDirectory}\``
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
async function executeCancelSession(interaction, adapter) {
|
|
177
|
+
const sessions = adapter.core.sessionManager.listSessions("discord").filter((s) => s.status === "active").sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
178
|
+
const session = sessions[0];
|
|
179
|
+
if (!session) {
|
|
180
|
+
await interaction.reply({ content: "No active sessions to cancel.", ephemeral: true });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await session.abortPrompt();
|
|
184
|
+
await interaction.reply({ content: `\u26D4 Cancelled session: **${session.name || session.id}**`, ephemeral: true });
|
|
185
|
+
}
|
|
186
|
+
async function handleCleanupButton(interaction, adapter) {
|
|
187
|
+
const { customId } = interaction;
|
|
188
|
+
switch (customId) {
|
|
189
|
+
case "m:cleanup:all":
|
|
190
|
+
await interaction.deferReply({ ephemeral: true });
|
|
191
|
+
await runCleanup(interaction, adapter, ["finished", "error", "cancelled"]);
|
|
192
|
+
break;
|
|
193
|
+
case "m:cleanup:finished":
|
|
194
|
+
await interaction.deferReply({ ephemeral: true });
|
|
195
|
+
await runCleanup(interaction, adapter, ["finished"]);
|
|
196
|
+
break;
|
|
197
|
+
case "m:cleanup:errors":
|
|
198
|
+
await interaction.deferReply({ ephemeral: true });
|
|
199
|
+
await runCleanup(interaction, adapter, ["error", "cancelled"]);
|
|
200
|
+
break;
|
|
201
|
+
case "m:cleanup:confirm":
|
|
202
|
+
await interaction.deferReply({ ephemeral: true });
|
|
203
|
+
await runCleanup(interaction, adapter, ["finished", "error", "cancelled", "active", "initializing"]);
|
|
204
|
+
break;
|
|
205
|
+
case "m:cleanup:cancel":
|
|
206
|
+
try {
|
|
207
|
+
await interaction.update({ components: [] });
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
try {
|
|
213
|
+
await interaction.reply({ content: "Unknown cleanup action.", ephemeral: true });
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function runCleanup(interaction, adapter, statuses) {
|
|
219
|
+
const allRecords = adapter.core.sessionManager.listRecords();
|
|
220
|
+
const cleanable = allRecords.filter((r) => statuses.includes(r.status));
|
|
221
|
+
if (cleanable.length === 0) {
|
|
222
|
+
await interaction.editReply("Nothing to clean up.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let deleted = 0;
|
|
226
|
+
let failed = 0;
|
|
227
|
+
for (const record of cleanable) {
|
|
228
|
+
try {
|
|
229
|
+
if (record.status === "active" || record.status === "initializing") {
|
|
230
|
+
try {
|
|
231
|
+
await adapter.core.sessionManager.cancelSession(record.sessionId);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
log.warn({ err, sessionId: record.sessionId }, "[discord-session] Failed to cancel session during cleanup");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const platform = record.platform;
|
|
237
|
+
const threadId = platform?.threadId ?? (platform?.topicId != null ? String(platform.topicId) : void 0);
|
|
238
|
+
if (threadId) {
|
|
239
|
+
try {
|
|
240
|
+
await deleteSessionThread(adapter.getGuild(), threadId);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
log.warn({ err, sessionId: record.sessionId, threadId }, "[discord-session] Failed to delete thread during cleanup");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
await adapter.core.sessionManager.removeRecord(record.sessionId);
|
|
246
|
+
deleted++;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log.error({ err, sessionId: record.sessionId }, "[discord-session] Failed to cleanup session");
|
|
249
|
+
failed++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
await interaction.editReply(
|
|
253
|
+
`\u{1F5D1} Cleaned up **${deleted}** sessions${failed > 0 ? ` (${failed} failed)` : ""}.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export {
|
|
258
|
+
handleCancel,
|
|
259
|
+
handleStatus,
|
|
260
|
+
handleSessions,
|
|
261
|
+
handleHandoff,
|
|
262
|
+
executeCancelSession,
|
|
263
|
+
handleCleanupButton
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=chunk-T22OLSET.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/discord/commands/session.ts"],"sourcesContent":["import {\n ActionRowBuilder,\n ButtonBuilder,\n ButtonStyle,\n} from 'discord.js'\nimport type { ChatInputCommandInteraction, ButtonInteraction } from 'discord.js'\nimport type { Session } from '../../../core/session.js'\nimport { log } from '../../../core/log.js'\nimport { deleteSessionThread } from '../forums.js'\n\n// TODO: Replace `any` with DiscordAdapter once Task 12 is implemented\n\nconst STATUS_EMOJI: Record<string, string> = {\n active: '🟢',\n initializing: '🟡',\n finished: '✅',\n error: '❌',\n cancelled: '⛔',\n}\n\nconst STATUS_ORDER: Record<string, number> = {\n active: 0,\n initializing: 1,\n error: 2,\n finished: 3,\n cancelled: 4,\n}\n\nexport async function handleCancel(\n interaction: ChatInputCommandInteraction,\n adapter: any,\n): Promise<void> {\n await interaction.deferReply({ ephemeral: true })\n\n const channelId = interaction.channelId\n const session = adapter.core.sessionManager.getSessionByThread('discord', channelId)\n\n if (session) {\n log.info({ sessionId: session.id }, '[discord-session] Cancel command')\n await session.abortPrompt()\n await interaction.editReply('⛔ Session cancelled.')\n return\n }\n\n // Fallback: cancel from store when session not in memory\n const record = adapter.core.sessionManager.getRecordByThread('discord', channelId)\n if (record && record.status !== 'cancelled' && record.status !== 'error') {\n log.info({ sessionId: record.sessionId }, '[discord-session] Cancel command (from store)')\n await adapter.core.sessionManager.cancelSession(record.sessionId)\n await interaction.editReply('⛔ Session cancelled.')\n return\n }\n\n await interaction.editReply('No active session in this channel.')\n}\n\nexport async function handleStatus(\n interaction: ChatInputCommandInteraction,\n adapter: any,\n): Promise<void> {\n await interaction.deferReply({ ephemeral: true })\n\n const channelId = interaction.channelId\n const session = adapter.core.sessionManager.getSessionByThread('discord', channelId)\n\n if (session) {\n await interaction.editReply(\n `**Session:** ${session.name || session.id}\\n` +\n `**Agent:** ${session.agentName}\\n` +\n `**Status:** ${session.status}\\n` +\n `**Workspace:** \\`${session.workingDirectory}\\`\\n` +\n `**Queue:** ${session.queueDepth} pending`,\n )\n return\n }\n\n // Try stored record\n const record = adapter.core.sessionManager.getRecordByThread('discord', channelId)\n if (record) {\n await interaction.editReply(\n `**Session:** ${record.name || record.sessionId}\\n` +\n `**Agent:** ${record.agentName}\\n` +\n `**Status:** ${record.status} (not loaded)\\n` +\n `**Workspace:** \\`${record.workingDir}\\``,\n )\n return\n }\n\n // Global status\n const sessions = adapter.core.sessionManager.listSessions('discord')\n const active = sessions.filter(\n (s: Session) => s.status === 'active' || s.status === 'initializing',\n )\n await interaction.editReply(\n `**OpenACP Status**\\n` +\n `Active sessions: ${active.length}\\n` +\n `Total sessions: ${sessions.length}`,\n )\n}\n\nexport async function handleSessions(\n interaction: ChatInputCommandInteraction,\n adapter: any,\n): Promise<void> {\n await interaction.deferReply({ ephemeral: true })\n\n try {\n const allRecords = adapter.core.sessionManager.listRecords()\n\n // Only show sessions that have a Discord thread\n const records = allRecords.filter((r: any) => {\n const platform = r.platform as { topicId?: string | number }\n return !!platform?.topicId\n })\n const headlessCount = allRecords.length - records.length\n\n if (records.length === 0) {\n const extra = headlessCount > 0 ? ` (${headlessCount} headless hidden)` : ''\n await interaction.editReply(`No sessions found.${extra}`)\n return\n }\n\n records.sort(\n (a: any, b: any) => (STATUS_ORDER[a.status] ?? 5) - (STATUS_ORDER[b.status] ?? 5),\n )\n\n const MAX_DISPLAY = 25\n const displayed = records.slice(0, MAX_DISPLAY)\n\n const lines = displayed.map((r: any) => {\n const emoji = STATUS_EMOJI[r.status] || '⚪'\n const name = r.name?.trim() || `${r.agentName} session`\n return `${emoji} **${name}** \\`[${r.status}]\\``\n })\n\n const header =\n `**Sessions: ${records.length}**` +\n (headlessCount > 0 ? ` (${headlessCount} headless hidden)` : '')\n const truncated =\n records.length > MAX_DISPLAY\n ? `\\n\\n*...and ${records.length - MAX_DISPLAY} more*`\n : ''\n\n // Cleanup buttons\n const finishedCount = allRecords.filter((r: any) => r.status === 'finished').length\n const errorCount = allRecords.filter(\n (r: any) => r.status === 'error' || r.status === 'cancelled',\n ).length\n\n const rows: ActionRowBuilder<ButtonBuilder>[] = []\n\n if (finishedCount + errorCount > 0) {\n const cleanupRow = new ActionRowBuilder<ButtonBuilder>()\n if (finishedCount > 0) {\n cleanupRow.addComponents(\n new ButtonBuilder()\n .setCustomId('m:cleanup:finished')\n .setLabel(`Cleanup finished (${finishedCount})`)\n .setStyle(ButtonStyle.Secondary),\n )\n }\n if (errorCount > 0) {\n cleanupRow.addComponents(\n new ButtonBuilder()\n .setCustomId('m:cleanup:errors')\n .setLabel(`Cleanup errors (${errorCount})`)\n .setStyle(ButtonStyle.Secondary),\n )\n }\n rows.push(cleanupRow)\n\n const cleanupAllRow = new ActionRowBuilder<ButtonBuilder>()\n cleanupAllRow.addComponents(\n new ButtonBuilder()\n .setCustomId('m:cleanup:all')\n .setLabel(`Cleanup all non-active (${finishedCount + errorCount})`)\n .setStyle(ButtonStyle.Secondary),\n )\n rows.push(cleanupAllRow)\n }\n\n await interaction.editReply({\n content: `${header}\\n\\n${lines.join('\\n')}${truncated}`,\n components: rows,\n })\n } catch (err) {\n log.error({ err }, '[discord-session] handleSessions error')\n await interaction.editReply('❌ Failed to list sessions.').catch(() => {})\n }\n}\n\nexport async function handleHandoff(\n interaction: ChatInputCommandInteraction,\n adapter: any,\n): Promise<void> {\n await interaction.deferReply({ ephemeral: true })\n\n const channelId = interaction.channelId\n const session = adapter.core.sessionManager.getSessionByThread('discord', channelId)\n\n if (!session) {\n const record = adapter.core.sessionManager.getRecordByThread('discord', channelId)\n if (!record) {\n await interaction.editReply('No session found in this channel.')\n return\n }\n const cmd = `openacp agents run ${record.agentName} --resume ${record.agentSessionId} -- --continue`\n await interaction.editReply(\n `**Resume in terminal:**\\n\\`\\`\\`\\n${cmd}\\n\\`\\`\\`\\n\\n*Run this from your project directory:* \\`${record.workingDir}\\``,\n )\n return\n }\n\n const cmd = `openacp agents run ${session.agentName} --resume ${session.agentSessionId} -- --continue`\n await interaction.editReply(\n `**Resume in terminal:**\\n\\`\\`\\`\\n${cmd}\\n\\`\\`\\`\\n\\n*Run this from your project directory:* \\`${session.workingDirectory}\\``,\n )\n}\n\nexport async function executeCancelSession(\n interaction: ButtonInteraction,\n adapter: any,\n): Promise<void> {\n const sessions: Session[] = adapter.core.sessionManager\n .listSessions('discord')\n .filter((s: Session) => s.status === 'active')\n .sort((a: Session, b: Session) => b.createdAt.getTime() - a.createdAt.getTime())\n\n const session = sessions[0]\n if (!session) {\n await interaction.reply({ content: 'No active sessions to cancel.', ephemeral: true })\n return\n }\n\n await session.abortPrompt()\n await interaction.reply({ content: `⛔ Cancelled session: **${session.name || session.id}**`, ephemeral: true })\n}\n\nexport async function handleCleanupButton(\n interaction: ButtonInteraction,\n adapter: any,\n): Promise<void> {\n const { customId } = interaction\n\n switch (customId) {\n case 'm:cleanup:all':\n await interaction.deferReply({ ephemeral: true })\n await runCleanup(interaction, adapter, ['finished', 'error', 'cancelled'])\n break\n\n case 'm:cleanup:finished':\n await interaction.deferReply({ ephemeral: true })\n await runCleanup(interaction, adapter, ['finished'])\n break\n\n case 'm:cleanup:errors':\n await interaction.deferReply({ ephemeral: true })\n await runCleanup(interaction, adapter, ['error', 'cancelled'])\n break\n\n case 'm:cleanup:confirm':\n await interaction.deferReply({ ephemeral: true })\n await runCleanup(interaction, adapter, ['finished', 'error', 'cancelled', 'active', 'initializing'])\n break\n\n case 'm:cleanup:cancel':\n try { await interaction.update({ components: [] }) } catch { /* ignore */ }\n break\n\n default:\n // Unknown cleanup variant — ignore\n try { await interaction.reply({ content: 'Unknown cleanup action.', ephemeral: true }) } catch { /* ignore */ }\n }\n}\n\nasync function runCleanup(\n interaction: ButtonInteraction,\n adapter: any,\n statuses: string[],\n): Promise<void> {\n const allRecords = adapter.core.sessionManager.listRecords()\n const cleanable = allRecords.filter((r: any) => statuses.includes(r.status))\n\n if (cleanable.length === 0) {\n await interaction.editReply('Nothing to clean up.')\n return\n }\n\n let deleted = 0\n let failed = 0\n\n for (const record of cleanable) {\n try {\n // Cancel active sessions first\n if (record.status === 'active' || record.status === 'initializing') {\n try {\n await adapter.core.sessionManager.cancelSession(record.sessionId)\n } catch (err) {\n log.warn({ err, sessionId: record.sessionId }, '[discord-session] Failed to cancel session during cleanup')\n }\n }\n\n const platform = record.platform as { topicId?: string | number; threadId?: string } | undefined\n const threadId = platform?.threadId ?? (platform?.topicId != null ? String(platform.topicId) : undefined)\n if (threadId) {\n try {\n await deleteSessionThread(adapter.getGuild(), threadId)\n } catch (err) {\n log.warn({ err, sessionId: record.sessionId, threadId }, '[discord-session] Failed to delete thread during cleanup')\n }\n }\n await adapter.core.sessionManager.removeRecord(record.sessionId)\n deleted++\n } catch (err) {\n log.error({ err, sessionId: record.sessionId }, '[discord-session] Failed to cleanup session')\n failed++\n }\n }\n\n await interaction.editReply(\n `🗑 Cleaned up **${deleted}** sessions${failed > 0 ? ` (${failed} failed)` : ''}.`,\n )\n}\n"],"mappings":"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","names":["cmd"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-ESOPMQAY.js";
|
|
4
|
+
|
|
5
|
+
// src/adapters/discord/commands/settings.ts
|
|
6
|
+
async function handleSettings(interaction, adapter) {
|
|
7
|
+
await interaction.deferReply({ ephemeral: true });
|
|
8
|
+
const config = adapter.core.configManager.get();
|
|
9
|
+
const configPath = adapter.core.configManager.getConfigPath();
|
|
10
|
+
const installedAgents = Object.keys(adapter.core.agentCatalog.getInstalledEntries());
|
|
11
|
+
const agentList = installedAgents.length > 0 ? installedAgents.map((a) => a === config.defaultAgent ? `${a} (default)` : a).join(", ") : "none";
|
|
12
|
+
await interaction.editReply(
|
|
13
|
+
`**\u2699\uFE0F Settings**
|
|
14
|
+
|
|
15
|
+
**Default Agent:** ${config.defaultAgent}
|
|
16
|
+
**Installed Agents:** ${agentList}
|
|
17
|
+
**Workspace:** \`${config.workspace.baseDir}\`
|
|
18
|
+
**Max Concurrent Sessions:** ${config.security.maxConcurrentSessions}
|
|
19
|
+
**Log Level:** ${config.logging.level}
|
|
20
|
+
**Config file:** \`${configPath}\`
|
|
21
|
+
|
|
22
|
+
*To change settings, edit the config file directly or use the CLI: \`openacp config set <key> <value>\`*`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
async function showSettingsInfo(interaction, adapter) {
|
|
26
|
+
const config = adapter.core.configManager.get();
|
|
27
|
+
const installedAgents = Object.keys(adapter.core.agentCatalog.getInstalledEntries());
|
|
28
|
+
const agentList = installedAgents.length > 0 ? installedAgents.map((a) => a === config.defaultAgent ? `${a} (default)` : a).join(", ") : "none";
|
|
29
|
+
await interaction.followUp({
|
|
30
|
+
content: `**\u2699\uFE0F Settings**
|
|
31
|
+
|
|
32
|
+
**Default Agent:** ${config.defaultAgent}
|
|
33
|
+
**Installed Agents:** ${agentList}
|
|
34
|
+
**Workspace:** \`${config.workspace.baseDir}\`
|
|
35
|
+
**Max Concurrent Sessions:** ${config.security.maxConcurrentSessions}
|
|
36
|
+
|
|
37
|
+
*Use \`/settings\` or \`openacp config\` to view full configuration.*`,
|
|
38
|
+
ephemeral: true
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function handleSettingsButton(interaction, adapter) {
|
|
42
|
+
log.debug({ customId: interaction.customId }, "[discord-settings] Button stub called");
|
|
43
|
+
try {
|
|
44
|
+
await showSettingsInfo(interaction, adapter);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
log.warn({ err }, "[discord-settings] Settings button handler failed");
|
|
47
|
+
try {
|
|
48
|
+
await interaction.reply({
|
|
49
|
+
content: "\u2699\uFE0F Use `/settings` to view configuration.",
|
|
50
|
+
ephemeral: true
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
handleSettings,
|
|
59
|
+
showSettingsInfo,
|
|
60
|
+
handleSettingsButton
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=chunk-THBR6OXH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/discord/commands/settings.ts"],"sourcesContent":["import type { ChatInputCommandInteraction, ButtonInteraction } from 'discord.js'\nimport { log } from '../../../core/log.js'\n\n// TODO: Replace `any` with DiscordAdapter once Task 12 is implemented\n\nexport async function handleSettings(\n interaction: ChatInputCommandInteraction,\n adapter: any,\n): Promise<void> {\n await interaction.deferReply({ ephemeral: true })\n\n const config = adapter.core.configManager.get()\n const configPath = adapter.core.configManager.getConfigPath()\n\n const installedAgents = Object.keys(adapter.core.agentCatalog.getInstalledEntries())\n const agentList = installedAgents.length > 0\n ? installedAgents.map((a: string) => a === config.defaultAgent ? `${a} (default)` : a).join(', ')\n : 'none'\n\n await interaction.editReply(\n `**⚙️ Settings**\\n\\n` +\n `**Default Agent:** ${config.defaultAgent}\\n` +\n `**Installed Agents:** ${agentList}\\n` +\n `**Workspace:** \\`${config.workspace.baseDir}\\`\\n` +\n `**Max Concurrent Sessions:** ${config.security.maxConcurrentSessions}\\n` +\n `**Log Level:** ${config.logging.level}\\n` +\n `**Config file:** \\`${configPath}\\`\\n\\n` +\n `*To change settings, edit the config file directly or use the CLI: \\`openacp config set <key> <value>\\`*`,\n )\n}\n\nexport async function showSettingsInfo(\n interaction: ButtonInteraction,\n adapter: any,\n): Promise<void> {\n const config = adapter.core.configManager.get()\n const installedAgents = Object.keys(adapter.core.agentCatalog.getInstalledEntries())\n const agentList = installedAgents.length > 0\n ? installedAgents.map((a: string) => a === config.defaultAgent ? `${a} (default)` : a).join(', ')\n : 'none'\n\n await interaction.followUp({\n content:\n `**⚙️ Settings**\\n\\n` +\n `**Default Agent:** ${config.defaultAgent}\\n` +\n `**Installed Agents:** ${agentList}\\n` +\n `**Workspace:** \\`${config.workspace.baseDir}\\`\\n` +\n `**Max Concurrent Sessions:** ${config.security.maxConcurrentSessions}\\n\\n` +\n `*Use \\`/settings\\` or \\`openacp config\\` to view full configuration.*`,\n ephemeral: true,\n })\n}\n\nexport async function handleSettingsButton(\n interaction: ButtonInteraction,\n adapter: any,\n): Promise<void> {\n // Stub: settings button callbacks not yet implemented for Discord\n log.debug({ customId: interaction.customId }, '[discord-settings] Button stub called')\n try {\n await showSettingsInfo(interaction, adapter)\n } catch (err) {\n log.warn({ err }, '[discord-settings] Settings button handler failed')\n try {\n await interaction.reply({\n content: '⚙️ Use `/settings` to view configuration.',\n ephemeral: true,\n })\n } catch { /* ignore */ }\n }\n}\n"],"mappings":";;;;;AAKA,eAAsB,eACpB,aACA,SACe;AACf,QAAM,YAAY,WAAW,EAAE,WAAW,KAAK,CAAC;AAEhD,QAAM,SAAS,QAAQ,KAAK,cAAc,IAAI;AAC9C,QAAM,aAAa,QAAQ,KAAK,cAAc,cAAc;AAE5D,QAAM,kBAAkB,OAAO,KAAK,QAAQ,KAAK,aAAa,oBAAoB,CAAC;AACnF,QAAM,YAAY,gBAAgB,SAAS,IACvC,gBAAgB,IAAI,CAAC,MAAc,MAAM,OAAO,eAAe,GAAG,CAAC,eAAe,CAAC,EAAE,KAAK,IAAI,IAC9F;AAEJ,QAAM,YAAY;AAAA,IAChB;AAAA;AAAA,qBACsB,OAAO,YAAY;AAAA,wBAChB,SAAS;AAAA,mBACd,OAAO,UAAU,OAAO;AAAA,+BACZ,OAAO,SAAS,qBAAqB;AAAA,iBACnD,OAAO,QAAQ,KAAK;AAAA,qBAChB,UAAU;AAAA;AAAA;AAAA,EAElC;AACF;AAEA,eAAsB,iBACpB,aACA,SACe;AACf,QAAM,SAAS,QAAQ,KAAK,cAAc,IAAI;AAC9C,QAAM,kBAAkB,OAAO,KAAK,QAAQ,KAAK,aAAa,oBAAoB,CAAC;AACnF,QAAM,YAAY,gBAAgB,SAAS,IACvC,gBAAgB,IAAI,CAAC,MAAc,MAAM,OAAO,eAAe,GAAG,CAAC,eAAe,CAAC,EAAE,KAAK,IAAI,IAC9F;AAEJ,QAAM,YAAY,SAAS;AAAA,IACzB,SACE;AAAA;AAAA,qBACsB,OAAO,YAAY;AAAA,wBAChB,SAAS;AAAA,mBACd,OAAO,UAAU,OAAO;AAAA,+BACZ,OAAO,SAAS,qBAAqB;AAAA;AAAA;AAAA,IAEvE,WAAW;AAAA,EACb,CAAC;AACH;AAEA,eAAsB,qBACpB,aACA,SACe;AAEf,MAAI,MAAM,EAAE,UAAU,YAAY,SAAS,GAAG,uCAAuC;AACrF,MAAI;AACF,UAAM,iBAAiB,aAAa,OAAO;AAAA,EAC7C,SAAS,KAAK;AACZ,QAAI,KAAK,EAAE,IAAI,GAAG,mDAAmD;AACrE,QAAI;AACF,YAAM,YAAY,MAAM;AAAA,QACtB,SAAS;AAAA,QACT,WAAW;AAAA,MACb,CAAC;AAAA,IACH,QAAQ;AAAA,IAAe;AAAA,EACzB;AACF;","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
expandHome
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4LFDEW22.js";
|
|
4
4
|
|
|
5
5
|
// src/core/daemon.ts
|
|
6
6
|
import { spawn } from "child_process";
|
|
@@ -78,23 +78,66 @@ function startDaemon(pidPath = DEFAULT_PID_PATH, logDir) {
|
|
|
78
78
|
child.unref();
|
|
79
79
|
return { pid: child.pid };
|
|
80
80
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
function sleep(ms) {
|
|
82
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
83
|
+
}
|
|
84
|
+
function isProcessAlive(pid) {
|
|
84
85
|
try {
|
|
85
86
|
process.kill(pid, 0);
|
|
86
|
-
|
|
87
|
+
return "alive";
|
|
88
|
+
} catch (e) {
|
|
89
|
+
const err = e;
|
|
90
|
+
if (err.code === "EPERM") return "eperm";
|
|
91
|
+
return "dead";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function stopDaemon(pidPath = DEFAULT_PID_PATH) {
|
|
95
|
+
const pid = readPidFile(pidPath);
|
|
96
|
+
if (pid === null) return { stopped: false, error: "Not running (no PID file)" };
|
|
97
|
+
const status = isProcessAlive(pid);
|
|
98
|
+
if (status === "dead") {
|
|
87
99
|
removePidFile(pidPath);
|
|
88
100
|
return { stopped: false, error: "Not running (stale PID file removed)" };
|
|
89
101
|
}
|
|
102
|
+
if (status === "eperm") {
|
|
103
|
+
removePidFile(pidPath);
|
|
104
|
+
return { stopped: false, error: "PID belongs to another process (stale PID file removed)" };
|
|
105
|
+
}
|
|
90
106
|
try {
|
|
91
107
|
process.kill(pid, "SIGTERM");
|
|
92
|
-
clearRunning();
|
|
93
|
-
removePidFile(pidPath);
|
|
94
|
-
return { stopped: true, pid };
|
|
95
108
|
} catch (e) {
|
|
96
109
|
return { stopped: false, error: `Failed to stop: ${e.message}` };
|
|
97
110
|
}
|
|
111
|
+
clearRunning();
|
|
112
|
+
const POLL_INTERVAL = 100;
|
|
113
|
+
const TIMEOUT = 5e3;
|
|
114
|
+
const start = Date.now();
|
|
115
|
+
while (Date.now() - start < TIMEOUT) {
|
|
116
|
+
await sleep(POLL_INTERVAL);
|
|
117
|
+
const s = isProcessAlive(pid);
|
|
118
|
+
if (s === "dead" || s === "eperm") {
|
|
119
|
+
removePidFile(pidPath);
|
|
120
|
+
return { stopped: true, pid };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, "SIGKILL");
|
|
125
|
+
} catch (e) {
|
|
126
|
+
const err = e;
|
|
127
|
+
if (err.code === "EPERM") {
|
|
128
|
+
return { stopped: false, pid, error: "PID may have been reused by another process. Run `openacp status` to verify, or manually delete the PID file." };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const killStart = Date.now();
|
|
132
|
+
while (Date.now() - killStart < 1e3) {
|
|
133
|
+
await sleep(POLL_INTERVAL);
|
|
134
|
+
const s = isProcessAlive(pid);
|
|
135
|
+
if (s === "dead" || s === "eperm") {
|
|
136
|
+
removePidFile(pidPath);
|
|
137
|
+
return { stopped: true, pid };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { stopped: false, pid, error: "Process did not exit after SIGKILL (possible uninterruptible I/O). PID file retained." };
|
|
98
141
|
}
|
|
99
142
|
function getPidPath() {
|
|
100
143
|
return DEFAULT_PID_PATH;
|
|
@@ -126,4 +169,4 @@ export {
|
|
|
126
169
|
clearRunning,
|
|
127
170
|
shouldAutoStart
|
|
128
171
|
};
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
172
|
+
//# sourceMappingURL=chunk-TOZQ3JFN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/daemon.ts"],"sourcesContent":["import { spawn } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport * as os from 'node:os'\nimport { expandHome } from './config.js'\n\nconst DEFAULT_PID_PATH = path.join(os.homedir(), '.openacp', 'openacp.pid')\nconst DEFAULT_LOG_DIR = path.join(os.homedir(), '.openacp', 'logs')\nconst RUNNING_MARKER = path.join(os.homedir(), '.openacp', 'running')\n\nexport function writePidFile(pidPath: string, pid: number): void {\n const dir = path.dirname(pidPath)\n fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(pidPath, String(pid))\n}\n\nexport function readPidFile(pidPath: string): number | null {\n try {\n const content = fs.readFileSync(pidPath, 'utf-8').trim()\n const pid = parseInt(content, 10)\n return isNaN(pid) ? null : pid\n } catch {\n return null\n }\n}\n\nexport function removePidFile(pidPath: string): void {\n try {\n fs.unlinkSync(pidPath)\n } catch {\n // ignore if already gone\n }\n}\n\nexport function isProcessRunning(pidPath: string): boolean {\n const pid = readPidFile(pidPath)\n if (pid === null) return false\n try {\n process.kill(pid, 0)\n return true\n } catch {\n // Process not running, clean up stale PID file\n removePidFile(pidPath)\n return false\n }\n}\n\nexport function getStatus(pidPath: string = DEFAULT_PID_PATH): { running: boolean; pid?: number } {\n const pid = readPidFile(pidPath)\n if (pid === null) return { running: false }\n try {\n process.kill(pid, 0)\n return { running: true, pid }\n } catch {\n removePidFile(pidPath)\n return { running: false }\n }\n}\n\nexport function startDaemon(pidPath: string = DEFAULT_PID_PATH, logDir?: string): { pid: number } | { error: string } {\n // Mark as running so auto-start works on next boot\n markRunning()\n\n // Check if already running\n if (isProcessRunning(pidPath)) {\n const pid = readPidFile(pidPath)!\n return { error: `Already running (PID ${pid})` }\n }\n\n const resolvedLogDir = logDir ? expandHome(logDir) : DEFAULT_LOG_DIR\n fs.mkdirSync(resolvedLogDir, { recursive: true })\n const logFile = path.join(resolvedLogDir, 'openacp.log')\n\n // Find the CLI entry point\n const cliPath = path.resolve(process.argv[1])\n const nodePath = process.execPath\n\n const out = fs.openSync(logFile, 'a')\n const err = fs.openSync(logFile, 'a')\n\n const child = spawn(nodePath, [cliPath, '--daemon-child'], {\n detached: true,\n stdio: ['ignore', out, err],\n })\n\n // Close file descriptors in parent — child has its own copies\n fs.closeSync(out)\n fs.closeSync(err)\n\n if (!child.pid) {\n return { error: 'Failed to spawn daemon process' }\n }\n\n // PID file is written by the child process itself (in main.ts startServer)\n // to avoid race conditions and ensure consistency with LaunchAgent/systemd starts.\n // We still write it here as a fallback in case the child hasn't written it yet\n // when the parent needs to report the PID.\n writePidFile(pidPath, child.pid)\n child.unref()\n\n return { pid: child.pid }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\nfunction isProcessAlive(pid: number): 'alive' | 'dead' | 'eperm' {\n try {\n process.kill(pid, 0)\n return 'alive'\n } catch (e) {\n const err = e as NodeJS.ErrnoException\n if (err.code === 'EPERM') return 'eperm'\n return 'dead'\n }\n}\n\nexport async function stopDaemon(pidPath: string = DEFAULT_PID_PATH): Promise<{ stopped: boolean; pid?: number; error?: string }> {\n const pid = readPidFile(pidPath)\n if (pid === null) return { stopped: false, error: 'Not running (no PID file)' }\n\n const status = isProcessAlive(pid)\n if (status === 'dead') {\n removePidFile(pidPath)\n return { stopped: false, error: 'Not running (stale PID file removed)' }\n }\n if (status === 'eperm') {\n removePidFile(pidPath)\n return { stopped: false, error: 'PID belongs to another process (stale PID file removed)' }\n }\n\n try {\n process.kill(pid, 'SIGTERM')\n } catch (e) {\n return { stopped: false, error: `Failed to stop: ${(e as Error).message}` }\n }\n\n clearRunning()\n\n const POLL_INTERVAL = 100\n const TIMEOUT = 5000\n const start = Date.now()\n\n while (Date.now() - start < TIMEOUT) {\n await sleep(POLL_INTERVAL)\n const s = isProcessAlive(pid)\n if (s === 'dead' || s === 'eperm') {\n removePidFile(pidPath)\n return { stopped: true, pid }\n }\n }\n\n try {\n process.kill(pid, 'SIGKILL')\n } catch (e) {\n const err = e as NodeJS.ErrnoException\n if (err.code === 'EPERM') {\n return { stopped: false, pid, error: 'PID may have been reused by another process. Run `openacp status` to verify, or manually delete the PID file.' }\n }\n }\n\n const killStart = Date.now()\n while (Date.now() - killStart < 1000) {\n await sleep(POLL_INTERVAL)\n const s = isProcessAlive(pid)\n if (s === 'dead' || s === 'eperm') {\n removePidFile(pidPath)\n return { stopped: true, pid }\n }\n }\n\n // SIGKILL sent but process still alive after 1s — extremely rare (uninterruptible I/O).\n return { stopped: false, pid, error: 'Process did not exit after SIGKILL (possible uninterruptible I/O). PID file retained.' }\n}\n\nexport function getPidPath(): string {\n return DEFAULT_PID_PATH\n}\n\n/** Mark that the daemon should auto-start on boot */\nexport function markRunning(): void {\n fs.mkdirSync(path.dirname(RUNNING_MARKER), { recursive: true })\n fs.writeFileSync(RUNNING_MARKER, '')\n}\n\n/** Remove running marker — daemon won't auto-start on boot */\nexport function clearRunning(): void {\n try { fs.unlinkSync(RUNNING_MARKER) } catch { /* ignore */ }\n}\n\n/** Check if the daemon was running before (should auto-start on boot) */\nexport function shouldAutoStart(): boolean {\n return fs.existsSync(RUNNING_MARKER)\n}\n"],"mappings":"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","names":["resolve"]}
|