slacklocalvibe 0.1.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.
@@ -0,0 +1,314 @@
1
+ const path = require("path");
2
+ const { loadConfig, normalizeConfig, assertNotifyConfig } = require("../lib/config");
3
+ const { createLogger, LEVELS, safeError } = require("../lib/logger");
4
+ const { notifyLogPath, daemonLogPath, wizardLogPath } = require("../lib/paths");
5
+ const {
6
+ createWebClient,
7
+ openDmChannel,
8
+ postParentMessage,
9
+ postThreadMessage,
10
+ postMessage,
11
+ } = require("../lib/slack");
12
+ const { splitText } = require("../lib/text");
13
+ const { parseCodexNotify, parseClaudeHook } = require("../lib/notify-input");
14
+ const { markdownToMrkdwn } = require("../lib/markdown-to-mrkdwn");
15
+ const { recordRoute } = require("../lib/route-store");
16
+
17
+ async function runNotify({ tool }) {
18
+ const { log } = createLogger({ filePath: notifyLogPath(), scope: "notify" });
19
+ const startedAt = Date.now();
20
+ log(LEVELS.INFO, "notify.start", { tool });
21
+
22
+ let config;
23
+ try {
24
+ config = loadConfig();
25
+ } catch (error) {
26
+ log(LEVELS.ERROR, "notify.config_parse_failed", { error: safeError(error) });
27
+ process.exitCode = 1;
28
+ throw error;
29
+ }
30
+ if (!config) {
31
+ log(LEVELS.ERROR, "notify.config_missing");
32
+ process.exitCode = 1;
33
+ throw new Error("設定ファイルが見つかりません。");
34
+ }
35
+
36
+ config = normalizeConfig(config);
37
+
38
+ try {
39
+ assertNotifyConfig(config);
40
+ } catch (error) {
41
+ log(LEVELS.ERROR, "notify.config_invalid", { error: safeError(error) });
42
+ process.exitCode = 1;
43
+ throw error;
44
+ }
45
+
46
+ const client = createWebClient(config.slack.bot_token);
47
+ const logLocationsText = buildLogLocationsMessage();
48
+ let channel = "";
49
+
50
+ let input;
51
+ try {
52
+ input = await readInput(tool);
53
+ } catch (error) {
54
+ log(LEVELS.ERROR, "notify.input_invalid", { error: safeError(error) });
55
+ await tryPostLogLocations({
56
+ log,
57
+ client,
58
+ config,
59
+ logLocationsText,
60
+ channel,
61
+ });
62
+ process.exitCode = 1;
63
+ throw error;
64
+ }
65
+
66
+ if (!input) {
67
+ log(LEVELS.ERROR, "notify.skip.not_target_event");
68
+ process.exitCode = 1;
69
+ throw new Error("通知対象のイベントではありません。");
70
+ }
71
+
72
+ if (!input.session_id) {
73
+ log(LEVELS.ERROR, "notify.session_missing");
74
+ await tryPostLogLocations({
75
+ log,
76
+ client,
77
+ config,
78
+ logLocationsText,
79
+ channel,
80
+ });
81
+ process.exitCode = 1;
82
+ throw new Error("セッションIDが取得できません。");
83
+ }
84
+ const userTextMissing = input.user_text === "(ユーザーメッセージ抽出失敗)";
85
+ const assistantTextMissing =
86
+ input.assistant_text === "(本文抽出失敗)" ||
87
+ String(input.assistant_text || "").startsWith("(本文抽出エラー:");
88
+
89
+ if (userTextMissing) {
90
+ log(LEVELS.WARNING, "notify.user_text_missing", {
91
+ tool: input.tool,
92
+ meta: input.meta || {},
93
+ });
94
+ }
95
+ if (assistantTextMissing) {
96
+ log(LEVELS.WARNING, "notify.assistant_text_missing", { tool: input.tool });
97
+ }
98
+
99
+ let userText = "";
100
+ let assistantText = "";
101
+ try {
102
+ channel = await openDmChannel({
103
+ client,
104
+ userId: config.destinations.dm.target_user_id,
105
+ log,
106
+ });
107
+ if (!channel) {
108
+ log(LEVELS.ERROR, "notify.dm_channel_missing");
109
+ await tryPostLogLocations({
110
+ log,
111
+ client,
112
+ config,
113
+ logLocationsText,
114
+ channel,
115
+ });
116
+ return;
117
+ }
118
+
119
+ try {
120
+ userText = markdownToMrkdwn(input.user_text);
121
+ assistantText = markdownToMrkdwn(input.assistant_text);
122
+ } catch (error) {
123
+ log(LEVELS.ERROR, "notify.markdown_convert_failed", {
124
+ error: safeError(error),
125
+ });
126
+ await tryPostLogLocations({
127
+ log,
128
+ client,
129
+ config,
130
+ logLocationsText,
131
+ channel,
132
+ });
133
+ return;
134
+ }
135
+
136
+ const userChunks = splitText(userText);
137
+ const toolLabel = input.tool === "codex" ? "Codex" : "Claude";
138
+ const projectName = extractProjectName(input.cwd);
139
+ const userHeader = `[ ${toolLabel} | ${projectName} ]`;
140
+ const parentText =
141
+ userChunks[0] ? `${userHeader}\n${userChunks[0]}` : userHeader;
142
+
143
+ const parentTs = await postParentMessage({
144
+ client,
145
+ log,
146
+ channel,
147
+ text: parentText,
148
+ });
149
+ try {
150
+ recordRoute({
151
+ channel,
152
+ threadTs: parentTs,
153
+ tool: input.tool,
154
+ sessionId: input.session_id,
155
+ turnId: input.turn_id,
156
+ cwd: input.cwd || "",
157
+ });
158
+ log(LEVELS.SUCCRSS, "notify.route_recorded", {
159
+ tool: input.tool,
160
+ thread_ts: parentTs,
161
+ });
162
+ } catch (error) {
163
+ log(LEVELS.ERROR, "notify.route_record_failed", {
164
+ error: safeError(error),
165
+ });
166
+ }
167
+
168
+ const extraUserChunks = userChunks.slice(1);
169
+ for (const chunk of extraUserChunks) {
170
+ if (!chunk) continue;
171
+ await postThreadMessage({
172
+ client,
173
+ log,
174
+ channel,
175
+ threadTs: parentTs,
176
+ text: chunk,
177
+ });
178
+ }
179
+
180
+ const assistantChunks = splitText(assistantText);
181
+ for (const chunk of assistantChunks) {
182
+ if (!chunk) continue;
183
+ await postThreadMessage({
184
+ client,
185
+ log,
186
+ channel,
187
+ threadTs: parentTs,
188
+ text: chunk,
189
+ });
190
+ }
191
+
192
+ if (userTextMissing || assistantTextMissing) {
193
+ await postThreadMessage({
194
+ client,
195
+ log,
196
+ channel,
197
+ threadTs: parentTs,
198
+ text: logLocationsText,
199
+ });
200
+ }
201
+
202
+ log(LEVELS.SUCCRSS, "notify.done", {
203
+ duration_ms: Date.now() - startedAt,
204
+ user_len: input.user_text?.length || 0,
205
+ assistant_len: input.assistant_text?.length || 0,
206
+ });
207
+ } catch (error) {
208
+ log(LEVELS.ERROR, "notify.slack_error", {
209
+ error: safeError(error),
210
+ duration_ms: Date.now() - startedAt,
211
+ });
212
+ await tryPostLogLocations({
213
+ log,
214
+ client,
215
+ config,
216
+ logLocationsText,
217
+ channel,
218
+ });
219
+ }
220
+ }
221
+
222
+ async function readInput(tool) {
223
+ if (tool === "codex") {
224
+ const raw = process.argv[process.argv.length - 1];
225
+ return parseCodexNotify(raw);
226
+ }
227
+ if (tool === "claude") {
228
+ const raw = await readStdin();
229
+ if (!raw) return null;
230
+ return parseClaudeHook(raw);
231
+ }
232
+ throw new Error(`Unsupported tool: ${tool}`);
233
+ }
234
+
235
+ function readStdin() {
236
+ return new Promise((resolve, reject) => {
237
+ let data = "";
238
+ process.stdin.setEncoding("utf8");
239
+ process.stdin.on("data", (chunk) => {
240
+ data += chunk;
241
+ });
242
+ process.stdin.on("end", () => resolve(data));
243
+ process.stdin.on("error", (err) => reject(err));
244
+ });
245
+ }
246
+
247
+ function buildLogLocationsMessage() {
248
+ return [
249
+ "ログの場所:",
250
+ `- notify: ${notifyLogPath()}`,
251
+ `- daemon: ${daemonLogPath()}`,
252
+ `- wizard: ${wizardLogPath()}`,
253
+ ].join("\n");
254
+ }
255
+
256
+ async function tryPostLogLocations({
257
+ log,
258
+ client,
259
+ config,
260
+ logLocationsText,
261
+ channel,
262
+ threadTs,
263
+ }) {
264
+ try {
265
+ if (!client || !config?.destinations?.dm?.target_user_id) {
266
+ log(LEVELS.ERROR, "notify.log_locations_unavailable");
267
+ process.exitCode = 1;
268
+ return;
269
+ }
270
+ let targetChannel = channel;
271
+ if (!targetChannel) {
272
+ targetChannel = await openDmChannel({
273
+ client,
274
+ userId: config.destinations.dm.target_user_id,
275
+ log,
276
+ });
277
+ }
278
+ if (!targetChannel) {
279
+ log(LEVELS.ERROR, "notify.log_locations_channel_missing");
280
+ process.exitCode = 1;
281
+ return;
282
+ }
283
+ if (threadTs) {
284
+ await postThreadMessage({
285
+ client,
286
+ log,
287
+ channel: targetChannel,
288
+ threadTs,
289
+ text: logLocationsText,
290
+ });
291
+ return;
292
+ }
293
+ await postMessage({
294
+ client,
295
+ log,
296
+ channel: targetChannel,
297
+ text: logLocationsText,
298
+ });
299
+ } catch (error) {
300
+ log(LEVELS.ERROR, "notify.log_locations_failed", { error: safeError(error) });
301
+ process.exitCode = 1;
302
+ }
303
+ }
304
+
305
+ function extractProjectName(cwd) {
306
+ if (!cwd || typeof cwd !== "string") return "unknown";
307
+ const normalized = cwd.endsWith(path.sep) ? cwd.slice(0, -1) : cwd;
308
+ const base = path.basename(normalized);
309
+ return base || "unknown";
310
+ }
311
+
312
+ module.exports = {
313
+ runNotify,
314
+ };