niahere 0.2.52 → 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.
Files changed (68) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/skills/ai-seo/SKILL.md +398 -0
  4. package/skills/ai-seo/evals/evals.json +90 -0
  5. package/skills/ai-seo/references/content-patterns.md +285 -0
  6. package/skills/ai-seo/references/platform-ranking-factors.md +152 -0
  7. package/skills/aws-cli/skill.md +198 -0
  8. package/skills/churn-prevention/SKILL.md +424 -0
  9. package/skills/churn-prevention/evals/evals.json +93 -0
  10. package/skills/churn-prevention/references/cancel-flow-patterns.md +316 -0
  11. package/skills/churn-prevention/references/dunning-playbook.md +408 -0
  12. package/skills/cold-email/SKILL.md +158 -0
  13. package/skills/cold-email/evals/evals.json +94 -0
  14. package/skills/cold-email/references/benchmarks.md +83 -0
  15. package/skills/cold-email/references/follow-up-sequences.md +81 -0
  16. package/skills/cold-email/references/frameworks.md +90 -0
  17. package/skills/cold-email/references/personalization.md +79 -0
  18. package/skills/cold-email/references/subject-lines.md +53 -0
  19. package/skills/competitor-alternatives/SKILL.md +256 -0
  20. package/skills/competitor-alternatives/evals/evals.json +93 -0
  21. package/skills/competitor-alternatives/references/content-architecture.md +271 -0
  22. package/skills/competitor-alternatives/references/templates.md +223 -0
  23. package/skills/copy-editing/SKILL.md +447 -0
  24. package/skills/copy-editing/evals/evals.json +89 -0
  25. package/skills/copy-editing/references/plain-english-alternatives.md +394 -0
  26. package/skills/copywriting/SKILL.md +252 -0
  27. package/skills/copywriting/evals/evals.json +111 -0
  28. package/skills/copywriting/references/copy-frameworks.md +344 -0
  29. package/skills/copywriting/references/natural-transitions.md +272 -0
  30. package/skills/customer-research/SKILL.md +269 -0
  31. package/skills/customer-research/evals/evals.json +162 -0
  32. package/skills/customer-research/references/source-guides.md +345 -0
  33. package/skills/email-sequence/SKILL.md +311 -0
  34. package/skills/email-sequence/evals/evals.json +93 -0
  35. package/skills/email-sequence/references/copy-guidelines.md +113 -0
  36. package/skills/email-sequence/references/email-types.md +515 -0
  37. package/skills/email-sequence/references/sequence-templates.md +168 -0
  38. package/skills/google-workspace-cli/SKILL.md +157 -0
  39. package/skills/launch-strategy/SKILL.md +353 -0
  40. package/skills/launch-strategy/evals/evals.json +91 -0
  41. package/skills/minimalist-review/SKILL.md +82 -0
  42. package/skills/onboarding-cro/SKILL.md +220 -0
  43. package/skills/onboarding-cro/evals/evals.json +92 -0
  44. package/skills/onboarding-cro/references/experiments.md +258 -0
  45. package/skills/page-cro/SKILL.md +182 -0
  46. package/skills/page-cro/evals/evals.json +111 -0
  47. package/skills/page-cro/references/experiments.md +248 -0
  48. package/skills/product-marketing-context/SKILL.md +241 -0
  49. package/skills/product-marketing-context/evals/evals.json +85 -0
  50. package/skills/seo-audit/SKILL.md +412 -0
  51. package/skills/seo-audit/evals/evals.json +136 -0
  52. package/skills/seo-audit/references/ai-writing-detection.md +200 -0
  53. package/skills/signup-flow-cro/SKILL.md +359 -0
  54. package/skills/signup-flow-cro/evals/evals.json +88 -0
  55. package/skills/social-content/SKILL.md +278 -0
  56. package/skills/social-content/evals/evals.json +92 -0
  57. package/skills/social-content/references/platforms.md +170 -0
  58. package/skills/social-content/references/post-templates.md +177 -0
  59. package/skills/social-content/references/reverse-engineering.md +195 -0
  60. package/src/channels/slack.ts +1 -1
  61. package/src/cli/job.ts +10 -3
  62. package/src/core/runner.ts +43 -2
  63. package/src/db/migrations/012_jobs_stateless.ts +7 -0
  64. package/src/db/models/job.ts +12 -8
  65. package/src/mcp/server.ts +3 -1
  66. package/src/mcp/tools.ts +7 -3
  67. package/src/prompts/environment.md +10 -1
  68. package/src/types/job.ts +1 -0
@@ -0,0 +1,177 @@
1
+ # Post Format Templates
2
+
3
+ Ready-to-use templates for different platforms and content types.
4
+
5
+ ## Contents
6
+ - LinkedIn Post Templates (The Story Post, The Contrarian Take, The List Post, The How-To)
7
+ - Twitter/X Thread Templates (The Tutorial Thread, The Story Thread, The Breakdown Thread)
8
+ - Instagram Templates (The Carousel Hook, The Reel Script)
9
+ - Hook Formulas (Curiosity Hooks, Story Hooks, Value Hooks, Contrarian Hooks, Social Proof Hooks)
10
+
11
+ ## LinkedIn Post Templates
12
+
13
+ ### The Story Post
14
+ ```
15
+ [Hook: Unexpected outcome or lesson]
16
+
17
+ [Set the scene: When/where this happened]
18
+
19
+ [The challenge you faced]
20
+
21
+ [What you tried / what happened]
22
+
23
+ [The turning point]
24
+
25
+ [The result]
26
+
27
+ [The lesson for readers]
28
+
29
+ [Question to prompt engagement]
30
+ ```
31
+
32
+ ### The Contrarian Take
33
+ ```
34
+ [Unpopular opinion stated boldly]
35
+
36
+ Here's why:
37
+
38
+ [Reason 1]
39
+ [Reason 2]
40
+ [Reason 3]
41
+
42
+ [What you recommend instead]
43
+
44
+ [Invite discussion: "Am I wrong?"]
45
+ ```
46
+
47
+ ### The List Post
48
+ ```
49
+ [X things I learned about [topic] after [credibility builder]:
50
+
51
+ 1. [Point] — [Brief explanation]
52
+
53
+ 2. [Point] — [Brief explanation]
54
+
55
+ 3. [Point] — [Brief explanation]
56
+
57
+ [Wrap-up insight]
58
+
59
+ Which resonates most with you?
60
+ ```
61
+
62
+ ### The How-To
63
+ ```
64
+ How to [achieve outcome] in [timeframe]:
65
+
66
+ Step 1: [Action]
67
+ ↳ [Why this matters]
68
+
69
+ Step 2: [Action]
70
+ ↳ [Key detail]
71
+
72
+ Step 3: [Action]
73
+ ↳ [Common mistake to avoid]
74
+
75
+ [Result you can expect]
76
+
77
+ [CTA or question]
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Twitter/X Thread Templates
83
+
84
+ ### The Tutorial Thread
85
+ ```
86
+ Tweet 1: [Hook + promise of value]
87
+
88
+ "Here's exactly how to [outcome] (step-by-step):"
89
+
90
+ Tweet 2-7: [One step per tweet with details]
91
+
92
+ Final tweet: [Summary + CTA]
93
+
94
+ "If this was helpful, follow me for more on [topic]"
95
+ ```
96
+
97
+ ### The Story Thread
98
+ ```
99
+ Tweet 1: [Intriguing hook]
100
+
101
+ "[Time] ago, [unexpected thing happened]. Here's the full story:"
102
+
103
+ Tweet 2-6: [Story beats, building tension]
104
+
105
+ Tweet 7: [Resolution and lesson]
106
+
107
+ Final tweet: [Takeaway + engagement ask]
108
+ ```
109
+
110
+ ### The Breakdown Thread
111
+ ```
112
+ Tweet 1: [Company/person] just [did thing].
113
+
114
+ Here's why it's genius (and what you can learn):
115
+
116
+ Tweet 2-6: [Analysis points]
117
+
118
+ Tweet 7: [Your key takeaway]
119
+
120
+ "[Related insight + follow CTA]"
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Instagram Templates
126
+
127
+ ### The Carousel Hook
128
+ ```
129
+ [Slide 1: Bold statement or question]
130
+ [Slides 2-9: One point per slide, visual + text]
131
+ [Slide 10: Summary + CTA]
132
+
133
+ Caption: [Expand on the topic, add context, include CTA]
134
+ ```
135
+
136
+ ### The Reel Script
137
+ ```
138
+ Hook (0-2 sec): [Pattern interrupt or bold claim]
139
+ Setup (2-5 sec): [Context for the tip]
140
+ Value (5-25 sec): [The actual advice/content]
141
+ CTA (25-30 sec): [Follow, comment, share, link]
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Hook Formulas
147
+
148
+ The first line determines whether anyone reads the rest.
149
+
150
+ ### Curiosity Hooks
151
+ - "I was wrong about [common belief]."
152
+ - "The real reason [outcome] happens isn't what you think."
153
+ - "[Impressive result] — and it only took [surprisingly short time]."
154
+ - "Nobody talks about [insider knowledge]."
155
+
156
+ ### Story Hooks
157
+ - "Last week, [unexpected thing] happened."
158
+ - "I almost [big mistake/failure]."
159
+ - "3 years ago, I [past state]. Today, [current state]."
160
+ - "[Person] told me something I'll never forget."
161
+
162
+ ### Value Hooks
163
+ - "How to [desirable outcome] (without [common pain]):"
164
+ - "[Number] [things] that [outcome]:"
165
+ - "The simplest way to [outcome]:"
166
+ - "Stop [common mistake]. Do this instead:"
167
+
168
+ ### Contrarian Hooks
169
+ - "Unpopular opinion: [bold statement]"
170
+ - "[Common advice] is wrong. Here's why:"
171
+ - "I stopped [common practice] and [positive result]."
172
+ - "Everyone says [X]. The truth is [Y]."
173
+
174
+ ### Social Proof Hooks
175
+ - "We [achieved result] in [timeframe]. Here's the full story:"
176
+ - "[Number] people asked me about [topic]. Here's my answer:"
177
+ - "[Authority figure] taught me [lesson]."
@@ -0,0 +1,195 @@
1
+ # Reverse Engineering Viral Content
2
+
3
+ Instead of guessing what works, systematically analyze top-performing content in your niche and extract proven patterns.
4
+
5
+ ## Contents
6
+ - The 6-Step Framework (Niche ID, Scrape, Analyze, Playbook, Layer Voice, Convert)
7
+ - The Formula
8
+ - Reverse Engineering Checklist
9
+
10
+ ## The 6-Step Framework
11
+
12
+ ### 1. NICHE ID — Find Top Creators
13
+
14
+ Identify 10-20 creators in your space who consistently get high engagement:
15
+
16
+ **Selection criteria:**
17
+ - Posting consistently (3+ times/week)
18
+ - High engagement rate relative to follower count
19
+ - Audience overlap with your target market
20
+ - Mix of established and rising creators
21
+
22
+ **Where to find them:**
23
+ - LinkedIn: Search by industry keywords, check "People also viewed"
24
+ - Twitter/X: Check who your target audience follows and engages with
25
+ - Use tools like SparkToro, Followerwonk, or manual research
26
+ - Look at who gets featured in industry newsletters
27
+
28
+ ### 2. SCRAPE — Collect Posts at Scale
29
+
30
+ Gather 500-1000+ posts from your identified creators for analysis:
31
+
32
+ **Tools:**
33
+ - **Apify** — LinkedIn scraper, Twitter scraper actors
34
+ - **Phantom Buster** — Multi-platform automation
35
+ - **Export tools** — Platform-specific export features
36
+ - **Manual collection** — For smaller datasets, copy/paste into spreadsheet
37
+
38
+ **Data to collect:**
39
+ - Post text/content
40
+ - Engagement metrics (likes, comments, shares, saves)
41
+ - Post format (text-only, carousel, video, image)
42
+ - Posting time/day
43
+ - Hook/first line
44
+ - CTA used
45
+ - Topic/theme
46
+
47
+ ### 3. ANALYZE — Extract What Actually Works
48
+
49
+ Sort and analyze the data to find patterns:
50
+
51
+ **Quantitative analysis:**
52
+ - Rank posts by engagement rate
53
+ - Identify top 10% performers
54
+ - Look for format patterns (do carousels outperform?)
55
+ - Check timing patterns (best days/times)
56
+ - Compare topic performance
57
+
58
+ **Qualitative analysis:**
59
+ - What hooks do top posts use?
60
+ - How long are high-performing posts?
61
+ - What emotional triggers appear?
62
+ - What formats repeat?
63
+ - What topics consistently perform?
64
+
65
+ **Questions to answer:**
66
+ - What's the average length of top posts?
67
+ - Which hook types appear most in top 10%?
68
+ - What CTAs drive most comments?
69
+ - What topics get saved/shared most?
70
+
71
+ ### 4. PLAYBOOK — Codify Patterns
72
+
73
+ Document repeatable patterns you can use:
74
+
75
+ **Hook patterns to codify:**
76
+ ```
77
+ Pattern: "I [unexpected action] and [surprising result]"
78
+ Example: "I stopped posting daily and my engagement doubled"
79
+ Why it works: Curiosity gap + contrarian
80
+
81
+ Pattern: "[Specific number] [things] that [outcome]:"
82
+ Example: "7 pricing mistakes that cost me $50K:"
83
+ Why it works: Specificity + loss aversion
84
+
85
+ Pattern: "[Controversial take]"
86
+ Example: "Cold outreach is dead."
87
+ Why it works: Pattern interrupt + invites debate
88
+ ```
89
+
90
+ **Format patterns:**
91
+ - Carousel: Hook slide → Problem → Solution steps → CTA
92
+ - Thread: Hook → Promise → Deliver → Recap → CTA
93
+ - Story post: Hook → Setup → Conflict → Resolution → Lesson
94
+
95
+ **CTA patterns:**
96
+ - Question: "What would you add?"
97
+ - Agreement: "Agree or disagree?"
98
+ - Share: "Tag someone who needs this"
99
+ - Save: "Save this for later"
100
+
101
+ ### 5. LAYER VOICE — Apply Direct Response Principles
102
+
103
+ Take proven patterns and make them yours with these voice principles:
104
+
105
+ **"Smart friend who figured something out"**
106
+ - Write like you're texting advice to a friend
107
+ - Share discoveries, not lectures
108
+ - Use "I found that..." not "You should..."
109
+ - Be helpful, not preachy
110
+
111
+ **Specific > Vague**
112
+ ```
113
+ ❌ "I made good revenue"
114
+ ✅ "I made $47,329"
115
+
116
+ ❌ "It took a while"
117
+ ✅ "It took 47 days"
118
+
119
+ ❌ "A lot of people"
120
+ ✅ "2,847 people"
121
+ ```
122
+
123
+ **Short. Breathe. Land.**
124
+ - One idea per sentence
125
+ - Use line breaks liberally
126
+ - Let important points stand alone
127
+ - Create rhythm: short, short, longer explanation
128
+
129
+ ```
130
+ ❌ "I spent three years building my business the wrong way before I finally realized that the key to success was focusing on fewer things and doing them exceptionally well."
131
+
132
+ ✅ "I built wrong for 3 years.
133
+
134
+ Then I figured it out.
135
+
136
+ Focus on less.
137
+ Do it exceptionally well.
138
+
139
+ Everything changed."
140
+ ```
141
+
142
+ **Write from emotion**
143
+ - Start with how you felt, not what you did
144
+ - Use emotional words: frustrated, excited, terrified, obsessed
145
+ - Show vulnerability when authentic
146
+ - Connect the feeling to the lesson
147
+
148
+ ```
149
+ ❌ "Here's what I learned about pricing"
150
+
151
+ ✅ "I was terrified to raise my prices.
152
+
153
+ My hands were shaking when I sent the email.
154
+
155
+ Here's what happened..."
156
+ ```
157
+
158
+ ### 6. CONVERT — Turn Attention into Action
159
+
160
+ Bridge from engagement to business results:
161
+
162
+ **Soft conversions:**
163
+ - Newsletter signups in bio/comments
164
+ - Free resource offers in follow-up comments
165
+ - DM triggers ("Comment X and I'll send you...")
166
+ - Profile visits → optimized profile with clear CTA
167
+
168
+ **Direct conversions:**
169
+ - Link in comments (not post body on LinkedIn)
170
+ - Contextual product mentions within valuable content
171
+ - Case study posts that naturally showcase your work
172
+ - "If you want help with this, DM me" (sparingly)
173
+
174
+ ---
175
+
176
+ ## The Formula
177
+
178
+ ```
179
+ 1. Find what's already working (don't guess)
180
+ 2. Extract the patterns (hooks, formats, CTAs)
181
+ 3. Layer your authentic voice on top
182
+ 4. Test and iterate based on your own data
183
+ ```
184
+
185
+ ## Reverse Engineering Checklist
186
+
187
+ - [ ] Identified 10-20 top creators in niche
188
+ - [ ] Collected 500+ posts for analysis
189
+ - [ ] Ranked by engagement rate
190
+ - [ ] Documented top 10 hook patterns
191
+ - [ ] Documented top 5 format patterns
192
+ - [ ] Documented top 5 CTA patterns
193
+ - [ ] Created voice guidelines (specificity, brevity, emotion)
194
+ - [ ] Built template library from patterns
195
+ - [ ] Set up tracking for your own content performance
@@ -304,7 +304,7 @@ class SlackChannel implements Channel {
304
304
 
305
305
  // Handle messages (DMs + @mentions)
306
306
  app.message(async ({ message, say, client }) => {
307
- if (message.subtype) return;
307
+ if (message.subtype && message.subtype !== "file_share") return;
308
308
  const msg = message as {
309
309
  text?: string;
310
310
  channel: string;
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.`;