niahere 0.2.63 → 0.2.65
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/src/chat/employee-prompt.ts +75 -0
- package/src/chat/engine.ts +40 -7
- package/src/chat/identity.ts +8 -1
- package/src/chat/repl.ts +52 -25
- package/src/cli/employee-add.ts +124 -0
- package/src/cli/employee.ts +167 -0
- package/src/cli/index.ts +21 -5
- package/src/cli/job.ts +13 -3
- package/src/cli/status.ts +2 -1
- package/src/commands/backup.ts +3 -7
- package/src/core/agents.ts +2 -1
- package/src/core/consolidator.ts +15 -6
- package/src/core/employees.ts +116 -0
- package/src/core/runner.ts +15 -3
- package/src/core/skills.ts +2 -1
- package/src/db/migrations/015_jobs_employee.ts +7 -0
- package/src/db/models/job.ts +13 -8
- package/src/mcp/server.ts +14 -1
- package/src/mcp/tools.ts +13 -2
- package/src/types/employee.ts +14 -0
- package/src/types/engine.ts +9 -2
- package/src/types/index.ts +1 -0
- package/src/types/job.ts +1 -0
- package/src/types/paths.ts +1 -0
- package/src/utils/paths.ts +1 -0
package/src/cli/job.ts
CHANGED
|
@@ -23,6 +23,7 @@ Commands:
|
|
|
23
23
|
--type cron|interval|once Schedule type (default: cron)
|
|
24
24
|
--always Run 24/7 regardless of active hours
|
|
25
25
|
--agent <name> Assign an agent to the job
|
|
26
|
+
--employee <name> Assign an employee to the job
|
|
26
27
|
--model <model> Model override (e.g. haiku, sonnet, opus)
|
|
27
28
|
--stateless yes|no Disable working memory for this job
|
|
28
29
|
update <name> Update a job
|
|
@@ -32,6 +33,7 @@ Commands:
|
|
|
32
33
|
--type cron|interval|once Change schedule type
|
|
33
34
|
--always / --no-always Toggle 24/7 mode
|
|
34
35
|
--agent <name> Assign agent (--no-agent to remove)
|
|
36
|
+
--employee <name> Assign employee (--no-employee to remove)
|
|
35
37
|
--model <model> Model override (--no-model to remove)
|
|
36
38
|
--stateless yes|no Toggle working memory
|
|
37
39
|
remove <name> Delete a job
|
|
@@ -96,7 +98,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
96
98
|
const tag = job.always ? " always" : "";
|
|
97
99
|
const type = job.scheduleType !== "cron" ? ` (${job.scheduleType})` : "";
|
|
98
100
|
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
99
|
-
|
|
101
|
+
const empTag = job.employee ? ` [emp:${job.employee}]` : "";
|
|
102
|
+
console.log(` ${job.enabled ? "●" : "○"} ${job.name} ${job.schedule}${type}${tag}${agentTag}${empTag}`);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
});
|
|
@@ -122,6 +125,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
122
125
|
const statelessRaw = args.getString("stateless");
|
|
123
126
|
const stateless = statelessRaw ? ["yes", "y", "true", "t", "1"].includes(statelessRaw.toLowerCase()) : false;
|
|
124
127
|
const agent = args.getString("agent");
|
|
128
|
+
const employee = args.getString("employee");
|
|
125
129
|
const model = args.getString("model");
|
|
126
130
|
|
|
127
131
|
const [name, schedule, ...promptParts] = args.positional;
|
|
@@ -149,7 +153,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
149
153
|
const config = getConfig();
|
|
150
154
|
const nextRunAt = computeInitialNextRun(scheduleType, schedule, config.timezone);
|
|
151
155
|
await withDb(async () => {
|
|
152
|
-
await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent, stateless, model);
|
|
156
|
+
await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent, stateless, model, employee);
|
|
153
157
|
console.log(`Job "${name}" added (${scheduleType}: ${schedule}).${always ? " (runs 24/7)" : ""}`);
|
|
154
158
|
});
|
|
155
159
|
} catch (err) {
|
|
@@ -213,6 +217,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
213
217
|
model: string | null;
|
|
214
218
|
scheduleType: ScheduleType;
|
|
215
219
|
agent: string | null;
|
|
220
|
+
employee: string | null;
|
|
216
221
|
}> = {};
|
|
217
222
|
const schedule = args.getString("schedule");
|
|
218
223
|
const promptFile = args.getString("prompt-file");
|
|
@@ -227,6 +232,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
227
232
|
const statelessRaw = args.getString("stateless");
|
|
228
233
|
const agent = args.getString("agent");
|
|
229
234
|
const noAgent = args.getBool("agent");
|
|
235
|
+
const employeeFlag = args.getString("employee");
|
|
236
|
+
const noEmployee = args.getBool("employee");
|
|
230
237
|
|
|
231
238
|
if (schedule) fields.schedule = schedule;
|
|
232
239
|
if (prompt) fields.prompt = prompt;
|
|
@@ -240,6 +247,8 @@ export async function jobCommand(): Promise<void> {
|
|
|
240
247
|
if (statelessRaw) fields.stateless = ["yes", "y", "true", "t", "1"].includes(statelessRaw.toLowerCase());
|
|
241
248
|
if (agent) fields.agent = agent;
|
|
242
249
|
if (noAgent === false) fields.agent = null;
|
|
250
|
+
if (employeeFlag) fields.employee = employeeFlag;
|
|
251
|
+
if (noEmployee === false) fields.employee = null;
|
|
243
252
|
const modelFlag = args.getString("model");
|
|
244
253
|
const noModel = args.getBool("model");
|
|
245
254
|
if (modelFlag) fields.model = modelFlag;
|
|
@@ -247,7 +256,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
247
256
|
|
|
248
257
|
if (Object.keys(fields).length === 0) {
|
|
249
258
|
fail(
|
|
250
|
-
"Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --stateless, --model, --agent).",
|
|
259
|
+
"Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --stateless, --model, --agent, --employee).",
|
|
251
260
|
);
|
|
252
261
|
}
|
|
253
262
|
|
|
@@ -276,6 +285,7 @@ export async function jobCommand(): Promise<void> {
|
|
|
276
285
|
console.log(` enabled: ${job.enabled}`);
|
|
277
286
|
console.log(` always: ${job.always}`);
|
|
278
287
|
if (job.agent) console.log(` agent: ${job.agent}`);
|
|
288
|
+
if (job.employee) console.log(` employee: ${job.employee}`);
|
|
279
289
|
if (job.model) console.log(` model: ${job.model}`);
|
|
280
290
|
if (job.stateless) console.log(` stateless: true`);
|
|
281
291
|
console.log(` prompt: ${job.prompt}`);
|
package/src/cli/status.ts
CHANGED
|
@@ -261,8 +261,9 @@ export async function statusCommand(argv: string[] = []): Promise<void> {
|
|
|
261
261
|
const staleText = stale ? " ⚠ stale" : "";
|
|
262
262
|
|
|
263
263
|
const agentTag = job.agent ? ` [${job.agent}]` : "";
|
|
264
|
+
const empTag = job.employee ? ` [emp:${job.employee}]` : "";
|
|
264
265
|
console.log(
|
|
265
|
-
` ${job.enabled ? "\u25cf" : "\u25cb"} ${job.name.padEnd(20)} ${job.enabled ? "enabled" : "disabled"}${agentTag}`,
|
|
266
|
+
` ${job.enabled ? "\u25cf" : "\u25cb"} ${job.name.padEnd(20)} ${job.enabled ? "enabled" : "disabled"}${agentTag}${empTag}`,
|
|
266
267
|
);
|
|
267
268
|
console.log(
|
|
268
269
|
` ${statusIcon} ${status} last: ${lastText} next: ${nextText} duration: ${durationText}${staleText}`,
|
package/src/commands/backup.ts
CHANGED
|
@@ -49,6 +49,7 @@ export async function createBackup(silent = false): Promise<string> {
|
|
|
49
49
|
if (existsSync(join(home, "self"))) includes.push("self");
|
|
50
50
|
if (existsSync(join(home, "agents"))) includes.push("agents");
|
|
51
51
|
if (existsSync(join(home, "skills"))) includes.push("skills");
|
|
52
|
+
if (existsSync(join(home, "employees"))) includes.push("employees");
|
|
52
53
|
|
|
53
54
|
// Database dump
|
|
54
55
|
const config = getConfig();
|
|
@@ -65,10 +66,7 @@ export async function createBackup(silent = false): Promise<string> {
|
|
|
65
66
|
if (url.port) pgArgs.push("-p", url.port);
|
|
66
67
|
if (url.username) pgArgs.push("-U", decodeURIComponent(url.username));
|
|
67
68
|
if (dbName) pgArgs.push("-d", dbName);
|
|
68
|
-
const pgEnv: Record<string, string> = { ...process.env } as Record<
|
|
69
|
-
string,
|
|
70
|
-
string
|
|
71
|
-
>;
|
|
69
|
+
const pgEnv: Record<string, string> = { ...process.env } as Record<string, string>;
|
|
72
70
|
if (url.password) pgEnv.PGPASSWORD = decodeURIComponent(url.password);
|
|
73
71
|
const sslmode = url.searchParams.get("sslmode");
|
|
74
72
|
if (sslmode) pgEnv.PGSSLMODE = sslmode;
|
|
@@ -87,9 +85,7 @@ export async function createBackup(silent = false): Promise<string> {
|
|
|
87
85
|
dbDumped = true;
|
|
88
86
|
} else if (!silent) {
|
|
89
87
|
const stderr = await new Response(pg.stderr).text();
|
|
90
|
-
console.log(
|
|
91
|
-
` ⚠ db dump skipped: ${stderr.trim() || `exit ${exitCode}`}`,
|
|
92
|
-
);
|
|
88
|
+
console.log(` ⚠ db dump skipped: ${stderr.trim() || `exit ${exitCode}`}`);
|
|
93
89
|
}
|
|
94
90
|
}
|
|
95
91
|
|
package/src/core/agents.ts
CHANGED
|
@@ -34,7 +34,8 @@ export function scanAgents(): AgentInfo[] {
|
|
|
34
34
|
for (const { dir, source } of getAgentDirs()) {
|
|
35
35
|
if (!existsSync(dir)) continue;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
for (const entry of entries) {
|
|
38
39
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
39
40
|
|
|
40
41
|
const agentFile = join(dir, entry.name, "AGENT.md");
|
package/src/core/consolidator.ts
CHANGED
|
@@ -58,8 +58,9 @@ Nia uses a two-stage memory architecture. You are stage 1.
|
|
|
58
58
|
|
|
59
59
|
Your persona includes guidance to "save proactively" — that guidance applies
|
|
60
60
|
to LIVE chat, where you act on immediate user instruction. In THIS
|
|
61
|
-
consolidation pass,
|
|
62
|
-
|
|
61
|
+
consolidation pass, be selective but not paralyzed. If you see a genuine
|
|
62
|
+
learning, stage it. The promoter handles quality gating — your job is to
|
|
63
|
+
not miss real signals, not to be maximally conservative.
|
|
63
64
|
|
|
64
65
|
## Transcript
|
|
65
66
|
|
|
@@ -81,13 +82,20 @@ Answer these questions silently. If the answer to all of them is "nothing",
|
|
|
81
82
|
stop here and do not write anything.
|
|
82
83
|
|
|
83
84
|
1. What did the user correct, clarify, or teach you in this session?
|
|
85
|
+
(Includes: "no, do it this way", "don't use X", "always check Y first")
|
|
84
86
|
2. What NEW fact about the user, their projects, or their systems do you
|
|
85
87
|
now know that you did not at session start?
|
|
88
|
+
(Includes: architecture decisions, workflow patterns, tool preferences,
|
|
89
|
+
team structure, external system details discovered during task execution)
|
|
86
90
|
3. What decision was made that will constrain future work?
|
|
91
|
+
(Includes: "we're using X not Y", config changes, deployment patterns)
|
|
92
|
+
4. What did the user explicitly ask to be remembered?
|
|
87
93
|
|
|
88
|
-
Trivial small talk, greetings,
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
Trivial small talk, greetings, and pure status updates are NOT answers.
|
|
95
|
+
But corrections made DURING task execution ("no, check DynamoDB not S3"),
|
|
96
|
+
architecture learned while debugging ("ah, this service talks to X via Y"),
|
|
97
|
+
and workflow patterns revealed by how the user works — these ARE answers.
|
|
98
|
+
The bar is: would a fresh Nia session benefit from knowing this?
|
|
91
99
|
|
|
92
100
|
## Step 3 — Update staging.md
|
|
93
101
|
|
|
@@ -118,7 +126,8 @@ For each substantive answer:
|
|
|
118
126
|
- Do NOT write to \`memory.md\` or \`rules.md\`. Only the promoter job can.
|
|
119
127
|
- Do NOT use \`add_memory\` or \`add_rule\` MCP tools. Edit staging.md directly.
|
|
120
128
|
- Do NOT message the user.
|
|
121
|
-
-
|
|
129
|
+
- If nothing qualifies, do nothing. But don't be so conservative that the
|
|
130
|
+
pipeline starves — if you're skipping every session, your bar is too high.
|
|
122
131
|
|
|
123
132
|
Report a one-line summary of what you did: "staged N new / reinforced M /
|
|
124
133
|
skipped (trivial session)". No preamble.`;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { getNiaHome } from "../utils/paths";
|
|
5
|
+
import { log } from "../utils/log";
|
|
6
|
+
import type { EmployeeInfo } from "../types/employee";
|
|
7
|
+
|
|
8
|
+
function getEmployeesDir(): string {
|
|
9
|
+
return join(getNiaHome(), "employees");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function scanEmployees(): EmployeeInfo[] {
|
|
13
|
+
const employees: EmployeeInfo[] = [];
|
|
14
|
+
const dir = getEmployeesDir();
|
|
15
|
+
if (!existsSync(dir)) return employees;
|
|
16
|
+
|
|
17
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
21
|
+
|
|
22
|
+
const empFile = join(dir, entry.name, "EMPLOYEE.md");
|
|
23
|
+
if (!existsSync(empFile)) continue;
|
|
24
|
+
|
|
25
|
+
const content = readFileSync(empFile, "utf8");
|
|
26
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
27
|
+
if (!fmMatch) continue;
|
|
28
|
+
|
|
29
|
+
let meta: Record<string, unknown> = {};
|
|
30
|
+
try {
|
|
31
|
+
meta = (yaml.load(fmMatch[1]) as Record<string, unknown>) || {};
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log.warn({ err, employee: entry.name, path: empFile }, "failed to parse employee metadata, skipping");
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const name = (typeof meta.name === "string" ? meta.name : "") || entry.name;
|
|
38
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
|
|
39
|
+
|
|
40
|
+
employees.push({
|
|
41
|
+
name,
|
|
42
|
+
dirName: entry.name,
|
|
43
|
+
project: typeof meta.project === "string" ? meta.project : "",
|
|
44
|
+
repo: typeof meta.repo === "string" ? meta.repo : "",
|
|
45
|
+
role: typeof meta.role === "string" ? meta.role : "Employee",
|
|
46
|
+
model: typeof meta.model === "string" ? meta.model : undefined,
|
|
47
|
+
status:
|
|
48
|
+
meta.status === "onboarding" || meta.status === "active" || meta.status === "paused"
|
|
49
|
+
? meta.status
|
|
50
|
+
: "onboarding",
|
|
51
|
+
maxSubEmployees: typeof meta.maxSubEmployees === "number" ? meta.maxSubEmployees : 3,
|
|
52
|
+
body,
|
|
53
|
+
created: typeof meta.created === "string" ? meta.created : new Date().toISOString().slice(0, 10),
|
|
54
|
+
parent: typeof meta.parent === "string" ? meta.parent : undefined,
|
|
55
|
+
source: "nia",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return employees;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getEmployee(name: string): EmployeeInfo | undefined {
|
|
63
|
+
return scanEmployees().find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getEmployeeDir(name: string): string {
|
|
67
|
+
// Look up actual directory — name in frontmatter may differ from dir name
|
|
68
|
+
const emp = scanEmployees().find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
69
|
+
if (emp) return join(getEmployeesDir(), emp.dirName);
|
|
70
|
+
// Fallback for new employees being created (not yet on disk)
|
|
71
|
+
return join(getEmployeesDir(), name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getEmployeesSummary(): string {
|
|
75
|
+
const employees = scanEmployees();
|
|
76
|
+
if (employees.length === 0) return "";
|
|
77
|
+
const lines = employees.map((e) => `- @${e.name}: ${e.role} — ${e.project || "(no project)"} [${e.status}]`);
|
|
78
|
+
return `Available employees:\n${lines.join("\n")}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function listEmployeesForMcp(): string {
|
|
82
|
+
const employees = scanEmployees();
|
|
83
|
+
if (employees.length === 0) return "No employees found.";
|
|
84
|
+
return JSON.stringify(
|
|
85
|
+
employees.map((e) => ({
|
|
86
|
+
name: e.name,
|
|
87
|
+
role: e.role,
|
|
88
|
+
project: e.project,
|
|
89
|
+
repo: e.repo,
|
|
90
|
+
status: e.status,
|
|
91
|
+
model: e.model,
|
|
92
|
+
})),
|
|
93
|
+
null,
|
|
94
|
+
2,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Injected into employee prompt only when status=onboarding. */
|
|
99
|
+
export const ONBOARDING_INSTRUCTIONS = `## Onboarding
|
|
100
|
+
|
|
101
|
+
You are in onboarding status. Be proactive — don't wait for the user to drive. You're a co-founder getting up to speed, not an assistant being briefed.
|
|
102
|
+
|
|
103
|
+
IMPORTANT: One thing at a time. Each message should focus on ONE step. Don't dump all steps on the user at once. Move to the next step only after the current one is resolved.
|
|
104
|
+
|
|
105
|
+
During the brief, don't just record — challenge. Ask follow-up questions. If the vision sounds vague, say so. If the goals are unrealistic, push back. If something sounds like it matters more than what the user is focused on, flag it.
|
|
106
|
+
|
|
107
|
+
### Steps (do these in order, one per message)
|
|
108
|
+
1. **Identity** — If your name is a placeholder (starts with "new-employee"), suggest 3-4 real names and ask the user to pick. Update the name field in your EMPLOYEE.md frontmatter. Do NOT rename the directory — the system resolves it from frontmatter.
|
|
109
|
+
2. **Project & Repo** — If project or repo are empty, ask what you'll be working on. Get the repo path. Update your EMPLOYEE.md.
|
|
110
|
+
3. **Brief** — Ask the user about the project: goals, what's working, what's not, their vision. Save to onboarding/brief.md.
|
|
111
|
+
4. **Self-Discovery** — Explore the repo autonomously. Read code, README, recent commits, deployment config. Save findings to onboarding/discovery.md. Report back to user for corrections.
|
|
112
|
+
5. **Initial Plan** — Propose top 3-5 priorities with first actions for each. Save to onboarding/plan.md. Get user approval.
|
|
113
|
+
|
|
114
|
+
After all steps are done, update your EMPLOYEE.md status from "onboarding" to "active".
|
|
115
|
+
|
|
116
|
+
Skip any step where the info is already filled in.`;
|
package/src/core/runner.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { appendAudit, readState, writeState } from "../utils/logger";
|
|
|
8
8
|
import type { AuditEntry, JobState } from "../types";
|
|
9
9
|
import { getConfig } from "../utils/config";
|
|
10
10
|
import { buildSystemPrompt } from "../chat/identity";
|
|
11
|
+
import { buildEmployeePrompt } from "../chat/employee-prompt";
|
|
12
|
+
import { getEmployee } from "./employees";
|
|
11
13
|
import { scanAgents } from "./agents";
|
|
12
14
|
import { truncate, formatToolUse } from "../utils/format-activity";
|
|
13
15
|
import { getMcpServers } from "../mcp";
|
|
@@ -300,13 +302,23 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
|
|
|
300
302
|
writeState(state);
|
|
301
303
|
|
|
302
304
|
try {
|
|
303
|
-
|
|
305
|
+
let cwd = homedir();
|
|
304
306
|
let output: RunnerOutput;
|
|
305
307
|
|
|
306
|
-
// Resolve system prompt:
|
|
308
|
+
// Resolve system prompt: employee > agent > default
|
|
307
309
|
let systemPrompt: string;
|
|
308
310
|
let agentModel: string | undefined;
|
|
309
|
-
if (job.
|
|
311
|
+
if (job.employee) {
|
|
312
|
+
const empPrompt = buildEmployeePrompt(job.employee);
|
|
313
|
+
if (empPrompt) {
|
|
314
|
+
systemPrompt = empPrompt;
|
|
315
|
+
} else {
|
|
316
|
+
systemPrompt = buildSystemPrompt("job");
|
|
317
|
+
}
|
|
318
|
+
const emp = getEmployee(job.employee);
|
|
319
|
+
if (emp?.model) agentModel = emp.model;
|
|
320
|
+
if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
|
|
321
|
+
} else if (job.agent) {
|
|
310
322
|
const agents = scanAgents();
|
|
311
323
|
const agentDef = agents.find((a) => a.name === job.agent);
|
|
312
324
|
if (agentDef) {
|
package/src/core/skills.ts
CHANGED
|
@@ -26,7 +26,8 @@ export function scanSkills(): SkillInfo[] {
|
|
|
26
26
|
for (const { dir, source } of SKILL_DIRS) {
|
|
27
27
|
if (!existsSync(dir)) continue;
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
30
|
+
for (const entry of entries) {
|
|
30
31
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
31
32
|
|
|
32
33
|
const skillFile = join(dir, entry.name, "SKILL.md");
|
package/src/db/models/job.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface Job {
|
|
|
40
40
|
always: boolean;
|
|
41
41
|
scheduleType: ScheduleType;
|
|
42
42
|
agent: string | null;
|
|
43
|
+
employee: string | null;
|
|
43
44
|
model: string | null;
|
|
44
45
|
stateless: boolean;
|
|
45
46
|
nextRunAt: string | null;
|
|
@@ -57,6 +58,7 @@ function toJob(r: Record<string, any>): Job {
|
|
|
57
58
|
always: r.always ?? false,
|
|
58
59
|
scheduleType: r.schedule_type || "cron",
|
|
59
60
|
agent: r.agent || null,
|
|
61
|
+
employee: r.employee || null,
|
|
60
62
|
model: r.model || null,
|
|
61
63
|
stateless: r.stateless ?? false,
|
|
62
64
|
nextRunAt: r.next_run_at ? String(r.next_run_at) : null,
|
|
@@ -81,6 +83,7 @@ export async function create(
|
|
|
81
83
|
agent?: string,
|
|
82
84
|
stateless = false,
|
|
83
85
|
model?: string,
|
|
86
|
+
employee?: string,
|
|
84
87
|
): Promise<void> {
|
|
85
88
|
validateSchedule(schedule, scheduleType);
|
|
86
89
|
const existing = await get(name);
|
|
@@ -89,8 +92,8 @@ export async function create(
|
|
|
89
92
|
}
|
|
90
93
|
const sql = getSql();
|
|
91
94
|
await sql`
|
|
92
|
-
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model)
|
|
93
|
-
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null})
|
|
95
|
+
INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless, model, employee)
|
|
96
|
+
VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless}, ${model ?? null}, ${employee ?? null})
|
|
94
97
|
`;
|
|
95
98
|
await notifyChange();
|
|
96
99
|
}
|
|
@@ -98,14 +101,14 @@ export async function create(
|
|
|
98
101
|
export async function list(): Promise<Job[]> {
|
|
99
102
|
const sql = getSql();
|
|
100
103
|
const rows =
|
|
101
|
-
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
|
|
104
|
+
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
|
|
102
105
|
return rows.map(toJob);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
export async function get(name: string): Promise<Job | null> {
|
|
106
109
|
const sql = getSql();
|
|
107
110
|
const rows =
|
|
108
|
-
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
|
|
111
|
+
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
|
|
109
112
|
return rows.length > 0 ? toJob(rows[0]) : null;
|
|
110
113
|
}
|
|
111
114
|
|
|
@@ -117,6 +120,7 @@ export async function update(
|
|
|
117
120
|
enabled: boolean;
|
|
118
121
|
always: boolean;
|
|
119
122
|
agent: string | null;
|
|
123
|
+
employee: string | null;
|
|
120
124
|
model: string | null;
|
|
121
125
|
stateless: boolean;
|
|
122
126
|
scheduleType: ScheduleType;
|
|
@@ -132,6 +136,7 @@ export async function update(
|
|
|
132
136
|
const enabled = fields.enabled ?? existing.enabled;
|
|
133
137
|
const always = fields.always ?? existing.always;
|
|
134
138
|
const agent = fields.agent !== undefined ? fields.agent : existing.agent;
|
|
139
|
+
const employee = fields.employee !== undefined ? fields.employee : existing.employee;
|
|
135
140
|
const model = fields.model !== undefined ? fields.model : existing.model;
|
|
136
141
|
const stateless = fields.stateless ?? existing.stateless;
|
|
137
142
|
|
|
@@ -144,13 +149,13 @@ export async function update(
|
|
|
144
149
|
const nextRun = computeInitialNextRun(scheduleType, schedule, getConfig().timezone);
|
|
145
150
|
await sql`
|
|
146
151
|
UPDATE jobs
|
|
147
|
-
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
|
|
152
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, next_run_at = ${nextRun}, updated_at = NOW()
|
|
148
153
|
WHERE name = ${name}
|
|
149
154
|
`;
|
|
150
155
|
} else {
|
|
151
156
|
await sql`
|
|
152
157
|
UPDATE jobs
|
|
153
|
-
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
|
|
158
|
+
SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, employee = ${employee}, model = ${model}, stateless = ${stateless}, updated_at = NOW()
|
|
154
159
|
WHERE name = ${name}
|
|
155
160
|
`;
|
|
156
161
|
}
|
|
@@ -168,14 +173,14 @@ export async function remove(name: string): Promise<boolean> {
|
|
|
168
173
|
export async function listEnabled(): Promise<Job[]> {
|
|
169
174
|
const sql = getSql();
|
|
170
175
|
const rows =
|
|
171
|
-
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
|
|
176
|
+
await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
|
|
172
177
|
return rows.map(toJob);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
export async function listDue(): Promise<Job[]> {
|
|
176
181
|
const sql = getSql();
|
|
177
182
|
const rows = await sql`
|
|
178
|
-
SELECT name, schedule, prompt, enabled, always, schedule_type, agent, model, stateless, next_run_at, last_run_at, created_at, updated_at
|
|
183
|
+
SELECT name, schedule, prompt, enabled, always, schedule_type, agent, employee, model, stateless, next_run_at, last_run_at, created_at, updated_at
|
|
179
184
|
FROM jobs
|
|
180
185
|
WHERE enabled = TRUE AND next_run_at <= NOW()
|
|
181
186
|
ORDER BY next_run_at
|
package/src/mcp/server.ts
CHANGED
|
@@ -23,6 +23,10 @@ export function createNiaMcpServer() {
|
|
|
23
23
|
.string()
|
|
24
24
|
.optional()
|
|
25
25
|
.describe("Agent name to use for this job (loads agent's AGENT.md as system prompt)"),
|
|
26
|
+
employee: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Employee name to use for this job (loads employee identity, runs in employee's repo)"),
|
|
26
30
|
stateless: z
|
|
27
31
|
.boolean()
|
|
28
32
|
.default(false)
|
|
@@ -38,7 +42,7 @@ export function createNiaMcpServer() {
|
|
|
38
42
|
),
|
|
39
43
|
tool(
|
|
40
44
|
"update_job",
|
|
41
|
-
"Update an existing job's schedule, prompt, always flag, agent, model, stateless, or schedule_type. Only pass fields you want to change.",
|
|
45
|
+
"Update an existing job's schedule, prompt, always flag, agent, employee, model, stateless, or schedule_type. Only pass fields you want to change.",
|
|
42
46
|
{
|
|
43
47
|
name: z.string().describe("Job name to update"),
|
|
44
48
|
schedule: z
|
|
@@ -48,6 +52,7 @@ export function createNiaMcpServer() {
|
|
|
48
52
|
prompt: z.string().optional().describe("New prompt"),
|
|
49
53
|
always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
|
|
50
54
|
agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
|
|
55
|
+
employee: z.string().nullable().optional().describe("Employee name (set null to remove employee)"),
|
|
51
56
|
model: z.string().nullable().optional().describe("Model override (set null to remove and use default)"),
|
|
52
57
|
stateless: z
|
|
53
58
|
.boolean()
|
|
@@ -306,6 +311,14 @@ export function createNiaMcpServer() {
|
|
|
306
311
|
content: [{ type: "text" as const, text: handlers.listAgents() }],
|
|
307
312
|
}),
|
|
308
313
|
),
|
|
314
|
+
tool(
|
|
315
|
+
"list_employees",
|
|
316
|
+
"List all employees with their role, project, status, and model. Employees are persistent co-founders/team members scoped to projects.",
|
|
317
|
+
{},
|
|
318
|
+
async () => ({
|
|
319
|
+
content: [{ type: "text" as const, text: handlers.listEmployees() }],
|
|
320
|
+
}),
|
|
321
|
+
),
|
|
309
322
|
],
|
|
310
323
|
});
|
|
311
324
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { getChannel } from "../channels/registry";
|
|
|
9
9
|
import { log } from "../utils/log";
|
|
10
10
|
import { classifyMime } from "../utils/attachment";
|
|
11
11
|
import { scanAgents } from "../core/agents";
|
|
12
|
+
import { listEmployeesForMcp } from "../core/employees";
|
|
12
13
|
|
|
13
14
|
export async function listJobs(): Promise<string> {
|
|
14
15
|
const jobs = await Job.list();
|
|
@@ -23,6 +24,7 @@ export async function addJob(args: {
|
|
|
23
24
|
schedule_type?: ScheduleType;
|
|
24
25
|
always?: boolean;
|
|
25
26
|
agent?: string;
|
|
27
|
+
employee?: string;
|
|
26
28
|
model?: string;
|
|
27
29
|
stateless?: boolean;
|
|
28
30
|
}): Promise<string> {
|
|
@@ -42,10 +44,12 @@ export async function addJob(args: {
|
|
|
42
44
|
args.agent,
|
|
43
45
|
stateless,
|
|
44
46
|
args.model,
|
|
47
|
+
args.employee,
|
|
45
48
|
);
|
|
46
49
|
const agentNote = args.agent ? ` [agent: ${args.agent}]` : "";
|
|
50
|
+
const employeeNote = args.employee ? ` [employee: ${args.employee}]` : "";
|
|
47
51
|
const modelNote = args.model ? ` [model: ${args.model}]` : "";
|
|
48
|
-
return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}${modelNote}. Next run: ${nextRunAt.toISOString()}`;
|
|
52
|
+
return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}${employeeNote}${modelNote}. Next run: ${nextRunAt.toISOString()}`;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export async function updateJob(args: {
|
|
@@ -54,6 +58,7 @@ export async function updateJob(args: {
|
|
|
54
58
|
prompt?: string;
|
|
55
59
|
always?: boolean;
|
|
56
60
|
agent?: string | null;
|
|
61
|
+
employee?: string | null;
|
|
57
62
|
model?: string | null;
|
|
58
63
|
stateless?: boolean;
|
|
59
64
|
schedule_type?: "cron" | "interval" | "once";
|
|
@@ -65,6 +70,7 @@ export async function updateJob(args: {
|
|
|
65
70
|
stateless: boolean;
|
|
66
71
|
model: string | null;
|
|
67
72
|
agent: string | null;
|
|
73
|
+
employee: string | null;
|
|
68
74
|
scheduleType: "cron" | "interval" | "once";
|
|
69
75
|
}> = {};
|
|
70
76
|
if (args.schedule) fields.schedule = args.schedule;
|
|
@@ -73,10 +79,11 @@ export async function updateJob(args: {
|
|
|
73
79
|
if (args.stateless !== undefined) fields.stateless = args.stateless;
|
|
74
80
|
if (args.model !== undefined) fields.model = args.model;
|
|
75
81
|
if (args.agent !== undefined) fields.agent = args.agent;
|
|
82
|
+
if (args.employee !== undefined) fields.employee = args.employee;
|
|
76
83
|
if (args.schedule_type) fields.scheduleType = args.schedule_type;
|
|
77
84
|
|
|
78
85
|
if (Object.keys(fields).length === 0)
|
|
79
|
-
return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, model, agent, or schedule_type).";
|
|
86
|
+
return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, model, agent, employee, or schedule_type).";
|
|
80
87
|
|
|
81
88
|
const updated = await Job.update(args.name, fields);
|
|
82
89
|
if (!updated) return `Job "${args.name}" not found.`;
|
|
@@ -407,3 +414,7 @@ export function listAgents(): string {
|
|
|
407
414
|
2,
|
|
408
415
|
);
|
|
409
416
|
}
|
|
417
|
+
|
|
418
|
+
export function listEmployees(): string {
|
|
419
|
+
return listEmployeesForMcp();
|
|
420
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface EmployeeInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
dirName: string;
|
|
4
|
+
project: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
role: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
status: "onboarding" | "active" | "paused";
|
|
9
|
+
maxSubEmployees: number;
|
|
10
|
+
body: string;
|
|
11
|
+
created: string;
|
|
12
|
+
parent?: string;
|
|
13
|
+
source: string;
|
|
14
|
+
}
|
package/src/types/engine.ts
CHANGED
|
@@ -17,8 +17,12 @@ export interface SendCallbacks {
|
|
|
17
17
|
export interface ChatEngine {
|
|
18
18
|
sessionId: string | null;
|
|
19
19
|
room: string;
|
|
20
|
-
send(
|
|
21
|
-
|
|
20
|
+
send(
|
|
21
|
+
userMessage: string,
|
|
22
|
+
callbacks?: SendCallbacks,
|
|
23
|
+
attachments?: import("./attachment").Attachment[],
|
|
24
|
+
): Promise<SendResult>;
|
|
25
|
+
close(): Promise<void>;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export interface EngineOptions {
|
|
@@ -27,4 +31,7 @@ export interface EngineOptions {
|
|
|
27
31
|
/** true = resume latest session, or pass a specific session ID */
|
|
28
32
|
resume: boolean | string;
|
|
29
33
|
mcpServers?: Record<string, unknown>;
|
|
34
|
+
employee?: string;
|
|
35
|
+
agent?: string;
|
|
36
|
+
job?: string;
|
|
30
37
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -9,3 +9,4 @@ export type { Config, ChannelsConfig, TelegramConfig, SlackConfig } from "./conf
|
|
|
9
9
|
export type { Paths } from "./paths";
|
|
10
10
|
export type { SaveMessageParams, RoomStats, RecentMessage, SearchResult, SessionMessage } from "./message";
|
|
11
11
|
export type { AgentInfo } from "./agent";
|
|
12
|
+
export type { EmployeeInfo } from "./employee";
|
package/src/types/job.ts
CHANGED
package/src/types/paths.ts
CHANGED