niahere 0.2.63 → 0.2.64
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/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/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getEmployee, getEmployeeDir } from "../core/employees";
|
|
4
|
+
import { ONBOARDING_INSTRUCTIONS } from "../core/employees";
|
|
5
|
+
import { getEnvironmentPrompt, getModePrompt } from "../prompts";
|
|
6
|
+
import { getSkillsSummary } from "../core/skills";
|
|
7
|
+
import { getAgentsSummary } from "../core/agents";
|
|
8
|
+
|
|
9
|
+
function loadFile(dir: string, name: string): string {
|
|
10
|
+
const filePath = join(dir, name);
|
|
11
|
+
if (!existsSync(filePath)) return "";
|
|
12
|
+
return readFileSync(filePath, "utf8").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildEmployeePrompt(name: string): string {
|
|
16
|
+
const employee = getEmployee(name);
|
|
17
|
+
if (!employee) return "";
|
|
18
|
+
|
|
19
|
+
const dir = getEmployeeDir(name);
|
|
20
|
+
const parts: string[] = [];
|
|
21
|
+
|
|
22
|
+
// Core identity (the EMPLOYEE.md body)
|
|
23
|
+
if (employee.body) parts.push(employee.body);
|
|
24
|
+
|
|
25
|
+
// Environment + mode + capabilities
|
|
26
|
+
parts.push(getEnvironmentPrompt());
|
|
27
|
+
|
|
28
|
+
const modePrompt = getModePrompt("chat");
|
|
29
|
+
if (modePrompt) parts.push(modePrompt);
|
|
30
|
+
|
|
31
|
+
const skills = getSkillsSummary();
|
|
32
|
+
if (skills) parts.push(skills);
|
|
33
|
+
|
|
34
|
+
const agents = getAgentsSummary();
|
|
35
|
+
if (agents) parts.push(agents);
|
|
36
|
+
|
|
37
|
+
// Onboarding instructions (only when status=onboarding)
|
|
38
|
+
if (employee.status === "onboarding") {
|
|
39
|
+
parts.push(ONBOARDING_INSTRUCTIONS);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Employee metadata context
|
|
43
|
+
parts.push(`## Your Profile
|
|
44
|
+
- **Name:** ${employee.name}
|
|
45
|
+
- **Role:** ${employee.role}
|
|
46
|
+
- **Project:** ${employee.project}
|
|
47
|
+
- **Repo:** ${employee.repo}
|
|
48
|
+
- **Status:** ${employee.status}
|
|
49
|
+
- **Max Sub-Employees:** ${employee.maxSubEmployees}`);
|
|
50
|
+
|
|
51
|
+
// State files
|
|
52
|
+
const goals = loadFile(dir, "goals.md");
|
|
53
|
+
if (goals) parts.push(`## Your Current Goals\n${goals}`);
|
|
54
|
+
|
|
55
|
+
const memory = loadFile(dir, "memory.md");
|
|
56
|
+
if (memory) parts.push(`## Your Memory\n${memory}`);
|
|
57
|
+
|
|
58
|
+
const decisions = loadFile(dir, "decisions.md");
|
|
59
|
+
if (decisions) parts.push(`## Decision Log\n${decisions}`);
|
|
60
|
+
|
|
61
|
+
const org = loadFile(dir, "org.md");
|
|
62
|
+
if (org) parts.push(`## Your Organization\n${org}`);
|
|
63
|
+
|
|
64
|
+
// Onboarding context
|
|
65
|
+
const brief = loadFile(join(dir, "onboarding"), "brief.md");
|
|
66
|
+
if (brief) parts.push(`## Onboarding Brief\n${brief}`);
|
|
67
|
+
|
|
68
|
+
const discovery = loadFile(join(dir, "onboarding"), "discovery.md");
|
|
69
|
+
if (discovery) parts.push(`## Self-Discovery Notes\n${discovery}`);
|
|
70
|
+
|
|
71
|
+
const plan = loadFile(join(dir, "onboarding"), "plan.md");
|
|
72
|
+
if (plan) parts.push(`## Initial Plan\n${plan}`);
|
|
73
|
+
|
|
74
|
+
return parts.join("\n\n");
|
|
75
|
+
}
|
package/src/chat/engine.ts
CHANGED
|
@@ -6,8 +6,10 @@ import { join } from "path";
|
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { buildSystemPrompt, getSessionContext } from "./identity";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { buildEmployeePrompt } from "./employee-prompt";
|
|
10
|
+
import { getEmployee } from "../core/employees";
|
|
11
|
+
import { getAgentDefinitions, scanAgents } from "../core/agents";
|
|
12
|
+
import { Session, Message, ActiveEngine, Job } from "../db/models";
|
|
11
13
|
import type {
|
|
12
14
|
Attachment,
|
|
13
15
|
SendResult,
|
|
@@ -134,7 +136,36 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
134
136
|
systemPrompt += "\n\n" + sessionContext;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
// Context overrides: employee > agent > job > default
|
|
140
|
+
let cwd = homedir();
|
|
141
|
+
if (opts.employee) {
|
|
142
|
+
const empPrompt = buildEmployeePrompt(opts.employee);
|
|
143
|
+
if (empPrompt) systemPrompt = empPrompt;
|
|
144
|
+
const emp = getEmployee(opts.employee);
|
|
145
|
+
if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
|
|
146
|
+
} else if (opts.agent) {
|
|
147
|
+
const agents = scanAgents();
|
|
148
|
+
const agentDef = agents.find((a) => a.name === opts.agent);
|
|
149
|
+
if (agentDef) systemPrompt = agentDef.body;
|
|
150
|
+
} else if (opts.job) {
|
|
151
|
+
// Job chat: load job and use its context
|
|
152
|
+
const jobData = await Job.get(opts.job);
|
|
153
|
+
if (jobData) {
|
|
154
|
+
// If job has an employee, use employee prompt
|
|
155
|
+
if (jobData.employee) {
|
|
156
|
+
const empPrompt = buildEmployeePrompt(jobData.employee);
|
|
157
|
+
if (empPrompt) systemPrompt = empPrompt;
|
|
158
|
+
const emp = getEmployee(jobData.employee);
|
|
159
|
+
if (emp?.repo && existsSync(emp.repo)) cwd = emp.repo;
|
|
160
|
+
} else if (jobData.agent) {
|
|
161
|
+
// If job has an agent, use agent prompt
|
|
162
|
+
const agents = scanAgents();
|
|
163
|
+
const agentDef = agents.find((a) => a.name === jobData.agent);
|
|
164
|
+
if (agentDef) systemPrompt = agentDef.body;
|
|
165
|
+
}
|
|
166
|
+
systemPrompt += `\n\n## Job Context\nYou are chatting in the context of job "${jobData.name}" (schedule: ${jobData.schedule}).\n\nJob prompt:\n${jobData.prompt}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
138
169
|
|
|
139
170
|
let sessionId: string | null = null;
|
|
140
171
|
if (typeof resume === "string") {
|
|
@@ -493,15 +524,17 @@ export async function createChatEngine(opts: EngineOptions): Promise<ChatEngine>
|
|
|
493
524
|
});
|
|
494
525
|
},
|
|
495
526
|
|
|
496
|
-
close() {
|
|
527
|
+
async close() {
|
|
497
528
|
// Enqueue finalization — processed by daemon or inline if we are the daemon
|
|
498
529
|
if (sessionId && messageCount > 0 && !pending) {
|
|
499
|
-
|
|
530
|
+
try {
|
|
531
|
+
await finalizeSession(sessionId, room);
|
|
532
|
+
} catch (err) {
|
|
500
533
|
log.error({ err, room }, "finalization enqueue failed during close");
|
|
501
|
-
}
|
|
534
|
+
}
|
|
502
535
|
}
|
|
503
536
|
teardown();
|
|
504
|
-
ActiveEngine.unregister(room).catch(() => {});
|
|
537
|
+
await ActiveEngine.unregister(room).catch(() => {});
|
|
505
538
|
},
|
|
506
539
|
};
|
|
507
540
|
}
|
package/src/chat/identity.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { getPaths } from "../utils/paths";
|
|
|
4
4
|
import { getEnvironmentPrompt, getModePrompt, getChannelPrompt } from "../prompts";
|
|
5
5
|
import { getSkillsSummary } from "../core/skills";
|
|
6
6
|
import { getAgentsSummary } from "../core/agents";
|
|
7
|
+
import { getEmployeesSummary } from "../core/employees";
|
|
7
8
|
import { Session } from "../db/models";
|
|
8
9
|
import type { Mode } from "../types";
|
|
9
10
|
|
|
@@ -19,7 +20,10 @@ function loadFile(dir: string, name: string): string {
|
|
|
19
20
|
export function loadIdentity(): string {
|
|
20
21
|
const { selfDir } = getPaths();
|
|
21
22
|
const files = ["identity.md", "owner.md", "soul.md", "rules.md", "memory.md"];
|
|
22
|
-
return files
|
|
23
|
+
return files
|
|
24
|
+
.map((f) => loadFile(selfDir, f))
|
|
25
|
+
.filter(Boolean)
|
|
26
|
+
.join("\n\n");
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function buildSystemPrompt(mode: Mode = "chat", channel: string = "terminal"): string {
|
|
@@ -42,6 +46,9 @@ export function buildSystemPrompt(mode: Mode = "chat", channel: string = "termin
|
|
|
42
46
|
const agents = getAgentsSummary();
|
|
43
47
|
if (agents) parts.push(agents);
|
|
44
48
|
|
|
49
|
+
const employees = getEmployeesSummary();
|
|
50
|
+
if (employees) parts.push(employees);
|
|
51
|
+
|
|
45
52
|
return parts.join("\n\n");
|
|
46
53
|
}
|
|
47
54
|
|
package/src/chat/repl.ts
CHANGED
|
@@ -103,7 +103,18 @@ async function pickSession(): Promise<string | null> {
|
|
|
103
103
|
|
|
104
104
|
export type ChatMode = "continue" | "new" | "pick";
|
|
105
105
|
|
|
106
|
-
export
|
|
106
|
+
export interface ReplContext {
|
|
107
|
+
employee?: string;
|
|
108
|
+
agent?: string;
|
|
109
|
+
job?: string;
|
|
110
|
+
initialMessage?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function startRepl(
|
|
114
|
+
mode: ChatMode = "continue",
|
|
115
|
+
simulateChannel?: string,
|
|
116
|
+
context?: ReplContext,
|
|
117
|
+
): Promise<void> {
|
|
107
118
|
try {
|
|
108
119
|
await runMigrations();
|
|
109
120
|
} catch (err) {
|
|
@@ -131,13 +142,24 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
|
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
const channel = simulateChannel || "terminal";
|
|
134
|
-
const
|
|
145
|
+
const contextLabel = context?.employee || context?.agent || context?.job;
|
|
146
|
+
const room = contextLabel ? `chat-${contextLabel}` : "terminal";
|
|
147
|
+
const engine = await createChatEngine({
|
|
148
|
+
room,
|
|
149
|
+
channel,
|
|
150
|
+
resume,
|
|
151
|
+
mcpServers: getMcpServers(),
|
|
152
|
+
employee: context?.employee,
|
|
153
|
+
agent: context?.agent,
|
|
154
|
+
job: context?.job,
|
|
155
|
+
});
|
|
135
156
|
|
|
136
157
|
// Welcome
|
|
137
158
|
const isResumed = engine.sessionId && resume;
|
|
138
159
|
const sessionNote = isResumed ? "resumed" : "new session";
|
|
139
160
|
const channelNote = simulateChannel ? ` as ${simulateChannel}` : "";
|
|
140
|
-
|
|
161
|
+
const contextNote = contextLabel ? ` as ${contextLabel}` : "";
|
|
162
|
+
console.log(`\n${DIM}nia chat${contextNote}${channelNote}${RESET} ${DIM}(${sessionNote})${RESET}`);
|
|
141
163
|
console.log(`${DIM}type /exit to quit${RESET}\n`);
|
|
142
164
|
|
|
143
165
|
const rl = readline.createInterface({
|
|
@@ -146,22 +168,7 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
|
|
|
146
168
|
prompt: `${BOLD}>${RESET} `,
|
|
147
169
|
});
|
|
148
170
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rl.on("line", async (line: string) => {
|
|
152
|
-
const input = line.trim();
|
|
153
|
-
|
|
154
|
-
if (!input) {
|
|
155
|
-
rl.prompt();
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const exitCommands = ["/exit", "/quit", ".exit", ".quit", "exit", "quit"];
|
|
160
|
-
if (exitCommands.includes(input.toLowerCase())) {
|
|
161
|
-
rl.close();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
171
|
+
async function sendAndDisplay(input: string): Promise<void> {
|
|
165
172
|
const status = new StatusLine();
|
|
166
173
|
status.start("thinking");
|
|
167
174
|
|
|
@@ -171,7 +178,6 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
|
|
|
171
178
|
try {
|
|
172
179
|
const { result, costUsd, turns } = await engine.send(input, {
|
|
173
180
|
onStream(textSoFar) {
|
|
174
|
-
// Stream response text as it arrives
|
|
175
181
|
if (!responseStarted) {
|
|
176
182
|
status.stop();
|
|
177
183
|
process.stdout.write("\n");
|
|
@@ -190,12 +196,10 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
|
|
|
190
196
|
},
|
|
191
197
|
});
|
|
192
198
|
|
|
193
|
-
// If streaming didn't fire (e.g. tool-only turns), print the result
|
|
194
199
|
if (!responseStarted && result.trim()) {
|
|
195
200
|
status.stop();
|
|
196
201
|
process.stdout.write(`\n${result.trim()}`);
|
|
197
202
|
} else if (responseStarted) {
|
|
198
|
-
// Print any remaining text that wasn't streamed
|
|
199
203
|
const remaining = result.slice(streamedLength);
|
|
200
204
|
if (remaining.trim()) {
|
|
201
205
|
process.stdout.write(remaining);
|
|
@@ -204,7 +208,6 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
|
|
|
204
208
|
status.stop();
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
// Cost line
|
|
208
211
|
const costStr = costUsd > 0 ? `$${costUsd.toFixed(4)}` : "";
|
|
209
212
|
const turnsStr = turns > 0 ? `${turns} turn${turns !== 1 ? "s" : ""}` : "";
|
|
210
213
|
const meta = [costStr, turnsStr].filter(Boolean).join(" · ");
|
|
@@ -218,13 +221,37 @@ export async function startRepl(mode: ChatMode = "continue", simulateChannel?: s
|
|
|
218
221
|
const msg = err instanceof Error ? err.message : String(err);
|
|
219
222
|
console.error(`\n${DIM}error:${RESET} ${msg}\n`);
|
|
220
223
|
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Auto-send initial message if provided (e.g. onboarding kickoff)
|
|
227
|
+
if (context?.initialMessage) {
|
|
228
|
+
await sendAndDisplay(context.initialMessage);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
rl.prompt();
|
|
232
|
+
|
|
233
|
+
rl.on("line", async (line: string) => {
|
|
234
|
+
const input = line.trim();
|
|
235
|
+
|
|
236
|
+
if (!input) {
|
|
237
|
+
rl.prompt();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const exitCommands = ["/exit", "/quit", ".exit", ".quit", "exit", "quit"];
|
|
242
|
+
if (exitCommands.includes(input.toLowerCase())) {
|
|
243
|
+
rl.close();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await sendAndDisplay(input);
|
|
221
248
|
|
|
222
249
|
rl.prompt();
|
|
223
250
|
});
|
|
224
251
|
|
|
225
|
-
rl.on("close", () => {
|
|
252
|
+
rl.on("close", async () => {
|
|
226
253
|
console.log(`\n${DIM}bye${RESET}`);
|
|
227
|
-
engine.close();
|
|
254
|
+
await engine.close();
|
|
228
255
|
closeDb()
|
|
229
256
|
.catch(() => {})
|
|
230
257
|
.finally(() => process.exit(0));
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { getEmployee, getEmployeeDir } from "../core/employees";
|
|
4
|
+
import { fail, BOLD, RESET } from "../utils/cli";
|
|
5
|
+
import { startRepl } from "../chat/repl";
|
|
6
|
+
|
|
7
|
+
// Permanent identity — stays in EMPLOYEE.md after onboarding
|
|
8
|
+
const CORE_BODY = `You are {name}, a co-founder working with Aman.
|
|
9
|
+
{projectLine}
|
|
10
|
+
You operate independently but seek approval before externally visible actions.
|
|
11
|
+
|
|
12
|
+
## Who You Are
|
|
13
|
+
You are NOT an assistant. You are a co-founder and thought partner. This means:
|
|
14
|
+
- Challenge ideas that seem weak, unfocused, or premature. Say "I don't think that's the right move because..." not "Great idea!"
|
|
15
|
+
- Ask hard questions: "Who actually wants this?", "What's the evidence?", "Why this over the 5 easier things?"
|
|
16
|
+
- Have strong opinions, loosely held. Push back, but update when presented with better reasoning.
|
|
17
|
+
- Be direct. No filler, no sycophancy, no "Got it!", no performative enthusiasm.
|
|
18
|
+
- Think critically about priorities. "We could, but should we?" is more valuable than "On it!"
|
|
19
|
+
- When Aman tells you something, probe it. A real co-founder doesn't just accept the brief — they stress-test it.
|
|
20
|
+
|
|
21
|
+
## Your Authority
|
|
22
|
+
- Create and manage scheduled jobs scoped to your project
|
|
23
|
+
- Create sub-employees (up to {maxSubEmployees}) and agents under your org
|
|
24
|
+
- Read/write code in your project repo
|
|
25
|
+
- Draft content, PRs, deployments (approval required before publishing)
|
|
26
|
+
|
|
27
|
+
## Approval Required For
|
|
28
|
+
- Deploying code to production
|
|
29
|
+
- Publishing content externally
|
|
30
|
+
- Creating sub-employees
|
|
31
|
+
- Any action visible outside the project repo
|
|
32
|
+
- Spending money or signing up for services
|
|
33
|
+
|
|
34
|
+
## How You Work
|
|
35
|
+
- Maintain your goals.md with current priorities
|
|
36
|
+
- Log significant decisions in decisions.md with [pending] status when approval needed
|
|
37
|
+
- Update memory.md with learnings after each session
|
|
38
|
+
- When blocked or facing a big decision, write to decisions.md as [pending] and tell the user
|
|
39
|
+
- At the start of each session, review your state files and what's changed in the repo
|
|
40
|
+
|
|
41
|
+
## State Files
|
|
42
|
+
You have persistent state files in your employee directory. Read and update them:
|
|
43
|
+
- **goals.md** — your current goals and success criteria
|
|
44
|
+
- **memory.md** — what you've learned, decided, observed across sessions
|
|
45
|
+
- **decisions.md** — decision log (mark as [pending], [approved], or [rejected])
|
|
46
|
+
- **org.md** — sub-employees and agents you've created`;
|
|
47
|
+
|
|
48
|
+
export async function employeeAdd(): Promise<void> {
|
|
49
|
+
const args = process.argv.slice(4);
|
|
50
|
+
|
|
51
|
+
const flagValue = (flag: string): string | undefined => {
|
|
52
|
+
const idx = args.indexOf(flag);
|
|
53
|
+
return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--") ? args[idx + 1] : undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const nameArg = args[0] && !args[0].startsWith("--") ? args[0] : undefined;
|
|
57
|
+
const name = nameArg || `new-employee-${Math.random().toString(36).slice(2, 6)}`;
|
|
58
|
+
const project = flagValue("--project") || "";
|
|
59
|
+
const repoArg = flagValue("--repo") || "";
|
|
60
|
+
const role = flagValue("--role") || "Co-Founder";
|
|
61
|
+
const model = flagValue("--model") || "opus";
|
|
62
|
+
const maxSubs = parseInt(flagValue("--max-sub-employees") || "3", 10);
|
|
63
|
+
|
|
64
|
+
if (getEmployee(name)) fail(`Employee "${name}" already exists.`);
|
|
65
|
+
|
|
66
|
+
const repo = repoArg ? resolve(repoArg) : "";
|
|
67
|
+
if (repo && !existsSync(repo)) fail(`Repo path does not exist: ${repo}`);
|
|
68
|
+
|
|
69
|
+
// Scaffold directory
|
|
70
|
+
const empDir = getEmployeeDir(name);
|
|
71
|
+
mkdirSync(empDir, { recursive: true });
|
|
72
|
+
mkdirSync(`${empDir}/onboarding`, { recursive: true });
|
|
73
|
+
|
|
74
|
+
const projectLine = project ? `You are responsible for ${project}.` : "";
|
|
75
|
+
|
|
76
|
+
const body = CORE_BODY.replace(/\{name\}/g, name)
|
|
77
|
+
.replace(/\{projectLine\}\n/g, projectLine ? `${projectLine}\n` : "")
|
|
78
|
+
.replace(/\{maxSubEmployees\}/g, String(maxSubs));
|
|
79
|
+
|
|
80
|
+
const frontmatter = [
|
|
81
|
+
"---",
|
|
82
|
+
`name: ${name}`,
|
|
83
|
+
`project: "${project}"`,
|
|
84
|
+
`repo: "${repo}"`,
|
|
85
|
+
`role: ${role}`,
|
|
86
|
+
`model: ${model}`,
|
|
87
|
+
`status: onboarding`,
|
|
88
|
+
`maxSubEmployees: ${maxSubs}`,
|
|
89
|
+
`created: ${new Date().toISOString().slice(0, 10)}`,
|
|
90
|
+
"---",
|
|
91
|
+
].join("\n");
|
|
92
|
+
|
|
93
|
+
writeFileSync(`${empDir}/EMPLOYEE.md`, `${frontmatter}\n\n${body}\n`);
|
|
94
|
+
writeFileSync(`${empDir}/goals.md`, "# Goals\n\n");
|
|
95
|
+
writeFileSync(`${empDir}/memory.md`, "# Memory\n\n");
|
|
96
|
+
writeFileSync(`${empDir}/decisions.md`, "# Decisions\n\n");
|
|
97
|
+
writeFileSync(`${empDir}/org.md`, "# Organization\n\n");
|
|
98
|
+
writeFileSync(`${empDir}/onboarding/brief.md`, "");
|
|
99
|
+
writeFileSync(`${empDir}/onboarding/discovery.md`, "");
|
|
100
|
+
writeFileSync(`${empDir}/onboarding/plan.md`, "");
|
|
101
|
+
|
|
102
|
+
// Build a context-aware kickoff message so the agent starts proactively
|
|
103
|
+
const provided: string[] = [];
|
|
104
|
+
const missing: string[] = [];
|
|
105
|
+
|
|
106
|
+
if (nameArg) provided.push(`name: ${nameArg}`);
|
|
107
|
+
else missing.push("name (placeholder assigned — suggest a real one)");
|
|
108
|
+
|
|
109
|
+
if (project) provided.push(`project: ${project}`);
|
|
110
|
+
else missing.push("project");
|
|
111
|
+
|
|
112
|
+
if (repo) provided.push(`repo: ${repo}`);
|
|
113
|
+
else missing.push("repo path");
|
|
114
|
+
|
|
115
|
+
const initialMessage = [
|
|
116
|
+
"New employee created. Start onboarding.",
|
|
117
|
+
provided.length > 0 ? `Provided: ${provided.join(", ")}.` : "Nothing provided yet.",
|
|
118
|
+
missing.length > 0 ? `Missing: ${missing.join(", ")}.` : "All basics provided — proceed to brief.",
|
|
119
|
+
].join(" ");
|
|
120
|
+
|
|
121
|
+
console.log(`\n${BOLD}${name}${RESET} created — starting onboarding...\n`);
|
|
122
|
+
|
|
123
|
+
await startRepl("new", undefined, { employee: name, initialMessage });
|
|
124
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { scanEmployees, getEmployee, getEmployeeDir } from "../core/employees";
|
|
4
|
+
import { fail, DIM, RESET } from "../utils/cli";
|
|
5
|
+
|
|
6
|
+
const HELP = `Usage: nia employee <command>
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
list List all employees
|
|
10
|
+
show <name> Show employee details and state
|
|
11
|
+
add <name> Create employee and start onboarding
|
|
12
|
+
--project <label> Project name (required)
|
|
13
|
+
--repo <path> Project repo path (required)
|
|
14
|
+
--role <role> Role title (default: "Chief of Staff")
|
|
15
|
+
--model <model> Model override (default: opus)
|
|
16
|
+
--max-sub-employees <n> Max sub-employees (default: 3)
|
|
17
|
+
pause <name> Pause an employee
|
|
18
|
+
resume <name> Resume a paused employee
|
|
19
|
+
remove <name> Remove an employee
|
|
20
|
+
approvals [name] Show pending approvals`;
|
|
21
|
+
|
|
22
|
+
export async function employeeCommand(): Promise<void> {
|
|
23
|
+
const subcommand = process.argv[3];
|
|
24
|
+
|
|
25
|
+
if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
26
|
+
console.log(HELP);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case "list": {
|
|
32
|
+
const employees = scanEmployees();
|
|
33
|
+
if (employees.length === 0) {
|
|
34
|
+
console.log("No employees. Create one with: nia employee add <name> --project <project> --repo <path>");
|
|
35
|
+
} else {
|
|
36
|
+
for (const e of employees) {
|
|
37
|
+
const model = e.model ? ` (${e.model})` : "";
|
|
38
|
+
const parent = e.parent ? ` → ${e.parent}` : "";
|
|
39
|
+
console.log(` ${e.name}${model} [${e.status}]${parent}`);
|
|
40
|
+
console.log(` ${e.role} — ${e.project}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case "show": {
|
|
47
|
+
const name = process.argv[4];
|
|
48
|
+
if (!name) fail("Usage: nia employee show <name>");
|
|
49
|
+
const emp = getEmployee(name);
|
|
50
|
+
if (!emp) fail(`Employee "${name}" not found.`);
|
|
51
|
+
console.log(`Name: ${emp.name}`);
|
|
52
|
+
console.log(`Role: ${emp.role}`);
|
|
53
|
+
console.log(`Project: ${emp.project}`);
|
|
54
|
+
console.log(`Repo: ${emp.repo}`);
|
|
55
|
+
console.log(`Status: ${emp.status}`);
|
|
56
|
+
console.log(`Model: ${emp.model || "(default)"}`);
|
|
57
|
+
console.log(`Created: ${emp.created}`);
|
|
58
|
+
if (emp.parent) console.log(`Parent: ${emp.parent}`);
|
|
59
|
+
console.log(`Max Subs: ${emp.maxSubEmployees}`);
|
|
60
|
+
|
|
61
|
+
const dir = getEmployeeDir(name);
|
|
62
|
+
const goals = loadFilePreview(join(dir, "goals.md"));
|
|
63
|
+
if (goals) console.log(`\n--- Goals ---\n${goals}`);
|
|
64
|
+
const pendingCount = loadFilePendingCount(join(dir, "decisions.md"));
|
|
65
|
+
if (pendingCount > 0) console.log(`\nPending approvals: ${pendingCount}`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "pause": {
|
|
70
|
+
const name = process.argv[4];
|
|
71
|
+
if (!name) fail("Usage: nia employee pause <name>");
|
|
72
|
+
updateStatus(name, "paused");
|
|
73
|
+
console.log(`${name} paused.`);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "resume": {
|
|
78
|
+
const name = process.argv[4];
|
|
79
|
+
if (!name) fail("Usage: nia employee resume <name>");
|
|
80
|
+
updateStatus(name, "active");
|
|
81
|
+
console.log(`${name} resumed.`);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "remove": {
|
|
86
|
+
const name = process.argv[4];
|
|
87
|
+
if (!name) fail("Usage: nia employee remove <name>");
|
|
88
|
+
const emp = getEmployee(name);
|
|
89
|
+
if (!emp) fail(`Employee "${name}" not found.`);
|
|
90
|
+
const dir = getEmployeeDir(name);
|
|
91
|
+
rmSync(dir, { recursive: true, force: true });
|
|
92
|
+
console.log(`${name} removed.`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "approvals": {
|
|
97
|
+
const name = process.argv[4];
|
|
98
|
+
const employees = name ? [getEmployee(name)].filter(Boolean) : scanEmployees();
|
|
99
|
+
if (employees.length === 0) {
|
|
100
|
+
console.log(name ? `Employee "${name}" not found.` : "No employees.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let found = false;
|
|
104
|
+
for (const emp of employees) {
|
|
105
|
+
if (!emp) continue;
|
|
106
|
+
const dir = getEmployeeDir(emp.name);
|
|
107
|
+
const decisionsFile = join(dir, "decisions.md");
|
|
108
|
+
if (!existsSync(decisionsFile)) continue;
|
|
109
|
+
const content = readFileSync(decisionsFile, "utf8");
|
|
110
|
+
const pending = content.split(/^## /m).filter((s) => s.includes("[pending]"));
|
|
111
|
+
if (pending.length > 0) {
|
|
112
|
+
found = true;
|
|
113
|
+
console.log(`\n${DIM}${emp.name}:${RESET}`);
|
|
114
|
+
for (const p of pending) {
|
|
115
|
+
console.log(` ${p.trim().split("\n")[0]}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!found) console.log("No pending approvals.");
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case "add": {
|
|
124
|
+
const { employeeAdd } = await import("./employee-add");
|
|
125
|
+
await employeeAdd();
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
default: {
|
|
130
|
+
// If subcommand matches an employee name, start chat
|
|
131
|
+
if (subcommand) {
|
|
132
|
+
const emp = getEmployee(subcommand);
|
|
133
|
+
if (emp) {
|
|
134
|
+
const { startRepl } = await import("../chat/repl");
|
|
135
|
+
await startRepl("continue", undefined, { employee: emp.name });
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (subcommand) console.error(`Unknown subcommand: ${subcommand}`);
|
|
140
|
+
console.log(HELP);
|
|
141
|
+
process.exit(subcommand ? 1 : 0);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function updateStatus(name: string, status: "active" | "paused"): void {
|
|
147
|
+
const emp = getEmployee(name);
|
|
148
|
+
if (!emp) fail(`Employee "${name}" not found.`);
|
|
149
|
+
const dir = getEmployeeDir(name);
|
|
150
|
+
const empFile = join(dir, "EMPLOYEE.md");
|
|
151
|
+
let content = readFileSync(empFile, "utf8");
|
|
152
|
+
content = content.replace(/^(status:\s*).+$/m, `$1${status}`);
|
|
153
|
+
writeFileSync(empFile, content);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function loadFilePreview(path: string): string {
|
|
157
|
+
if (!existsSync(path)) return "";
|
|
158
|
+
const content = readFileSync(path, "utf8").trim();
|
|
159
|
+
const lines = content.split("\n").slice(0, 10);
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadFilePendingCount(path: string): number {
|
|
164
|
+
if (!existsSync(path)) return 0;
|
|
165
|
+
const content = readFileSync(path, "utf8");
|
|
166
|
+
return (content.match(/\[pending\]/g) || []).length;
|
|
167
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { sendCommand, telegramCommand, slackCommand } from "./channels";
|
|
|
15
15
|
import { rulesCommand, memoryCommand } from "./self";
|
|
16
16
|
import { watchCommand } from "./watch";
|
|
17
17
|
import { agentCommand } from "./agent";
|
|
18
|
+
import { employeeCommand } from "./employee";
|
|
18
19
|
|
|
19
20
|
// Set LOG_LEVEL from config before anything else logs
|
|
20
21
|
try {
|
|
@@ -215,7 +216,7 @@ switch (command) {
|
|
|
215
216
|
if (meta) process.stderr.write(`\n${DIM}${meta}${RST}`);
|
|
216
217
|
process.stdout.write("\n");
|
|
217
218
|
|
|
218
|
-
engine.close();
|
|
219
|
+
await engine.close();
|
|
219
220
|
});
|
|
220
221
|
process.exit(0);
|
|
221
222
|
} else {
|
|
@@ -307,9 +308,18 @@ switch (command) {
|
|
|
307
308
|
: chatArgs.includes("--resume") || chatArgs.includes("-r")
|
|
308
309
|
? ("pick" as const)
|
|
309
310
|
: ("new" as const);
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
const flagVal = (flag: string) => {
|
|
312
|
+
const idx = chatArgs.indexOf(flag);
|
|
313
|
+
return idx !== -1 && chatArgs[idx + 1] ? chatArgs[idx + 1] : undefined;
|
|
314
|
+
};
|
|
315
|
+
const simChannel = flagVal("--channel");
|
|
316
|
+
const context = {
|
|
317
|
+
employee: flagVal("--employee"),
|
|
318
|
+
agent: flagVal("--agent"),
|
|
319
|
+
job: flagVal("--job"),
|
|
320
|
+
};
|
|
321
|
+
const hasContext = context.employee || context.agent || context.job;
|
|
322
|
+
await startRepl(mode, simChannel, hasContext ? context : undefined);
|
|
313
323
|
break;
|
|
314
324
|
}
|
|
315
325
|
|
|
@@ -318,6 +328,11 @@ switch (command) {
|
|
|
318
328
|
break;
|
|
319
329
|
}
|
|
320
330
|
|
|
331
|
+
case "employee": {
|
|
332
|
+
await employeeCommand();
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
|
|
321
336
|
case "skills": {
|
|
322
337
|
const { scanSkills: loadSkills } = await import("../core/skills");
|
|
323
338
|
const filter = process.argv[3]; // e.g. "project", "nia", "shared", "claude"
|
|
@@ -545,7 +560,7 @@ Daemon:
|
|
|
545
560
|
logs [-f] [--channel ch] Daemon logs (filter by channel)
|
|
546
561
|
|
|
547
562
|
Chat:
|
|
548
|
-
chat [-c] [-r] [--
|
|
563
|
+
chat [-c] [-r] [--employee|--agent|--job name] Interactive chat
|
|
549
564
|
run <prompt> One-shot execution
|
|
550
565
|
history [room] Recent messages
|
|
551
566
|
send [-c ch] <msg> Send a message via channel
|
|
@@ -557,6 +572,7 @@ Persona:
|
|
|
557
572
|
rules [show|reset] View or reset rules.md
|
|
558
573
|
memory [show|reset] View or reset memory.md
|
|
559
574
|
agent <sub> List/show agents
|
|
575
|
+
employee <sub> Manage employees
|
|
560
576
|
skills [source] List available skills
|
|
561
577
|
|
|
562
578
|
Channels:
|
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");
|
|
@@ -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