niahere 0.2.19 → 0.2.20

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.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "A personal AI assistant daemon — scheduled jobs, chat across Telegram and Slack, persona system, and visual identity.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: gh-stamp
3
+ description: "Stamp a GitHub PR with an LGTM approval comment. Use when someone wants to approve, stamp, or give a thumbs-up to a pull request."
4
+ ---
5
+
6
+ # gh-stamp
7
+
8
+ Approve-stamp a GitHub PR by posting a comment.
9
+
10
+ ## Trigger
11
+
12
+ User says "stamp", "stamp it", "stmap", or similar.
13
+
14
+ - Stamp only → use this skill alone.
15
+ - Stamp + review → run this skill first, then the pr-reviewer skill.
16
+ - Review only → use the pr-reviewer skill, not this.
17
+
18
+ ## Steps
19
+
20
+ 1. **Find the PR.** Accept a full URL, `owner/repo#number`, or a bare number if the repo is obvious from context. If unclear, ask and stop.
21
+ 2. **Post the comment.**
22
+ ```sh
23
+ gh pr comment <pr-url-or-number> --body "LGTM, Stamped ✅"
24
+ # or with repo qualifier:
25
+ gh pr comment --repo <owner/repo> <number> --body "LGTM, Stamped ✅"
26
+ ```
27
+ 3. **Confirm to user:** `LGTM, Stamped ✅`
28
+
29
+ Do not touch anything else on the PR.
@@ -1,24 +1,119 @@
1
1
  import { homedir } from "os";
2
2
  import { existsSync } from "fs";
3
+ import { randomUUID } from "crypto";
4
+ import { query } from "@anthropic-ai/claude-agent-sdk";
3
5
  import type { JobInput, JobResult } from "../types";
4
6
  import { appendAudit, readState, writeState } from "../utils/logger";
5
7
  import type { AuditEntry, JobState } from "../types";
6
8
  import { getConfig } from "../utils/config";
7
9
  import { buildSystemPrompt } from "../chat/identity";
8
10
 
9
- // Resolve full path to codex so daemon doesn't depend on PATH
10
- const CODEX_CANDIDATES = ["/opt/homebrew/bin/codex", "/usr/local/bin/codex"];
11
- const codexPath = CODEX_CANDIDATES.find((p) => existsSync(p)) || "codex";
11
+ interface RunnerOutput {
12
+ agentText: string;
13
+ sessionId: string;
14
+ error?: string;
15
+ }
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Codex runner
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function resolveCodexPath(): string {
22
+ const candidates = ["/opt/homebrew/bin/codex", "/usr/local/bin/codex"];
23
+ return candidates.find((p) => existsSync(p)) || "codex";
24
+ }
25
+
26
+ async function runJobWithCodex(fullPrompt: string, cwd: string, model: string): Promise<RunnerOutput> {
27
+ const codexPath = resolveCodexPath();
28
+ const args = [codexPath, "exec", fullPrompt, "-C", cwd, "--json", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
29
+ if (model && model !== "default") {
30
+ args.splice(3, 0, "-m", model);
31
+ }
12
32
 
33
+ const proc = Bun.spawn(args, {
34
+ stdout: "pipe",
35
+ stderr: "pipe",
36
+ env: { ...process.env },
37
+ });
38
+
39
+ const stdout = await new Response(proc.stdout).text();
40
+ const stderr = await new Response(proc.stderr).text();
41
+ const exitCode = await proc.exited;
42
+
43
+ let agentText = "";
44
+ let sessionId = "";
45
+ for (const line of stdout.split("\n")) {
46
+ if (!line.trim()) continue;
47
+ try {
48
+ const event = JSON.parse(line);
49
+ if (event.type === "thread.started" && event.thread_id) {
50
+ sessionId = event.thread_id;
51
+ }
52
+ if (event.type === "item.completed" && event.item?.type === "agent_message") {
53
+ agentText = event.item.text || "";
54
+ }
55
+ } catch {}
56
+ }
13
57
 
14
- function buildPrompt(job: JobInput): string {
15
- const systemPrompt = buildSystemPrompt("job");
16
- return `${systemPrompt}\n\n---\n\nJob: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`;
58
+ if (exitCode !== 0) {
59
+ return { agentText, sessionId, error: stderr.trim() || `exit code ${exitCode}` };
60
+ }
61
+ return { agentText, sessionId };
17
62
  }
18
63
 
64
+ // ---------------------------------------------------------------------------
65
+ // Claude Agent SDK runner
66
+ // ---------------------------------------------------------------------------
67
+
68
+ async function runJobWithClaude(systemPrompt: string, jobPrompt: string, cwd: string): Promise<RunnerOutput> {
69
+ const sessionId = randomUUID();
70
+
71
+ // One-shot async iterable: emit a single user message then close
72
+ async function* singleMessage() {
73
+ yield {
74
+ type: "user" as const,
75
+ message: { role: "user" as const, content: jobPrompt },
76
+ parent_tool_use_id: null,
77
+ session_id: "",
78
+ };
79
+ }
80
+
81
+ const handle = query({
82
+ prompt: singleMessage() as any,
83
+ options: {
84
+ systemPrompt,
85
+ cwd,
86
+ permissionMode: "bypassPermissions",
87
+ sessionId,
88
+ } as any,
89
+ });
90
+
91
+ let agentText = "";
92
+ let actualSessionId = sessionId;
93
+
94
+ for await (const message of handle) {
95
+ if (message.type === "system" && (message as any).subtype === "init") {
96
+ actualSessionId = (message as any).session_id || sessionId;
97
+ }
98
+ if (message.type === "result") {
99
+ if (!(message as any).is_error) {
100
+ agentText = (message as any).result || "";
101
+ } else {
102
+ const errors = (message as any).errors;
103
+ return { agentText: "", sessionId: actualSessionId, error: errors?.join(", ") || "unknown error" };
104
+ }
105
+ }
106
+ }
107
+
108
+ return { agentText, sessionId: actualSessionId };
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Public API
113
+ // ---------------------------------------------------------------------------
114
+
19
115
  export async function runJob(job: JobInput): Promise<JobResult> {
20
116
  const config = getConfig();
21
- const model = config.model;
22
117
  const timestamp = new Date().toISOString();
23
118
  const startMs = performance.now();
24
119
 
@@ -28,48 +123,29 @@ export async function runJob(job: JobInput): Promise<JobResult> {
28
123
  writeState(state);
29
124
 
30
125
  try {
31
- const fullPrompt = buildPrompt(job);
32
126
  const cwd = homedir();
33
- const args = [codexPath, "exec", fullPrompt, "-C", cwd, "--json", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
34
- if (model && model !== "default") {
35
- args.splice(3, 0, "-m", model);
127
+ let output: RunnerOutput;
128
+
129
+ if (config.runner === "codex") {
130
+ const fullPrompt = `${buildSystemPrompt("job")}\n\n---\n\nJob: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`;
131
+ output = await runJobWithCodex(fullPrompt, cwd, config.model);
132
+ } else {
133
+ const systemPrompt = buildSystemPrompt("job");
134
+ const jobPrompt = `Job: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`;
135
+ output = await runJobWithClaude(systemPrompt, jobPrompt, cwd);
36
136
  }
37
- const proc = Bun.spawn(args, {
38
- stdout: "pipe",
39
- stderr: "pipe",
40
- env: { ...process.env },
41
- });
42
137
 
43
- const stdout = await new Response(proc.stdout).text();
44
- const stderr = await new Response(proc.stderr).text();
45
- const exitCode = await proc.exited;
46
138
  const duration_ms = Math.round(performance.now() - startMs);
139
+ const ok = !output.error;
47
140
 
48
- // Parse JSONL events for session ID and final agent message
49
- let agentText = "";
50
- let sessionId = "";
51
- for (const line of stdout.split("\n")) {
52
- if (!line.trim()) continue;
53
- try {
54
- const event = JSON.parse(line);
55
- if (event.type === "thread.started" && event.thread_id) {
56
- sessionId = event.thread_id;
57
- }
58
- if (event.type === "item.completed" && event.item?.type === "agent_message") {
59
- agentText = event.item.text || "";
60
- }
61
- } catch {}
62
- }
63
-
64
- const ok = exitCode === 0;
65
141
  const result: JobResult = {
66
142
  job: job.name,
67
143
  timestamp,
68
144
  status: ok ? "ok" : "error",
69
- result: agentText.trim(),
145
+ result: output.agentText.trim(),
70
146
  duration_ms,
71
- session_id: sessionId || undefined,
72
- error: ok ? undefined : stderr.trim() || `exit code ${exitCode}`,
147
+ session_id: output.sessionId || undefined,
148
+ error: output.error,
73
149
  };
74
150
 
75
151
  const auditEntry: AuditEntry = {
@@ -25,6 +25,7 @@ export interface ChannelsConfig {
25
25
 
26
26
  export interface Config {
27
27
  model: string;
28
+ runner: "claude" | "codex";
28
29
  timezone: string;
29
30
  activeHours: { start: string; end: string };
30
31
  database_url: string;
@@ -10,6 +10,7 @@ const TIME_RE = /^\d{2}:\d{2}$/;
10
10
 
11
11
  const DEFAULTS: Config = {
12
12
  model: "default",
13
+ runner: "claude",
13
14
  timezone: process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone,
14
15
  activeHours: { start: "00:00", end: "23:59" },
15
16
  database_url: DEFAULT_DATABASE_URL,
@@ -54,6 +55,9 @@ export function loadConfig(): Config {
54
55
  // Model
55
56
  const model = typeof raw.model === "string" ? raw.model : DEFAULTS.model;
56
57
 
58
+ // Runner — "codex" is opt-in, everything else defaults to "claude"
59
+ const runner: Config["runner"] = raw.runner === "codex" ? "codex" : DEFAULTS.runner;
60
+
57
61
  // Timezone
58
62
  let timezone = DEFAULTS.timezone;
59
63
  if (typeof raw.timezone === "string") {
@@ -140,6 +144,7 @@ export function loadConfig(): Config {
140
144
 
141
145
  return {
142
146
  model,
147
+ runner,
143
148
  timezone,
144
149
  activeHours: { start, end },
145
150
  database_url,