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 +1 -1
- package/skills/gh-stamp/SKILL.md +29 -0
- package/src/core/runner.ts +115 -39
- package/src/types/config.ts +1 -0
- package/src/utils/config.ts +5 -0
package/package.json
CHANGED
|
@@ -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.
|
package/src/core/runner.ts
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
147
|
+
session_id: output.sessionId || undefined,
|
|
148
|
+
error: output.error,
|
|
73
149
|
};
|
|
74
150
|
|
|
75
151
|
const auditEntry: AuditEntry = {
|
package/src/types/config.ts
CHANGED
package/src/utils/config.ts
CHANGED
|
@@ -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,
|