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/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:
|