niahere 0.2.56 → 0.2.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,8 +63,8 @@ nia update — update to latest version (auto-backup + resta
63
63
  nia job list — list all jobs
64
64
  nia job show [name] — full details + recent runs
65
65
  nia job status [name] — quick status check
66
- nia job add <n> <s> <p> — add a job (--type, --always, --agent, --prompt-file)
67
- nia job update <name> — update a job (--schedule, --prompt, --prompt-file, --type, --always, --agent)
66
+ nia job add <n> <s> <p> — add a job (--type, --always, --agent, --stateless, --prompt-file)
67
+ nia job update <name> — update a job (--schedule, --prompt, --prompt-file, --type, --always, --agent, --stateless)
68
68
  nia job remove <name> — delete a job
69
69
  nia job enable / disable <n> — toggle a job
70
70
  nia job run <name> — run a job once
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.2.56",
3
+ "version": "0.2.57",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
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
+ --stateless yes|no Disable working memory for this job
26
27
  update <name> Update a job
27
28
  --schedule <schedule> New schedule
28
29
  --prompt <text> New prompt
@@ -30,6 +31,7 @@ Commands:
30
31
  --type cron|interval|once Change schedule type
31
32
  --always / --no-always Toggle 24/7 mode
32
33
  --agent <name> Assign agent (--no-agent to remove)
34
+ --stateless yes|no Toggle working memory
33
35
  remove <name> Delete a job
34
36
  enable <name> Enable a job
35
37
  disable <name> Disable a job
@@ -100,6 +102,8 @@ export async function jobCommand(): Promise<void> {
100
102
  }
101
103
 
102
104
  const always = args.getBool("always") ?? false;
105
+ const statelessRaw = args.getString("stateless");
106
+ const stateless = statelessRaw ? ["yes", "y", "true", "t", "1"].includes(statelessRaw.toLowerCase()) : false;
103
107
  const agent = args.getString("agent");
104
108
 
105
109
  const [name, schedule, ...promptParts] = args.positional;
@@ -125,7 +129,7 @@ export async function jobCommand(): Promise<void> {
125
129
  const config = getConfig();
126
130
  const nextRunAt = computeInitialNextRun(scheduleType, schedule, config.timezone);
127
131
  await withDb(async () => {
128
- await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent);
132
+ await Job.create(name, schedule, prompt, always, scheduleType, nextRunAt, agent, stateless);
129
133
  console.log(`Job "${name}" added (${scheduleType}: ${schedule}).${always ? " (runs 24/7)" : ""}`);
130
134
  });
131
135
  } catch (err) {
@@ -176,7 +180,7 @@ export async function jobCommand(): Promise<void> {
176
180
  fail('Example: nia job update curator --schedule "4h" --prompt "New prompt"');
177
181
  }
178
182
 
179
- const fields: Partial<{ schedule: string; prompt: string; always: boolean; scheduleType: ScheduleType; agent: string | null }> = {};
183
+ const fields: Partial<{ schedule: string; prompt: string; always: boolean; stateless: boolean; scheduleType: ScheduleType; agent: string | null }> = {};
180
184
  const schedule = args.getString("schedule");
181
185
  const promptFile = args.getString("prompt-file");
182
186
  let prompt = args.getString("prompt");
@@ -187,6 +191,7 @@ export async function jobCommand(): Promise<void> {
187
191
  }
188
192
  const scheduleType = args.getString("type") as ScheduleType | undefined;
189
193
  const always = args.getBool("always");
194
+ const statelessRaw = args.getString("stateless");
190
195
  const agent = args.getString("agent");
191
196
  const noAgent = args.getBool("agent");
192
197
 
@@ -199,11 +204,12 @@ export async function jobCommand(): Promise<void> {
199
204
  fields.scheduleType = scheduleType;
200
205
  }
201
206
  if (always !== undefined) fields.always = always;
207
+ if (statelessRaw) fields.stateless = ["yes", "y", "true", "t", "1"].includes(statelessRaw.toLowerCase());
202
208
  if (agent) fields.agent = agent;
203
209
  if (noAgent === false) fields.agent = null;
204
210
 
205
211
  if (Object.keys(fields).length === 0) {
206
- fail("Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --agent).");
212
+ fail("Nothing to update. Pass at least one flag (--schedule, --prompt, --type, --always, --stateless, --agent).");
207
213
  }
208
214
 
209
215
  try {
@@ -231,6 +237,7 @@ export async function jobCommand(): Promise<void> {
231
237
  console.log(` enabled: ${job.enabled}`);
232
238
  console.log(` always: ${job.always}`);
233
239
  if (job.agent) console.log(` agent: ${job.agent}`);
240
+ if (job.stateless) console.log(` stateless: true`);
234
241
  console.log(` prompt: ${job.prompt}`);
235
242
 
236
243
  const state = readState();
@@ -1,5 +1,6 @@
1
1
  import { homedir } from "os";
2
- import { existsSync } from "fs";
2
+ import { existsSync, mkdirSync, readFileSync } from "fs";
3
+ import { join } from "path";
3
4
  import { randomUUID } from "crypto";
4
5
  import { query } from "@anthropic-ai/claude-agent-sdk";
5
6
  import type { JobInput, JobResult } from "../types";
@@ -11,6 +12,7 @@ import { scanAgents } from "./agents";
11
12
  import { truncate, formatToolUse } from "../utils/format-activity";
12
13
  import { getMcpServers } from "../mcp";
13
14
  import { ActiveEngine } from "../db/models";
15
+ import { getPaths } from "../utils/paths";
14
16
  import { log } from "../utils/log";
15
17
 
16
18
  export type ActivityCallback = (line: string) => void;
@@ -222,6 +224,42 @@ export async function runTask(opts: TaskOptions): Promise<RunnerOutput> {
222
224
  }
223
225
  }
224
226
 
227
+ // ---------------------------------------------------------------------------
228
+ // Working memory
229
+ // ---------------------------------------------------------------------------
230
+
231
+ /** Build the working memory block for a stateful job. Returns empty string for stateless jobs. */
232
+ export function buildWorkingMemory(jobName: string, stateless?: boolean): string {
233
+ if (stateless) return "";
234
+
235
+ const jobDir = join(getPaths().jobsDir, jobName);
236
+ mkdirSync(jobDir, { recursive: true });
237
+ const statePath = join(jobDir, "state.md");
238
+ let stateContent = "";
239
+ if (existsSync(statePath)) {
240
+ try {
241
+ stateContent = readFileSync(statePath, "utf8").trim();
242
+ } catch {
243
+ stateContent = "";
244
+ }
245
+ }
246
+
247
+ const stateBlock = stateContent
248
+ ? `\n${stateContent}\n`
249
+ : "(first run — no prior state)";
250
+
251
+ return `
252
+
253
+ ## Working Memory
254
+
255
+ You have a persistent workspace at \`${jobDir}/\`. This directory is yours — create files, organize data, track history, maintain state however you need.
256
+
257
+ Your \`state.md\` from last run:
258
+ ${stateBlock}
259
+
260
+ Before finishing, update \`state.md\` with: what you did this run, what you noticed, and what to do or focus on next time. Keep it concise — a working notebook, not a log.`;
261
+ }
262
+
225
263
  // ---------------------------------------------------------------------------
226
264
  // Public API
227
265
  // ---------------------------------------------------------------------------
@@ -256,10 +294,13 @@ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Prom
256
294
  systemPrompt = buildSystemPrompt("job");
257
295
  }
258
296
 
259
- const jobPrompt = job.prompt
297
+ let jobPrompt = job.prompt
260
298
  ? `Job: ${job.name} (schedule: ${job.schedule})\n\n${job.prompt}`
261
299
  : `Job: ${job.name} (schedule: ${job.schedule})\n\nExecute your scheduled tasks.`;
262
300
 
301
+ // Working memory: give stateful jobs a persistent workspace
302
+ jobPrompt += buildWorkingMemory(job.name, job.stateless);
303
+
263
304
  if (config.runner === "codex") {
264
305
  const fullPrompt = `${systemPrompt}\n\n---\n\n${jobPrompt}`;
265
306
  output = await runJobWithCodex(fullPrompt, cwd, agentModel || config.model);
@@ -0,0 +1,7 @@
1
+ import type postgres from "postgres";
2
+
3
+ export const name = "012_jobs_stateless";
4
+
5
+ export async function up(sql: postgres.Sql): Promise<void> {
6
+ await sql`ALTER TABLE jobs ADD COLUMN IF NOT EXISTS stateless BOOLEAN DEFAULT FALSE`;
7
+ }
@@ -38,6 +38,7 @@ export interface Job {
38
38
  always: boolean;
39
39
  scheduleType: ScheduleType;
40
40
  agent: string | null;
41
+ stateless: boolean;
41
42
  nextRunAt: string | null;
42
43
  lastRunAt: string | null;
43
44
  createdAt: string;
@@ -53,6 +54,7 @@ function toJob(r: Record<string, any>): Job {
53
54
  always: r.always ?? false,
54
55
  scheduleType: r.schedule_type || "cron",
55
56
  agent: r.agent || null,
57
+ stateless: r.stateless ?? false,
56
58
  nextRunAt: r.next_run_at ? String(r.next_run_at) : null,
57
59
  lastRunAt: r.last_run_at ? String(r.last_run_at) : null,
58
60
  createdAt: String(r.created_at),
@@ -73,6 +75,7 @@ export async function create(
73
75
  scheduleType: ScheduleType = "cron",
74
76
  nextRunAt?: Date,
75
77
  agent?: string,
78
+ stateless = false,
76
79
  ): Promise<void> {
77
80
  validateSchedule(schedule, scheduleType);
78
81
  const existing = await get(name);
@@ -81,27 +84,27 @@ export async function create(
81
84
  }
82
85
  const sql = getSql();
83
86
  await sql`
84
- INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent)
85
- VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null})
87
+ INSERT INTO jobs (name, schedule, prompt, always, schedule_type, next_run_at, agent, stateless)
88
+ VALUES (${name}, ${schedule}, ${prompt}, ${always}, ${scheduleType}, ${nextRunAt ?? null}, ${agent ?? null}, ${stateless})
86
89
  `;
87
90
  await notifyChange();
88
91
  }
89
92
 
90
93
  export async function list(): Promise<Job[]> {
91
94
  const sql = getSql();
92
- const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
95
+ const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs ORDER BY name`;
93
96
  return rows.map(toJob);
94
97
  }
95
98
 
96
99
  export async function get(name: string): Promise<Job | null> {
97
100
  const sql = getSql();
98
- const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
101
+ const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE name = ${name}`;
99
102
  return rows.length > 0 ? toJob(rows[0]) : null;
100
103
  }
101
104
 
102
105
  export async function update(
103
106
  name: string,
104
- fields: Partial<{ schedule: string; prompt: string; enabled: boolean; always: boolean; agent: string | null; scheduleType: ScheduleType }>,
107
+ fields: Partial<{ schedule: string; prompt: string; enabled: boolean; always: boolean; agent: string | null; stateless: boolean; scheduleType: ScheduleType }>,
105
108
  ): Promise<boolean> {
106
109
  const sql = getSql();
107
110
  const existing = await get(name);
@@ -113,6 +116,7 @@ export async function update(
113
116
  const enabled = fields.enabled ?? existing.enabled;
114
117
  const always = fields.always ?? existing.always;
115
118
  const agent = fields.agent !== undefined ? fields.agent : existing.agent;
119
+ const stateless = fields.stateless ?? existing.stateless;
116
120
 
117
121
  if (fields.schedule || fields.scheduleType) {
118
122
  validateSchedule(schedule, scheduleType);
@@ -120,7 +124,7 @@ export async function update(
120
124
 
121
125
  await sql`
122
126
  UPDATE jobs
123
- SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, updated_at = NOW()
127
+ SET schedule = ${schedule}, schedule_type = ${scheduleType}, prompt = ${prompt}, enabled = ${enabled}, always = ${always}, agent = ${agent}, stateless = ${stateless}, updated_at = NOW()
124
128
  WHERE name = ${name}
125
129
  `;
126
130
  await notifyChange();
@@ -136,14 +140,14 @@ export async function remove(name: string): Promise<boolean> {
136
140
 
137
141
  export async function listEnabled(): Promise<Job[]> {
138
142
  const sql = getSql();
139
- const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
143
+ const rows = await sql`SELECT name, schedule, prompt, enabled, always, schedule_type, agent, stateless, next_run_at, last_run_at, created_at, updated_at FROM jobs WHERE enabled = TRUE ORDER BY name`;
140
144
  return rows.map(toJob);
141
145
  }
142
146
 
143
147
  export async function listDue(): Promise<Job[]> {
144
148
  const sql = getSql();
145
149
  const rows = await sql`
146
- SELECT name, schedule, prompt, enabled, always, schedule_type, agent, next_run_at, last_run_at, created_at, updated_at
150
+ SELECT name, schedule, prompt, enabled, always, schedule_type, agent, stateless, next_run_at, last_run_at, created_at, updated_at
147
151
  FROM jobs
148
152
  WHERE enabled = TRUE AND next_run_at <= NOW()
149
153
  ORDER BY next_run_at
package/src/mcp/server.ts CHANGED
@@ -25,6 +25,7 @@ export function createNiaMcpServer() {
25
25
  schedule_type: z.enum(["cron", "interval", "once"]).default("cron").describe("Schedule type"),
26
26
  always: z.boolean().default(false).describe("If true, runs 24/7 ignoring active hours"),
27
27
  agent: z.string().optional().describe("Agent name to use for this job (loads agent's AGENT.md as system prompt)"),
28
+ stateless: z.boolean().default(false).describe("If true, disables working memory (no state.md injection or workspace)"),
28
29
  },
29
30
  async (args) => ({
30
31
  content: [{ type: "text" as const, text: await handlers.addJob(args) }],
@@ -32,13 +33,14 @@ export function createNiaMcpServer() {
32
33
  ),
33
34
  tool(
34
35
  "update_job",
35
- "Update an existing job's schedule, prompt, always flag, agent, or schedule_type. Only pass fields you want to change.",
36
+ "Update an existing job's schedule, prompt, always flag, agent, stateless, or schedule_type. Only pass fields you want to change.",
36
37
  {
37
38
  name: z.string().describe("Job name to update"),
38
39
  schedule: z.string().optional().describe("New schedule (cron expression, interval duration, or ISO timestamp)"),
39
40
  prompt: z.string().optional().describe("New prompt"),
40
41
  always: z.boolean().optional().describe("If true, runs 24/7 ignoring active hours"),
41
42
  agent: z.string().nullable().optional().describe("Agent name (set null to remove agent)"),
43
+ stateless: z.boolean().optional().describe("If true, disables working memory (no state.md injection or workspace)"),
42
44
  schedule_type: z.enum(["cron", "interval", "once"]).optional().describe("Schedule type (must match the schedule format)"),
43
45
  },
44
46
  async (args) => ({
package/src/mcp/tools.ts CHANGED
@@ -23,13 +23,15 @@ export async function addJob(args: {
23
23
  schedule_type?: ScheduleType;
24
24
  always?: boolean;
25
25
  agent?: string;
26
+ stateless?: boolean;
26
27
  }): Promise<string> {
27
28
  const scheduleType = args.schedule_type || "cron";
28
29
  const always = args.always || false;
30
+ const stateless = args.stateless || false;
29
31
  const config = getConfig();
30
32
 
31
33
  const nextRunAt = computeInitialNextRun(scheduleType, args.schedule, config.timezone);
32
- await Job.create(args.name, args.schedule, args.prompt, always, scheduleType, nextRunAt, args.agent);
34
+ await Job.create(args.name, args.schedule, args.prompt, always, scheduleType, nextRunAt, args.agent, stateless);
33
35
  const agentNote = args.agent ? ` [agent: ${args.agent}]` : "";
34
36
  return `Job "${args.name}" created (${scheduleType}: ${args.schedule})${agentNote}. Next run: ${nextRunAt.toISOString()}`;
35
37
  }
@@ -40,16 +42,18 @@ export async function updateJob(args: {
40
42
  prompt?: string;
41
43
  always?: boolean;
42
44
  agent?: string | null;
45
+ stateless?: boolean;
43
46
  schedule_type?: "cron" | "interval" | "once";
44
47
  }): Promise<string> {
45
- const fields: Partial<{ schedule: string; prompt: string; always: boolean; agent: string | null; scheduleType: "cron" | "interval" | "once" }> = {};
48
+ const fields: Partial<{ schedule: string; prompt: string; always: boolean; stateless: boolean; agent: string | null; scheduleType: "cron" | "interval" | "once" }> = {};
46
49
  if (args.schedule) fields.schedule = args.schedule;
47
50
  if (args.prompt) fields.prompt = args.prompt;
48
51
  if (args.always !== undefined) fields.always = args.always;
52
+ if (args.stateless !== undefined) fields.stateless = args.stateless;
49
53
  if (args.agent !== undefined) fields.agent = args.agent;
50
54
  if (args.schedule_type) fields.scheduleType = args.schedule_type;
51
55
 
52
- if (Object.keys(fields).length === 0) return "Nothing to update. Pass at least one field (schedule, prompt, always, agent, or schedule_type).";
56
+ if (Object.keys(fields).length === 0) return "Nothing to update. Pass at least one field (schedule, prompt, always, stateless, agent, or schedule_type).";
53
57
 
54
58
  const updated = await Job.update(args.name, fields);
55
59
  if (!updated) return `Job "${args.name}" not found.`;
@@ -25,7 +25,8 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
25
25
  - `interval`: duration string (e.g., "5m", "2h", "1d" = every 5 min/2 hours/1 day)
26
26
  - `once`: ISO timestamp for one-time execution (e.g., "2026-03-14T10:00:00")
27
27
  - Set `always: true` to run 24/7 (ignores active hours)
28
- - **update_job** update an existing job's schedule, prompt, or always flag
28
+ - Set `stateless: true` to disable working memory (no state.md or workspace)
29
+ - **update_job** — update an existing job's schedule, prompt, always, stateless, or agent
29
30
  - **remove_job** — delete a job by name
30
31
  - **enable_job** / **disable_job** — toggle a job on or off
31
32
  - **run_job** — trigger a job to run immediately
@@ -43,6 +44,14 @@ You have MCP tools for managing jobs directly (preferred over CLI for speed):
43
44
 
44
45
  Active hours: {{activeStart}}–{{activeEnd}} ({{timezone}}). Jobs respect this; crons (always=true) don't.
45
46
 
47
+ ### Job Working Memory
48
+
49
+ Jobs are **stateful by default**. Each job gets a persistent workspace at `~/.niahere/jobs/<job-name>/`. Before each run, the runner reads `state.md` from that directory and injects it into the prompt. The agent should update `state.md` at the end of each run with what it did, what it noticed, and what to focus on next time.
50
+
51
+ The workspace is freeform — the agent can create any files it needs (data, cache, history, etc.). `state.md` is the convention for the runner to inject automatically; everything else is the agent's to organize.
52
+
53
+ To disable working memory for a specific job, set `stateless: true` when creating or updating it.
54
+
46
55
  ## Managing Config
47
56
 
48
57
  Config file: `{{configPath}}`
package/src/types/job.ts CHANGED
@@ -5,6 +5,7 @@ export interface JobInput {
5
5
  schedule: string;
6
6
  prompt: string;
7
7
  agent?: string | null;
8
+ stateless?: boolean;
8
9
  }
9
10
 
10
11
  export interface JobResult {