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.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +37 -0
- package/src/cli.js +38 -0
- package/src/commands/daemon.js +289 -0
- package/src/commands/launchd.js +46 -0
- package/src/commands/notify.js +314 -0
- package/src/commands/wizard.js +1143 -0
- package/src/lib/config.js +101 -0
- package/src/lib/launchd.js +237 -0
- package/src/lib/logger.js +67 -0
- package/src/lib/markdown-to-mrkdwn.js +15 -0
- package/src/lib/messages.js +58 -0
- package/src/lib/notify-input.js +191 -0
- package/src/lib/paths.js +88 -0
- package/src/lib/resume.js +86 -0
- package/src/lib/route-store.js +60 -0
- package/src/lib/slack.js +140 -0
- package/src/lib/text.js +62 -0
- package/src/lib/user-config.js +90 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { configPath, configDir } = require("./paths");
|
|
4
|
+
|
|
5
|
+
function loadConfig() {
|
|
6
|
+
const filePath = configPath();
|
|
7
|
+
if (!fs.existsSync(filePath)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const err = new Error(`設定ファイルのJSON解析に失敗しました: ${filePath}`);
|
|
15
|
+
err.cause = error;
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeConfig(config) {
|
|
21
|
+
const dir = configDir();
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
const filePath = configPath();
|
|
24
|
+
backupFileIfExists(filePath);
|
|
25
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf8");
|
|
26
|
+
try {
|
|
27
|
+
fs.chmodSync(filePath, 0o600);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const err = new Error(`設定ファイルの権限変更に失敗しました: ${filePath}`);
|
|
30
|
+
err.cause = error;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function backupFileIfExists(filePath) {
|
|
36
|
+
if (!fs.existsSync(filePath)) return;
|
|
37
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
38
|
+
const backupPath = `${filePath}.bak-${timestamp}`;
|
|
39
|
+
fs.copyFileSync(filePath, backupPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeConfig(config) {
|
|
43
|
+
const normalized = {
|
|
44
|
+
slack: {
|
|
45
|
+
bot_token: config?.slack?.bot_token || "",
|
|
46
|
+
app_token: config?.slack?.app_token || "",
|
|
47
|
+
},
|
|
48
|
+
destinations: {
|
|
49
|
+
dm: {
|
|
50
|
+
enabled: Boolean(config?.destinations?.dm?.enabled),
|
|
51
|
+
target_user_id: config?.destinations?.dm?.target_user_id || "",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
features: {
|
|
55
|
+
reply_resume: Boolean(config?.features?.reply_resume),
|
|
56
|
+
launchd_enabled: Boolean(config?.features?.launchd_enabled),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!normalized.features.reply_resume) {
|
|
61
|
+
normalized.features.launchd_enabled = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertNotifyConfig(config) {
|
|
68
|
+
if (!config?.slack?.bot_token) {
|
|
69
|
+
throw new Error("slack.bot_token が未設定です。");
|
|
70
|
+
}
|
|
71
|
+
const dmEnabled = Boolean(config?.destinations?.dm?.enabled);
|
|
72
|
+
if (!dmEnabled) {
|
|
73
|
+
throw new Error("destinations.dm.enabled が false のため通知できません。");
|
|
74
|
+
}
|
|
75
|
+
if (!config?.destinations?.dm?.target_user_id) {
|
|
76
|
+
throw new Error("destinations.dm.target_user_id が未設定です。");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertDaemonConfig(config) {
|
|
81
|
+
if (!config?.slack?.bot_token) {
|
|
82
|
+
throw new Error("slack.bot_token が未設定です。");
|
|
83
|
+
}
|
|
84
|
+
if (!config?.slack?.app_token) {
|
|
85
|
+
throw new Error("slack.app_token が未設定です。");
|
|
86
|
+
}
|
|
87
|
+
if (!config?.features?.reply_resume) {
|
|
88
|
+
throw new Error("features.reply_resume が false のため daemon を起動できません。");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
loadConfig,
|
|
94
|
+
writeConfig,
|
|
95
|
+
backupFileIfExists,
|
|
96
|
+
normalizeConfig,
|
|
97
|
+
assertNotifyConfig,
|
|
98
|
+
assertDaemonConfig,
|
|
99
|
+
configPath,
|
|
100
|
+
configDir,
|
|
101
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const {
|
|
5
|
+
launchAgentsDir,
|
|
6
|
+
launchdPlistPath,
|
|
7
|
+
daemonLogPath,
|
|
8
|
+
resolveCommandPathStrict,
|
|
9
|
+
} = require("./paths");
|
|
10
|
+
|
|
11
|
+
function buildDaemonPathEnv() {
|
|
12
|
+
const parts = [];
|
|
13
|
+
const add = (value) => {
|
|
14
|
+
if (!value) return;
|
|
15
|
+
const dir = value.trim();
|
|
16
|
+
if (!dir) return;
|
|
17
|
+
if (parts.includes(dir)) return;
|
|
18
|
+
parts.push(dir);
|
|
19
|
+
};
|
|
20
|
+
add(path.dirname(process.execPath));
|
|
21
|
+
const codexPath = resolveCommandPathStrict("codex", { optional: true });
|
|
22
|
+
const claudePath = resolveCommandPathStrict("claude", { optional: true });
|
|
23
|
+
add(codexPath ? path.dirname(codexPath) : "");
|
|
24
|
+
add(claudePath ? path.dirname(claudePath) : "");
|
|
25
|
+
return parts.join(":");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function installLaunchd({ cliPath } = {}) {
|
|
29
|
+
const binaryPath = cliPath || resolveBinaryPath();
|
|
30
|
+
if (!binaryPath) {
|
|
31
|
+
throw new Error("slacklocalvibe の実行ファイルが見つかりません。");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
uninstallLaunchd({ allowMissing: true });
|
|
35
|
+
|
|
36
|
+
const plistPath = launchdPlistPath();
|
|
37
|
+
fs.mkdirSync(launchAgentsDir(), { recursive: true });
|
|
38
|
+
fs.writeFileSync(plistPath, buildPlist(binaryPath), "utf8");
|
|
39
|
+
|
|
40
|
+
const uid = process.getuid();
|
|
41
|
+
const label = "dev.slacklocalvibe.daemon";
|
|
42
|
+
const launchctlPath = resolveCommandPathStrict("launchctl");
|
|
43
|
+
let result = spawnSync(launchctlPath, ["bootstrap", `gui/${uid}`, plistPath], {
|
|
44
|
+
stdio: "pipe",
|
|
45
|
+
env: process.env,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
const stderrText = (result.stderr || "").toString("utf8").trim();
|
|
50
|
+
const stdoutText = (result.stdout || "").toString("utf8").trim();
|
|
51
|
+
const detail = stderrText || stdoutText || "";
|
|
52
|
+
const shouldRetry =
|
|
53
|
+
result.status === 5 ||
|
|
54
|
+
/input\/output error/i.test(detail);
|
|
55
|
+
if (shouldRetry) {
|
|
56
|
+
spawnSync(launchctlPath, ["bootout", `gui/${uid}/${label}`], {
|
|
57
|
+
stdio: "pipe",
|
|
58
|
+
env: process.env,
|
|
59
|
+
});
|
|
60
|
+
const sleepPath = resolveCommandPathStrict("sleep");
|
|
61
|
+
spawnSync(sleepPath, ["0.2"], { stdio: "ignore", env: process.env });
|
|
62
|
+
result = spawnSync(launchctlPath, ["bootstrap", `gui/${uid}`, plistPath], {
|
|
63
|
+
stdio: "pipe",
|
|
64
|
+
env: process.env,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (result.status !== 0) {
|
|
70
|
+
const err = new Error("launchctl bootstrap に失敗しました。");
|
|
71
|
+
err.code = result.status;
|
|
72
|
+
err.stderrLen = result.stderr?.length || 0;
|
|
73
|
+
err.stderrText = (result.stderr || "").toString("utf8").trim();
|
|
74
|
+
err.stdoutText = (result.stdout || "").toString("utf8").trim();
|
|
75
|
+
err.detail = err.stderrText || err.stdoutText || "";
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { plistPath };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function uninstallLaunchd({ allowMissing = false } = {}) {
|
|
83
|
+
const uid = process.getuid();
|
|
84
|
+
const label = "dev.slacklocalvibe.daemon";
|
|
85
|
+
const launchctlPath = resolveCommandPathStrict("launchctl");
|
|
86
|
+
const statusResult = spawnSync(launchctlPath, ["print", `gui/${uid}/${label}`], {
|
|
87
|
+
stdio: "pipe",
|
|
88
|
+
env: process.env,
|
|
89
|
+
});
|
|
90
|
+
const plistPath = launchdPlistPath();
|
|
91
|
+
const hasPlist = fs.existsSync(plistPath);
|
|
92
|
+
|
|
93
|
+
if (statusResult.status !== 0 && !hasPlist) {
|
|
94
|
+
if (allowMissing) {
|
|
95
|
+
return { installed: false, bootoutStatus: statusResult.status };
|
|
96
|
+
}
|
|
97
|
+
const err = new Error("launchd が未登録のためアンインストールできません。");
|
|
98
|
+
err.code = statusResult.status;
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let result = statusResult;
|
|
103
|
+
if (statusResult.status === 0) {
|
|
104
|
+
result = spawnSync(launchctlPath, ["bootout", `gui/${uid}/${label}`], {
|
|
105
|
+
stdio: "pipe",
|
|
106
|
+
env: process.env,
|
|
107
|
+
});
|
|
108
|
+
if (result.status !== 0) {
|
|
109
|
+
const stderrText = (result.stderr || "").toString("utf8").trim();
|
|
110
|
+
const stdoutText = (result.stdout || "").toString("utf8").trim();
|
|
111
|
+
const detail = stderrText || stdoutText || "";
|
|
112
|
+
const err = new Error("launchctl bootout に失敗しました。");
|
|
113
|
+
err.code = result.status;
|
|
114
|
+
err.detail = detail;
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (hasPlist) {
|
|
120
|
+
fs.unlinkSync(plistPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
installed: statusResult.status === 0 || hasPlist,
|
|
125
|
+
bootoutStatus: result.status,
|
|
126
|
+
bootoutStderrLen: result.stderr?.length || 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function statusLaunchd() {
|
|
131
|
+
const uid = process.getuid();
|
|
132
|
+
const label = "dev.slacklocalvibe.daemon";
|
|
133
|
+
const launchctlPath = resolveCommandPathStrict("launchctl");
|
|
134
|
+
const result = spawnSync(launchctlPath, ["print", `gui/${uid}/${label}`], {
|
|
135
|
+
stdio: "pipe",
|
|
136
|
+
env: process.env,
|
|
137
|
+
});
|
|
138
|
+
return { status: result.status };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveNpmBinDir() {
|
|
142
|
+
const npmPath = resolveCommandPathStrict("npm");
|
|
143
|
+
const npmResult = spawnSync(npmPath, ["bin", "-g"], {
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
shell: false,
|
|
146
|
+
env: process.env,
|
|
147
|
+
});
|
|
148
|
+
if (npmResult.status === 0) {
|
|
149
|
+
const binDir = npmResult.stdout.trim();
|
|
150
|
+
if (binDir) return binDir;
|
|
151
|
+
}
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveNpmPrefix() {
|
|
156
|
+
const npmPath = resolveCommandPathStrict("npm");
|
|
157
|
+
const prefixResult = spawnSync(npmPath, ["prefix", "-g"], {
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
shell: false,
|
|
160
|
+
env: process.env,
|
|
161
|
+
});
|
|
162
|
+
if (prefixResult.status === 0) {
|
|
163
|
+
const prefix = prefixResult.stdout.trim();
|
|
164
|
+
if (prefix) return prefix;
|
|
165
|
+
}
|
|
166
|
+
const configResult = spawnSync(npmPath, ["config", "get", "prefix"], {
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
shell: false,
|
|
169
|
+
env: process.env,
|
|
170
|
+
});
|
|
171
|
+
if (configResult.status === 0) {
|
|
172
|
+
const prefix = configResult.stdout.trim();
|
|
173
|
+
if (prefix) return prefix;
|
|
174
|
+
}
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveBinaryPath() {
|
|
179
|
+
const npmBinDir = resolveNpmBinDir();
|
|
180
|
+
if (npmBinDir) {
|
|
181
|
+
const candidate = path.join(npmBinDir, "slacklocalvibe");
|
|
182
|
+
if (fs.existsSync(candidate)) {
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const npmPrefix = resolveNpmPrefix();
|
|
188
|
+
if (npmPrefix) {
|
|
189
|
+
const candidate = path.join(npmPrefix, "bin", "slacklocalvibe");
|
|
190
|
+
if (fs.existsSync(candidate)) {
|
|
191
|
+
return candidate;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
throw new Error("slacklocalvibe のグローバル実行ファイルが見つかりません。");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildPlist(cliPath) {
|
|
198
|
+
const logPath = daemonLogPath();
|
|
199
|
+
const envPath = buildDaemonPathEnv();
|
|
200
|
+
const nodePath = process.execPath;
|
|
201
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
202
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
203
|
+
<plist version="1.0">
|
|
204
|
+
<dict>
|
|
205
|
+
<key>Label</key>
|
|
206
|
+
<string>dev.slacklocalvibe.daemon</string>
|
|
207
|
+
<key>ProgramArguments</key>
|
|
208
|
+
<array>
|
|
209
|
+
<string>${nodePath}</string>
|
|
210
|
+
<string>${cliPath}</string>
|
|
211
|
+
<string>daemon</string>
|
|
212
|
+
</array>
|
|
213
|
+
<key>RunAtLoad</key>
|
|
214
|
+
<true/>
|
|
215
|
+
<key>KeepAlive</key>
|
|
216
|
+
<true/>
|
|
217
|
+
<key>ThrottleInterval</key>
|
|
218
|
+
<integer>10</integer>
|
|
219
|
+
<key>StandardOutPath</key>
|
|
220
|
+
<string>${logPath}</string>
|
|
221
|
+
<key>StandardErrorPath</key>
|
|
222
|
+
<string>${logPath}</string>
|
|
223
|
+
<key>EnvironmentVariables</key>
|
|
224
|
+
<dict>
|
|
225
|
+
<key>PATH</key>
|
|
226
|
+
<string>${envPath}</string>
|
|
227
|
+
</dict>
|
|
228
|
+
</dict>
|
|
229
|
+
</plist>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
installLaunchd,
|
|
234
|
+
uninstallLaunchd,
|
|
235
|
+
statusLaunchd,
|
|
236
|
+
resolveBinaryPath,
|
|
237
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const LEVELS = {
|
|
5
|
+
INFO: "INFO",
|
|
6
|
+
DEBUG: "DEBUG",
|
|
7
|
+
STATES: "STATES",
|
|
8
|
+
SUCCRSS: "SUCCRSS",
|
|
9
|
+
WARNING: "WARNING",
|
|
10
|
+
ERROR: "ERROR",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function ensureDir(dirPath) {
|
|
14
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createLogger({ filePath, scope }) {
|
|
18
|
+
ensureDir(path.dirname(filePath));
|
|
19
|
+
|
|
20
|
+
function log(level, message, data = {}) {
|
|
21
|
+
const payload = {
|
|
22
|
+
ts: new Date().toISOString(),
|
|
23
|
+
level,
|
|
24
|
+
scope,
|
|
25
|
+
message,
|
|
26
|
+
...data,
|
|
27
|
+
};
|
|
28
|
+
fs.appendFileSync(filePath, `${JSON.stringify(payload)}\n`, "utf8");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
log,
|
|
33
|
+
LEVELS,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function withTiming(log, label, fn, data = {}) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await fn();
|
|
41
|
+
log(LEVELS.SUCCRSS, label, { ...data, duration_ms: Date.now() - start });
|
|
42
|
+
return result;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
log(LEVELS.ERROR, label, {
|
|
45
|
+
...data,
|
|
46
|
+
duration_ms: Date.now() - start,
|
|
47
|
+
error: safeError(error),
|
|
48
|
+
});
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function safeError(error) {
|
|
54
|
+
if (!error) return undefined;
|
|
55
|
+
return {
|
|
56
|
+
name: error.name,
|
|
57
|
+
message: error.message,
|
|
58
|
+
code: error.code,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
createLogger,
|
|
64
|
+
withTiming,
|
|
65
|
+
LEVELS,
|
|
66
|
+
safeError,
|
|
67
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { marked } = require("marked");
|
|
2
|
+
const htmlToMrkdwn = require("html-to-mrkdwn");
|
|
3
|
+
|
|
4
|
+
function markdownToMrkdwn(text) {
|
|
5
|
+
if (!text) return "";
|
|
6
|
+
const html = marked.parse(text);
|
|
7
|
+
const result = htmlToMrkdwn(html);
|
|
8
|
+
if (typeof result === "string") return result;
|
|
9
|
+
if (result && typeof result.text === "string") return result.text;
|
|
10
|
+
throw new Error("markdown_to_mrkdwn_invalid_output");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
markdownToMrkdwn,
|
|
15
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function quoteShell(value) {
|
|
2
|
+
const text = String(value ?? "");
|
|
3
|
+
if (!text) return "''";
|
|
4
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text;
|
|
5
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function buildResumeCommand({ tool, sessionId, cwd }) {
|
|
9
|
+
const safeSession = quoteShell(sessionId || "");
|
|
10
|
+
if (tool === "codex") {
|
|
11
|
+
const parts = [
|
|
12
|
+
"codex",
|
|
13
|
+
"exec",
|
|
14
|
+
"--skip-git-repo-check",
|
|
15
|
+
"resume",
|
|
16
|
+
safeSession,
|
|
17
|
+
"-",
|
|
18
|
+
];
|
|
19
|
+
const command = parts.join(" ");
|
|
20
|
+
if (cwd) {
|
|
21
|
+
return `cd ${quoteShell(cwd)} && ${command}`;
|
|
22
|
+
}
|
|
23
|
+
return command;
|
|
24
|
+
}
|
|
25
|
+
const command = `claude -r ${safeSession}`;
|
|
26
|
+
if (cwd) {
|
|
27
|
+
return `cd ${quoteShell(cwd)} && ${command}`;
|
|
28
|
+
}
|
|
29
|
+
return command;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildReplyReceivedMessage({ tool, sessionId, cwd }) {
|
|
33
|
+
const resumeCommand = buildResumeCommand({ tool, sessionId, cwd });
|
|
34
|
+
return [
|
|
35
|
+
"返信を受領しました。",
|
|
36
|
+
"これを `resume` として実行しました。結果は新規スレッドが作成されます。",
|
|
37
|
+
`CLI再開:\`${resumeCommand}\``,
|
|
38
|
+
"VSCode拡張機能など:ウィンドウ再読込など",
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const REPLY_INVALID_MESSAGE =
|
|
43
|
+
"この返信は `SlackLocalVibe` の通知スレッドとして認識できませんでした(route 情報が見つからない/不正)。\n" +
|
|
44
|
+
"**通知(親)メッセージ**に対してスレッド返信してください。\n" +
|
|
45
|
+
"(補足:このスレッドでは `resume` は実行しません)";
|
|
46
|
+
|
|
47
|
+
const RESUME_FAILED_MESSAGE =
|
|
48
|
+
"`resume` の実行に失敗しました。\n" +
|
|
49
|
+
"詳細はCLIログをご確認ください。";
|
|
50
|
+
|
|
51
|
+
const TEST_PROMPT = "あなたは誰?";
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
buildReplyReceivedMessage,
|
|
55
|
+
REPLY_INVALID_MESSAGE,
|
|
56
|
+
RESUME_FAILED_MESSAGE,
|
|
57
|
+
TEST_PROMPT,
|
|
58
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
|
|
3
|
+
function parseCodexNotify(rawJson) {
|
|
4
|
+
const payload = JSON.parse(rawJson);
|
|
5
|
+
if (payload?.type !== "agent-turn-complete") {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const sessionId = String(payload["thread-id"] || "");
|
|
9
|
+
const turnId = payload["turn-id"] ? String(payload["turn-id"]) : undefined;
|
|
10
|
+
const inputMessages = payload["input-messages"];
|
|
11
|
+
const meta = buildCodexInputMeta(inputMessages);
|
|
12
|
+
const userText = extractUserTextFromCodex(inputMessages);
|
|
13
|
+
const assistantText = extractAssistantText(payload["last-assistant-message"]);
|
|
14
|
+
const cwd = payload?.cwd ? String(payload.cwd) : "";
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
tool: "codex",
|
|
18
|
+
session_id: sessionId,
|
|
19
|
+
turn_id: turnId,
|
|
20
|
+
cwd,
|
|
21
|
+
user_text: userText || "(ユーザーメッセージ抽出失敗)",
|
|
22
|
+
assistant_text: assistantText || "(本文抽出失敗)",
|
|
23
|
+
meta,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractUserTextFromCodex(inputMessages) {
|
|
28
|
+
if (!Array.isArray(inputMessages) || inputMessages.length === 0) return "";
|
|
29
|
+
const lastMessage = inputMessages[inputMessages.length - 1];
|
|
30
|
+
return normalizeContent(lastMessage);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildCodexInputMeta(inputMessages) {
|
|
34
|
+
const meta = {
|
|
35
|
+
input_messages_type: Array.isArray(inputMessages) ? "array" : typeof inputMessages,
|
|
36
|
+
input_messages_len: Array.isArray(inputMessages) ? inputMessages.length : 0,
|
|
37
|
+
input_messages_roles: [],
|
|
38
|
+
input_messages_last_role: "",
|
|
39
|
+
input_messages_last_type: "",
|
|
40
|
+
input_messages_last_keys: [],
|
|
41
|
+
input_messages_last_content_keys: [],
|
|
42
|
+
};
|
|
43
|
+
if (Array.isArray(inputMessages) && inputMessages.length > 0) {
|
|
44
|
+
const roles = [];
|
|
45
|
+
for (const message of inputMessages) {
|
|
46
|
+
const role = message?.role;
|
|
47
|
+
if (role && !roles.includes(role)) {
|
|
48
|
+
roles.push(role);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
meta.input_messages_roles = roles;
|
|
52
|
+
const lastMessage = inputMessages[inputMessages.length - 1];
|
|
53
|
+
meta.input_messages_last_role = lastMessage?.role || "";
|
|
54
|
+
meta.input_messages_last_type = Array.isArray(lastMessage)
|
|
55
|
+
? "array"
|
|
56
|
+
: typeof lastMessage;
|
|
57
|
+
if (lastMessage && typeof lastMessage === "object" && !Array.isArray(lastMessage)) {
|
|
58
|
+
meta.input_messages_last_keys = Object.keys(lastMessage).slice(0, 20);
|
|
59
|
+
const content = lastMessage.content;
|
|
60
|
+
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
61
|
+
meta.input_messages_last_content_keys = Object.keys(content).slice(0, 20);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return meta;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractAssistantText(content) {
|
|
69
|
+
return normalizeContent(content);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractTextDeep(value, depth = 0) {
|
|
73
|
+
if (depth > 6) return "";
|
|
74
|
+
if (value === null || value === undefined) return "";
|
|
75
|
+
if (typeof value === "string") return value;
|
|
76
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
return value.map((item) => extractTextDeep(item, depth + 1)).join("");
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "object") {
|
|
81
|
+
const preferredKeys = [
|
|
82
|
+
"text",
|
|
83
|
+
"content",
|
|
84
|
+
"message",
|
|
85
|
+
"input",
|
|
86
|
+
"prompt",
|
|
87
|
+
"input_text",
|
|
88
|
+
"inputText",
|
|
89
|
+
"value",
|
|
90
|
+
];
|
|
91
|
+
for (const key of preferredKeys) {
|
|
92
|
+
if (value[key] !== undefined) {
|
|
93
|
+
const text = extractTextDeep(value[key], depth + 1);
|
|
94
|
+
if (text) return text;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return Object.values(value)
|
|
98
|
+
.map((item) => extractTextDeep(item, depth + 1))
|
|
99
|
+
.join("");
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeContent(content) {
|
|
105
|
+
return extractTextDeep(content);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseClaudeHook(rawJson) {
|
|
109
|
+
const payload = JSON.parse(rawJson);
|
|
110
|
+
if (payload?.hook_event_name !== "Stop") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (payload?.stop_hook_active === true) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sessionId = String(payload?.session_id || "");
|
|
118
|
+
const transcriptPath = payload?.transcript_path;
|
|
119
|
+
const cwd = payload?.cwd ? String(payload.cwd) : "";
|
|
120
|
+
let userText = "";
|
|
121
|
+
let assistantText = "";
|
|
122
|
+
let transcriptError = "";
|
|
123
|
+
|
|
124
|
+
if (transcriptPath) {
|
|
125
|
+
try {
|
|
126
|
+
const { lastUser, lastAssistant } = readTranscript(transcriptPath);
|
|
127
|
+
userText = lastUser || "";
|
|
128
|
+
assistantText = lastAssistant || "";
|
|
129
|
+
} catch (error) {
|
|
130
|
+
transcriptError = error?.message || "transcript_read_failed";
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
transcriptError = "transcript_path_missing";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
tool: "claude",
|
|
138
|
+
session_id: sessionId,
|
|
139
|
+
cwd,
|
|
140
|
+
user_text: userText || "(ユーザーメッセージ抽出失敗)",
|
|
141
|
+
assistant_text: assistantText || `(本文抽出エラー: ${transcriptError || "unknown"})`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function readTranscript(filePath) {
|
|
146
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
147
|
+
const lines = content.split("\n").filter(Boolean);
|
|
148
|
+
let lastUser = "";
|
|
149
|
+
let lastAssistant = "";
|
|
150
|
+
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
let record;
|
|
153
|
+
try {
|
|
154
|
+
record = JSON.parse(line);
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const { role, text } = extractRoleAndText(record);
|
|
159
|
+
if (!role || !text) continue;
|
|
160
|
+
if (role === "user") lastUser = text;
|
|
161
|
+
if (role === "assistant") lastAssistant = text;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { lastUser, lastAssistant };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function extractRoleAndText(record) {
|
|
168
|
+
if (!record || typeof record !== "object") return {};
|
|
169
|
+
|
|
170
|
+
let role = record.role || record.message?.role || record.data?.role || "";
|
|
171
|
+
if (!role) {
|
|
172
|
+
if (record.type === "assistant") role = "assistant";
|
|
173
|
+
if (record.type === "user") role = "user";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const content =
|
|
177
|
+
record.content ||
|
|
178
|
+
record.message?.content ||
|
|
179
|
+
record.data?.content ||
|
|
180
|
+
record.text ||
|
|
181
|
+
record.message?.text;
|
|
182
|
+
|
|
183
|
+
const text = normalizeContent(content);
|
|
184
|
+
return { role, text };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
parseCodexNotify,
|
|
189
|
+
parseClaudeHook,
|
|
190
|
+
normalizeContent,
|
|
191
|
+
};
|