niahere 0.2.73 → 0.2.75
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 +2 -2
- package/src/channels/slack.ts +4 -6
- package/src/cli/channels.ts +120 -68
- package/src/cli/index.ts +3 -3
- package/src/commands/init.ts +5 -5
- package/src/core/alive.ts +1 -1
- package/src/mcp/tools.ts +5 -8
- package/src/prompts/environment.md +1 -2
- package/src/types/config.ts +0 -1
- package/src/utils/config.ts +6 -5
- package/src/utils/pid.ts +44 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niahere",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.75",
|
|
4
4
|
"description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"private": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.
|
|
47
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.119",
|
|
48
48
|
"@anthropic-ai/sdk": "^0.88.0",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
50
50
|
"@slack/bolt": "^4.6.0",
|
package/src/channels/slack.ts
CHANGED
|
@@ -22,15 +22,14 @@ function cleanSentinel(text: string): string {
|
|
|
22
22
|
class SlackChannel implements Channel {
|
|
23
23
|
name = "slack";
|
|
24
24
|
private app: App | null = null;
|
|
25
|
-
private defaultChannelId: string | null = null;
|
|
26
25
|
private dmUserId: string | null = null;
|
|
27
26
|
/** Timestamps of messages Nia posted proactively (used to detect replies to our own messages) */
|
|
28
27
|
private outboundTs = new Set<string>();
|
|
29
28
|
|
|
30
29
|
async sendMessage(text: string): Promise<void> {
|
|
31
30
|
if (!this.app) throw new Error("Slack not started");
|
|
32
|
-
const target = this.
|
|
33
|
-
if (!target) throw new Error("No Slack recipient —
|
|
31
|
+
const target = this.dmUserId;
|
|
32
|
+
if (!target) throw new Error("No Slack recipient — set dm_user_id in config");
|
|
34
33
|
const result = await this.app.client.chat.postMessage({ channel: target, text });
|
|
35
34
|
if (result.ts) this.outboundTs.add(result.ts);
|
|
36
35
|
}
|
|
@@ -45,8 +44,8 @@ class SlackChannel implements Channel {
|
|
|
45
44
|
|
|
46
45
|
async sendMedia(data: Buffer, mimeType: string, filename?: string): Promise<void> {
|
|
47
46
|
if (!this.app) throw new Error("Slack not started");
|
|
48
|
-
const target = this.
|
|
49
|
-
if (!target) throw new Error("No Slack recipient —
|
|
47
|
+
const target = this.dmUserId;
|
|
48
|
+
if (!target) throw new Error("No Slack recipient — set dm_user_id in config");
|
|
50
49
|
await this.app.client.filesUploadV2({
|
|
51
50
|
channel_id: target,
|
|
52
51
|
file: data,
|
|
@@ -61,7 +60,6 @@ class SlackChannel implements Channel {
|
|
|
61
60
|
|
|
62
61
|
await runMigrations();
|
|
63
62
|
|
|
64
|
-
this.defaultChannelId = config.channels.slack.channel_id;
|
|
65
63
|
this.dmUserId = config.channels.slack.dm_user_id;
|
|
66
64
|
|
|
67
65
|
const chats = new Map<string, ChatState>();
|
package/src/cli/channels.ts
CHANGED
|
@@ -7,21 +7,38 @@ import { log } from "../utils/log";
|
|
|
7
7
|
export async function sendCommand(): Promise<void> {
|
|
8
8
|
const args = process.argv.slice(3);
|
|
9
9
|
let channel: string | undefined;
|
|
10
|
+
let toChannelId: string | undefined;
|
|
11
|
+
let threadTs: string | undefined;
|
|
10
12
|
const msgParts: string[] = [];
|
|
11
13
|
for (let i = 0; i < args.length; i++) {
|
|
12
14
|
if ((args[i] === "--channel" || args[i] === "-c") && args[i + 1]) {
|
|
13
15
|
channel = args[++i];
|
|
16
|
+
} else if (args[i] === "--to" && args[i + 1]) {
|
|
17
|
+
toChannelId = args[++i];
|
|
18
|
+
} else if (args[i] === "--thread" && args[i + 1]) {
|
|
19
|
+
threadTs = args[++i];
|
|
14
20
|
} else {
|
|
15
21
|
msgParts.push(args[i]);
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
const message = msgParts.join(" ");
|
|
19
|
-
if (!message) fail("Usage: nia send [-c channel] <message>");
|
|
25
|
+
if (!message) fail("Usage: nia send [-c channel] [--to <slack-channel-id>] [--thread <ts>] <message>");
|
|
26
|
+
|
|
27
|
+
// --to implies slack channel
|
|
28
|
+
if (toChannelId) channel = channel || "slack";
|
|
29
|
+
// --thread requires --to
|
|
30
|
+
if (threadTs && !toChannelId) fail("--thread requires --to <slack-channel-id>");
|
|
20
31
|
|
|
21
32
|
const { sendMessage } = await import("../mcp/tools");
|
|
22
33
|
|
|
34
|
+
// Build sourceCtx for targeted channel/thread sends
|
|
35
|
+
const sourceCtx = toChannelId
|
|
36
|
+
? { slackChannelId: toChannelId, slackThreadTs: threadTs, channel: "slack" as const }
|
|
37
|
+
: undefined;
|
|
38
|
+
const target = toChannelId ? "thread" as const : "auto" as const;
|
|
39
|
+
|
|
23
40
|
try {
|
|
24
|
-
const result = await sendMessage(message, channel);
|
|
41
|
+
const result = await sendMessage(message, channel, undefined, sourceCtx, target);
|
|
25
42
|
console.log(result);
|
|
26
43
|
} catch (err) {
|
|
27
44
|
fail(`Failed to send: ${errMsg(err)}`);
|
|
@@ -29,27 +46,43 @@ export async function sendCommand(): Promise<void> {
|
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
export function telegramCommand(): void {
|
|
32
|
-
const
|
|
33
|
-
const chatId = process.argv[4];
|
|
49
|
+
const sub = process.argv[3];
|
|
34
50
|
|
|
35
|
-
if (
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
if (sub === "setup") {
|
|
52
|
+
const args = process.argv.slice(4);
|
|
53
|
+
let token: string | undefined;
|
|
54
|
+
let chatId: string | undefined;
|
|
55
|
+
|
|
56
|
+
for (const arg of args) {
|
|
57
|
+
if (arg.startsWith("--bot-token=")) token = arg.slice("--bot-token=".length);
|
|
58
|
+
else if (arg.startsWith("--chat-id=")) chatId = arg.slice("--chat-id=".length);
|
|
41
59
|
}
|
|
42
|
-
|
|
60
|
+
|
|
61
|
+
if (!token) {
|
|
62
|
+
fail("Usage: nia telegram setup --bot-token=<token> [--chat-id=<id>]");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tg: Record<string, unknown> = { bot_token: token };
|
|
66
|
+
if (chatId) tg.chat_id = Number(chatId);
|
|
67
|
+
updateRawConfig({ channels: { telegram: tg } });
|
|
68
|
+
|
|
69
|
+
console.log(`Telegram bot token saved to ${getPaths().config}`);
|
|
70
|
+
if (chatId) console.log(`Chat ID: ${chatId}`);
|
|
71
|
+
console.log("Run `nia restart` to activate.");
|
|
43
72
|
return;
|
|
44
73
|
}
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
// Default: show status
|
|
76
|
+
const config = getConfig();
|
|
77
|
+
if (config.channels.telegram.bot_token) {
|
|
78
|
+
console.log(`Telegram: configured (...${config.channels.telegram.bot_token.slice(-6)})`);
|
|
79
|
+
if (config.channels.telegram.chat_id) {
|
|
80
|
+
console.log(` Chat ID: ${config.channels.telegram.chat_id}`);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
console.log("Telegram: not configured");
|
|
84
|
+
}
|
|
85
|
+
console.log("\nUsage: nia telegram setup --bot-token=<token> [--chat-id=<id>]");
|
|
53
86
|
}
|
|
54
87
|
|
|
55
88
|
/** Call Slack auth.test to enrich config with workspace/bot info. */
|
|
@@ -80,60 +113,79 @@ export async function enrichSlackConfig(botToken: string): Promise<Record<string
|
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
export async function slackCommand(): Promise<void> {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const resp = await fetch("https://slack.com/api/auth.test", {
|
|
97
|
-
headers: { Authorization: `Bearer ${config.channels.slack.bot_token}` },
|
|
98
|
-
});
|
|
99
|
-
const data = (await resp.json()) as Record<string, unknown>;
|
|
100
|
-
if (data.ok) {
|
|
101
|
-
console.log(` Auth: ${ICON_PASS} valid`);
|
|
102
|
-
// Backfill workspace info if missing
|
|
103
|
-
if (!config.channels.slack.workspace) {
|
|
104
|
-
const enriched = await enrichSlackConfig(config.channels.slack.bot_token);
|
|
105
|
-
if (Object.keys(enriched).length > 0) {
|
|
106
|
-
updateRawConfig({ channels: { slack: enriched } });
|
|
107
|
-
console.log(" (workspace info backfilled)");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
console.log(` Auth: ${ICON_FAIL} ${data.error}`);
|
|
112
|
-
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.log(` Auth: ${ICON_FAIL} could not reach Slack API`);
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
console.log("Slack: not configured");
|
|
116
|
+
const sub = process.argv[3];
|
|
117
|
+
|
|
118
|
+
if (sub === "setup") {
|
|
119
|
+
const args = process.argv.slice(4);
|
|
120
|
+
let botToken: string | undefined;
|
|
121
|
+
let appToken: string | undefined;
|
|
122
|
+
let dmUserId: string | undefined;
|
|
123
|
+
|
|
124
|
+
for (const arg of args) {
|
|
125
|
+
if (arg.startsWith("--bot-token=")) botToken = arg.slice("--bot-token=".length);
|
|
126
|
+
else if (arg.startsWith("--app-token=")) appToken = arg.slice("--app-token=".length);
|
|
127
|
+
else if (arg.startsWith("--dm-user-id=")) dmUserId = arg.slice("--dm-user-id=".length);
|
|
118
128
|
}
|
|
119
|
-
console.log("\nUsage: nia slack <bot-token> <app-token> [channel-id]");
|
|
120
|
-
console.log("\nCreate a Slack app: https://api.slack.com/apps (use defaults/channels/slack-manifest.json)");
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
if (!botToken || !appToken) {
|
|
131
|
+
fail("Usage: nia slack setup --bot-token=xoxb-... --app-token=xapp-... [--dm-user-id=U...]");
|
|
132
|
+
}
|
|
125
133
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
if (!botToken.startsWith("xoxb-")) {
|
|
135
|
+
fail(`Invalid bot token — must start with "xoxb-" (got "${botToken.slice(0, 10)}...")`);
|
|
136
|
+
}
|
|
137
|
+
if (!appToken.startsWith("xapp-")) {
|
|
138
|
+
fail(`Invalid app token — must start with "xapp-" (got "${appToken.slice(0, 10)}...")`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sl: Record<string, unknown> = { bot_token: botToken, app_token: appToken };
|
|
142
|
+
if (dmUserId) sl.dm_user_id = dmUserId;
|
|
129
143
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Object.assign(sl, enriched);
|
|
144
|
+
const enriched = await enrichSlackConfig(botToken);
|
|
145
|
+
Object.assign(sl, enriched);
|
|
133
146
|
|
|
134
|
-
|
|
147
|
+
updateRawConfig({ channels: { slack: sl } });
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
console.log(`Slack tokens saved to ${getPaths().config}`);
|
|
150
|
+
if (dmUserId) console.log(`DM user: ${dmUserId}`);
|
|
151
|
+
console.log("Run `nia restart` to activate.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Default: show status
|
|
156
|
+
const config = getConfig();
|
|
157
|
+
if (config.channels.slack.bot_token) {
|
|
158
|
+
console.log(`Slack: configured (...${config.channels.slack.bot_token.slice(-6)})`);
|
|
159
|
+
if (config.channels.slack.workspace) {
|
|
160
|
+
console.log(` Workspace: ${config.channels.slack.workspace} (${config.channels.slack.workspace_url})`);
|
|
161
|
+
console.log(` Bot: @${config.channels.slack.bot_name} (${config.channels.slack.bot_user_id})`);
|
|
162
|
+
}
|
|
163
|
+
if (config.channels.slack.dm_user_id) {
|
|
164
|
+
console.log(` DM user: ${config.channels.slack.dm_user_id}`);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const resp = await fetch("https://slack.com/api/auth.test", {
|
|
168
|
+
headers: { Authorization: `Bearer ${config.channels.slack.bot_token}` },
|
|
169
|
+
});
|
|
170
|
+
const data = (await resp.json()) as Record<string, unknown>;
|
|
171
|
+
if (data.ok) {
|
|
172
|
+
console.log(` Auth: ${ICON_PASS} valid`);
|
|
173
|
+
if (!config.channels.slack.workspace) {
|
|
174
|
+
const enriched = await enrichSlackConfig(config.channels.slack.bot_token);
|
|
175
|
+
if (Object.keys(enriched).length > 0) {
|
|
176
|
+
updateRawConfig({ channels: { slack: enriched } });
|
|
177
|
+
console.log(" (workspace info backfilled)");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
console.log(` Auth: ${ICON_FAIL} ${data.error}`);
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(` Auth: ${ICON_FAIL} could not reach Slack API`);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
console.log("Slack: not configured");
|
|
188
|
+
}
|
|
189
|
+
console.log("\nUsage: nia slack setup --bot-token=xoxb-... --app-token=xapp-... [--dm-user-id=U...]");
|
|
190
|
+
console.log("\nCreate a Slack app: https://api.slack.com/apps (use defaults/channels/slack-manifest.json)");
|
|
139
191
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -572,7 +572,7 @@ Chat:
|
|
|
572
572
|
chat [-c] [-r] [--employee|--agent|--job name] Interactive chat
|
|
573
573
|
run <prompt> One-shot execution
|
|
574
574
|
history [room] Recent messages
|
|
575
|
-
send [-c ch] <msg>
|
|
575
|
+
send [-c ch] [--to C --thread T] <msg> Send message (DM, channel, or thread)
|
|
576
576
|
|
|
577
577
|
Jobs:
|
|
578
578
|
job <sub> Manage jobs (list|add|update|remove|run|...)
|
|
@@ -587,8 +587,8 @@ Persona:
|
|
|
587
587
|
Channels:
|
|
588
588
|
channels [on|off] Toggle channels
|
|
589
589
|
watch <sub> Manage Slack watch channels
|
|
590
|
-
telegram
|
|
591
|
-
slack
|
|
590
|
+
telegram [setup] Configure telegram
|
|
591
|
+
slack [setup] Configure slack
|
|
592
592
|
|
|
593
593
|
System:
|
|
594
594
|
config <set|get|list> Manage config values
|
package/src/commands/init.ts
CHANGED
|
@@ -168,7 +168,7 @@ export async function runInit(): Promise<void> {
|
|
|
168
168
|
// Slack
|
|
169
169
|
let slackBotToken = "";
|
|
170
170
|
let slackAppToken = "";
|
|
171
|
-
let
|
|
171
|
+
let slackDmUserId = (exSl.dm_user_id as string) || "";
|
|
172
172
|
|
|
173
173
|
const existingSlackBot = (exSl.bot_token as string) || "";
|
|
174
174
|
|
|
@@ -182,12 +182,12 @@ export async function runInit(): Promise<void> {
|
|
|
182
182
|
const appInput = await ask(rl, "App token (xapp-...)", "");
|
|
183
183
|
slackAppToken = appInput || existingSlackApp;
|
|
184
184
|
if (slackBotToken && slackAppToken) {
|
|
185
|
-
|
|
185
|
+
slackDmUserId = await ask(rl, "DM user ID for outbound messages (U...)", slackDmUserId);
|
|
186
186
|
}
|
|
187
187
|
} else {
|
|
188
188
|
slackBotToken = existingSlackBot;
|
|
189
189
|
slackAppToken = (exSl.app_token as string) || "";
|
|
190
|
-
|
|
190
|
+
slackDmUserId = (exSl.dm_user_id as string) || "";
|
|
191
191
|
}
|
|
192
192
|
} else {
|
|
193
193
|
const setupSlack = await ask(rl, "\nSet up Slack? (y/n)", "n");
|
|
@@ -210,7 +210,7 @@ export async function runInit(): Promise<void> {
|
|
|
210
210
|
slackAppToken = await ask(rl, "App token (xapp-...)", "");
|
|
211
211
|
|
|
212
212
|
if (slackBotToken && slackAppToken) {
|
|
213
|
-
|
|
213
|
+
slackDmUserId = await ask(rl, "DM user ID for outbound messages (U...)", "");
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
}
|
|
@@ -421,7 +421,7 @@ export async function runInit(): Promise<void> {
|
|
|
421
421
|
}
|
|
422
422
|
if (slackBotToken && slackAppToken) {
|
|
423
423
|
const sl: Record<string, unknown> = { bot_token: slackBotToken, app_token: slackAppToken };
|
|
424
|
-
if (
|
|
424
|
+
if (slackDmUserId) sl.dm_user_id = slackDmUserId;
|
|
425
425
|
// Enrich with workspace/bot info from auth.test
|
|
426
426
|
const enriched = await enrichSlackConfig(slackBotToken);
|
|
427
427
|
Object.assign(sl, enriched);
|
package/src/core/alive.ts
CHANGED
|
@@ -100,7 +100,7 @@ async function notifyUser(message: string): Promise<void> {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const slToken = config.channels.slack.bot_token;
|
|
103
|
-
const slRecipient = config.channels.slack.dm_user_id
|
|
103
|
+
const slRecipient = config.channels.slack.dm_user_id;
|
|
104
104
|
if (slToken && slRecipient) {
|
|
105
105
|
try {
|
|
106
106
|
const resp = await fetch("https://slack.com/api/chat.postMessage", {
|
package/src/mcp/tools.ts
CHANGED
|
@@ -171,9 +171,9 @@ async function sendDirect(target: string, text: string): Promise<void> {
|
|
|
171
171
|
|
|
172
172
|
if (target === "slack") {
|
|
173
173
|
const token = config.channels.slack.bot_token;
|
|
174
|
-
const recipient = config.channels.slack.
|
|
174
|
+
const recipient = config.channels.slack.dm_user_id;
|
|
175
175
|
if (!token) throw new Error("Slack not configured (no bot token)");
|
|
176
|
-
if (!recipient) throw new Error("No Slack recipient —
|
|
176
|
+
if (!recipient) throw new Error("No Slack recipient — set dm_user_id in config");
|
|
177
177
|
const { App } = await import("@slack/bolt");
|
|
178
178
|
const app = new App({ token, signingSecret: "unused" });
|
|
179
179
|
await app.client.chat.postMessage({ token, channel: recipient, text });
|
|
@@ -205,9 +205,9 @@ async function sendMediaDirect(target: string, data: Buffer, mimeType: string, f
|
|
|
205
205
|
|
|
206
206
|
if (target === "slack") {
|
|
207
207
|
const token = config.channels.slack.bot_token;
|
|
208
|
-
const recipient = config.channels.slack.
|
|
208
|
+
const recipient = config.channels.slack.dm_user_id;
|
|
209
209
|
if (!token) throw new Error("Slack not configured (no bot token)");
|
|
210
|
-
if (!recipient) throw new Error("No Slack recipient —
|
|
210
|
+
if (!recipient) throw new Error("No Slack recipient — set dm_user_id in config");
|
|
211
211
|
const { App } = await import("@slack/bolt");
|
|
212
212
|
const app = new App({ token, signingSecret: "unused" });
|
|
213
213
|
await app.client.filesUploadV2({
|
|
@@ -245,11 +245,8 @@ export async function sendMessage(text: string, channelName?: string, mediaPath?
|
|
|
245
245
|
// Replying in-thread: use the source session's room prefix
|
|
246
246
|
roomPrefix = sourceCtx.room.replace(/-\d+$/, "");
|
|
247
247
|
} else {
|
|
248
|
-
const channelId = config.channels.slack.channel_id;
|
|
249
248
|
const dmUserId = config.channels.slack.dm_user_id;
|
|
250
|
-
if (
|
|
251
|
-
roomPrefix = `slack-${channelId}`;
|
|
252
|
-
} else if (dmUserId) {
|
|
249
|
+
if (dmUserId) {
|
|
253
250
|
roomPrefix = `slack-dm-${dmUserId}`;
|
|
254
251
|
}
|
|
255
252
|
}
|
|
@@ -100,8 +100,7 @@ Config reference:
|
|
|
100
100
|
- `channels.telegram.open` — if true, anyone can message the bot
|
|
101
101
|
- `channels.slack.bot_token` — Slack bot token (xoxb-...)
|
|
102
102
|
- `channels.slack.app_token` — Slack app token (xapp-...)
|
|
103
|
-
- `channels.slack.
|
|
104
|
-
- `channels.slack.dm_user_id` — auto-registered DM user
|
|
103
|
+
- `channels.slack.dm_user_id` — Slack user ID for DM-based outbound messages
|
|
105
104
|
- `channels.slack.watch` — per-channel proactive monitoring. Keys use `channel_id#channel_name` format. The `behavior` field is optional and has three forms: (1) omitted — loads `~/.niahere/watches/<channel_name>/behavior.md`; (2) single word like `deal-monitor` — loads `~/.niahere/watches/deal-monitor/behavior.md` (dir-per-watch, like agents); (3) inline prose. File-backed watches hot-reload via mtime tracking, no restart needed.
|
|
106
105
|
{{slackWatch}}
|
|
107
106
|
|
package/src/types/config.ts
CHANGED
package/src/utils/config.ts
CHANGED
|
@@ -23,7 +23,6 @@ const DEFAULTS: Config = {
|
|
|
23
23
|
slack: {
|
|
24
24
|
bot_token: null,
|
|
25
25
|
app_token: null,
|
|
26
|
-
channel_id: null,
|
|
27
26
|
dm_user_id: null,
|
|
28
27
|
bot_user_id: null,
|
|
29
28
|
bot_name: null,
|
|
@@ -124,9 +123,12 @@ export function loadConfig(): Config {
|
|
|
124
123
|
|
|
125
124
|
const slAppToken = process.env.SLACK_APP_TOKEN || (typeof chSl.app_token === "string" ? chSl.app_token : null);
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const slDmUserId =
|
|
126
|
+
// Legacy: channel_id was removed in favor of dm_user_id. Fall back to channel_id if dm_user_id is not set.
|
|
127
|
+
const legacyChannelId = process.env.SLACK_CHANNEL_ID || (typeof chSl.channel_id === "string" ? chSl.channel_id : null);
|
|
128
|
+
const slDmUserId =
|
|
129
|
+
process.env.SLACK_DM_USER_ID ||
|
|
130
|
+
(typeof chSl.dm_user_id === "string" ? chSl.dm_user_id : null) ||
|
|
131
|
+
legacyChannelId;
|
|
130
132
|
|
|
131
133
|
const slBotUserId = typeof chSl.bot_user_id === "string" ? chSl.bot_user_id : null;
|
|
132
134
|
const slBotName = typeof chSl.bot_name === "string" ? chSl.bot_name : null;
|
|
@@ -164,7 +166,6 @@ export function loadConfig(): Config {
|
|
|
164
166
|
slack: {
|
|
165
167
|
bot_token: slBotToken,
|
|
166
168
|
app_token: slAppToken,
|
|
167
|
-
channel_id: slChannelId,
|
|
168
169
|
dm_user_id: slDmUserId,
|
|
169
170
|
bot_user_id: slBotUserId,
|
|
170
171
|
bot_name: slBotName,
|
package/src/utils/pid.ts
CHANGED
|
@@ -3,23 +3,50 @@ import { dirname } from "path";
|
|
|
3
3
|
import { getPaths } from "./paths";
|
|
4
4
|
import { log } from "./log";
|
|
5
5
|
|
|
6
|
+
type PidEntry = { pid: number; lstart: string };
|
|
7
|
+
|
|
8
|
+
function getLstart(pid: number): string {
|
|
9
|
+
try {
|
|
10
|
+
const result = Bun.spawnSync(["ps", "-o", "lstart=", "-p", String(pid)]);
|
|
11
|
+
return new TextDecoder().decode(result.stdout).trim();
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
export function writePid(pid: number): void {
|
|
7
18
|
const { pid: pidPath } = getPaths();
|
|
19
|
+
const lstart = getLstart(pid);
|
|
20
|
+
if (!lstart) {
|
|
21
|
+
log.warn({ pid }, "could not capture pid identity (ps returned nothing)");
|
|
22
|
+
}
|
|
8
23
|
mkdirSync(dirname(pidPath), { recursive: true });
|
|
9
|
-
writeFileSync(pidPath,
|
|
24
|
+
writeFileSync(pidPath, JSON.stringify({ pid, lstart }));
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
|
|
27
|
+
function readEntry(): PidEntry | null {
|
|
13
28
|
const { pid: pidPath } = getPaths();
|
|
14
29
|
if (!existsSync(pidPath)) return null;
|
|
15
30
|
|
|
16
31
|
try {
|
|
17
|
-
|
|
32
|
+
const raw = readFileSync(pidPath, "utf8").trim();
|
|
33
|
+
if (/^\d+$/.test(raw)) {
|
|
34
|
+
return { pid: parseInt(raw, 10), lstart: "" };
|
|
35
|
+
}
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (typeof parsed?.pid === "number" && typeof parsed?.lstart === "string") {
|
|
38
|
+
return parsed as PidEntry;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
18
41
|
} catch {
|
|
19
42
|
return null;
|
|
20
43
|
}
|
|
21
44
|
}
|
|
22
45
|
|
|
46
|
+
export function readPid(): number | null {
|
|
47
|
+
return readEntry()?.pid ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
export function removePid(): void {
|
|
24
51
|
const { pid: pidPath } = getPaths();
|
|
25
52
|
try {
|
|
@@ -30,15 +57,22 @@ export function removePid(): void {
|
|
|
30
57
|
}
|
|
31
58
|
|
|
32
59
|
export function isRunning(): boolean {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
60
|
+
const entry = readEntry();
|
|
61
|
+
if (entry === null) return false;
|
|
35
62
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
const currentLstart = getLstart(entry.pid);
|
|
64
|
+
if (!currentLstart) {
|
|
65
|
+
log.warn({ stalePid: entry.pid }, "removing stale pid file (process not running)");
|
|
66
|
+
removePid();
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (entry.lstart && currentLstart !== entry.lstart) {
|
|
70
|
+
log.warn(
|
|
71
|
+
{ stalePid: entry.pid, recorded: entry.lstart, current: currentLstart },
|
|
72
|
+
"removing stale pid file (process identity mismatch)",
|
|
73
|
+
);
|
|
41
74
|
removePid();
|
|
42
75
|
return false;
|
|
43
76
|
}
|
|
77
|
+
return true;
|
|
44
78
|
}
|