niahere 0.3.4 → 0.3.6

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": "niahere",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -430,10 +430,20 @@ class SlackChannel implements Channel {
430
430
  );
431
431
 
432
432
  const reply = result.trim();
433
-
434
- // [NO_REPLY] or empty = agent chose not to respond (thread judgement)
435
- if (!reply || cleanSentinel(reply) === "[NO_REPLY]") {
436
- log.info({ channel: msg.channel, key }, "slack: agent chose not to reply");
433
+ const cleaned = cleanSentinel(reply);
434
+
435
+ // [NO_REPLY] anywhere in the reply suppresses the send. If it appeared
436
+ // alongside real content the model got confused warn so we can spot it.
437
+ if (!reply || cleaned.includes("[NO_REPLY]")) {
438
+ const exact = !reply || cleaned === "[NO_REPLY]";
439
+ if (exact) {
440
+ log.info({ channel: msg.channel, key }, "slack: agent chose not to reply");
441
+ } else {
442
+ log.warn(
443
+ { channel: msg.channel, key, reply },
444
+ "slack: [NO_REPLY] sentinel mixed with content; suppressing send",
445
+ );
446
+ }
437
447
  if (messageId) await Message.updateDeliveryStatus(messageId, "sent").catch(() => {});
438
448
  return;
439
449
  }
@@ -27,6 +27,7 @@ import { isRetryableApiError, sleep } from "../utils/retry";
27
27
  import { registerActiveHandle, unregisterActiveHandle } from "../core/active-handles";
28
28
  import { resolveJobPrompt } from "../core/job-prompt";
29
29
  import { getSdkSkillsSetting } from "../core/skills";
30
+ import { getSdkHooks } from "../core/sdk-hooks";
30
31
 
31
32
  const IDLE_TIMEOUT = 10 * 60 * 1000; // 10 minutes
32
33
  const LONG_RUNNING_WARN = 30 * 60 * 1000; // 30 minutes
@@ -332,6 +333,7 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
332
333
  includePartialMessages: true,
333
334
  settingSources: ["project", "user"],
334
335
  skills: getSdkSkillsSetting(),
336
+ hooks: getSdkHooks(),
335
337
  };
336
338
  const model = resolveSdkModel(contextModel);
337
339
  if (model) {
@@ -18,6 +18,7 @@ import { log } from "../utils/log";
18
18
  import { isRetryableApiError, sleep } from "../utils/retry";
19
19
  import { registerActiveHandle, unregisterActiveHandle } from "./active-handles";
20
20
  import { getSdkSkillsSetting } from "./skills";
21
+ import { getSdkHooks } from "./sdk-hooks";
21
22
 
22
23
  export { buildWorkingMemory } from "./job-prompt";
23
24
 
@@ -121,6 +122,7 @@ export async function runJobWithClaude(
121
122
  permissionMode: "bypassPermissions",
122
123
  sessionId,
123
124
  skills: getSdkSkillsSetting(),
125
+ hooks: getSdkHooks(),
124
126
  };
125
127
 
126
128
  if (model && model !== "default") {
@@ -0,0 +1,43 @@
1
+ import type {
2
+ HookCallbackMatcher,
3
+ HookEvent,
4
+ HookJSONOutput,
5
+ PreToolUseHookInput,
6
+ } from "@anthropic-ai/claude-agent-sdk";
7
+
8
+ const GH_MERGE_PATTERN = /(?:^|[\s;&|(])gh\s+pr\s+merge(?:\s|$)/;
9
+
10
+ const STAMP_WARNING_CONTEXT = [
11
+ "PreToolUse warning: this Bash command merges a GitHub PR.",
12
+ "If the user only wants to APPROVE the PR (LGTM), use the gh-stamp skill",
13
+ '(`gh pr comment <pr> --body "LGTM, Stamped ✅"`) instead of merging.',
14
+ "Confirm intent before proceeding.",
15
+ ].join(" ");
16
+
17
+ const STAMP_WARNING_MESSAGE =
18
+ "Heads up: about to run `gh pr merge`. Did you mean to STAMP (LGTM approval) instead? See the gh-stamp skill.";
19
+
20
+ const warnOnGhMerge: HookCallbackMatcher = {
21
+ matcher: "Bash",
22
+ hooks: [
23
+ async (input): Promise<HookJSONOutput> => {
24
+ if (input.hook_event_name !== "PreToolUse") return {};
25
+ const command = (input as PreToolUseHookInput).tool_input as { command?: unknown } | undefined;
26
+ const cmd = command?.command;
27
+ if (typeof cmd !== "string" || !GH_MERGE_PATTERN.test(cmd)) return {};
28
+ return {
29
+ systemMessage: STAMP_WARNING_MESSAGE,
30
+ hookSpecificOutput: {
31
+ hookEventName: "PreToolUse",
32
+ additionalContext: STAMP_WARNING_CONTEXT,
33
+ },
34
+ };
35
+ },
36
+ ],
37
+ };
38
+
39
+ export function getSdkHooks(): Partial<Record<HookEvent, HookCallbackMatcher[]>> {
40
+ return {
41
+ PreToolUse: [warnOnGhMerge],
42
+ };
43
+ }