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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.63",
3
+ "version": "0.2.65",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
+ }
@@ -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 { getAgentDefinitions } from "../core/agents";
10
- import { Session, Message, ActiveEngine } from "../db/models";
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
- const cwd = homedir();
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
- finalizeSession(sessionId, room).catch((err) => {
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
  }
@@ -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.map((f) => loadFile(selfDir, f)).filter(Boolean).join("\n\n");
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 async function startRepl(mode: ChatMode = "continue", simulateChannel?: string): Promise<void> {
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 engine = await createChatEngine({ room: "terminal", channel, resume, mcpServers: getMcpServers() });
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
- console.log(`\n${DIM}nia chat${channelNote}${RESET} ${DIM}(${sessionNote})${RESET}`);
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
- rl.prompt();
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 chIdx = chatArgs.indexOf("--channel");
311
- const simChannel = chIdx !== -1 && chatArgs[chIdx + 1] ? chatArgs[chIdx + 1] : undefined;
312
- await startRepl(mode, simChannel);
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] [--channel ch] Interactive chat (new session by default)
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: