telecodex 0.1.3 → 0.1.4
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/dist/bot/auth.js +9 -8
- package/dist/bot/commandSupport.js +27 -14
- package/dist/bot/handlers/messageHandlers.js +6 -5
- package/dist/bot/handlers/operationalHandlers.js +84 -72
- package/dist/bot/handlers/projectHandlers.js +90 -76
- package/dist/bot/handlers/sessionConfigHandlers.js +141 -111
- package/dist/bot/inputService.js +30 -14
- package/dist/codex/configOverrides.js +50 -0
- package/dist/codex/sessionCatalog.js +66 -12
- package/dist/runtime/bootstrap.js +60 -37
- package/dist/runtime/startTelecodex.js +28 -21
- package/dist/store/fileState.js +100 -8
- package/dist/store/projects.js +3 -0
- package/dist/store/sessions.js +3 -0
- package/dist/telegram/delivery.js +41 -5
- package/dist/telegram/formatted.js +183 -0
- package/dist/telegram/messageBuffer.js +10 -0
- package/dist/telegram/splitMessage.js +1 -1
- package/package.json +5 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { contextLogFields, ensureTopicSession, formatPrivateProjectList, formatProjectStatus, formatTopicName, getProjectForContext,
|
|
2
|
+
import { contextLogFields, ensureTopicSession, formatPrivateProjectList, formatProjectStatus, formatTopicName, getProjectForContext, hasTopicContext, isPrivateChat, isSupergroupChat, parseSubcommand, postTopicReadyMessage, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
3
3
|
import { formatSessionRuntimeStatus } from "../../runtime/sessionRuntime.js";
|
|
4
|
+
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
4
5
|
const PROJECT_REQUIRED_MESSAGE = "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.";
|
|
5
6
|
export function registerProjectHandlers(deps) {
|
|
6
7
|
const { bot, config, store, projects, logger } = deps;
|
|
@@ -8,24 +9,24 @@ export function registerProjectHandlers(deps) {
|
|
|
8
9
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
9
10
|
if (isPrivateChat(ctx)) {
|
|
10
11
|
if (!command || command === "list" || command === "status") {
|
|
11
|
-
await ctx
|
|
12
|
+
await replyNotice(ctx, formatPrivateProjectList(projects));
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
|
-
await ctx
|
|
15
|
+
await replyNotice(ctx, "Use /project bind inside a supergroup with topics enabled. Private chat is only for admin entry points.");
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
if (!isSupergroupChat(ctx)) {
|
|
18
|
-
await ctx
|
|
19
|
+
await replyNotice(ctx, "Use telecodex inside a supergroup with forum topics enabled.");
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
if (!command || command === "status") {
|
|
22
23
|
const project = getProjectForContext(ctx, projects);
|
|
23
|
-
await ctx
|
|
24
|
+
await replyNotice(ctx, project ? formatProjectStatus(project) : PROJECT_REQUIRED_MESSAGE);
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
if (command === "bind") {
|
|
27
28
|
if (!args) {
|
|
28
|
-
await ctx
|
|
29
|
+
await replyUsage(ctx, "/project bind <absolute-path>");
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
try {
|
|
@@ -39,19 +40,21 @@ export function registerProjectHandlers(deps) {
|
|
|
39
40
|
project: project.name,
|
|
40
41
|
cwd: project.cwd,
|
|
41
42
|
});
|
|
42
|
-
const session =
|
|
43
|
+
const session = await requireScopedSession(ctx, store, projects, config, { requireTopic: false });
|
|
43
44
|
if (session && session.cwd !== project.cwd) {
|
|
44
45
|
store.setCwd(session.sessionKey, project.cwd);
|
|
45
46
|
}
|
|
46
|
-
await ctx
|
|
47
|
-
"Project binding updated.",
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
await replyDocument(ctx, {
|
|
48
|
+
title: "Project binding updated.",
|
|
49
|
+
fields: [
|
|
50
|
+
codeField("project", project.name),
|
|
51
|
+
codeField("root", project.cwd),
|
|
52
|
+
],
|
|
53
|
+
footer: "This supergroup now represents one project, and each topic maps to one Codex thread.",
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
catch (error) {
|
|
54
|
-
await ctx
|
|
57
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
55
58
|
}
|
|
56
59
|
return;
|
|
57
60
|
}
|
|
@@ -60,36 +63,36 @@ export function registerProjectHandlers(deps) {
|
|
|
60
63
|
...contextLogFields(ctx),
|
|
61
64
|
});
|
|
62
65
|
projects.remove(String(ctx.chat.id));
|
|
63
|
-
await ctx
|
|
66
|
+
await replyNotice(ctx, "Removed the project binding for this supergroup.");
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
66
|
-
await ctx
|
|
69
|
+
await replyUsage(ctx, ["/project", "/project bind <absolute-path>", "/project unbind"]);
|
|
67
70
|
});
|
|
68
71
|
bot.command("thread", async (ctx) => {
|
|
69
72
|
if (isPrivateChat(ctx)) {
|
|
70
|
-
await ctx
|
|
73
|
+
await replyNotice(ctx, "The thread command is only available inside project supergroups.");
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
74
77
|
if (!command) {
|
|
75
78
|
if (hasTopicContext(ctx)) {
|
|
76
|
-
const session =
|
|
79
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
77
80
|
if (!session)
|
|
78
81
|
return;
|
|
79
|
-
await ctx
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"/thread new <topic-name>",
|
|
89
|
-
|
|
82
|
+
await replyDocument(ctx, {
|
|
83
|
+
title: "Current thread",
|
|
84
|
+
fields: [
|
|
85
|
+
codeField("thread", session.codexThreadId ?? "not created"),
|
|
86
|
+
textField("state", formatSessionRuntimeStatus(session.runtimeStatus)),
|
|
87
|
+
textField("state detail", session.runtimeStatusDetail ?? "none"),
|
|
88
|
+
textField("queue", store.getQueuedInputCount(session.sessionKey)),
|
|
89
|
+
codeField("cwd", session.cwd),
|
|
90
|
+
],
|
|
91
|
+
footer: ["Manage threads in this project:", "/thread list", "/thread resume <threadId>", "/thread new <topic-name>"],
|
|
92
|
+
});
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
|
-
await ctx
|
|
95
|
+
await replyUsage(ctx, ["/thread list", "/thread resume <threadId>", "/thread new <topic-name>"]);
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
if (command === "list") {
|
|
@@ -98,7 +101,7 @@ export function registerProjectHandlers(deps) {
|
|
|
98
101
|
}
|
|
99
102
|
if (command === "resume") {
|
|
100
103
|
if (!args) {
|
|
101
|
-
await ctx
|
|
104
|
+
await replyUsage(ctx, "/thread resume <threadId>");
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
104
107
|
await resumeThreadIntoTopic(ctx, deps, args);
|
|
@@ -108,7 +111,7 @@ export function registerProjectHandlers(deps) {
|
|
|
108
111
|
await createFreshThreadTopic(ctx, deps, args);
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
|
-
await ctx
|
|
114
|
+
await replyUsage(ctx, ["/thread list", "/thread resume <threadId>", "/thread new <topic-name>"]);
|
|
112
115
|
});
|
|
113
116
|
bot.on(["message:forum_topic_created", "message:forum_topic_edited"], async (ctx) => {
|
|
114
117
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -128,7 +131,7 @@ async function resumeThreadIntoTopic(ctx, deps, threadId) {
|
|
|
128
131
|
const { bot, config, store, projects, logger, threadCatalog } = deps;
|
|
129
132
|
const project = getProjectForContext(ctx, projects);
|
|
130
133
|
if (!project) {
|
|
131
|
-
await ctx
|
|
134
|
+
await replyNotice(ctx, PROJECT_REQUIRED_MESSAGE);
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
const thread = await threadCatalog.findProjectThreadById({
|
|
@@ -136,12 +139,14 @@ async function resumeThreadIntoTopic(ctx, deps, threadId) {
|
|
|
136
139
|
threadId,
|
|
137
140
|
});
|
|
138
141
|
if (!thread) {
|
|
139
|
-
await ctx
|
|
140
|
-
"Could not find a saved Codex thread with that id under this project.",
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
await replyDocument(ctx, {
|
|
143
|
+
title: "Could not find a saved Codex thread with that id under this project.",
|
|
144
|
+
fields: [
|
|
145
|
+
codeField("project root", project.cwd),
|
|
146
|
+
codeField("thread", threadId),
|
|
147
|
+
],
|
|
148
|
+
footer: "Run /thread list to inspect the saved project threads first.",
|
|
149
|
+
});
|
|
145
150
|
return;
|
|
146
151
|
}
|
|
147
152
|
const topicName = formatTopicName(thread.preview, `Resumed ${thread.id.slice(0, 8)}`);
|
|
@@ -161,14 +166,16 @@ async function resumeThreadIntoTopic(ctx, deps, threadId) {
|
|
|
161
166
|
threadId: thread.id,
|
|
162
167
|
topicName: forumTopic.name,
|
|
163
168
|
});
|
|
164
|
-
await ctx
|
|
165
|
-
"Created a topic and bound it to the existing thread id.",
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
await replyDocument(ctx, {
|
|
170
|
+
title: "Created a topic and bound it to the existing thread id.",
|
|
171
|
+
fields: [
|
|
172
|
+
textField("topic", forumTopic.name),
|
|
173
|
+
textField("topic id", forumTopic.message_thread_id),
|
|
174
|
+
codeField("thread", thread.id),
|
|
175
|
+
codeField("cwd", thread.cwd),
|
|
176
|
+
],
|
|
177
|
+
footer: "Future messages in this topic will continue on that thread through the Codex SDK.",
|
|
178
|
+
});
|
|
172
179
|
await postTopicReadyMessage(bot, session, [
|
|
173
180
|
"This topic is now bound to an existing Codex thread id.",
|
|
174
181
|
`thread: ${thread.id}`,
|
|
@@ -179,7 +186,7 @@ async function createFreshThreadTopic(ctx, deps, requestedName) {
|
|
|
179
186
|
const { bot, config, store, projects, logger } = deps;
|
|
180
187
|
const project = getProjectForContext(ctx, projects);
|
|
181
188
|
if (!project) {
|
|
182
|
-
await ctx
|
|
189
|
+
await replyNotice(ctx, PROJECT_REQUIRED_MESSAGE);
|
|
183
190
|
return;
|
|
184
191
|
}
|
|
185
192
|
const topicName = formatTopicName(requestedName, "New Thread");
|
|
@@ -197,12 +204,14 @@ async function createFreshThreadTopic(ctx, deps, requestedName) {
|
|
|
197
204
|
sessionKey: session.sessionKey,
|
|
198
205
|
topicName: forumTopic.name,
|
|
199
206
|
});
|
|
200
|
-
await ctx
|
|
201
|
-
"Created a new topic.",
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
await replyDocument(ctx, {
|
|
208
|
+
title: "Created a new topic.",
|
|
209
|
+
fields: [
|
|
210
|
+
textField("topic", forumTopic.name),
|
|
211
|
+
textField("topic id", forumTopic.message_thread_id),
|
|
212
|
+
],
|
|
213
|
+
footer: "Your first normal message will start a new Codex SDK thread.",
|
|
214
|
+
});
|
|
206
215
|
await postTopicReadyMessage(bot, session, [
|
|
207
216
|
"New topic created.",
|
|
208
217
|
"Send a message to start a new Codex thread.",
|
|
@@ -214,7 +223,7 @@ async function listProjectThreads(ctx, deps) {
|
|
|
214
223
|
const { projects, store, threadCatalog } = deps;
|
|
215
224
|
const project = getProjectForContext(ctx, projects);
|
|
216
225
|
if (!project) {
|
|
217
|
-
await ctx
|
|
226
|
+
await replyNotice(ctx, PROJECT_REQUIRED_MESSAGE);
|
|
218
227
|
return;
|
|
219
228
|
}
|
|
220
229
|
const threads = await threadCatalog.listProjectThreads({
|
|
@@ -222,30 +231,35 @@ async function listProjectThreads(ctx, deps) {
|
|
|
222
231
|
limit: 8,
|
|
223
232
|
});
|
|
224
233
|
if (threads.length === 0) {
|
|
225
|
-
await ctx
|
|
226
|
-
"No saved Codex threads were found for this project yet.",
|
|
227
|
-
|
|
228
|
-
|
|
234
|
+
await replyDocument(ctx, {
|
|
235
|
+
title: "No saved Codex threads were found for this project yet.",
|
|
236
|
+
fields: [codeField("project root", project.cwd)],
|
|
237
|
+
});
|
|
229
238
|
return;
|
|
230
239
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
await replyDocument(ctx, {
|
|
241
|
+
title: "Saved Codex threads",
|
|
242
|
+
fields: [
|
|
243
|
+
codeField("project", project.name),
|
|
244
|
+
codeField("root", project.cwd),
|
|
245
|
+
],
|
|
246
|
+
sections: threads.map((thread, index) => {
|
|
234
247
|
const relativeCwd = path.relative(project.cwd, thread.cwd) || ".";
|
|
235
248
|
const bound = store.getByThreadId(thread.id);
|
|
236
|
-
return
|
|
237
|
-
`${index + 1}. ${thread.preview}`,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
return {
|
|
250
|
+
title: `${index + 1}. ${thread.preview}`,
|
|
251
|
+
fields: [
|
|
252
|
+
codeField("id", thread.id),
|
|
253
|
+
codeField("resume", `/thread resume ${thread.id}`),
|
|
254
|
+
codeField("cwd", relativeCwd),
|
|
255
|
+
textField("updated", thread.updatedAt),
|
|
256
|
+
textField("source", thread.source ?? "unknown"),
|
|
257
|
+
...(bound
|
|
258
|
+
? [textField("bound", bound.telegramTopicName ?? bound.messageThreadId ?? bound.sessionKey)]
|
|
259
|
+
: []),
|
|
260
|
+
],
|
|
261
|
+
};
|
|
246
262
|
}),
|
|
247
|
-
"",
|
|
248
|
-
|
|
249
|
-
];
|
|
250
|
-
await ctx.reply(lines.join("\n"));
|
|
263
|
+
footer: "Copy an id or resume command from the code-formatted fields above.",
|
|
264
|
+
});
|
|
251
265
|
}
|