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
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
function homeDir() {
|
|
7
|
+
return os.homedir();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function configDir() {
|
|
11
|
+
return path.join(homeDir(), ".config", "slacklocalvibe");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function configPath() {
|
|
15
|
+
return path.join(configDir(), "config.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function routesPath() {
|
|
19
|
+
return path.join(configDir(), "routes.jsonl");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function logsDir() {
|
|
23
|
+
return path.join(homeDir(), "Library", "Logs", "slacklocalvibe");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function notifyLogPath() {
|
|
27
|
+
return path.join(logsDir(), "notify.log");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function daemonLogPath() {
|
|
31
|
+
return path.join(logsDir(), "daemon.log");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function wizardLogPath() {
|
|
35
|
+
return path.join(logsDir(), "wizard.log");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function launchAgentsDir() {
|
|
39
|
+
return path.join(homeDir(), "Library", "LaunchAgents");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function launchdPlistPath() {
|
|
43
|
+
return path.join(launchAgentsDir(), "dev.slacklocalvibe.daemon.plist");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function defaultPathEnv() {
|
|
47
|
+
return process.env.PATH || "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
homeDir,
|
|
52
|
+
configDir,
|
|
53
|
+
configPath,
|
|
54
|
+
routesPath,
|
|
55
|
+
logsDir,
|
|
56
|
+
notifyLogPath,
|
|
57
|
+
daemonLogPath,
|
|
58
|
+
wizardLogPath,
|
|
59
|
+
launchAgentsDir,
|
|
60
|
+
launchdPlistPath,
|
|
61
|
+
defaultPathEnv,
|
|
62
|
+
resolveCommandPathStrict,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function resolveCommandPathStrict(command, { allowNpx = false, optional = false } = {}) {
|
|
66
|
+
const pathEnv = process.env.PATH || "";
|
|
67
|
+
const result = spawnSync("command", ["-v", command], {
|
|
68
|
+
encoding: "utf8",
|
|
69
|
+
shell: false,
|
|
70
|
+
env: { ...process.env, PATH: pathEnv },
|
|
71
|
+
});
|
|
72
|
+
if (result.status !== 0) {
|
|
73
|
+
if (optional) return "";
|
|
74
|
+
const err = new Error(`コマンドが見つかりません: ${command}`);
|
|
75
|
+
err.detail = (result.stderr || "").toString("utf8").trim();
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
const resolved = (result.stdout || "").toString("utf8").trim();
|
|
79
|
+
if (!resolved) {
|
|
80
|
+
if (optional) return "";
|
|
81
|
+
throw new Error(`コマンドのパス解決に失敗しました: ${command}`);
|
|
82
|
+
}
|
|
83
|
+
if (!allowNpx && resolved.includes("/.npm/_npx/")) {
|
|
84
|
+
if (optional) return "";
|
|
85
|
+
throw new Error(`npx 由来のパスは許可しません: ${resolved}`);
|
|
86
|
+
}
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const { defaultPathEnv, resolveCommandPathStrict } = require("./paths");
|
|
3
|
+
|
|
4
|
+
function normalizeClaudePrompt(text) {
|
|
5
|
+
const normalized = (text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
6
|
+
return normalized.replace(/\n/g, "\\n");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function runCommand({ command, args, input, env, cwd }) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(command, args, {
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
env: { ...process.env, PATH: defaultPathEnv(), ...env },
|
|
14
|
+
cwd,
|
|
15
|
+
});
|
|
16
|
+
let stdoutLen = 0;
|
|
17
|
+
let stderrLen = 0;
|
|
18
|
+
let stdoutHead = "";
|
|
19
|
+
let stderrHead = "";
|
|
20
|
+
const MAX_HEAD = 400;
|
|
21
|
+
child.stdout.on("data", (chunk) => {
|
|
22
|
+
stdoutLen += chunk.length;
|
|
23
|
+
if (stdoutHead.length < MAX_HEAD) {
|
|
24
|
+
stdoutHead += chunk.toString("utf8");
|
|
25
|
+
if (stdoutHead.length > MAX_HEAD) {
|
|
26
|
+
stdoutHead = stdoutHead.slice(0, MAX_HEAD);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
child.stderr.on("data", (chunk) => {
|
|
31
|
+
stderrLen += chunk.length;
|
|
32
|
+
if (stderrHead.length < MAX_HEAD) {
|
|
33
|
+
stderrHead += chunk.toString("utf8");
|
|
34
|
+
if (stderrHead.length > MAX_HEAD) {
|
|
35
|
+
stderrHead = stderrHead.slice(0, MAX_HEAD);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
child.on("error", (error) => reject(error));
|
|
40
|
+
child.on("close", (code, signal) => {
|
|
41
|
+
resolve({
|
|
42
|
+
code,
|
|
43
|
+
signal,
|
|
44
|
+
stdoutLen,
|
|
45
|
+
stderrLen,
|
|
46
|
+
stdoutHead,
|
|
47
|
+
stderrHead,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
if (input) {
|
|
51
|
+
child.stdin.write(input);
|
|
52
|
+
}
|
|
53
|
+
child.stdin.end();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runCodexResume({ sessionId, prompt, cwd }) {
|
|
58
|
+
const codexPath = resolveCommandPathStrict("codex");
|
|
59
|
+
const args = ["exec", "--skip-git-repo-check"];
|
|
60
|
+
if (cwd) {
|
|
61
|
+
args.push("--cd", cwd);
|
|
62
|
+
}
|
|
63
|
+
args.push("resume", sessionId, "-");
|
|
64
|
+
return runCommand({
|
|
65
|
+
command: codexPath,
|
|
66
|
+
args,
|
|
67
|
+
input: prompt || "",
|
|
68
|
+
cwd: cwd || undefined,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runClaudeResume({ sessionId, prompt, cwd }) {
|
|
73
|
+
const normalized = normalizeClaudePrompt(prompt || "");
|
|
74
|
+
const claudePath = resolveCommandPathStrict("claude");
|
|
75
|
+
return runCommand({
|
|
76
|
+
command: claudePath,
|
|
77
|
+
args: ["-r", sessionId, normalized],
|
|
78
|
+
cwd: cwd || undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
runCodexResume,
|
|
84
|
+
runClaudeResume,
|
|
85
|
+
normalizeClaudePrompt,
|
|
86
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { routesPath } = require("./paths");
|
|
4
|
+
|
|
5
|
+
const MAX_ROUTES_BYTES = 1024 * 1024;
|
|
6
|
+
const MAX_ROUTES_LINES = 2000;
|
|
7
|
+
|
|
8
|
+
function recordRoute({ channel, threadTs, tool, sessionId, turnId, cwd }) {
|
|
9
|
+
const filePath = routesPath();
|
|
10
|
+
const dir = path.dirname(filePath);
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
const entry = {
|
|
13
|
+
ts: new Date().toISOString(),
|
|
14
|
+
channel,
|
|
15
|
+
thread_ts: threadTs,
|
|
16
|
+
tool,
|
|
17
|
+
session_id: sessionId,
|
|
18
|
+
turn_id: turnId || "",
|
|
19
|
+
cwd: cwd || "",
|
|
20
|
+
};
|
|
21
|
+
fs.appendFileSync(filePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
22
|
+
pruneRoutesIfNeeded(filePath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findRoute({ channel, threadTs }) {
|
|
26
|
+
const filePath = routesPath();
|
|
27
|
+
if (!fs.existsSync(filePath)) return null;
|
|
28
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
29
|
+
const lines = content.split("\n").filter(Boolean);
|
|
30
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
31
|
+
const line = lines[i];
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(line);
|
|
34
|
+
if (entry.channel === channel && entry.thread_ts === threadTs) {
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// skip invalid line
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pruneRoutesIfNeeded(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(filePath);
|
|
47
|
+
if (stat.size <= MAX_ROUTES_BYTES) return;
|
|
48
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
49
|
+
const lines = content.split("\n").filter(Boolean);
|
|
50
|
+
const kept = lines.slice(-MAX_ROUTES_LINES);
|
|
51
|
+
fs.writeFileSync(filePath, `${kept.join("\n")}\n`, "utf8");
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore prune errors
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
recordRoute,
|
|
59
|
+
findRoute,
|
|
60
|
+
};
|
package/src/lib/slack.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { WebClient } = require("@slack/web-api");
|
|
2
|
+
const { withTiming, LEVELS, safeError } = require("./logger");
|
|
3
|
+
|
|
4
|
+
const MAX_RETRIES = 2;
|
|
5
|
+
|
|
6
|
+
function createWebClient(token) {
|
|
7
|
+
return new WebClient(token);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function slackApiCall(log, label, fn) {
|
|
11
|
+
let lastError;
|
|
12
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt += 1) {
|
|
13
|
+
try {
|
|
14
|
+
log(LEVELS.DEBUG, `${label}:call`, { attempt });
|
|
15
|
+
const result = await fn();
|
|
16
|
+
if (result?.ok === false) {
|
|
17
|
+
const err = new Error(result.error || "Slack API error");
|
|
18
|
+
err.code = result.error;
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
log(LEVELS.SUCCRSS, `${label}:ok`, { attempt });
|
|
22
|
+
return result;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
lastError = error;
|
|
25
|
+
log(LEVELS.WARNING, `${label}:retry`, {
|
|
26
|
+
attempt,
|
|
27
|
+
error: safeError(error),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastError;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function openDmChannel({ client, userId, log }) {
|
|
35
|
+
return withTiming(log, "slack.conversations.open", async () => {
|
|
36
|
+
const response = await slackApiCall(log, "conversations.open", () =>
|
|
37
|
+
client.conversations.open({ users: userId })
|
|
38
|
+
);
|
|
39
|
+
return response.channel?.id;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function logSlackMessage(log, label, payload) {
|
|
44
|
+
log(LEVELS.STATES, label, payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function postParentMessage({ client, log, channel, text, metadata }) {
|
|
48
|
+
logSlackMessage(log, "slack.chat.postMessage.parent.request", {
|
|
49
|
+
channel,
|
|
50
|
+
text,
|
|
51
|
+
text_len: text?.length || 0,
|
|
52
|
+
has_metadata: Boolean(metadata),
|
|
53
|
+
});
|
|
54
|
+
return withTiming(log, "slack.chat.postMessage.parent", async () => {
|
|
55
|
+
const payload = {
|
|
56
|
+
channel,
|
|
57
|
+
text,
|
|
58
|
+
};
|
|
59
|
+
if (metadata) {
|
|
60
|
+
payload.metadata = metadata;
|
|
61
|
+
}
|
|
62
|
+
const response = await slackApiCall(log, "chat.postMessage.parent", () =>
|
|
63
|
+
client.chat.postMessage(payload)
|
|
64
|
+
);
|
|
65
|
+
logSlackMessage(log, "slack.chat.postMessage.parent.response", {
|
|
66
|
+
channel,
|
|
67
|
+
ts: response.ts,
|
|
68
|
+
});
|
|
69
|
+
return response.ts;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function postMessage({ client, log, channel, text }) {
|
|
74
|
+
logSlackMessage(log, "slack.chat.postMessage.simple.request", {
|
|
75
|
+
channel,
|
|
76
|
+
text,
|
|
77
|
+
text_len: text?.length || 0,
|
|
78
|
+
});
|
|
79
|
+
return withTiming(log, "slack.chat.postMessage.simple", async () => {
|
|
80
|
+
const response = await slackApiCall(log, "chat.postMessage.simple", () =>
|
|
81
|
+
client.chat.postMessage({
|
|
82
|
+
channel,
|
|
83
|
+
text,
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
logSlackMessage(log, "slack.chat.postMessage.simple.response", {
|
|
87
|
+
channel,
|
|
88
|
+
ts: response.ts,
|
|
89
|
+
});
|
|
90
|
+
return response.ts;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function postThreadMessage({ client, log, channel, threadTs, text }) {
|
|
95
|
+
logSlackMessage(log, "slack.chat.postMessage.thread.request", {
|
|
96
|
+
channel,
|
|
97
|
+
thread_ts: threadTs,
|
|
98
|
+
text,
|
|
99
|
+
text_len: text?.length || 0,
|
|
100
|
+
});
|
|
101
|
+
return withTiming(log, "slack.chat.postMessage.thread", async () => {
|
|
102
|
+
const response = await slackApiCall(log, "chat.postMessage.thread", () =>
|
|
103
|
+
client.chat.postMessage({
|
|
104
|
+
channel,
|
|
105
|
+
thread_ts: threadTs,
|
|
106
|
+
text,
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
logSlackMessage(log, "slack.chat.postMessage.thread.response", {
|
|
110
|
+
channel,
|
|
111
|
+
thread_ts: threadTs,
|
|
112
|
+
ts: response.ts,
|
|
113
|
+
});
|
|
114
|
+
return response.ts;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function fetchParentMessage({ client, log, channel, threadTs }) {
|
|
119
|
+
return withTiming(log, "slack.conversations.history", async () => {
|
|
120
|
+
const response = await slackApiCall(log, "conversations.history", () =>
|
|
121
|
+
client.conversations.history({
|
|
122
|
+
channel,
|
|
123
|
+
latest: threadTs,
|
|
124
|
+
inclusive: true,
|
|
125
|
+
limit: 1,
|
|
126
|
+
include_all_metadata: true,
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
return response.messages?.[0];
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
createWebClient,
|
|
135
|
+
openDmChannel,
|
|
136
|
+
postMessage,
|
|
137
|
+
postParentMessage,
|
|
138
|
+
postThreadMessage,
|
|
139
|
+
fetchParentMessage,
|
|
140
|
+
};
|
package/src/lib/text.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const MAX_TEXT = 3800;
|
|
2
|
+
|
|
3
|
+
function splitText(text, maxLen = MAX_TEXT) {
|
|
4
|
+
if (!text) return [""];
|
|
5
|
+
if (text.length <= maxLen) return [text];
|
|
6
|
+
|
|
7
|
+
const chunks = [];
|
|
8
|
+
let remaining = text;
|
|
9
|
+
|
|
10
|
+
while (remaining.length > maxLen) {
|
|
11
|
+
let slice = safeSlice(remaining, maxLen);
|
|
12
|
+
let cutIndex = findCutIndex(slice);
|
|
13
|
+
if (cutIndex <= 0 || cutIndex > slice.length) {
|
|
14
|
+
cutIndex = slice.length;
|
|
15
|
+
}
|
|
16
|
+
chunks.push(remaining.slice(0, cutIndex));
|
|
17
|
+
remaining = remaining.slice(cutIndex);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (remaining.length > 0) {
|
|
21
|
+
chunks.push(remaining);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return chunks;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findCutIndex(slice) {
|
|
28
|
+
const boundaries = ["\n\n", "\n", " "];
|
|
29
|
+
for (const boundary of boundaries) {
|
|
30
|
+
const idx = slice.lastIndexOf(boundary);
|
|
31
|
+
if (idx > 0) {
|
|
32
|
+
return idx + boundary.length;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return slice.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeSlice(text, maxLen) {
|
|
39
|
+
let slice = text.slice(0, maxLen);
|
|
40
|
+
if (slice.length === 0) return slice;
|
|
41
|
+
const lastChar = slice.charCodeAt(slice.length - 1);
|
|
42
|
+
if (lastChar >= 0xd800 && lastChar <= 0xdbff) {
|
|
43
|
+
slice = slice.slice(0, -1);
|
|
44
|
+
}
|
|
45
|
+
return slice;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function numberedChunks(text, maxLen = MAX_TEXT) {
|
|
49
|
+
const tempChunks = splitText(text, maxLen);
|
|
50
|
+
if (tempChunks.length <= 1) return tempChunks;
|
|
51
|
+
|
|
52
|
+
const total = tempChunks.length;
|
|
53
|
+
const prefixLen = `(${total}/${total}) `.length;
|
|
54
|
+
const chunks = splitText(text, maxLen - prefixLen);
|
|
55
|
+
return chunks.map((chunk, index) => `(${index + 1}/${chunks.length}) ${chunk}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
MAX_TEXT,
|
|
60
|
+
splitText,
|
|
61
|
+
numberedChunks,
|
|
62
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const toml = require("@iarna/toml");
|
|
4
|
+
const { backupFileIfExists } = require("./config");
|
|
5
|
+
const { homeDir } = require("./paths");
|
|
6
|
+
|
|
7
|
+
const CODEX_NOTIFY_COMMAND = [
|
|
8
|
+
"slacklocalvibe",
|
|
9
|
+
"notify",
|
|
10
|
+
"--tool",
|
|
11
|
+
"codex",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const CLAUDE_NOTIFY_COMMAND = "slacklocalvibe notify --tool claude";
|
|
15
|
+
|
|
16
|
+
function codexConfigPath() {
|
|
17
|
+
const base = process.env.CODEX_HOME || path.join(homeDir(), ".codex");
|
|
18
|
+
return path.join(base, "config.toml");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function claudeSettingsPath() {
|
|
22
|
+
return path.join(homeDir(), ".claude", "settings.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function updateCodexNotify() {
|
|
26
|
+
const filePath = codexConfigPath();
|
|
27
|
+
if (!fs.existsSync(filePath)) {
|
|
28
|
+
throw new Error(`Codex設定ファイルが見つかりません: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
31
|
+
const parsed = toml.parse(raw);
|
|
32
|
+
parsed.notify = CODEX_NOTIFY_COMMAND;
|
|
33
|
+
backupFileIfExists(filePath);
|
|
34
|
+
fs.writeFileSync(filePath, toml.stringify(parsed), "utf8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function updateClaudeStopHook() {
|
|
38
|
+
const filePath = claudeSettingsPath();
|
|
39
|
+
if (!fs.existsSync(filePath)) {
|
|
40
|
+
throw new Error(`Claude設定ファイルが見つかりません: ${filePath}`);
|
|
41
|
+
}
|
|
42
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
if (parsed.hooks && typeof parsed.hooks !== "object") {
|
|
45
|
+
throw new Error("Claude settings.json の hooks が不正な型です。");
|
|
46
|
+
}
|
|
47
|
+
parsed.hooks = parsed.hooks || {};
|
|
48
|
+
|
|
49
|
+
const stopHooks = parsed.hooks.Stop;
|
|
50
|
+
if (stopHooks && !Array.isArray(stopHooks)) {
|
|
51
|
+
throw new Error("Claude settings.json の hooks.Stop が配列ではありません。");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const newHook = { type: "command", command: CLAUDE_NOTIFY_COMMAND };
|
|
55
|
+
|
|
56
|
+
if (!Array.isArray(parsed.hooks.Stop) || parsed.hooks.Stop.length === 0) {
|
|
57
|
+
parsed.hooks.Stop = [{ hooks: [newHook] }];
|
|
58
|
+
} else {
|
|
59
|
+
let inserted = false;
|
|
60
|
+
for (const pipeline of parsed.hooks.Stop) {
|
|
61
|
+
if (!pipeline || typeof pipeline !== "object") continue;
|
|
62
|
+
if (!Array.isArray(pipeline.hooks)) {
|
|
63
|
+
pipeline.hooks = [];
|
|
64
|
+
}
|
|
65
|
+
const exists = pipeline.hooks.some(
|
|
66
|
+
(hook) => hook?.type === "command" && hook?.command === CLAUDE_NOTIFY_COMMAND
|
|
67
|
+
);
|
|
68
|
+
if (!exists) {
|
|
69
|
+
pipeline.hooks.push(newHook);
|
|
70
|
+
}
|
|
71
|
+
inserted = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (!inserted) {
|
|
75
|
+
parsed.hooks.Stop.push({ hooks: [newHook] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
backupFileIfExists(filePath);
|
|
80
|
+
fs.writeFileSync(filePath, JSON.stringify(parsed, null, 2), "utf8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
CODEX_NOTIFY_COMMAND,
|
|
85
|
+
CLAUDE_NOTIFY_COMMAND,
|
|
86
|
+
codexConfigPath,
|
|
87
|
+
claudeSettingsPath,
|
|
88
|
+
updateCodexNotify,
|
|
89
|
+
updateClaudeStopHook,
|
|
90
|
+
};
|