slacklocalvibe 0.1.1 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slacklocalvibe",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "SlackLocalVibe: Codex/Claude Code turn notifications and reply→resume bridge for Slack DM (Socket Mode)",
5
5
  "bin": {
6
6
  "slacklocalvibe": "src/cli.js"
package/src/cli.js CHANGED
@@ -4,13 +4,14 @@ const { runWizard } = require("./commands/wizard");
4
4
  const { runNotify } = require("./commands/notify");
5
5
  const { runDaemon } = require("./commands/daemon");
6
6
  const { runLaunchd } = require("./commands/launchd");
7
+ const packageJson = require("../package.json");
7
8
 
8
9
  const program = new Command();
9
10
 
10
11
  program
11
12
  .name("slacklocalvibe")
12
13
  .description("SlackLocalVibe: Slack DM通知 + 返信resumeブリッジ")
13
- .version("0.1.0");
14
+ .version(packageJson.version);
14
15
 
15
16
  program
16
17
  .command("notify")
@@ -75,6 +75,19 @@ async function runNotify({ tool }) {
75
75
  });
76
76
  return;
77
77
  }
78
+ if (input.tool === "codex") {
79
+ const meta = input.meta || {};
80
+ log(LEVELS.INFO, "notify.codex_prompt_source", {
81
+ rollout_found: meta.codex_rollout_found,
82
+ rollout_source: meta.codex_rollout_source,
83
+ rollout_path: meta.codex_rollout_path,
84
+ rollout_user_messages: meta.codex_rollout_user_message_count,
85
+ rollout_line_count: meta.codex_rollout_line_count,
86
+ input_messages_len: meta.input_messages_len,
87
+ input_messages_has_content: meta.input_messages_has_content,
88
+ rollout_error: meta.codex_rollout_error,
89
+ });
90
+ }
78
91
 
79
92
  if (!input.session_id) {
80
93
  log(LEVELS.ERROR, "notify.session_missing");
@@ -102,21 +102,76 @@ async function runWizard() {
102
102
  console.log("SlackLocalVibe セットアップを開始します。");
103
103
  if (normalized) {
104
104
  console.log("既存設定が見つかりました。開始方法を選んでください。");
105
- const choice = await promptSelect({
106
- message: "どこから始めますか?",
107
- choices: [
108
- { title: "テストから始める", value: "test" },
109
- { title: "リセットして最初から", value: "reset" },
110
- { title: "終了(保存せず終了)", value: "exit" },
111
- ],
112
- initial: 0,
113
- });
114
- if (choice === "reset") {
115
- resetStoredConfig({ log });
116
- } else if (choice === "test") {
117
- startFromTest = true;
118
- console.log("既存設定を使って通知テストから開始します。");
119
- } else {
105
+ while (true) {
106
+ const choice = await promptSelect({
107
+ message: "どこから始めますか?",
108
+ choices: [
109
+ { title: "アップデートを確認する", value: "update" },
110
+ { title: "テストから始める", value: "test" },
111
+ { title: "リセットして最初からセットアップ", value: "reset" },
112
+ { title: "終了(保存せず終了)", value: "exit" },
113
+ ],
114
+ initial: 0,
115
+ });
116
+ if (choice === "update") {
117
+ log(LEVELS.INFO, "wizard.update_check_start");
118
+ const versionInfo = await fetchUpdateVersions({ log });
119
+ log(LEVELS.INFO, "wizard.update_versions", {
120
+ installed: versionInfo.installedVersion || "unknown",
121
+ latest: versionInfo.latestVersion || "unknown",
122
+ });
123
+ console.log(formatInfo(`現在: ${formatVersionLabel(versionInfo.installedVersion)}`));
124
+ console.log(formatInfo(`最新: ${formatVersionLabel(versionInfo.latestVersion)}`));
125
+ if (
126
+ versionInfo.installedVersion &&
127
+ versionInfo.latestVersion &&
128
+ versionInfo.installedVersion === versionInfo.latestVersion
129
+ ) {
130
+ log(LEVELS.SUCCRSS, "wizard.update_latest_confirmed");
131
+ console.log(formatSuccess("最新バージョンです。"));
132
+ continue;
133
+ }
134
+
135
+ const updateChoice = await promptSelect({
136
+ message: "アップデートしますか?",
137
+ choices: [
138
+ { title: "アップデートする", value: "update" },
139
+ { title: "戻る", value: "back" },
140
+ { title: "終了(保存せず終了)", value: "exit" },
141
+ ],
142
+ initial: 0,
143
+ });
144
+ if (updateChoice === "back") {
145
+ log(LEVELS.INFO, "wizard.update_skipped");
146
+ continue;
147
+ }
148
+ if (updateChoice === "exit") {
149
+ throw new UserExit();
150
+ }
151
+
152
+ await ensureGlobalInstall({ log });
153
+ const postUpdate = await fetchUpdateVersions({ log });
154
+ log(LEVELS.SUCCRSS, "wizard.update_applied", {
155
+ installed: postUpdate.installedVersion || "unknown",
156
+ latest: postUpdate.latestVersion || "unknown",
157
+ });
158
+ console.log(formatSuccess("アップデートが完了しました。"));
159
+ console.log(formatInfo(`現在: ${formatVersionLabel(postUpdate.installedVersion)}`));
160
+ console.log(formatInfo(`最新: ${formatVersionLabel(postUpdate.latestVersion)}`));
161
+
162
+ const launchdRecommended = Boolean(normalized?.features?.launchd_enabled);
163
+ await promptLaunchdReinstall({ log, recommended: launchdRecommended });
164
+ continue;
165
+ }
166
+ if (choice === "reset") {
167
+ resetStoredConfig({ log });
168
+ break;
169
+ }
170
+ if (choice === "test") {
171
+ startFromTest = true;
172
+ console.log("既存設定を使って通知テストから開始します。");
173
+ break;
174
+ }
120
175
  throw new UserExit();
121
176
  }
122
177
  }
@@ -1070,6 +1125,124 @@ async function ensureGlobalInstall({ log }) {
1070
1125
  }
1071
1126
  }
1072
1127
 
1128
+ async function fetchUpdateVersions({ log }) {
1129
+ const [installedVersion, latestVersion] = await Promise.all([
1130
+ fetchInstalledVersion({ log }),
1131
+ fetchRegistryVersion({ log }),
1132
+ ]);
1133
+ return { installedVersion, latestVersion };
1134
+ }
1135
+
1136
+ async function fetchInstalledVersion({ log }) {
1137
+ try {
1138
+ const binaryPath = resolveBinaryPath();
1139
+ const result = await spawnCommand({
1140
+ command: binaryPath,
1141
+ args: ["--version"],
1142
+ cwd: process.cwd(),
1143
+ });
1144
+ if (result.code === 0) {
1145
+ const version = (result.stdoutText || "").trim().split(/\s+/)[0];
1146
+ if (version) return version;
1147
+ }
1148
+ log(LEVELS.WARNING, "wizard.update_installed_version_empty", {
1149
+ stdout_len: result.stdoutLen,
1150
+ stderr_len: result.stderrLen,
1151
+ });
1152
+ } catch (error) {
1153
+ log(LEVELS.WARNING, "wizard.update_installed_version_failed", {
1154
+ error: safeError(error),
1155
+ });
1156
+ }
1157
+ return "";
1158
+ }
1159
+
1160
+ async function fetchRegistryVersion({ log }) {
1161
+ try {
1162
+ const npmPath = resolveCommandPathStrict("npm");
1163
+ const result = await spawnCommand({
1164
+ command: npmPath,
1165
+ args: ["view", "slacklocalvibe", "version"],
1166
+ cwd: process.cwd(),
1167
+ });
1168
+ if (result.code === 0) {
1169
+ const version = (result.stdoutText || "").trim().split(/\s+/)[0];
1170
+ if (version) return version;
1171
+ }
1172
+ log(LEVELS.WARNING, "wizard.update_registry_version_empty", {
1173
+ stdout_len: result.stdoutLen,
1174
+ stderr_len: result.stderrLen,
1175
+ });
1176
+ } catch (error) {
1177
+ log(LEVELS.WARNING, "wizard.update_registry_version_failed", {
1178
+ error: safeError(error),
1179
+ });
1180
+ }
1181
+ return "";
1182
+ }
1183
+
1184
+ function formatVersionLabel(version) {
1185
+ return version ? version : "不明";
1186
+ }
1187
+
1188
+ async function promptLaunchdReinstall({ log, recommended }) {
1189
+ const title = recommended
1190
+ ? "launchd を再登録する(推奨)"
1191
+ : "launchd を再登録する";
1192
+ while (true) {
1193
+ const choice = await promptSelect({
1194
+ message: "更新後に launchd を再登録します",
1195
+ choices: [
1196
+ { title, value: "install" },
1197
+ { title: "あとでやる", value: "skip" },
1198
+ { title: "終了(保存せず終了)", value: "exit" },
1199
+ ],
1200
+ initial: 0,
1201
+ });
1202
+ if (choice === "skip") {
1203
+ log(LEVELS.INFO, "wizard.launchd_reinstall_skipped");
1204
+ return;
1205
+ }
1206
+ if (choice === "exit") {
1207
+ throw new UserExit();
1208
+ }
1209
+ try {
1210
+ installLaunchd();
1211
+ log(LEVELS.SUCCRSS, "wizard.launchd_reinstalled");
1212
+ console.log(formatSuccess("launchd を再登録しました。"));
1213
+ return;
1214
+ } catch (error) {
1215
+ log(LEVELS.ERROR, "wizard.launchd_reinstall_failed", {
1216
+ error: safeError(error),
1217
+ });
1218
+ console.log(formatError("launchd の再登録に失敗しました。"));
1219
+ const detail =
1220
+ error?.detail ||
1221
+ error?.stderrText ||
1222
+ error?.stdoutText ||
1223
+ error?.message ||
1224
+ "unknown";
1225
+ if (detail) {
1226
+ console.log(formatError(detail));
1227
+ }
1228
+ const next = await promptSelect({
1229
+ message: "次の操作を選んでください",
1230
+ choices: [
1231
+ { title: "再試行", value: "retry" },
1232
+ { title: "あとでやる", value: "skip" },
1233
+ { title: "終了(保存せず終了)", value: "exit" },
1234
+ ],
1235
+ });
1236
+ if (next === "retry") continue;
1237
+ if (next === "skip") {
1238
+ log(LEVELS.WARNING, "wizard.launchd_reinstall_skipped_after_error");
1239
+ return;
1240
+ }
1241
+ throw new UserExit();
1242
+ }
1243
+ }
1244
+ }
1245
+
1073
1246
  async function promptPassword({ message, validate }) {
1074
1247
  while (true) {
1075
1248
  const value = await promptVisible({ message });
@@ -1,4 +1,6 @@
1
1
  const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
2
4
 
3
5
  function parseCodexNotify(rawJson) {
4
6
  const payload = JSON.parse(rawJson);
@@ -9,16 +11,9 @@ function parseCodexNotify(rawJson) {
9
11
  const turnId = payload["turn-id"] ? String(payload["turn-id"]) : undefined;
10
12
  const inputMessages = payload["input-messages"];
11
13
  const meta = buildCodexInputMeta(inputMessages);
12
- const hasInputMessages = hasNonEmptyInputMessages(inputMessages);
13
- if (!hasInputMessages) {
14
- return {
15
- tool: "codex",
16
- skip: true,
17
- skip_reason: "empty_input_messages",
18
- meta,
19
- };
20
- }
21
- const userText = extractUserTextFromCodex(inputMessages);
14
+ const rolloutResult = readCodexUserMessageFromRollout(sessionId);
15
+ Object.assign(meta, rolloutResult.meta || {});
16
+ const userText = rolloutResult.userText || "";
22
17
  const assistantText = extractAssistantText(payload["last-assistant-message"]);
23
18
  const cwd = payload?.cwd ? String(payload.cwd) : "";
24
19
 
@@ -33,16 +28,11 @@ function parseCodexNotify(rawJson) {
33
28
  };
34
29
  }
35
30
 
36
- function extractUserTextFromCodex(inputMessages) {
37
- if (!Array.isArray(inputMessages) || inputMessages.length === 0) return "";
38
- const lastMessage = inputMessages[inputMessages.length - 1];
39
- return normalizeContent(lastMessage);
40
- }
41
-
42
31
  function buildCodexInputMeta(inputMessages) {
43
32
  const meta = {
44
33
  input_messages_type: Array.isArray(inputMessages) ? "array" : typeof inputMessages,
45
34
  input_messages_len: Array.isArray(inputMessages) ? inputMessages.length : 0,
35
+ input_messages_has_content: hasNonEmptyInputMessages(inputMessages),
46
36
  input_messages_roles: [],
47
37
  input_messages_last_role: "",
48
38
  input_messages_last_type: "",
@@ -78,6 +68,177 @@ function extractAssistantText(content) {
78
68
  return normalizeContent(content);
79
69
  }
80
70
 
71
+ function readCodexUserMessageFromRollout(sessionId) {
72
+ const meta = {
73
+ codex_rollout_found: false,
74
+ codex_rollout_source: "",
75
+ codex_rollout_path: "",
76
+ codex_rollout_mtime_ms: 0,
77
+ codex_rollout_total: 0,
78
+ codex_rollout_user_message_count: 0,
79
+ codex_rollout_line_count: 0,
80
+ codex_rollout_error: "",
81
+ };
82
+ const rolloutPath = findCodexRolloutPath(sessionId, meta);
83
+ if (!rolloutPath) {
84
+ return { userText: "", meta };
85
+ }
86
+ try {
87
+ const content = fs.readFileSync(rolloutPath, "utf8");
88
+ const lines = content.split("\n").filter(Boolean);
89
+ meta.codex_rollout_line_count = lines.length;
90
+ let lastUser = "";
91
+ let lastTs = "";
92
+ let count = 0;
93
+ for (const line of lines) {
94
+ let record;
95
+ try {
96
+ record = JSON.parse(line);
97
+ } catch {
98
+ continue;
99
+ }
100
+ if (record?.type !== "event_msg") continue;
101
+ const payload = record?.payload;
102
+ if (payload?.type !== "user_message") continue;
103
+ const text = extractCodexUserMessage(payload);
104
+ if (text) {
105
+ lastUser = text;
106
+ lastTs = record?.timestamp || "";
107
+ }
108
+ count += 1;
109
+ }
110
+ meta.codex_rollout_user_message_count = count;
111
+ if (lastTs) {
112
+ meta.codex_rollout_last_user_ts = lastTs;
113
+ }
114
+ return { userText: lastUser, meta };
115
+ } catch (error) {
116
+ meta.codex_rollout_error = error?.message || "rollout_read_failed";
117
+ return { userText: "", meta };
118
+ }
119
+ }
120
+
121
+ function extractCodexUserMessage(payload) {
122
+ if (!payload || typeof payload !== "object") return "";
123
+ const content =
124
+ payload.message ||
125
+ payload.text ||
126
+ payload.prompt ||
127
+ payload.input ||
128
+ payload.content;
129
+ return normalizeContent(content);
130
+ }
131
+
132
+ function findCodexRolloutPath(sessionId, meta) {
133
+ if (!sessionId) {
134
+ meta.codex_rollout_error = "session_id_missing";
135
+ return "";
136
+ }
137
+ const sessionsDir = path.join(os.homedir(), ".codex", "sessions");
138
+ meta.codex_sessions_dir = sessionsDir;
139
+ if (!fs.existsSync(sessionsDir)) {
140
+ meta.codex_rollout_error = "sessions_dir_missing";
141
+ return "";
142
+ }
143
+
144
+ const files = collectRolloutFiles(sessionsDir);
145
+ meta.codex_rollout_total = files.length;
146
+ const byName = files.filter((item) => item.name.includes(sessionId));
147
+ if (byName.length > 0) {
148
+ const best = pickLatestFile(byName);
149
+ if (best) {
150
+ meta.codex_rollout_found = true;
151
+ meta.codex_rollout_source = "filename";
152
+ meta.codex_rollout_path = best.path;
153
+ meta.codex_rollout_mtime_ms = best.mtimeMs;
154
+ return best.path;
155
+ }
156
+ }
157
+
158
+ const byContent = findRolloutByContent(files, sessionId, meta);
159
+ if (byContent) {
160
+ meta.codex_rollout_found = true;
161
+ meta.codex_rollout_source = "content";
162
+ meta.codex_rollout_path = byContent.path;
163
+ meta.codex_rollout_mtime_ms = byContent.mtimeMs;
164
+ return byContent.path;
165
+ }
166
+
167
+ meta.codex_rollout_error = meta.codex_rollout_error || "rollout_not_found";
168
+ return "";
169
+ }
170
+
171
+ function collectRolloutFiles(rootDir) {
172
+ const results = [];
173
+ const stack = [rootDir];
174
+ while (stack.length > 0) {
175
+ const dir = stack.pop();
176
+ let entries;
177
+ try {
178
+ entries = fs.readdirSync(dir, { withFileTypes: true });
179
+ } catch {
180
+ continue;
181
+ }
182
+ for (const entry of entries) {
183
+ const fullPath = path.join(dir, entry.name);
184
+ if (entry.isDirectory()) {
185
+ stack.push(fullPath);
186
+ continue;
187
+ }
188
+ if (!entry.isFile()) continue;
189
+ if (!entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl")) {
190
+ continue;
191
+ }
192
+ let stat;
193
+ try {
194
+ stat = fs.statSync(fullPath);
195
+ } catch {
196
+ continue;
197
+ }
198
+ results.push({
199
+ path: fullPath,
200
+ name: entry.name,
201
+ mtimeMs: stat.mtimeMs || 0,
202
+ });
203
+ }
204
+ }
205
+ return results;
206
+ }
207
+
208
+ function pickLatestFile(files) {
209
+ if (!files || files.length === 0) return null;
210
+ let best = files[0];
211
+ for (const item of files) {
212
+ if (item.mtimeMs > best.mtimeMs) {
213
+ best = item;
214
+ }
215
+ }
216
+ return best;
217
+ }
218
+
219
+ function findRolloutByContent(files, sessionId, meta) {
220
+ if (!files || files.length === 0) {
221
+ meta.codex_rollout_error = "rollout_files_empty";
222
+ return null;
223
+ }
224
+ const sorted = [...files].sort((a, b) => b.mtimeMs - a.mtimeMs);
225
+ const limit = 30;
226
+ for (let i = 0; i < sorted.length && i < limit; i += 1) {
227
+ const file = sorted[i];
228
+ let content = "";
229
+ try {
230
+ content = fs.readFileSync(file.path, "utf8");
231
+ } catch {
232
+ continue;
233
+ }
234
+ if (content.includes(sessionId)) {
235
+ return file;
236
+ }
237
+ }
238
+ meta.codex_rollout_error = "rollout_not_matched_in_recent_files";
239
+ return null;
240
+ }
241
+
81
242
  function extractTextDeep(value, depth = 0) {
82
243
  if (depth > 6) return "";
83
244
  if (value === null || value === undefined) return "";