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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/skills/ai-seo/SKILL.md +398 -0
- package/skills/ai-seo/evals/evals.json +90 -0
- package/skills/ai-seo/references/content-patterns.md +285 -0
- package/skills/ai-seo/references/platform-ranking-factors.md +152 -0
- package/skills/aws-cli/skill.md +198 -0
- package/skills/churn-prevention/SKILL.md +424 -0
- package/skills/churn-prevention/evals/evals.json +93 -0
- package/skills/churn-prevention/references/cancel-flow-patterns.md +316 -0
- package/skills/churn-prevention/references/dunning-playbook.md +408 -0
- package/skills/cold-email/SKILL.md +158 -0
- package/skills/cold-email/evals/evals.json +94 -0
- package/skills/cold-email/references/benchmarks.md +83 -0
- package/skills/cold-email/references/follow-up-sequences.md +81 -0
- package/skills/cold-email/references/frameworks.md +90 -0
- package/skills/cold-email/references/personalization.md +79 -0
- package/skills/cold-email/references/subject-lines.md +53 -0
- package/skills/competitor-alternatives/SKILL.md +256 -0
- package/skills/competitor-alternatives/evals/evals.json +93 -0
- package/skills/competitor-alternatives/references/content-architecture.md +271 -0
- package/skills/competitor-alternatives/references/templates.md +223 -0
- package/skills/copy-editing/SKILL.md +447 -0
- package/skills/copy-editing/evals/evals.json +89 -0
- package/skills/copy-editing/references/plain-english-alternatives.md +394 -0
- package/skills/copywriting/SKILL.md +252 -0
- package/skills/copywriting/evals/evals.json +111 -0
- package/skills/copywriting/references/copy-frameworks.md +344 -0
- package/skills/copywriting/references/natural-transitions.md +272 -0
- package/skills/customer-research/SKILL.md +269 -0
- package/skills/customer-research/evals/evals.json +162 -0
- package/skills/customer-research/references/source-guides.md +345 -0
- package/skills/email-sequence/SKILL.md +311 -0
- package/skills/email-sequence/evals/evals.json +93 -0
- package/skills/email-sequence/references/copy-guidelines.md +113 -0
- package/skills/email-sequence/references/email-types.md +515 -0
- package/skills/email-sequence/references/sequence-templates.md +168 -0
- package/skills/google-workspace-cli/SKILL.md +157 -0
- package/skills/launch-strategy/SKILL.md +353 -0
- package/skills/launch-strategy/evals/evals.json +91 -0
- package/skills/minimalist-review/SKILL.md +82 -0
- package/skills/onboarding-cro/SKILL.md +220 -0
- package/skills/onboarding-cro/evals/evals.json +92 -0
- package/skills/onboarding-cro/references/experiments.md +258 -0
- package/skills/page-cro/SKILL.md +182 -0
- package/skills/page-cro/evals/evals.json +111 -0
- package/skills/page-cro/references/experiments.md +248 -0
- package/skills/product-marketing-context/SKILL.md +241 -0
- package/skills/product-marketing-context/evals/evals.json +85 -0
- package/skills/seo-audit/SKILL.md +412 -0
- package/skills/seo-audit/evals/evals.json +136 -0
- package/skills/seo-audit/references/ai-writing-detection.md +200 -0
- package/skills/signup-flow-cro/SKILL.md +359 -0
- package/skills/signup-flow-cro/evals/evals.json +88 -0
- package/skills/social-content/SKILL.md +278 -0
- package/skills/social-content/evals/evals.json +92 -0
- package/skills/social-content/references/platforms.md +170 -0
- package/skills/social-content/references/post-templates.md +177 -0
- package/skills/social-content/references/reverse-engineering.md +195 -0
- package/src/channels/slack.ts +1 -1
- package/src/cli/job.ts +10 -3
- package/src/core/runner.ts +43 -2
- package/src/db/migrations/012_jobs_stateless.ts +7 -0
- package/src/db/models/job.ts +12 -8
- package/src/mcp/server.ts +3 -1
- package/src/mcp/tools.ts +7 -3
- package/src/prompts/environment.md +10 -1
- 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
|
package/src/channels/slack.ts
CHANGED
|
@@ -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();
|
package/src/core/runner.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/db/models/job.ts
CHANGED
|
@@ -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.`;
|