niahere 0.2.83 → 0.2.85
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 +15 -2
- package/package.json +1 -1
- package/src/chat/engine.ts +22 -2
- package/src/cli/active.ts +36 -0
- package/src/cli/index.ts +14 -0
- package/src/cli/model.ts +29 -0
- package/src/commands/validate.ts +23 -0
- package/src/core/finalizer.ts +35 -9
- package/src/types/config.ts +7 -0
- package/src/utils/config.ts +14 -0
package/README.md
CHANGED
|
@@ -43,8 +43,8 @@ nia start # starts daemon + registers OS service
|
|
|
43
43
|
- **Skills** — loads skills from multiple directories, invokable as slash commands
|
|
44
44
|
- **Cross-platform service** — launchd (macOS), systemd (Linux), service-aware restart
|
|
45
45
|
- **MCP tools** — 20 tools for job management, messaging, memory, rules, and channel control
|
|
46
|
-
- **Background memory consolidation** —
|
|
47
|
-
- **Session summaries** — handoff notes between sessions for continuity
|
|
46
|
+
- **Background memory consolidation** — stages memory candidates from conversations automatically
|
|
47
|
+
- **Session summaries** — optional handoff notes between sessions for continuity
|
|
48
48
|
- **Backups** — `nia backup` with auto-backup before updates
|
|
49
49
|
- **Optional integrations** — add Gmail, Discord, and more via skills
|
|
50
50
|
|
|
@@ -55,6 +55,8 @@ nia init — interactive setup (db, channels, persona, age
|
|
|
55
55
|
nia start / stop — daemon + OS service (launchd/systemd)
|
|
56
56
|
nia restart — restart daemon (service-aware)
|
|
57
57
|
nia status — show daemon, jobs, channels, chat rooms
|
|
58
|
+
nia active [--full] — show active engine count or details
|
|
59
|
+
nia model [name] — show or set global Claude model
|
|
58
60
|
nia health — check daemon, db, channels, config
|
|
59
61
|
nia chat [-c|-r] [--channel ch] — terminal chat (new by default, -c continue, -r pick)
|
|
60
62
|
nia run <prompt> — one-shot prompt execution
|
|
@@ -119,6 +121,17 @@ All config and data lives in `~/.niahere/`:
|
|
|
119
121
|
nia.pid, daemon.log, cron-state.json, cron-audit.jsonl
|
|
120
122
|
```
|
|
121
123
|
|
|
124
|
+
Post-session background LLM work can be disabled in `config.yaml`:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
session_finalization:
|
|
128
|
+
enabled: true
|
|
129
|
+
memory_consolidation: true
|
|
130
|
+
summaries: true
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Use `nia config set session_finalization.memory_consolidation false` to stop memory staging, `nia config set session_finalization.summaries false` to stop session summaries, or `nia config set session_finalization.enabled false` to disable both.
|
|
134
|
+
|
|
122
135
|
## Contributing
|
|
123
136
|
|
|
124
137
|
**Don't add features. Add skills.**
|
package/package.json
CHANGED
package/src/chat/engine.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
import { truncate, formatToolUse } from "../utils/format-activity";
|
|
23
23
|
import { finalizeSession, cancelPending } from "../core/finalizer";
|
|
24
24
|
import { log } from "../utils/log";
|
|
25
|
+
import { getConfig } from "../utils/config";
|
|
25
26
|
import { isRetryableApiError, sleep } from "../utils/retry";
|
|
26
27
|
import { registerActiveHandle, unregisterActiveHandle } from "../core/active-handles";
|
|
27
28
|
import { resolveJobPrompt } from "../core/job-prompt";
|
|
@@ -108,6 +109,11 @@ export function formatChatError(rawError: string | null | undefined): string {
|
|
|
108
109
|
return `[error] ${error}`;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
export function resolveSdkModel(contextModel?: string | null): string | undefined {
|
|
113
|
+
const model = contextModel || getConfig().model;
|
|
114
|
+
return model && model !== "default" ? model : undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
/**
|
|
112
118
|
* Push-based async iterable for streaming user messages to the SDK.
|
|
113
119
|
* Keeps the query subprocess alive between messages.
|
|
@@ -177,30 +183,40 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
177
183
|
|
|
178
184
|
// Context overrides: employee > agent > job > default
|
|
179
185
|
let cwd = homedir();
|
|
186
|
+
let contextModel: string | null | undefined;
|
|
180
187
|
if (opts.employee) {
|
|
181
188
|
const empPrompt = buildEmployeePrompt(opts.employee);
|
|
182
189
|
if (empPrompt) systemPrompt = empPrompt;
|
|
183
190
|
const emp = getEmployee(opts.employee);
|
|
191
|
+
contextModel = emp?.model;
|
|
184
192
|
if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
|
|
185
193
|
} else if (opts.agent) {
|
|
186
194
|
const agents = scanAgents();
|
|
187
195
|
const agentDef = agents.find((a) => a.name === opts.agent);
|
|
188
|
-
if (agentDef)
|
|
196
|
+
if (agentDef) {
|
|
197
|
+
systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
|
|
198
|
+
contextModel = agentDef.model;
|
|
199
|
+
}
|
|
189
200
|
} else if (opts.job) {
|
|
190
201
|
// Job chat: load job and use its context
|
|
191
202
|
const jobData = await Job.get(opts.job);
|
|
192
203
|
if (jobData) {
|
|
204
|
+
contextModel = jobData.model;
|
|
193
205
|
// If job has an employee, use employee prompt
|
|
194
206
|
if (jobData.employee) {
|
|
195
207
|
const empPrompt = buildEmployeePrompt(jobData.employee);
|
|
196
208
|
if (empPrompt) systemPrompt = empPrompt;
|
|
197
209
|
const emp = getEmployee(jobData.employee);
|
|
210
|
+
if (!contextModel) contextModel = emp?.model;
|
|
198
211
|
if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
|
|
199
212
|
} else if (jobData.agent) {
|
|
200
213
|
// If job has an agent, use agent prompt + context
|
|
201
214
|
const agents = scanAgents();
|
|
202
215
|
const agentDef = agents.find((a) => a.name === jobData.agent);
|
|
203
|
-
if (agentDef)
|
|
216
|
+
if (agentDef) {
|
|
217
|
+
systemPrompt = agentDef.body + "\n\n" + buildContextSuffix("chat");
|
|
218
|
+
if (!contextModel) contextModel = agentDef.model;
|
|
219
|
+
}
|
|
204
220
|
}
|
|
205
221
|
const resolvedPrompt = resolveJobPrompt(jobData);
|
|
206
222
|
const source = resolvedPrompt.source === "file" ? ` from ${resolvedPrompt.filePath}` : "";
|
|
@@ -316,6 +332,10 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
316
332
|
settingSources: ["project", "user"],
|
|
317
333
|
skills: [],
|
|
318
334
|
};
|
|
335
|
+
const model = resolveSdkModel(contextModel);
|
|
336
|
+
if (model) {
|
|
337
|
+
options.model = model;
|
|
338
|
+
}
|
|
319
339
|
|
|
320
340
|
if (sessionId) {
|
|
321
341
|
options.resume = sessionId;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ActiveEngine } from "../db/models";
|
|
2
|
+
import { withDb } from "../db/with-db";
|
|
3
|
+
import { errMsg } from "../utils/errors";
|
|
4
|
+
import { dateSortValue, formatTimeLine } from "../utils/format";
|
|
5
|
+
|
|
6
|
+
function hasFullFlag(argv: string[]): boolean {
|
|
7
|
+
return argv.includes("--full");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function activeCommand(argv: string[] = []): Promise<void> {
|
|
11
|
+
const full = hasFullFlag(argv);
|
|
12
|
+
const now = new Date();
|
|
13
|
+
let engines: Awaited<ReturnType<typeof ActiveEngine.list>> = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
await withDb(async () => {
|
|
17
|
+
engines = await ActiveEngine.list();
|
|
18
|
+
});
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error(`active engines unavailable: ${errMsg(err)}`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!full) {
|
|
26
|
+
console.log(String(engines.length));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`Active engines: ${engines.length === 0 ? "none" : engines.length}`);
|
|
31
|
+
for (const engine of engines.sort((a, b) => dateSortValue(a.startedAt) - dateSortValue(b.startedAt))) {
|
|
32
|
+
const started = formatTimeLine(engine.startedAt, now);
|
|
33
|
+
const ping = formatTimeLine(engine.lastPing, now);
|
|
34
|
+
console.log(` ${engine.room} (${engine.channel}) • started ${started} • last ping ${ping}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { errMsg } from "../utils/errors";
|
|
|
11
11
|
import { fail, ICON_PASS, ICON_WARN } from "../utils/cli";
|
|
12
12
|
import { jobCommand } from "./job";
|
|
13
13
|
import { statusCommand } from "./status";
|
|
14
|
+
import { activeCommand } from "./active";
|
|
15
|
+
import { modelCommand } from "./model";
|
|
14
16
|
import { sendCommand, telegramCommand, slackCommand } from "./channels";
|
|
15
17
|
import { rulesCommand, memoryCommand } from "./self";
|
|
16
18
|
import { watchCommand } from "./watch";
|
|
@@ -127,6 +129,16 @@ switch (command) {
|
|
|
127
129
|
break;
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
case "active": {
|
|
133
|
+
await activeCommand(process.argv.slice(3));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "model": {
|
|
138
|
+
await modelCommand(process.argv.slice(3));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
case "health": {
|
|
131
143
|
const { healthCommand } = await import("../commands/health");
|
|
132
144
|
await healthCommand();
|
|
@@ -565,6 +577,8 @@ Daemon:
|
|
|
565
577
|
restart [--wait N] [--force] Restart daemon
|
|
566
578
|
update [--wait N] [--force] Update to latest version
|
|
567
579
|
status [--json --rooms N --all] Show daemon, jobs, channels
|
|
580
|
+
active [--full] Show active engine count or details
|
|
581
|
+
model [name] Show or set global Claude model
|
|
568
582
|
health Check daemon, db, channels, config
|
|
569
583
|
logs [-f] [--channel ch] Daemon logs (filter by channel)
|
|
570
584
|
|
package/src/cli/model.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isRunning, readPid } from "../core/daemon";
|
|
2
|
+
import { getConfig, resetConfig, updateRawConfig } from "../utils/config";
|
|
3
|
+
|
|
4
|
+
function printUsage(): void {
|
|
5
|
+
console.log("Usage: nia model [default|sonnet|opus|opusplan|haiku|<model-id>]");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function modelCommand(argv: string[] = []): Promise<void> {
|
|
9
|
+
const model = argv[0];
|
|
10
|
+
|
|
11
|
+
if (!model) {
|
|
12
|
+
console.log(`model = ${getConfig().model}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (model === "--help" || model === "-h") {
|
|
17
|
+
printUsage();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
updateRawConfig({ model });
|
|
22
|
+
resetConfig();
|
|
23
|
+
console.log(`model = ${model}`);
|
|
24
|
+
|
|
25
|
+
const pid = readPid();
|
|
26
|
+
if (pid && isRunning()) {
|
|
27
|
+
process.kill(pid, "SIGHUP");
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -85,6 +85,29 @@ export function validateConfig(): Result {
|
|
|
85
85
|
messages.push(`${PASS} runner: ${runner}`);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Session finalization
|
|
89
|
+
const sf = raw.session_finalization as Record<string, unknown> | undefined;
|
|
90
|
+
if (sf) {
|
|
91
|
+
let sessionFinalizationOk = true;
|
|
92
|
+
for (const key of ["enabled", "memory_consolidation", "summaries"]) {
|
|
93
|
+
const val = sf[key];
|
|
94
|
+
if (val !== undefined && typeof val !== "boolean") {
|
|
95
|
+
messages.push(`${FAIL} session_finalization.${key} must be true or false`);
|
|
96
|
+
ok = false;
|
|
97
|
+
sessionFinalizationOk = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (sessionFinalizationOk) {
|
|
101
|
+
const enabled = sf.enabled !== false;
|
|
102
|
+
const memoryConsolidation = sf.memory_consolidation !== false;
|
|
103
|
+
const summaries = sf.summaries !== false;
|
|
104
|
+
messages.push(
|
|
105
|
+
`${PASS} session_finalization: ${enabled ? "enabled" : "disabled"} ` +
|
|
106
|
+
`(memory_consolidation=${memoryConsolidation}, summaries=${summaries})`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
// Channels
|
|
89
112
|
const ch = raw.channels as Record<string, unknown> | undefined;
|
|
90
113
|
if (ch) {
|
package/src/core/finalizer.ts
CHANGED
|
@@ -10,10 +10,25 @@
|
|
|
10
10
|
import { getSql } from "../db/connection";
|
|
11
11
|
import { consolidateSession } from "./consolidator";
|
|
12
12
|
import { summarizeSession } from "./summarizer";
|
|
13
|
+
import { loadConfig } from "../utils/config";
|
|
13
14
|
import { log } from "../utils/log";
|
|
14
15
|
|
|
16
|
+
type FinalizationTask = "consolidate" | "summarize";
|
|
17
|
+
|
|
18
|
+
function getEnabledTasks(): FinalizationTask[] {
|
|
19
|
+
const { sessionFinalization } = loadConfig();
|
|
20
|
+
if (!sessionFinalization.enabled) return [];
|
|
21
|
+
|
|
22
|
+
const tasks: FinalizationTask[] = [];
|
|
23
|
+
if (sessionFinalization.memoryConsolidation) tasks.push("consolidate");
|
|
24
|
+
if (sessionFinalization.summaries) tasks.push("summarize");
|
|
25
|
+
return tasks;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/** Enqueue a session for finalization. Always returns immediately. */
|
|
16
29
|
export async function finalizeSession(sessionId: string, room: string): Promise<void> {
|
|
30
|
+
if (getEnabledTasks().length === 0) return;
|
|
31
|
+
|
|
17
32
|
const sql = getSql();
|
|
18
33
|
|
|
19
34
|
// Get current message count for idempotency
|
|
@@ -77,19 +92,30 @@ async function processOne(sessionId: string, room: string, messageCount: number)
|
|
|
77
92
|
if (claimed.length === 0) return; // Already claimed or cancelled
|
|
78
93
|
|
|
79
94
|
const requestId = claimed[0].id;
|
|
95
|
+
const tasks = getEnabledTasks();
|
|
96
|
+
if (tasks.length === 0) {
|
|
97
|
+
await sql`
|
|
98
|
+
UPDATE finalization_requests
|
|
99
|
+
SET status = 'done', updated_at = NOW()
|
|
100
|
+
WHERE id = ${requestId}
|
|
101
|
+
`;
|
|
102
|
+
log.info({ sessionId, room, messageCount }, "finalizer: skipped because all tasks are disabled");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
80
105
|
|
|
81
106
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
const results = await Promise.allSettled(
|
|
108
|
+
tasks.map((task) =>
|
|
109
|
+
task === "consolidate" ? consolidateSession(sessionId, room) : summarizeSession(sessionId, room),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
86
112
|
|
|
87
113
|
const errors: string[] = [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
for (let i = 0; i < results.length; i++) {
|
|
115
|
+
const result = results[i];
|
|
116
|
+
if (result.status === "rejected") {
|
|
117
|
+
errors.push(`${tasks[i]}: ${formatRejection(result.reason)}`);
|
|
118
|
+
}
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
const finalStatus = errors.length === 0 ? "done" : "failed";
|
package/src/types/config.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface ChannelsConfig {
|
|
|
35
35
|
slack: SlackConfig;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface SessionFinalizationConfig {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
memoryConsolidation: boolean;
|
|
41
|
+
summaries: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
export interface Config {
|
|
39
45
|
model: string;
|
|
40
46
|
runner: "claude" | "codex";
|
|
@@ -43,5 +49,6 @@ export interface Config {
|
|
|
43
49
|
database_url: string;
|
|
44
50
|
log_level: string;
|
|
45
51
|
gemini_api_key: string | null;
|
|
52
|
+
sessionFinalization: SessionFinalizationConfig;
|
|
46
53
|
channels: ChannelsConfig;
|
|
47
54
|
}
|
package/src/utils/config.ts
CHANGED
|
@@ -16,6 +16,11 @@ const DEFAULTS: Config = {
|
|
|
16
16
|
database_url: DEFAULT_DATABASE_URL,
|
|
17
17
|
log_level: "info",
|
|
18
18
|
gemini_api_key: null,
|
|
19
|
+
sessionFinalization: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
memoryConsolidation: true,
|
|
22
|
+
summaries: true,
|
|
23
|
+
},
|
|
19
24
|
channels: {
|
|
20
25
|
enabled: true,
|
|
21
26
|
default: "telegram",
|
|
@@ -100,6 +105,14 @@ export function loadConfig(): Config {
|
|
|
100
105
|
const gemini_api_key =
|
|
101
106
|
process.env.GEMINI_API_KEY || (typeof raw.gemini_api_key === "string" ? raw.gemini_api_key : null);
|
|
102
107
|
|
|
108
|
+
// Session finalization — controls post-session background LLM work.
|
|
109
|
+
const sf = (raw.session_finalization || {}) as Record<string, unknown>;
|
|
110
|
+
const sessionFinalization = {
|
|
111
|
+
enabled: sf.enabled !== false,
|
|
112
|
+
memoryConsolidation: sf.memory_consolidation !== false,
|
|
113
|
+
summaries: sf.summaries !== false,
|
|
114
|
+
};
|
|
115
|
+
|
|
103
116
|
// --- Channels (nested under `channels:` in yaml) ---
|
|
104
117
|
const ch = (raw.channels || {}) as Record<string, unknown>;
|
|
105
118
|
const chTg = (ch.telegram || {}) as Record<string, unknown>;
|
|
@@ -159,6 +172,7 @@ export function loadConfig(): Config {
|
|
|
159
172
|
database_url,
|
|
160
173
|
log_level,
|
|
161
174
|
gemini_api_key,
|
|
175
|
+
sessionFinalization,
|
|
162
176
|
channels: {
|
|
163
177
|
enabled: channelsEnabled,
|
|
164
178
|
default: defaultChannel,
|