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