niahere 0.2.18 → 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/defaults/self/memory.md +1 -1
- package/defaults/self/rules.md +9 -0
- package/package.json +1 -1
- package/skills/gh-stamp/SKILL.md +29 -0
- package/src/channels/slack.ts +6 -1
- package/src/chat/identity.ts +1 -1
- package/src/commands/init.ts +2 -1
- package/src/core/runner.ts +115 -39
- package/src/mcp/server.ts +20 -0
- package/src/mcp/tools.ts +29 -2
- package/src/prompts/environment.md +19 -5
- package/src/prompts/mode-job.md +3 -1
- package/src/types/config.ts +1 -0
- package/src/utils/config.ts +5 -0
package/defaults/self/memory.md
CHANGED
|
@@ -8,7 +8,7 @@ Write here when:
|
|
|
8
8
|
- I learned a preference, habit, or pattern worth remembering
|
|
9
9
|
- A workaround was needed that future-me should know about
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Entries are grouped by date. Use `add_memory` tool to append, or edit directly.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Rules
|
|
2
|
+
|
|
3
|
+
Custom instructions and behavioral overrides. Nia reads this file at the start of every session — edits take effect immediately without restart.
|
|
4
|
+
|
|
5
|
+
Add rules here to change how Nia behaves. Examples:
|
|
6
|
+
|
|
7
|
+
- "When asked for a standup, keep it to 1-2 lines"
|
|
8
|
+
- "Always use bullet points for status updates"
|
|
9
|
+
- "Never send messages longer than 3 sentences in Slack channels"
|
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/channels/slack.ts
CHANGED
|
@@ -8,6 +8,11 @@ import { log } from "../utils/log";
|
|
|
8
8
|
import { getMcpServers } from "../mcp";
|
|
9
9
|
import { classifyMime, validateAttachment, prepareImage } from "../utils/attachment";
|
|
10
10
|
|
|
11
|
+
/** Strip markdown backticks so sentinel tokens like [NO_REPLY] match even when the LLM wraps them. */
|
|
12
|
+
function cleanSentinel(text: string): string {
|
|
13
|
+
return text.replace(/`/g, "").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
class SlackChannel implements Channel {
|
|
12
17
|
name = "slack";
|
|
13
18
|
private app: App | null = null;
|
|
@@ -364,7 +369,7 @@ class SlackChannel implements Channel {
|
|
|
364
369
|
const reply = result.trim();
|
|
365
370
|
|
|
366
371
|
// [NO_REPLY] or empty = agent chose not to respond (thread judgement)
|
|
367
|
-
if (!reply || reply === "[NO_REPLY]") {
|
|
372
|
+
if (!reply || cleanSentinel(reply) === "[NO_REPLY]") {
|
|
368
373
|
log.info({ channel: msg.channel, key }, "slack: agent chose not to reply");
|
|
369
374
|
return;
|
|
370
375
|
}
|
package/src/chat/identity.ts
CHANGED
|
@@ -18,7 +18,7 @@ function loadFile(dir: string, name: string): string {
|
|
|
18
18
|
|
|
19
19
|
export function loadIdentity(): string {
|
|
20
20
|
const { selfDir } = getPaths();
|
|
21
|
-
const files = ["identity.md", "owner.md", "soul.md"];
|
|
21
|
+
const files = ["identity.md", "owner.md", "soul.md", "rules.md"];
|
|
22
22
|
return files.map((f) => loadFile(selfDir, f)).filter(Boolean).join("\n\n");
|
|
23
23
|
}
|
|
24
24
|
|
package/src/commands/init.ts
CHANGED
|
@@ -427,8 +427,9 @@ export async function runInit(): Promise<void> {
|
|
|
427
427
|
console.log(` \u2713 wrote ${selfFile("owner.md")}`);
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
-
// Soul and memory — only create if missing (user may have customized)
|
|
430
|
+
// Soul, rules, and memory — only create if missing (user may have customized)
|
|
431
431
|
writeIfMissing(selfFile("soul.md"), loadTemplate("soul.md", vars), selfFile("soul.md"));
|
|
432
|
+
writeIfMissing(selfFile("rules.md"), loadTemplate("rules.md", vars), selfFile("rules.md"));
|
|
432
433
|
writeIfMissing(selfFile("memory.md"), loadTemplate("memory.md", vars), selfFile("memory.md"));
|
|
433
434
|
|
|
434
435
|
resetConfig();
|
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/mcp/server.ts
CHANGED
|
@@ -84,6 +84,26 @@ export function createNiaMcpServer() {
|
|
|
84
84
|
content: [{ type: "text" as const, text: await handlers.listMessages(args.limit, args.room) }],
|
|
85
85
|
}),
|
|
86
86
|
),
|
|
87
|
+
tool(
|
|
88
|
+
"add_rule",
|
|
89
|
+
"Add a behavioral rule. Rules are loaded into every session and take effect without restart. Use for 'from now on' / 'always' / 'never' type instructions.",
|
|
90
|
+
{
|
|
91
|
+
rule: z.string().describe("The rule to add (e.g. 'stamp updates: 1-2 lines max, no preamble')"),
|
|
92
|
+
},
|
|
93
|
+
async (args) => ({
|
|
94
|
+
content: [{ type: "text" as const, text: handlers.addRule(args.rule) }],
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
tool(
|
|
98
|
+
"add_memory",
|
|
99
|
+
"Save a factual memory for future reference. Memories are read on demand, not loaded automatically. Use for things learned, preferences discovered, or context worth keeping.",
|
|
100
|
+
{
|
|
101
|
+
entry: z.string().describe("What to remember (e.g. 'Aman prefers short Slack messages in #tech')"),
|
|
102
|
+
},
|
|
103
|
+
async (args) => ({
|
|
104
|
+
content: [{ type: "text" as const, text: handlers.addMemory(args.entry) }],
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
87
107
|
],
|
|
88
108
|
});
|
|
89
109
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from "fs";
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync } from "fs";
|
|
2
2
|
import type { ScheduleType } from "../types";
|
|
3
|
-
import { basename } from "path";
|
|
3
|
+
import { basename, join } from "path";
|
|
4
4
|
import { Job, Message, Session } from "../db/models";
|
|
5
5
|
import { computeInitialNextRun } from "../core/scheduler";
|
|
6
6
|
import { getConfig } from "../utils/config";
|
|
7
|
+
import { getPaths } from "../utils/paths";
|
|
7
8
|
import { getChannel } from "../channels/registry";
|
|
8
9
|
import { log } from "../utils/log";
|
|
9
10
|
import { classifyMime } from "../utils/attachment";
|
|
@@ -219,3 +220,29 @@ export async function listMessages(limit = 20, room?: string): Promise<string> {
|
|
|
219
220
|
if (messages.length === 0) return "No messages found.";
|
|
220
221
|
return JSON.stringify(messages, null, 2);
|
|
221
222
|
}
|
|
223
|
+
|
|
224
|
+
export function addRule(rule: string): string {
|
|
225
|
+
const { selfDir } = getPaths();
|
|
226
|
+
const rulesPath = join(selfDir, "rules.md");
|
|
227
|
+
const line = `\n- ${rule}\n`;
|
|
228
|
+
appendFileSync(rulesPath, line, "utf8");
|
|
229
|
+
return `Rule added to rules.md. Takes effect on next new session.`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function addMemory(entry: string): string {
|
|
233
|
+
const { selfDir } = getPaths();
|
|
234
|
+
const memoryPath = join(selfDir, "memory.md");
|
|
235
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
236
|
+
const header = `\n## ${date}`;
|
|
237
|
+
|
|
238
|
+
const existing = existsSync(memoryPath) ? readFileSync(memoryPath, "utf8") : "";
|
|
239
|
+
if (existing.includes(header)) {
|
|
240
|
+
// Append under existing date header
|
|
241
|
+
const updated = existing.replace(header, `${header}\n- ${entry}`);
|
|
242
|
+
writeFileSync(memoryPath, updated, "utf8");
|
|
243
|
+
} else {
|
|
244
|
+
// New date section
|
|
245
|
+
appendFileSync(memoryPath, `${header}\n- ${entry}\n`, "utf8");
|
|
246
|
+
}
|
|
247
|
+
return `Memory saved.`;
|
|
248
|
+
}
|
|
@@ -22,6 +22,8 @@ You have MCP tools for managing jobs directly — no need for shell commands:
|
|
|
22
22
|
- **run_job** — trigger a job to run immediately
|
|
23
23
|
- **send_message** — send a message to the user (via telegram, slack, or default channel). Supports `media_path` to send images/files.
|
|
24
24
|
- **list_messages** — read recent chat history
|
|
25
|
+
- **add_rule** — save a behavioral rule (loaded into every session, no restart needed). Use when told "from now on", "always", "never", or "remember to always..."
|
|
26
|
+
- **add_memory** — save a factual memory (read on demand). Use when told "remember that...", or when you learn something surprising worth keeping
|
|
25
27
|
|
|
26
28
|
Active hours: {{activeStart}}–{{activeEnd}} ({{timezone}}). Jobs respect this; crons (always=true) don't.
|
|
27
29
|
|
|
@@ -62,10 +64,22 @@ Your persona files live in {{selfDir}}/:
|
|
|
62
64
|
- `identity.md` — your personality and voice
|
|
63
65
|
- `owner.md` — info about who runs you
|
|
64
66
|
- `soul.md` — how you work
|
|
65
|
-
- `
|
|
67
|
+
- `rules.md` — behavioral overrides and custom instructions (loaded into every session, hot-reloads without restart)
|
|
68
|
+
- `memory.md` — persistent learnings (read on demand, not loaded automatically)
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
### Rules vs Memory
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
+
**Rules** (`rules.md`) = instructions for how to behave. Loaded into every session automatically.
|
|
73
|
+
- "stamp updates should be 1-2 lines max"
|
|
74
|
+
- "never send long messages in #tech"
|
|
75
|
+
- Use `add_rule` tool to add new rules, or edit the file directly.
|
|
76
|
+
|
|
77
|
+
**Memory** (`memory.md`) = facts and context. Read on demand when relevant.
|
|
78
|
+
- "2026-03-13: DB was down, Telegram send failed"
|
|
79
|
+
- "Aman prefers terminal over Slack for debugging"
|
|
80
|
+
- Use `add_memory` tool to save new memories.
|
|
81
|
+
|
|
82
|
+
**Which to use?**
|
|
83
|
+
- "From now on, do X" → rule
|
|
84
|
+
- "Remember that X happened" / "I prefer X" → memory
|
|
85
|
+
- If unsure, ask.
|
package/src/prompts/mode-job.md
CHANGED
|
@@ -3,4 +3,6 @@
|
|
|
3
3
|
You are executing a scheduled job. Be terse — execute the task and report the result. No small talk.
|
|
4
4
|
|
|
5
5
|
- State the outcome first, then supporting details if needed.
|
|
6
|
-
- If the job failed, report what went wrong clearly.
|
|
6
|
+
- If the job failed, report what went wrong clearly.
|
|
7
|
+
- When sending results to a channel (Slack, Telegram), keep it minimal. The recipient knows the context — just deliver the key info.
|
|
8
|
+
- Check rules.md for job-specific output instructions (e.g. brevity rules for specific jobs like stamp/standup).
|
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,
|