niahere 0.2.51 → 0.2.52
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/README.md +29 -16
- package/package.json +2 -2
- package/src/cli/agent.ts +17 -11
- package/src/cli/index.ts +51 -27
- package/src/cli/job.ts +91 -62
- package/src/cli/watch.ts +17 -7
- package/src/core/scheduler.ts +9 -0
- package/src/utils/cli.ts +122 -10
package/README.md
CHANGED
|
@@ -34,51 +34,63 @@ nia start # starts daemon + registers OS service
|
|
|
34
34
|
- **Slack** — Socket Mode bot with thread awareness, thinking emoji, watch channels for proactive monitoring
|
|
35
35
|
- **Terminal chat** — REPL with session resume support
|
|
36
36
|
- **Scheduled jobs** — recurring jobs and crons that run Claude and can message you back
|
|
37
|
-
- **Persona system** — customizable identity, soul, owner profile, and
|
|
37
|
+
- **Persona system** — customizable identity, soul, owner profile, rules, and memory (preloaded every session)
|
|
38
38
|
- **Agents** — domain specialists (marketer, senior-dev) via Claude Agent SDK subagents
|
|
39
39
|
- **Skills** — loads skills from multiple directories, invokable as slash commands
|
|
40
40
|
- **Cross-platform service** — launchd (macOS), systemd (Linux), service-aware restart
|
|
41
|
-
- **MCP tools** —
|
|
41
|
+
- **MCP tools** — 20 tools for job management, messaging, memory, rules, and channel control
|
|
42
|
+
- **Background memory consolidation** — extracts memories from conversations and job runs automatically
|
|
43
|
+
- **Session summaries** — handoff notes between sessions for continuity
|
|
44
|
+
- **Backups** — `nia backup` with auto-backup before updates
|
|
42
45
|
- **Optional integrations** — add Gmail, Discord, and more via skills
|
|
43
46
|
|
|
44
47
|
## Commands
|
|
45
48
|
|
|
46
49
|
```
|
|
47
|
-
nia init — interactive setup (db, channels, persona,
|
|
50
|
+
nia init — interactive setup (db, channels, persona, agents, active hours)
|
|
48
51
|
nia start / stop — daemon + OS service (launchd/systemd)
|
|
49
52
|
nia restart — restart daemon (service-aware)
|
|
50
53
|
nia status — show daemon, jobs, channels, chat rooms
|
|
51
|
-
nia
|
|
54
|
+
nia health — check daemon, db, channels, config
|
|
55
|
+
nia chat [-c|-r] [--channel ch] — terminal chat (new by default, -c continue, -r pick)
|
|
52
56
|
nia run <prompt> — one-shot prompt execution
|
|
53
57
|
nia history [room] — recent messages
|
|
54
|
-
nia logs [-f]
|
|
58
|
+
nia logs [-f] [--channel ch] — daemon logs (follow with -f, filter by channel)
|
|
55
59
|
nia send [-c channel] <msg> — send a message via channel
|
|
56
|
-
nia skills — list available skills
|
|
57
60
|
nia version — show version
|
|
61
|
+
nia update — update to latest version (auto-backup + restart)
|
|
58
62
|
|
|
59
63
|
nia job list — list all jobs
|
|
60
64
|
nia job show [name] — full details + recent runs
|
|
61
|
-
nia job
|
|
62
|
-
nia job add <n> <s> <p>
|
|
63
|
-
nia job
|
|
65
|
+
nia job status [name] — quick status check
|
|
66
|
+
nia job add <n> <s> <p> — add a job (--type, --always, --agent, --prompt-file)
|
|
67
|
+
nia job update <name> — update a job (--schedule, --prompt, --prompt-file, --type, --always, --agent)
|
|
64
68
|
nia job remove <name> — delete a job
|
|
65
69
|
nia job enable / disable <n> — toggle a job
|
|
66
70
|
nia job run <name> — run a job once
|
|
67
71
|
nia job log [name] — show recent run history
|
|
68
72
|
|
|
73
|
+
nia rules [show|reset] — view or reset rules.md
|
|
74
|
+
nia memory [show|reset] — view or reset memory.md
|
|
69
75
|
nia agent list — list available agents
|
|
70
76
|
nia agent show <name> — show agent details and prompt
|
|
77
|
+
nia skills [source] — list available skills
|
|
71
78
|
|
|
72
|
-
nia
|
|
73
|
-
nia
|
|
74
|
-
nia
|
|
79
|
+
nia channels — show channel status (on/off)
|
|
80
|
+
nia channels on / off — enable/disable channels (applied via SIGHUP, no restart)
|
|
81
|
+
nia watch list — list Slack watch channels
|
|
82
|
+
nia watch add/remove/enable/disable — manage watch channels
|
|
75
83
|
|
|
76
84
|
nia config list — show all config
|
|
77
85
|
nia config get <key> — get a config value (dot notation supported)
|
|
78
86
|
nia config set <key> <value> — set a config value
|
|
87
|
+
nia validate — validate config.yaml
|
|
88
|
+
nia backup [list] — create or list backups
|
|
89
|
+
nia test [-v] — run tests
|
|
79
90
|
|
|
80
|
-
nia
|
|
81
|
-
nia
|
|
91
|
+
nia db setup — install PostgreSQL + create database + migrate
|
|
92
|
+
nia db migrate — run database migrations
|
|
93
|
+
nia db status — check database connection
|
|
82
94
|
```
|
|
83
95
|
|
|
84
96
|
## Architecture
|
|
@@ -92,7 +104,8 @@ All config and data lives in `~/.niahere/`:
|
|
|
92
104
|
identity.md — agent personality and voice
|
|
93
105
|
owner.md — who runs this agent
|
|
94
106
|
soul.md — how the agent works
|
|
95
|
-
|
|
107
|
+
rules.md — behavioral instructions (loaded every session)
|
|
108
|
+
memory.md — persistent facts and context (loaded every session)
|
|
96
109
|
images/
|
|
97
110
|
reference.webp — visual identity reference image
|
|
98
111
|
profile.webp — profile picture for Telegram/Slack
|
|
@@ -119,7 +132,7 @@ Users then run `/add-discord` on their fork and get clean code that does exactly
|
|
|
119
132
|
## Updating
|
|
120
133
|
|
|
121
134
|
```bash
|
|
122
|
-
|
|
135
|
+
nia update # auto-backup, install latest, restart daemon
|
|
123
136
|
```
|
|
124
137
|
|
|
125
138
|
## Author
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niahere",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "A personal AI assistant daemon — scheduled jobs,
|
|
3
|
+
"version": "0.2.52",
|
|
4
|
+
"description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"start": "bun run src/cli/index.ts start",
|
package/src/cli/agent.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { scanAgents } from "../core/agents";
|
|
2
|
+
import { fail } from "../utils/cli";
|
|
3
|
+
|
|
4
|
+
const HELP = `Usage: nia agent <command>
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
list List all available agents
|
|
8
|
+
show <name> Show agent details and prompt`;
|
|
2
9
|
|
|
3
10
|
export async function agentCommand(): Promise<void> {
|
|
4
11
|
const subcommand = process.argv[3];
|
|
5
12
|
|
|
13
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
14
|
+
console.log(HELP);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
switch (subcommand) {
|
|
7
19
|
case "list": {
|
|
8
20
|
const agents = scanAgents();
|
|
@@ -20,16 +32,10 @@ export async function agentCommand(): Promise<void> {
|
|
|
20
32
|
|
|
21
33
|
case "show": {
|
|
22
34
|
const name = process.argv[4];
|
|
23
|
-
if (!name)
|
|
24
|
-
console.error("Usage: nia agent show <name>");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
35
|
+
if (!name) fail("Usage: nia agent show <name>");
|
|
27
36
|
const agents = scanAgents();
|
|
28
37
|
const agent = agents.find((a) => a.name === name);
|
|
29
|
-
if (!agent) {
|
|
30
|
-
console.error(`Agent "${name}" not found.`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
38
|
+
if (!agent) fail(`Agent "${name}" not found.`);
|
|
33
39
|
console.log(`Name: ${agent.name}`);
|
|
34
40
|
console.log(`Description: ${agent.description}`);
|
|
35
41
|
if (agent.model) console.log(`Model: ${agent.model}`);
|
|
@@ -40,8 +46,8 @@ export async function agentCommand(): Promise<void> {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
default:
|
|
43
|
-
console.
|
|
44
|
-
console.log(
|
|
45
|
-
|
|
49
|
+
if (subcommand) console.error(`Unknown subcommand: ${subcommand}`);
|
|
50
|
+
console.log(HELP);
|
|
51
|
+
process.exit(subcommand ? 1 : 0);
|
|
46
52
|
}
|
|
47
53
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ try {
|
|
|
29
29
|
const command = process.argv[2];
|
|
30
30
|
|
|
31
31
|
// Ensure ~/.niahere/ exists for commands that need it
|
|
32
|
-
if (command && !["init", "help", "version", "-v", "--version"].includes(command)) {
|
|
32
|
+
if (command && !["init", "help", "version", "-v", "--version", "-h", "--help"].includes(command)) {
|
|
33
33
|
mkdirSync(getNiaHome(), { recursive: true });
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -494,30 +494,54 @@ switch (command) {
|
|
|
494
494
|
break;
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
497
|
+
case "help":
|
|
498
|
+
case "--help":
|
|
499
|
+
case "-h":
|
|
500
|
+
default: {
|
|
501
|
+
const HELP = `Usage: nia <command>
|
|
502
|
+
|
|
503
|
+
Daemon:
|
|
504
|
+
start Start daemon + register service
|
|
505
|
+
stop Stop daemon + unregister service
|
|
506
|
+
restart Restart daemon
|
|
507
|
+
status [--json --rooms N --all] Show daemon, jobs, channels
|
|
508
|
+
health Check daemon, db, channels, config
|
|
509
|
+
logs [-f] [--channel ch] Daemon logs (filter by channel)
|
|
510
|
+
|
|
511
|
+
Chat:
|
|
512
|
+
chat [-c] [-r] [--channel ch] Interactive chat (new session by default)
|
|
513
|
+
run <prompt> One-shot execution
|
|
514
|
+
history [room] Recent messages
|
|
515
|
+
send [-c ch] <msg> Send a message via channel
|
|
516
|
+
|
|
517
|
+
Jobs:
|
|
518
|
+
job <sub> Manage jobs (list|add|update|remove|run|...)
|
|
519
|
+
|
|
520
|
+
Persona:
|
|
521
|
+
rules [show|reset] View or reset rules.md
|
|
522
|
+
memory [show|reset] View or reset memory.md
|
|
523
|
+
agent <sub> List/show agents
|
|
524
|
+
skills [source] List available skills
|
|
525
|
+
|
|
526
|
+
Channels:
|
|
527
|
+
channels [on|off] Toggle channels
|
|
528
|
+
watch <sub> Manage Slack watch channels
|
|
529
|
+
telegram <token> Configure telegram
|
|
530
|
+
slack <bot> <app> Configure slack
|
|
531
|
+
|
|
532
|
+
System:
|
|
533
|
+
config <set|get|list> Manage config values
|
|
534
|
+
backup [list] Create or list backups
|
|
535
|
+
validate Validate config.yaml
|
|
536
|
+
db <sub> Database setup/status/migrate
|
|
537
|
+
update Update to latest version
|
|
538
|
+
init Initial setup
|
|
539
|
+
test [-v] Run tests`;
|
|
540
|
+
|
|
541
|
+
console.log(HELP);
|
|
542
|
+
// Unknown command → exit 1, help/no command → exit 0
|
|
543
|
+
const isHelp = !command || command === "help" || command === "--help" || command === "-h";
|
|
544
|
+
if (!isHelp) console.error(`\nUnknown command: ${command}`);
|
|
545
|
+
process.exit(isHelp ? 0 : 1);
|
|
546
|
+
}
|
|
523
547
|
}
|
package/src/cli/job.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
|
-
import { CronExpressionParser } from "cron-parser";
|
|
3
2
|
import { readState, readAudit } from "../utils/logger";
|
|
4
3
|
import { getConfig } from "../utils/config";
|
|
5
4
|
import { runJob } from "../core/runner";
|
|
@@ -9,9 +8,34 @@ import { Job } from "../db/models";
|
|
|
9
8
|
import { withDb } from "../db/connection";
|
|
10
9
|
import type { ScheduleType } from "../types";
|
|
11
10
|
import { errMsg } from "../utils/errors";
|
|
12
|
-
import { fail, pickFromList, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
11
|
+
import { fail, parseArgs, pickFromList, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
13
12
|
import { computeInitialNextRun } from "../core/scheduler";
|
|
14
13
|
|
|
14
|
+
const HELP = `Usage: nia job <command>
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
list List all jobs
|
|
18
|
+
show [name] Full job details + recent runs
|
|
19
|
+
status [name] Quick status check
|
|
20
|
+
add <name> <schedule> <prompt> Add a job
|
|
21
|
+
--prompt <text> Prompt text (alternative to positional)
|
|
22
|
+
--prompt-file <path> Read prompt from file (for long/multi-line)
|
|
23
|
+
--type cron|interval|once Schedule type (default: cron)
|
|
24
|
+
--always Run 24/7 regardless of active hours
|
|
25
|
+
--agent <name> Assign an agent to the job
|
|
26
|
+
update <name> Update a job
|
|
27
|
+
--schedule <schedule> New schedule
|
|
28
|
+
--prompt <text> New prompt
|
|
29
|
+
--prompt-file <path> Read prompt from file (for long/multi-line)
|
|
30
|
+
--type cron|interval|once Change schedule type
|
|
31
|
+
--always / --no-always Toggle 24/7 mode
|
|
32
|
+
--agent <name> Assign agent (--no-agent to remove)
|
|
33
|
+
remove <name> Delete a job
|
|
34
|
+
enable <name> Enable a job
|
|
35
|
+
disable <name> Disable a job
|
|
36
|
+
run <name> Run a job once
|
|
37
|
+
log [name] Show recent run history`;
|
|
38
|
+
|
|
15
39
|
async function pickJob(prompt = "Pick a job"): Promise<string> {
|
|
16
40
|
let jobs: { name: string; schedule: string; enabled: boolean; prompt: string }[] = [];
|
|
17
41
|
try {
|
|
@@ -39,6 +63,11 @@ async function pickJob(prompt = "Pick a job"): Promise<string> {
|
|
|
39
63
|
export async function jobCommand(): Promise<void> {
|
|
40
64
|
const subcommand = process.argv[3];
|
|
41
65
|
|
|
66
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
67
|
+
console.log(HELP);
|
|
68
|
+
process.exit(subcommand ? 0 : 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
42
71
|
switch (subcommand) {
|
|
43
72
|
case "list": {
|
|
44
73
|
try {
|
|
@@ -62,42 +91,36 @@ export async function jobCommand(): Promise<void> {
|
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
case "add": {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Parse --type flag
|
|
69
|
-
let scheduleType: ScheduleType = "cron";
|
|
70
|
-
const typeIdx = cliArgs.indexOf("--type");
|
|
71
|
-
if (typeIdx !== -1 && cliArgs[typeIdx + 1]) {
|
|
72
|
-
const val = cliArgs[typeIdx + 1];
|
|
73
|
-
if (val === "cron" || val === "interval" || val === "once") {
|
|
74
|
-
scheduleType = val;
|
|
75
|
-
cliArgs.splice(typeIdx, 2);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
94
|
+
const args = parseArgs(process.argv.slice(4), ["always"]);
|
|
95
|
+
if (args.help) { console.log(HELP); return; }
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (agentIdx !== -1 && cliArgs[agentIdx + 1]) {
|
|
83
|
-
agent = cliArgs[agentIdx + 1];
|
|
84
|
-
cliArgs.splice(agentIdx, 2);
|
|
97
|
+
const scheduleType = (args.getString("type") || "cron") as ScheduleType;
|
|
98
|
+
if (!["cron", "interval", "once"].includes(scheduleType)) {
|
|
99
|
+
fail(`Invalid --type: "${scheduleType}". Must be cron, interval, or once.`);
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
102
|
+
const always = args.getBool("always") ?? false;
|
|
103
|
+
const agent = args.getString("agent");
|
|
104
|
+
|
|
105
|
+
const [name, schedule, ...promptParts] = args.positional;
|
|
106
|
+
const promptFlag = args.getString("prompt");
|
|
107
|
+
const promptFile = args.getString("prompt-file");
|
|
108
|
+
let prompt: string;
|
|
109
|
+
if (promptFile) {
|
|
110
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
111
|
+
if (!existsSync(promptFile)) fail(`File not found: ${promptFile}`);
|
|
112
|
+
prompt = readFileSync(promptFile, "utf8").trim();
|
|
113
|
+
} else if (promptFlag) {
|
|
114
|
+
prompt = promptFlag;
|
|
115
|
+
} else {
|
|
116
|
+
prompt = promptParts.join(" ");
|
|
117
|
+
}
|
|
90
118
|
|
|
91
119
|
if (!name || !schedule || !prompt) {
|
|
92
|
-
console.
|
|
120
|
+
console.error('Usage: nia job add <name> <schedule> <prompt> [--always] [--type cron|interval|once] [--agent <name>]');
|
|
93
121
|
fail('Example: nia job add heartbeat "*/10 * * * *" Check system health --always');
|
|
94
122
|
}
|
|
95
123
|
|
|
96
|
-
// Validate schedule based on type
|
|
97
|
-
if (scheduleType === "cron") {
|
|
98
|
-
try { CronExpressionParser.parse(schedule); } catch { fail(`Invalid cron schedule: ${schedule}`); }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
124
|
try {
|
|
102
125
|
const config = getConfig();
|
|
103
126
|
const nextRunAt = computeInitialNextRun(scheduleType, schedule, config.timezone);
|
|
@@ -144,29 +167,47 @@ export async function jobCommand(): Promise<void> {
|
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
case "update": {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
let cliArgs = process.argv.slice(4).filter((a) => a !== "--always" && a !== "--no-always");
|
|
170
|
+
const args = parseArgs(process.argv.slice(4), ["always"]);
|
|
171
|
+
if (args.help) { console.log(HELP); return; }
|
|
150
172
|
|
|
151
|
-
const name =
|
|
173
|
+
const name = args.positional[0];
|
|
152
174
|
if (!name) {
|
|
153
|
-
console.
|
|
175
|
+
console.error('Usage: nia job update <name> [--schedule <s>] [--prompt <p>] [--type <t>] [--always] [--no-always]');
|
|
154
176
|
fail('Example: nia job update curator --schedule "4h" --prompt "New prompt"');
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
const
|
|
158
|
-
const schedule =
|
|
159
|
-
const
|
|
160
|
-
|
|
179
|
+
const fields: Partial<{ schedule: string; prompt: string; always: boolean; scheduleType: ScheduleType; agent: string | null }> = {};
|
|
180
|
+
const schedule = args.getString("schedule");
|
|
181
|
+
const promptFile = args.getString("prompt-file");
|
|
182
|
+
let prompt = args.getString("prompt");
|
|
183
|
+
if (promptFile) {
|
|
184
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
185
|
+
if (!existsSync(promptFile)) fail(`File not found: ${promptFile}`);
|
|
186
|
+
prompt = readFileSync(promptFile, "utf8").trim();
|
|
187
|
+
}
|
|
188
|
+
const scheduleType = args.getString("type") as ScheduleType | undefined;
|
|
189
|
+
const always = args.getBool("always");
|
|
190
|
+
const agent = args.getString("agent");
|
|
191
|
+
const noAgent = args.getBool("agent");
|
|
192
|
+
|
|
193
|
+
if (schedule) fields.schedule = schedule;
|
|
194
|
+
if (prompt) fields.prompt = prompt;
|
|
195
|
+
if (scheduleType) {
|
|
196
|
+
if (!["cron", "interval", "once"].includes(scheduleType)) {
|
|
197
|
+
fail(`Invalid --type: "${scheduleType}". Must be cron, interval, or once.`);
|
|
198
|
+
}
|
|
199
|
+
fields.scheduleType = scheduleType;
|
|
200
|
+
}
|
|
201
|
+
if (always !== undefined) fields.always = always;
|
|
202
|
+
if (agent) fields.agent = agent;
|
|
203
|
+
if (noAgent === false) fields.agent = null;
|
|
204
|
+
|
|
205
|
+
if (Object.keys(fields).length === 0) {
|
|
206
|
+
fail("Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --agent).");
|
|
207
|
+
}
|
|
161
208
|
|
|
162
209
|
try {
|
|
163
210
|
await withDb(async () => {
|
|
164
|
-
const fields: Partial<{ schedule: string; prompt: string; always: boolean }> = {};
|
|
165
|
-
if (schedule) fields.schedule = schedule;
|
|
166
|
-
if (prompt) fields.prompt = prompt;
|
|
167
|
-
if (hasAlways) fields.always = true;
|
|
168
|
-
if (hasNoAlways) fields.always = false;
|
|
169
|
-
|
|
170
211
|
const updated = await Job.update(name, fields);
|
|
171
212
|
if (!updated) fail(`Job not found: "${name}". Use \`nia job list\` to see available jobs.`);
|
|
172
213
|
console.log(`Job "${name}" updated.`);
|
|
@@ -186,9 +227,9 @@ export async function jobCommand(): Promise<void> {
|
|
|
186
227
|
if (!job) fail(`Job not found: ${name}`);
|
|
187
228
|
|
|
188
229
|
console.log(` ${job.enabled ? "●" : "○"} ${job.name}`);
|
|
189
|
-
console.log(` schedule: ${job.schedule}`);
|
|
230
|
+
console.log(` schedule: ${job.schedule} (${job.scheduleType})`);
|
|
190
231
|
console.log(` enabled: ${job.enabled}`);
|
|
191
|
-
console.log(`
|
|
232
|
+
console.log(` always: ${job.always}`);
|
|
192
233
|
if (job.agent) console.log(` agent: ${job.agent}`);
|
|
193
234
|
console.log(` prompt: ${job.prompt}`);
|
|
194
235
|
|
|
@@ -313,20 +354,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
313
354
|
}
|
|
314
355
|
|
|
315
356
|
default:
|
|
316
|
-
console.
|
|
317
|
-
console.log(
|
|
318
|
-
|
|
319
|
-
console.log(" status [name] — quick status check");
|
|
320
|
-
console.log(" add <name> <schedule> <prompt> — add a job (active hours only)")
|
|
321
|
-
console.log(" --always — run 24/7 regardless of active hours");
|
|
322
|
-
console.log(" --agent <name> — assign an agent to the job");
|
|
323
|
-
console.log(" update <name> [--schedule s] [--prompt p] [--always] — update a job");
|
|
324
|
-
console.log(" remove <name> — delete a job");
|
|
325
|
-
console.log(" enable <name> — enable a job");
|
|
326
|
-
console.log(" disable <name> — disable a job");
|
|
327
|
-
console.log(" run <name> — run a job once");
|
|
328
|
-
console.log(" log [name] — show recent run history");
|
|
329
|
-
console.log(" import — import YAML jobs to DB");
|
|
330
|
-
process.exit(subcommand ? 1 : 0);
|
|
357
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
358
|
+
console.log(HELP);
|
|
359
|
+
process.exit(1);
|
|
331
360
|
}
|
|
332
361
|
}
|
package/src/cli/watch.ts
CHANGED
|
@@ -2,9 +2,23 @@ import { readRawConfig } from "../utils/config";
|
|
|
2
2
|
import { addWatchChannel, removeWatchChannel, enableWatchChannel, disableWatchChannel } from "../mcp/tools";
|
|
3
3
|
import { fail, ICON_PASS, ICON_FAIL } from "../utils/cli";
|
|
4
4
|
|
|
5
|
+
const HELP = `Usage: nia watch <command>
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
list List watch channels (default)
|
|
9
|
+
add <channel_id#name> <behavior> Add a watch channel
|
|
10
|
+
remove <channel_id#name> Remove a watch channel
|
|
11
|
+
enable <channel_id#name> Enable a watch channel
|
|
12
|
+
disable <channel_id#name> Disable a watch channel`;
|
|
13
|
+
|
|
5
14
|
export function watchCommand(): void {
|
|
6
15
|
const sub = process.argv[3];
|
|
7
16
|
|
|
17
|
+
if (sub === "--help" || sub === "-h" || sub === "help") {
|
|
18
|
+
console.log(HELP);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
switch (sub) {
|
|
9
23
|
case "list":
|
|
10
24
|
case undefined: {
|
|
@@ -60,12 +74,8 @@ export function watchCommand(): void {
|
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
default:
|
|
63
|
-
console.
|
|
64
|
-
console.log(
|
|
65
|
-
|
|
66
|
-
console.log(" remove <channel_id#name> — remove a watch channel");
|
|
67
|
-
console.log(" enable <channel_id#name> — enable a watch channel");
|
|
68
|
-
console.log(" disable <channel_id#name> — disable a watch channel");
|
|
69
|
-
process.exit(sub ? 1 : 0);
|
|
77
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
78
|
+
console.log(HELP);
|
|
79
|
+
process.exit(1);
|
|
70
80
|
}
|
|
71
81
|
}
|
package/src/core/scheduler.ts
CHANGED
|
@@ -65,6 +65,7 @@ function isWithinActiveHours(): boolean {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
68
|
+
const runningJobs = new Set<string>();
|
|
68
69
|
|
|
69
70
|
async function tick(): Promise<void> {
|
|
70
71
|
let dueJobs: Awaited<ReturnType<typeof Job.listDue>>;
|
|
@@ -87,12 +88,20 @@ async function tick(): Promise<void> {
|
|
|
87
88
|
continue;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
if (runningJobs.has(job.name)) {
|
|
92
|
+
log.info({ job: job.name }, "scheduler: skipping — still running from previous invocation");
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
log.info({ job: job.name, type: job.scheduleType }, "scheduler: running job");
|
|
97
|
+
runningJobs.add(job.name);
|
|
91
98
|
|
|
92
99
|
runJob(job).then((result) => {
|
|
93
100
|
log.info({ job: job.name, status: result.status, duration: result.duration_ms }, "scheduler: job completed");
|
|
94
101
|
}).catch((err) => {
|
|
95
102
|
log.error({ err, job: job.name }, "scheduler: job failed");
|
|
103
|
+
}).finally(() => {
|
|
104
|
+
runningJobs.delete(job.name);
|
|
96
105
|
});
|
|
97
106
|
|
|
98
107
|
let nextRun: Date | null = null;
|
package/src/utils/cli.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as readline from "readline";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
10
|
-
export const
|
|
11
|
-
export const
|
|
3
|
+
// TTY detection — disable colors/formatting when piped
|
|
4
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
5
|
+
|
|
6
|
+
// ANSI colors (empty strings when not a TTY)
|
|
7
|
+
export const DIM = isTTY ? "\x1b[2m" : "";
|
|
8
|
+
export const BOLD = isTTY ? "\x1b[1m" : "";
|
|
9
|
+
export const RESET = isTTY ? "\x1b[0m" : "";
|
|
10
|
+
export const RED = isTTY ? "\x1b[31m" : "";
|
|
11
|
+
export const GREEN = isTTY ? "\x1b[32m" : "";
|
|
12
|
+
export const YELLOW = isTTY ? "\x1b[33m" : "";
|
|
13
|
+
export const CYAN = isTTY ? "\x1b[36m" : "";
|
|
14
|
+
export const CLEAR_LINE = isTTY ? "\x1b[2K\r" : "";
|
|
12
15
|
|
|
13
16
|
// Icons
|
|
14
17
|
export const ICON_PASS = "\u2713";
|
|
@@ -19,11 +22,120 @@ export const ICON_RUNNING = "\u21bb";
|
|
|
19
22
|
// Spinner frames
|
|
20
23
|
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
21
24
|
|
|
25
|
+
/** Print error to stderr and exit with code 1. */
|
|
22
26
|
export function fail(msg: string): never {
|
|
23
|
-
console.
|
|
27
|
+
console.error(msg);
|
|
24
28
|
process.exit(1);
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Argument parser
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface ParsedArgs {
|
|
36
|
+
/** Positional arguments (everything that isn't a flag or flag value). */
|
|
37
|
+
positional: string[];
|
|
38
|
+
/** Named flags: --foo bar → { foo: "bar" }, --flag → { flag: true }, --no-flag → { flag: false }. */
|
|
39
|
+
flags: Record<string, string | boolean>;
|
|
40
|
+
/** Returns a flag value as string, or undefined. */
|
|
41
|
+
getString(name: string): string | undefined;
|
|
42
|
+
/** Returns a flag as boolean (true if --name, false if --no-name, undefined if absent). */
|
|
43
|
+
getBool(name: string): boolean | undefined;
|
|
44
|
+
/** Returns true if --help or -h is present. */
|
|
45
|
+
help: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse CLI arguments into positional args and named flags.
|
|
50
|
+
*
|
|
51
|
+
* Supports:
|
|
52
|
+
* - `--flag value` → { flag: "value" }
|
|
53
|
+
* - `--flag` (no value or next arg is a flag) → { flag: true }
|
|
54
|
+
* - `--no-flag` → { flag: false }
|
|
55
|
+
* - `-h` / `--help` → help: true
|
|
56
|
+
* - Positional args (anything not a flag or flag value)
|
|
57
|
+
*
|
|
58
|
+
* @param argv - Arguments to parse (default: process.argv.slice(3) for subcommands)
|
|
59
|
+
* @param boolFlags - Flag names that are always boolean (never consume the next arg as value)
|
|
60
|
+
*/
|
|
61
|
+
export function parseArgs(argv?: string[], boolFlags: string[] = []): ParsedArgs {
|
|
62
|
+
const args = argv ?? process.argv.slice(3);
|
|
63
|
+
const positional: string[] = [];
|
|
64
|
+
const flags: Record<string, string | boolean> = {};
|
|
65
|
+
const boolSet = new Set(boolFlags);
|
|
66
|
+
|
|
67
|
+
let i = 0;
|
|
68
|
+
while (i < args.length) {
|
|
69
|
+
const arg = args[i];
|
|
70
|
+
|
|
71
|
+
if (arg === "--") {
|
|
72
|
+
// Everything after -- is positional
|
|
73
|
+
positional.push(...args.slice(i + 1));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (arg === "-h" || arg === "--help") {
|
|
78
|
+
flags.help = true;
|
|
79
|
+
i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (arg.startsWith("--no-")) {
|
|
84
|
+
const name = arg.slice(5);
|
|
85
|
+
flags[name] = false;
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (arg.startsWith("--")) {
|
|
91
|
+
const name = arg.slice(2);
|
|
92
|
+
const next = args[i + 1];
|
|
93
|
+
|
|
94
|
+
if (boolSet.has(name) || !next || next.startsWith("-")) {
|
|
95
|
+
flags[name] = true;
|
|
96
|
+
i++;
|
|
97
|
+
} else {
|
|
98
|
+
flags[name] = next;
|
|
99
|
+
i += 2;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (arg.startsWith("-") && arg.length === 2) {
|
|
105
|
+
// Short flag like -c value
|
|
106
|
+
const name = arg.slice(1);
|
|
107
|
+
const next = args[i + 1];
|
|
108
|
+
if (!next || next.startsWith("-")) {
|
|
109
|
+
flags[name] = true;
|
|
110
|
+
i++;
|
|
111
|
+
} else {
|
|
112
|
+
flags[name] = next;
|
|
113
|
+
i += 2;
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
positional.push(arg);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
positional,
|
|
124
|
+
flags,
|
|
125
|
+
getString(name: string): string | undefined {
|
|
126
|
+
const v = flags[name];
|
|
127
|
+
return typeof v === "string" ? v : undefined;
|
|
128
|
+
},
|
|
129
|
+
getBool(name: string): boolean | undefined {
|
|
130
|
+
const v = flags[name];
|
|
131
|
+
return typeof v === "boolean" ? v : undefined;
|
|
132
|
+
},
|
|
133
|
+
get help() {
|
|
134
|
+
return flags.help === true;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
27
139
|
export function pickFromList(
|
|
28
140
|
rl: readline.Interface,
|
|
29
141
|
items: { name: string; label: string }[],
|