jfl 0.1.0 → 0.2.0

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 (262) hide show
  1. package/README.md +443 -145
  2. package/clawdbot-plugin/clawdbot.plugin.json +20 -0
  3. package/clawdbot-plugin/index.js +555 -0
  4. package/clawdbot-plugin/index.ts +582 -0
  5. package/clawdbot-skill/SKILL.md +33 -336
  6. package/clawdbot-skill/index.ts +491 -321
  7. package/clawdbot-skill/skill.json +4 -13
  8. package/dist/commands/clawdbot.d.ts +11 -0
  9. package/dist/commands/clawdbot.d.ts.map +1 -0
  10. package/dist/commands/clawdbot.js +215 -0
  11. package/dist/commands/clawdbot.js.map +1 -0
  12. package/dist/commands/context-hub.d.ts +5 -0
  13. package/dist/commands/context-hub.d.ts.map +1 -1
  14. package/dist/commands/context-hub.js +394 -28
  15. package/dist/commands/context-hub.js.map +1 -1
  16. package/dist/commands/gtm-process-update.d.ts +10 -0
  17. package/dist/commands/gtm-process-update.d.ts.map +1 -0
  18. package/dist/commands/gtm-process-update.js +101 -0
  19. package/dist/commands/gtm-process-update.js.map +1 -0
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +278 -4
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/login.d.ts.map +1 -1
  24. package/dist/commands/login.js +32 -33
  25. package/dist/commands/login.js.map +1 -1
  26. package/dist/commands/memory.d.ts +38 -0
  27. package/dist/commands/memory.d.ts.map +1 -0
  28. package/dist/commands/memory.js +229 -0
  29. package/dist/commands/memory.js.map +1 -0
  30. package/dist/commands/migrate-services.d.ts +8 -0
  31. package/dist/commands/migrate-services.d.ts.map +1 -0
  32. package/dist/commands/migrate-services.js +182 -0
  33. package/dist/commands/migrate-services.js.map +1 -0
  34. package/dist/commands/onboard.d.ts +24 -0
  35. package/dist/commands/onboard.d.ts.map +1 -0
  36. package/dist/commands/onboard.js +663 -0
  37. package/dist/commands/onboard.js.map +1 -0
  38. package/dist/commands/openclaw.d.ts +56 -0
  39. package/dist/commands/openclaw.d.ts.map +1 -0
  40. package/dist/commands/openclaw.js +700 -0
  41. package/dist/commands/openclaw.js.map +1 -0
  42. package/dist/commands/orchestrate.d.ts +14 -0
  43. package/dist/commands/orchestrate.d.ts.map +1 -0
  44. package/dist/commands/orchestrate.js +270 -0
  45. package/dist/commands/orchestrate.js.map +1 -0
  46. package/dist/commands/profile.d.ts +46 -0
  47. package/dist/commands/profile.d.ts.map +1 -0
  48. package/dist/commands/profile.js +498 -0
  49. package/dist/commands/profile.js.map +1 -0
  50. package/dist/commands/repair.d.ts.map +1 -1
  51. package/dist/commands/repair.js +37 -0
  52. package/dist/commands/repair.js.map +1 -1
  53. package/dist/commands/service-agent.d.ts +16 -0
  54. package/dist/commands/service-agent.d.ts.map +1 -0
  55. package/dist/commands/service-agent.js +375 -0
  56. package/dist/commands/service-agent.js.map +1 -0
  57. package/dist/commands/service-manager.d.ts +12 -0
  58. package/dist/commands/service-manager.d.ts.map +1 -0
  59. package/dist/commands/service-manager.js +967 -0
  60. package/dist/commands/service-manager.js.map +1 -0
  61. package/dist/commands/service-validate.d.ts +12 -0
  62. package/dist/commands/service-validate.d.ts.map +1 -0
  63. package/dist/commands/service-validate.js +611 -0
  64. package/dist/commands/service-validate.js.map +1 -0
  65. package/dist/commands/services-create.d.ts +15 -0
  66. package/dist/commands/services-create.d.ts.map +1 -0
  67. package/dist/commands/services-create.js +1452 -0
  68. package/dist/commands/services-create.js.map +1 -0
  69. package/dist/commands/services-scan.d.ts +13 -0
  70. package/dist/commands/services-scan.d.ts.map +1 -0
  71. package/dist/commands/services-scan.js +251 -0
  72. package/dist/commands/services-scan.js.map +1 -0
  73. package/dist/commands/services-sync-agents.d.ts +23 -0
  74. package/dist/commands/services-sync-agents.d.ts.map +1 -0
  75. package/dist/commands/services-sync-agents.js +207 -0
  76. package/dist/commands/services-sync-agents.js.map +1 -0
  77. package/dist/commands/services.d.ts +19 -0
  78. package/dist/commands/services.d.ts.map +1 -0
  79. package/dist/commands/services.js +742 -0
  80. package/dist/commands/services.js.map +1 -0
  81. package/dist/commands/session.d.ts +5 -1
  82. package/dist/commands/session.d.ts.map +1 -1
  83. package/dist/commands/session.js +68 -586
  84. package/dist/commands/session.js.map +1 -1
  85. package/dist/commands/status.d.ts.map +1 -1
  86. package/dist/commands/status.js +17 -0
  87. package/dist/commands/status.js.map +1 -1
  88. package/dist/commands/update.d.ts.map +1 -1
  89. package/dist/commands/update.js +75 -21
  90. package/dist/commands/update.js.map +1 -1
  91. package/dist/commands/validate-settings.d.ts +37 -0
  92. package/dist/commands/validate-settings.d.ts.map +1 -0
  93. package/dist/commands/validate-settings.js +197 -0
  94. package/dist/commands/validate-settings.js.map +1 -0
  95. package/dist/commands/voice.d.ts +0 -1
  96. package/dist/commands/voice.d.ts.map +1 -1
  97. package/dist/commands/voice.js +16 -15
  98. package/dist/commands/voice.js.map +1 -1
  99. package/dist/index.js +395 -141
  100. package/dist/index.js.map +1 -1
  101. package/dist/lib/agent-generator.d.ts +26 -0
  102. package/dist/lib/agent-generator.d.ts.map +1 -0
  103. package/dist/lib/agent-generator.js +331 -0
  104. package/dist/lib/agent-generator.js.map +1 -0
  105. package/dist/lib/memory-db.d.ts +102 -0
  106. package/dist/lib/memory-db.d.ts.map +1 -0
  107. package/dist/lib/memory-db.js +313 -0
  108. package/dist/lib/memory-db.js.map +1 -0
  109. package/dist/lib/memory-indexer.d.ts +47 -0
  110. package/dist/lib/memory-indexer.d.ts.map +1 -0
  111. package/dist/lib/memory-indexer.js +215 -0
  112. package/dist/lib/memory-indexer.js.map +1 -0
  113. package/dist/lib/memory-search.d.ts +41 -0
  114. package/dist/lib/memory-search.d.ts.map +1 -0
  115. package/dist/lib/memory-search.js +246 -0
  116. package/dist/lib/memory-search.js.map +1 -0
  117. package/dist/lib/openclaw-registry.d.ts +48 -0
  118. package/dist/lib/openclaw-registry.d.ts.map +1 -0
  119. package/dist/lib/openclaw-registry.js +181 -0
  120. package/dist/lib/openclaw-registry.js.map +1 -0
  121. package/dist/lib/openclaw-sdk.d.ts +107 -0
  122. package/dist/lib/openclaw-sdk.d.ts.map +1 -0
  123. package/dist/lib/openclaw-sdk.js +208 -0
  124. package/dist/lib/openclaw-sdk.js.map +1 -0
  125. package/dist/lib/peer-agent-generator.d.ts +44 -0
  126. package/dist/lib/peer-agent-generator.d.ts.map +1 -0
  127. package/dist/lib/peer-agent-generator.js +286 -0
  128. package/dist/lib/peer-agent-generator.js.map +1 -0
  129. package/dist/lib/service-dependencies.d.ts +44 -0
  130. package/dist/lib/service-dependencies.d.ts.map +1 -0
  131. package/dist/lib/service-dependencies.js +314 -0
  132. package/dist/lib/service-dependencies.js.map +1 -0
  133. package/dist/lib/service-detector.d.ts +61 -0
  134. package/dist/lib/service-detector.d.ts.map +1 -0
  135. package/dist/lib/service-detector.js +521 -0
  136. package/dist/lib/service-detector.js.map +1 -0
  137. package/dist/lib/service-gtm.d.ts +157 -0
  138. package/dist/lib/service-gtm.d.ts.map +1 -0
  139. package/dist/lib/service-gtm.js +786 -0
  140. package/dist/lib/service-gtm.js.map +1 -0
  141. package/dist/lib/service-mcp-base.d.ts +103 -0
  142. package/dist/lib/service-mcp-base.d.ts.map +1 -0
  143. package/dist/lib/service-mcp-base.js +263 -0
  144. package/dist/lib/service-mcp-base.js.map +1 -0
  145. package/dist/lib/service-utils.d.ts +103 -0
  146. package/dist/lib/service-utils.d.ts.map +1 -0
  147. package/dist/lib/service-utils.js +368 -0
  148. package/dist/lib/service-utils.js.map +1 -0
  149. package/dist/lib/skill-generator.d.ts +21 -0
  150. package/dist/lib/skill-generator.d.ts.map +1 -0
  151. package/dist/lib/skill-generator.js +253 -0
  152. package/dist/lib/skill-generator.js.map +1 -0
  153. package/dist/lib/stratus-client.d.ts +100 -0
  154. package/dist/lib/stratus-client.d.ts.map +1 -0
  155. package/dist/lib/stratus-client.js +255 -0
  156. package/dist/lib/stratus-client.js.map +1 -0
  157. package/dist/mcp/context-hub-mcp.js +135 -53
  158. package/dist/mcp/context-hub-mcp.js.map +1 -1
  159. package/dist/mcp/service-mcp-server.d.ts +12 -0
  160. package/dist/mcp/service-mcp-server.d.ts.map +1 -0
  161. package/dist/mcp/service-mcp-server.js +434 -0
  162. package/dist/mcp/service-mcp-server.js.map +1 -0
  163. package/dist/mcp/service-peer-mcp.d.ts +36 -0
  164. package/dist/mcp/service-peer-mcp.d.ts.map +1 -0
  165. package/dist/mcp/service-peer-mcp.js +220 -0
  166. package/dist/mcp/service-peer-mcp.js.map +1 -0
  167. package/dist/mcp/service-registry-mcp.d.ts +13 -0
  168. package/dist/mcp/service-registry-mcp.d.ts.map +1 -0
  169. package/dist/mcp/service-registry-mcp.js +330 -0
  170. package/dist/mcp/service-registry-mcp.js.map +1 -0
  171. package/dist/ui/banner.js +1 -1
  172. package/dist/ui/banner.js.map +1 -1
  173. package/dist/ui/context-hub-logs.d.ts +10 -0
  174. package/dist/ui/context-hub-logs.d.ts.map +1 -0
  175. package/dist/ui/context-hub-logs.js +175 -0
  176. package/dist/ui/context-hub-logs.js.map +1 -0
  177. package/dist/ui/service-dashboard.d.ts +11 -0
  178. package/dist/ui/service-dashboard.d.ts.map +1 -0
  179. package/dist/ui/service-dashboard.js +357 -0
  180. package/dist/ui/service-dashboard.js.map +1 -0
  181. package/dist/ui/services-manager.d.ts +11 -0
  182. package/dist/ui/services-manager.d.ts.map +1 -0
  183. package/dist/ui/services-manager.js +507 -0
  184. package/dist/ui/services-manager.js.map +1 -0
  185. package/dist/utils/auth-guard.d.ts.map +1 -1
  186. package/dist/utils/auth-guard.js +8 -9
  187. package/dist/utils/auth-guard.js.map +1 -1
  188. package/dist/utils/claude-md-generator.d.ts +10 -0
  189. package/dist/utils/claude-md-generator.d.ts.map +1 -0
  190. package/dist/utils/claude-md-generator.js +215 -0
  191. package/dist/utils/claude-md-generator.js.map +1 -0
  192. package/dist/utils/ensure-context-hub.d.ts +20 -0
  193. package/dist/utils/ensure-context-hub.d.ts.map +1 -0
  194. package/dist/utils/ensure-context-hub.js +65 -0
  195. package/dist/utils/ensure-context-hub.js.map +1 -0
  196. package/dist/utils/ensure-project.d.ts.map +1 -1
  197. package/dist/utils/ensure-project.js +3 -4
  198. package/dist/utils/ensure-project.js.map +1 -1
  199. package/dist/utils/jfl-config.d.ts +19 -0
  200. package/dist/utils/jfl-config.d.ts.map +1 -0
  201. package/dist/utils/jfl-config.js +112 -0
  202. package/dist/utils/jfl-config.js.map +1 -0
  203. package/dist/utils/jfl-migration.d.ts +29 -0
  204. package/dist/utils/jfl-migration.d.ts.map +1 -0
  205. package/dist/utils/jfl-migration.js +142 -0
  206. package/dist/utils/jfl-migration.js.map +1 -0
  207. package/dist/utils/jfl-paths.d.ts +55 -0
  208. package/dist/utils/jfl-paths.d.ts.map +1 -0
  209. package/dist/utils/jfl-paths.js +120 -0
  210. package/dist/utils/jfl-paths.js.map +1 -0
  211. package/dist/utils/settings-validator.d.ts +73 -0
  212. package/dist/utils/settings-validator.d.ts.map +1 -0
  213. package/dist/utils/settings-validator.js +222 -0
  214. package/dist/utils/settings-validator.js.map +1 -0
  215. package/package.json +19 -3
  216. package/scripts/commit-gtm.sh +56 -0
  217. package/scripts/commit-product.sh +68 -0
  218. package/scripts/context-query.sh +45 -0
  219. package/scripts/session/auto-commit.sh +297 -0
  220. package/scripts/session/jfl-doctor.sh +707 -0
  221. package/scripts/session/session-cleanup.sh +268 -0
  222. package/scripts/session/session-end.sh +198 -0
  223. package/scripts/session/session-init.sh +350 -0
  224. package/scripts/session/session-init.sh.backup +292 -0
  225. package/scripts/session/session-sync.sh +167 -0
  226. package/scripts/session/test-context-preservation.sh +160 -0
  227. package/scripts/session/test-critical-infrastructure.sh +293 -0
  228. package/scripts/session/test-experience-level.sh +336 -0
  229. package/scripts/session/test-session-cleanup.sh +268 -0
  230. package/scripts/session/test-session-sync.sh +320 -0
  231. package/scripts/voice-start.sh +36 -8
  232. package/scripts/where-am-i.sh +78 -0
  233. package/template/.claude/service-settings.json +32 -0
  234. package/template/.claude/settings.json +14 -1
  235. package/template/.claude/skills/end/SKILL.md +1780 -0
  236. package/template/.jfl/config.json +2 -1
  237. package/template/CLAUDE.md +1039 -134
  238. package/template/CLAUDE.md.bak +1187 -0
  239. package/template/scripts/commit-gtm.sh +56 -0
  240. package/template/scripts/commit-product.sh +68 -0
  241. package/template/scripts/migrate-to-branch-sessions.sh +201 -0
  242. package/template/scripts/session/auto-commit.sh +58 -6
  243. package/template/scripts/session/jfl-doctor.sh +137 -17
  244. package/template/scripts/session/session-cleanup.sh +268 -0
  245. package/template/scripts/session/session-end.sh +4 -0
  246. package/template/scripts/session/session-init.sh +253 -66
  247. package/template/scripts/session/test-critical-infrastructure.sh +293 -0
  248. package/template/scripts/session/test-experience-level.sh +336 -0
  249. package/template/scripts/session/test-session-cleanup.sh +268 -0
  250. package/template/scripts/session/test-session-sync.sh +320 -0
  251. package/template/scripts/where-am-i.sh +78 -0
  252. package/template/templates/service-agent/.claude/settings.json +32 -0
  253. package/template/templates/service-agent/CLAUDE.md +334 -0
  254. package/template/templates/service-agent/knowledge/ARCHITECTURE.md +115 -0
  255. package/template/templates/service-agent/knowledge/DEPLOYMENT.md +199 -0
  256. package/template/templates/service-agent/knowledge/RUNBOOK.md +412 -0
  257. package/template/templates/service-agent/knowledge/SERVICE_SPEC.md +77 -0
  258. package/dist/commands/session-mgmt.d.ts +0 -33
  259. package/dist/commands/session-mgmt.d.ts.map +0 -1
  260. package/dist/commands/session-mgmt.js +0 -404
  261. package/dist/commands/session-mgmt.js.map +0 -1
  262. package/template/scripts/session/auto-merge.sh +0 -325
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * JFL GTM Clawdbot Skill
3
3
  *
4
- * Provides full JFL CLI access from Telegram/Slack with proper session isolation.
5
- * Uses `jfl session create` and `jfl session exec` for worktree isolation, auto-commit, and journaling.
6
- * Session state persisted to ~/.clawd/memory/jfl-sessions.json
4
+ * Dormant until user engages via /jfl. Then activates sessions, context,
5
+ * journaling, and coordination via OpenClaw protocol.
6
+ *
7
+ * On install: explains what JFL does and how to use it.
8
+ * On /jfl: lets user pick a GTM workspace, then activates.
9
+ * While active: auto-injects context, captures decisions, auto-commits.
10
+ *
11
+ * @purpose Clawdbot skill using OpenClaw for full JFL integration
12
+ * @spec specs/OPENCLAW_SPEC.md
7
13
  */
8
14
 
9
15
  import { exec } from "child_process"
10
16
  import { promisify } from "util"
11
- import { existsSync, readFileSync } from "fs"
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs"
12
18
  import { join } from "path"
13
19
  import { homedir } from "os"
14
20
 
@@ -25,462 +31,626 @@ interface GTM {
25
31
  path: string
26
32
  }
27
33
 
28
- /**
29
- * Boot sequence - runs when skill first activates
30
- */
31
- export async function onBoot(ctx: Context) {
32
- // Check if JFL CLI is installed
33
- const hasJFL = await checkJFLInstalled()
34
+ interface SessionState {
35
+ gtmPath: string
36
+ gtmName: string
37
+ agentId: string
38
+ sessionBranch: string | null
39
+ activated: boolean
40
+ }
34
41
 
35
- if (!hasJFL) {
36
- return {
37
- text: "⚠️ JFL CLI not found\n\n" +
38
- "Install: npm install -g jfl\n\n" +
39
- "Then run /jfl again",
40
- buttons: []
41
- }
42
- }
42
+ // ============================================================================
43
+ // State persistence
44
+ // ============================================================================
43
45
 
44
- // Find available GTMs
45
- const gtms = await findGTMs()
46
+ const STATE_DIR = join(homedir(), ".clawd", "memory")
47
+ const STATE_FILE = join(STATE_DIR, "jfl-openclaw.json")
46
48
 
47
- if (gtms.length === 0) {
48
- return {
49
- text: "🚀 JFL - Just Fucking Launch\n\n" +
50
- "No GTMs found.\n\n" +
51
- "Create one: jfl init\n" +
52
- "Then run /jfl again"
49
+ function loadState(): Map<string, SessionState> {
50
+ try {
51
+ if (existsSync(STATE_FILE)) {
52
+ return new Map(Object.entries(JSON.parse(readFileSync(STATE_FILE, "utf-8"))))
53
53
  }
54
- }
55
-
56
- // Show GTM picker (like screenshot)
57
- const buttons = gtms.map(g => ({
58
- text: `📂 ${g.name}`,
59
- callbackData: `select:${g.path}`
60
- }))
61
-
62
- return {
63
- text: "🚀 JFL - Just Fucking Launch\n\n" +
64
- "Your team's context layer. Any AI. Any task.\n\n" +
65
- "Open a project:",
66
- buttons
67
- }
54
+ } catch { /* ignore */ }
55
+ return new Map()
68
56
  }
69
57
 
70
- /**
71
- * Handle button clicks
72
- */
73
- export async function onCallback(data: string, ctx: Context) {
74
- const [action, value] = data.split(":")
75
-
76
- if (action === "select") {
77
- return await handleSelectGTM(value, ctx)
78
- }
79
-
80
- if (action === "cmd") {
81
- return await handleCommand(value, ctx)
82
- }
83
-
84
- return { text: "Unknown action" }
58
+ function saveState(state: Map<string, SessionState>) {
59
+ mkdirSync(STATE_DIR, { recursive: true })
60
+ writeFileSync(STATE_FILE, JSON.stringify(Object.fromEntries(state), null, 2))
85
61
  }
86
62
 
87
- /**
88
- * Handle slash commands
89
- */
90
- export async function onCommand(cmd: string, args: string[], ctx: Context) {
91
- const session = getSession(ctx.threadId)
63
+ const state = loadState()
92
64
 
93
- if (!session && cmd !== "gtm") {
94
- return {
95
- text: "⚠️ No GTM selected.\n\nRun /jfl to select a project."
96
- }
97
- }
98
-
99
- switch (cmd) {
100
- case "gtm":
101
- return await onBoot(ctx)
102
-
103
- case "hud":
104
- return await runJFLCommand(session!, "hud")
65
+ function getState(threadId: string): SessionState | undefined {
66
+ return state.get(threadId)
67
+ }
105
68
 
106
- case "crm":
107
- if (args.length === 0) {
108
- return showCRMMenu()
109
- }
110
- return await runCRMCommand(session!, args)
69
+ function setState(threadId: string, s: SessionState) {
70
+ state.set(threadId, s)
71
+ saveState(state)
72
+ }
111
73
 
112
- case "brand":
113
- return showBrandMenu()
74
+ // ============================================================================
75
+ // OpenClaw helpers
76
+ // ============================================================================
114
77
 
115
- case "content":
116
- if (args.length === 0) {
117
- return showContentMenu()
118
- }
119
- return await runJFLCommand(session!, `content ${args.join(" ")}`)
78
+ async function openclaw(cmd: string): Promise<string> {
79
+ const { stdout } = await execAsync(`jfl openclaw ${cmd}`, {
80
+ timeout: 30000,
81
+ env: { ...process.env },
82
+ })
83
+ return stdout.trim()
84
+ }
120
85
 
121
- default:
122
- return { text: `Unknown command: /${cmd}` }
123
- }
86
+ async function openclawJSON(cmd: string): Promise<any> {
87
+ const raw = await openclaw(`${cmd} --json`)
88
+ return JSON.parse(raw)
124
89
  }
125
90
 
126
- /**
127
- * Check if JFL CLI is installed
128
- */
129
- async function checkJFLInstalled(): Promise<boolean> {
91
+ async function ensureJFL(): Promise<boolean> {
130
92
  try {
131
- await execAsync("jfl --version")
93
+ await execAsync("jfl --version", { timeout: 5000 })
132
94
  return true
133
95
  } catch {
134
- return false
96
+ try {
97
+ await execAsync("npm install -g jfl", { timeout: 60000 })
98
+ return true
99
+ } catch {
100
+ return false
101
+ }
135
102
  }
136
103
  }
137
104
 
138
- /**
139
- * Find GTMs on this machine
140
- *
141
- * A GTM has: .jfl/ + knowledge/ + CLAUDE.md
142
- * Product repos (jfl-cli, jfl-platform) have .jfl/ but aren't GTMs
143
- */
144
- async function findGTMs(): Promise<GTM[]> {
145
- const gtms: GTM[] = []
105
+ function agentId(ctx: Context): string {
106
+ return `clawd-${ctx.platform}-${ctx.userId}`.slice(0, 30)
107
+ }
146
108
 
147
- // Check common locations
148
- const searchPaths = [
149
- join(homedir(), "CascadeProjects"),
150
- join(homedir(), "Projects"),
151
- join(homedir(), "code"),
152
- ]
109
+ // ============================================================================
110
+ // Welcome / Intro (shown on first use or install)
111
+ // ============================================================================
153
112
 
154
- for (const basePath of searchPaths) {
155
- if (!existsSync(basePath)) continue
113
+ const WELCOME_MESSAGE = `JFL - Just Fucking Launch
156
114
 
157
- try {
158
- const { stdout } = await execAsync(`find ${basePath} -maxdepth 2 -name .jfl -type d`)
159
- const dirs = stdout.trim().split("\n").filter(Boolean)
115
+ Your project context layer. I track decisions, save context, and keep everything synced across sessions.
160
116
 
161
- for (const dir of dirs) {
162
- const gtmPath = dir.replace("/.jfl", "")
117
+ What I do when active:
118
+ - Search project context before responding
119
+ - Capture decisions into the journal automatically
120
+ - Auto-commit your work every 2 minutes
121
+ - Coordinate with other agents on the team
163
122
 
164
- // Filter: Must have knowledge/ and CLAUDE.md to be a GTM
165
- const hasKnowledge = existsSync(join(gtmPath, "knowledge"))
166
- const hasClaude = existsSync(join(gtmPath, "CLAUDE.md"))
123
+ Commands:
124
+ /jfl Activate / select a project
125
+ /context <query> Search project knowledge
126
+ /journal <type> <title> | <summary> — Log work
127
+ /hud — Project dashboard
167
128
 
168
- if (!hasKnowledge || !hasClaude) {
169
- continue // Skip product repos
170
- }
129
+ To get started, use /jfl to pick a project.`
171
130
 
172
- const name = gtmPath.split("/").pop() || "unknown"
173
- gtms.push({ name, path: gtmPath })
174
- }
175
- } catch {
176
- // Skip if find fails
177
- }
178
- }
131
+ const ACTIVATION_MESSAGE = (gtmName: string) => `JFL activated: ${gtmName}
179
132
 
180
- return gtms
181
- }
133
+ I'll now:
134
+ - Search context before each response
135
+ - Capture decisions automatically
136
+ - Auto-commit work periodically
182
137
 
183
- /**
184
- * Session storage (persisted to disk)
185
- */
186
- const SESSIONS_FILE = join(homedir(), ".clawd", "memory", "jfl-sessions.json")
138
+ Commands:
139
+ /context <query> Search project context
140
+ /journal <type> <title> | <summary> — Log work
141
+ /hud Project dashboard
142
+ /jfl — Status`
187
143
 
188
- interface SessionData {
189
- gtmPath: string
190
- gtmName: string
191
- sessionId: string
192
- platform: string
193
- }
144
+ // ============================================================================
145
+ // Boot (on /jfl command)
146
+ // ============================================================================
194
147
 
195
- function loadSessions(): Map<string, SessionData> {
196
- try {
197
- if (existsSync(SESSIONS_FILE)) {
198
- const data = JSON.parse(readFileSync(SESSIONS_FILE, "utf-8"))
199
- return new Map(Object.entries(data))
148
+ export async function onBoot(ctx: Context) {
149
+ const hasJFL = await ensureJFL()
150
+ if (!hasJFL) {
151
+ return {
152
+ text: "JFL CLI not found and auto-install failed.\n\nInstall manually:\n npm install -g jfl\n\nThen run /jfl again.",
200
153
  }
201
- } catch {
202
- // Ignore parse errors
203
154
  }
204
- return new Map()
205
- }
206
155
 
207
- function saveSessions(sessions: Map<string, SessionData>) {
208
- try {
209
- const data = Object.fromEntries(sessions)
210
- const fs = require("fs")
211
- fs.mkdirSync(join(homedir(), ".clawd", "memory"), { recursive: true })
212
- fs.writeFileSync(SESSIONS_FILE, JSON.stringify(data, null, 2))
213
- } catch (error) {
214
- console.error("Failed to save sessions:", error)
156
+ // Check if already activated
157
+ const existing = getState(ctx.threadId)
158
+ if (existing?.activated && existing?.sessionBranch) {
159
+ return await showStatus(existing)
215
160
  }
216
- }
217
161
 
218
- const sessions = loadSessions()
162
+ // Find available GTMs
163
+ const gtms = await findGTMs()
164
+ if (gtms.length === 0) {
165
+ return {
166
+ text: "No GTM workspaces found.\n\nCreate one:\n jfl init -n 'My Project'\n\nThen run /jfl again.",
167
+ }
168
+ }
219
169
 
220
- function getSession(threadId: string) {
221
- return sessions.get(threadId)
222
- }
170
+ // Single GTM → auto-activate
171
+ if (gtms.length === 1) {
172
+ return await activateGTM(gtms[0].path, ctx)
173
+ }
223
174
 
224
- function setSession(threadId: string, data: SessionData) {
225
- sessions.set(threadId, data)
226
- saveSessions(sessions)
175
+ // Multiple GTMs let user pick
176
+ const buttons = gtms.map((g) => ({
177
+ text: g.name,
178
+ callbackData: `select:${g.path}`,
179
+ }))
180
+
181
+ return {
182
+ text: "Select a project:",
183
+ buttons,
184
+ }
227
185
  }
228
186
 
229
- /**
230
- * Handle GTM selection
231
- */
232
- async function handleSelectGTM(gtmPath: string, ctx: Context) {
233
- const gtmName = gtmPath.split("/").pop() || "unknown"
187
+ // ============================================================================
188
+ // Activation
189
+ // ============================================================================
190
+
191
+ async function activateGTM(gtmPath: string, ctx: Context) {
192
+ const agent = agentId(ctx)
234
193
 
194
+ // Read GTM name from config
195
+ let gtmName = gtmPath.split("/").pop() || "unknown"
235
196
  try {
236
- // Create or get session with proper worktree isolation
237
- const { stdout } = await execAsync(
238
- `cd ${gtmPath} && jfl session create --platform ${ctx.platform} --thread ${ctx.threadId}`
239
- )
240
- const sessionId = stdout.trim()
197
+ const cfg = JSON.parse(readFileSync(join(gtmPath, ".jfl", "config.json"), "utf-8"))
198
+ if (cfg.name) gtmName = cfg.name
199
+ } catch {}
200
+
201
+ try {
202
+ // Register agent with GTM (idempotent)
203
+ await openclawJSON(`register -g "${gtmPath}" -a ${agent}`)
204
+
205
+ // Start session
206
+ const session = await openclawJSON(`session-start -a ${agent} -g "${gtmPath}"`)
241
207
 
242
- // Store session with ID
243
- setSession(ctx.threadId, {
208
+ setState(ctx.threadId, {
244
209
  gtmPath,
245
210
  gtmName,
246
- sessionId,
247
- platform: ctx.platform
211
+ agentId: agent,
212
+ sessionBranch: session.session_id,
213
+ activated: true,
248
214
  })
249
215
 
250
- // Show command menu
251
- const commands = [
216
+ return { text: ACTIVATION_MESSAGE(gtmName) }
217
+ } catch (error: any) {
218
+ // Session start failed but registration may have worked
219
+ setState(ctx.threadId, {
220
+ gtmPath,
221
+ gtmName,
222
+ agentId: agent,
223
+ sessionBranch: null,
224
+ activated: true,
225
+ })
226
+ return { text: `JFL activated: ${gtmName}\n\nSession start failed: ${error.message}\nContext and journaling still work.` }
227
+ }
228
+ }
229
+
230
+ // ============================================================================
231
+ // Status (shown when /jfl is called while active)
232
+ // ============================================================================
233
+
234
+ async function showStatus(s: SessionState) {
235
+ let hubStatus = "unknown"
236
+ try {
237
+ const status = await openclawJSON("status")
238
+ hubStatus = status.context_hub?.healthy ? "running" : "offline"
239
+ } catch {
240
+ hubStatus = "offline"
241
+ }
242
+
243
+ return {
244
+ text: [
245
+ `JFL active: ${s.gtmName}`,
246
+ `Session: ${s.sessionBranch || "none"}`,
247
+ `Context Hub: ${hubStatus}`,
248
+ ``,
249
+ `Commands:`,
250
+ `/context <query> — Search`,
251
+ `/journal <type> <title> | <summary> — Log`,
252
+ `/hud — Dashboard`,
253
+ ].join("\n"),
254
+ buttons: [
252
255
  [
253
- { text: "📊 Dashboard", callbackData: "cmd:hud" },
254
- { text: "👥 CRM", callbackData: "cmd:crm" }
256
+ { text: "Dashboard", callbackData: "cmd:hud" },
257
+ { text: "Search Context", callbackData: "cmd:context" },
255
258
  ],
256
259
  [
257
- { text: "🎨 Brand", callbackData: "cmd:brand" },
258
- { text: "✍️ Content", callbackData: "cmd:content" }
260
+ { text: "Switch Project", callbackData: "cmd:switch" },
261
+ { text: "End Session", callbackData: "cmd:end" },
259
262
  ],
260
- [
261
- { text: "🔄 Sync", callbackData: "cmd:sync" },
262
- { text: "📝 Status", callbackData: "cmd:status" }
263
- ]
264
- ]
263
+ ],
264
+ }
265
+ }
265
266
 
266
- return {
267
- text: `✓ Session created: ${gtmName}\n\n` +
268
- `Session ID: ${sessionId}\n` +
269
- `Isolated worktree with auto-commit enabled.\n\n` +
270
- `What do you want to do?`,
271
- buttons: commands
267
+ // ============================================================================
268
+ // Callbacks
269
+ // ============================================================================
270
+
271
+ export async function onCallback(data: string, ctx: Context) {
272
+ const [action, value] = data.split(":")
273
+
274
+ if (action === "select") return await activateGTM(value, ctx)
275
+ if (action === "cmd") return await handleCommand(value, ctx)
276
+
277
+ return { text: "Unknown action" }
278
+ }
279
+
280
+ async function handleCommand(cmd: string, ctx: Context) {
281
+ const s = getState(ctx.threadId)
282
+ if (!s?.activated) return { text: "JFL not active. Use /jfl to select a project." }
283
+
284
+ switch (cmd) {
285
+ case "hud":
286
+ return await runInGTM(s, "jfl hud")
287
+ case "crm":
288
+ return showCRMMenu()
289
+ case "context":
290
+ return await runContext(s)
291
+ case "status":
292
+ return await showStatus(s)
293
+ case "brand":
294
+ return showBrandMenu()
295
+ case "content":
296
+ return showContentMenu()
297
+ case "switch": {
298
+ // Deactivate current
299
+ setState(ctx.threadId, { ...s, activated: false, sessionBranch: null })
300
+ try { await openclawJSON("session-end --sync") } catch {}
301
+ return await onBoot(ctx)
272
302
  }
273
- } catch (error: any) {
274
- return {
275
- text: `❌ Failed to create session\n\n${error.message}\n\n` +
276
- `Make sure you're in a JFL GTM directory.`
303
+ case "end": {
304
+ try { await openclawJSON("session-end --sync") } catch {}
305
+ setState(ctx.threadId, { ...s, activated: false, sessionBranch: null })
306
+ return { text: `Session ended for ${s.gtmName}.\n\nUse /jfl to start again.` }
277
307
  }
308
+ default:
309
+ return { text: "Unknown command" }
278
310
  }
279
311
  }
280
312
 
281
- /**
282
- * Handle command button
283
- */
284
- async function handleCommand(cmd: string, ctx: Context) {
285
- const session = getSession(ctx.threadId)
313
+ // ============================================================================
314
+ // Commands
315
+ // ============================================================================
286
316
 
287
- if (!session) {
288
- return { text: "⚠️ Session expired. Run /jfl to select a GTM." }
289
- }
317
+ export async function onCommand(cmd: string, args: string[], ctx: Context) {
318
+ if (cmd === "jfl" || cmd === "gtm") return await onBoot(ctx)
319
+
320
+ const s = getState(ctx.threadId)
321
+ if (!s?.activated) return { text: "JFL not active. Use /jfl to select a project first." }
290
322
 
291
323
  switch (cmd) {
292
324
  case "hud":
293
- return await runJFLCommand(session, "hud")
325
+ return await runInGTM(s, "jfl hud")
294
326
 
295
327
  case "crm":
296
- return showCRMMenu()
328
+ if (args.length === 0) return showCRMMenu()
329
+ return await runInGTM(s, `./crm ${args.join(" ")}`)
330
+
331
+ case "context":
332
+ if (args.length === 0) return await runContext(s)
333
+ return await runContextQuery(s, args.join(" "))
334
+
335
+ case "journal": {
336
+ const type = args[0] || "discovery"
337
+ const title = args.slice(1).join(" ") || "Note"
338
+ await openclaw(`journal --type ${type} --title "${title}" --summary "${title}"`)
339
+ return { text: `Journal entry written: [${type}] ${title}` }
340
+ }
297
341
 
298
342
  case "brand":
299
343
  return showBrandMenu()
300
344
 
301
345
  case "content":
302
- return showContentMenu()
303
-
304
- case "sync":
305
- return await runGitSync(session)
306
-
307
- case "status":
308
- return await runGitStatus(session)
346
+ if (args.length === 0) return showContentMenu()
347
+ return await runInGTM(s, `jfl content ${args.join(" ")}`)
309
348
 
310
349
  default:
311
- return { text: "Unknown command" }
350
+ return { text: `Unknown command: /${cmd}` }
312
351
  }
313
352
  }
314
353
 
315
- /**
316
- * Run JFL command (uses session exec for isolation)
317
- */
318
- async function runJFLCommand(session: SessionData, command: string) {
319
- try {
320
- // Use jfl session exec to run command in isolated worktree
321
- const { stdout, stderr } = await execAsync(
322
- `cd ${session.gtmPath} && jfl session exec "${session.sessionId}" "${command}"`,
323
- {
324
- env: { ...process.env, JFL_PLATFORM: session.platform }
325
- }
326
- )
354
+ // ============================================================================
355
+ // Runners
356
+ // ============================================================================
327
357
 
328
- const output = stdout || stderr
329
- const formatted = formatForTelegram(output)
330
-
331
- return {
332
- text: formatted,
333
- parseMode: "Markdown"
334
- }
358
+ async function runInGTM(s: SessionState, command: string): Promise<any> {
359
+ try {
360
+ const { stdout } = await execAsync(command, {
361
+ cwd: s.gtmPath,
362
+ timeout: 30000,
363
+ env: { ...process.env },
364
+ })
365
+ return { text: stripAnsi(stdout).slice(0, 4000), parseMode: "Markdown" }
335
366
  } catch (error: any) {
336
- return {
337
- text: `❌ Error running jfl ${command}\n\n${error.message}`
338
- }
367
+ return { text: `Command failed: ${error.message}` }
339
368
  }
340
369
  }
341
370
 
342
- /**
343
- * Run CRM command (uses session exec for isolation)
344
- */
345
- async function runCRMCommand(session: SessionData, args: string[]) {
371
+ async function runContext(s: SessionState) {
346
372
  try {
347
- // Use session exec to run CRM in isolated worktree
348
- const { stdout } = await execAsync(
349
- `cd ${session.gtmPath} && jfl session exec "${session.sessionId}" "./crm ${args.join(" ")}"`
373
+ const items = await openclawJSON("context")
374
+ if (!Array.isArray(items) || items.length === 0) {
375
+ return { text: "No context items found. Context Hub may not be running.\n\nStart it: jfl context-hub start" }
376
+ }
377
+ const lines = items.slice(0, 8).map((i: any) =>
378
+ `[${i.source || i.type}] ${i.title}\n ${(i.content || "").slice(0, 100)}`
350
379
  )
351
- const formatted = formatForTelegram(stdout)
380
+ return { text: `Project Context\n\n${lines.join("\n\n")}` }
381
+ } catch (error: any) {
382
+ return { text: `Context Hub unreachable: ${error.message}` }
383
+ }
384
+ }
352
385
 
353
- return {
354
- text: formatted,
355
- parseMode: "Markdown"
386
+ async function runContextQuery(s: SessionState, query: string) {
387
+ try {
388
+ const items = await openclawJSON(`context -q "${query}"`)
389
+ if (!Array.isArray(items) || items.length === 0) {
390
+ return { text: `No results for: ${query}` }
356
391
  }
392
+ const lines = items.slice(0, 5).map((i: any) =>
393
+ `[${i.source || i.type}] ${i.title}\n ${(i.content || "").slice(0, 120)}`
394
+ )
395
+ return { text: `Results for "${query}"\n\n${lines.join("\n\n")}` }
357
396
  } catch (error: any) {
358
- return {
359
- text: `❌ Error running crm\n\n${error.message}`
360
- }
397
+ return { text: `Search failed: ${error.message}` }
361
398
  }
362
399
  }
363
400
 
364
- /**
365
- * Show CRM menu
366
- */
401
+ // ============================================================================
402
+ // Menus
403
+ // ============================================================================
404
+
367
405
  function showCRMMenu() {
368
406
  return {
369
- text: "👥 CRM\n\nWhat do you want to do?",
407
+ text: "CRM\n\nWhat do you want to do?",
370
408
  buttons: [
371
409
  [
372
- { text: "📋 List Pipeline", callbackData: "crm:list" },
373
- { text: "Stale Deals", callbackData: "crm:stale" }
410
+ { text: "Pipeline", callbackData: "crm:list" },
411
+ { text: "Stale Deals", callbackData: "crm:stale" },
374
412
  ],
375
413
  [
376
- { text: "📞 Prep Call", callbackData: "crm:prep" },
377
- { text: "✏️ Log Touch", callbackData: "crm:touch" }
378
- ]
379
- ]
414
+ { text: "Prep Call", callbackData: "crm:prep" },
415
+ { text: "Log Touch", callbackData: "crm:touch" },
416
+ ],
417
+ ],
380
418
  }
381
419
  }
382
420
 
383
- /**
384
- * Show brand menu
385
- */
386
421
  function showBrandMenu() {
387
422
  return {
388
- text: "🎨 Brand Architect\n\nWhat do you want to create?",
423
+ text: "Brand Architect\n\nWhat do you want to create?",
389
424
  buttons: [
390
425
  [
391
426
  { text: "Logo Marks", callbackData: "brand:marks" },
392
- { text: "Color Palette", callbackData: "brand:colors" }
427
+ { text: "Colors", callbackData: "brand:colors" },
393
428
  ],
394
429
  [
395
430
  { text: "Typography", callbackData: "brand:typography" },
396
- { text: "Full System", callbackData: "brand:full" }
397
- ]
398
- ]
431
+ { text: "Full System", callbackData: "brand:full" },
432
+ ],
433
+ ],
399
434
  }
400
435
  }
401
436
 
402
- /**
403
- * Show content menu
404
- */
405
437
  function showContentMenu() {
406
438
  return {
407
- text: "✍️ Content Creator\n\nWhat do you want to create?",
439
+ text: "Content Creator\n\nWhat do you want to create?",
408
440
  buttons: [
409
441
  [
410
- { text: "Twitter Thread", callbackData: "content:thread" },
411
- { text: "Single Post", callbackData: "content:post" }
442
+ { text: "Thread", callbackData: "content:thread" },
443
+ { text: "Post", callbackData: "content:post" },
412
444
  ],
413
445
  [
414
446
  { text: "Article", callbackData: "content:article" },
415
- { text: "One-Pager", callbackData: "content:onepager" }
416
- ]
417
- ]
447
+ { text: "One-Pager", callbackData: "content:onepager" },
448
+ ],
449
+ ],
418
450
  }
419
451
  }
420
452
 
453
+ // ============================================================================
454
+ // GTM discovery (only returns type:"gtm", never services)
455
+ // ============================================================================
456
+
457
+ function readJflConfig(dir: string): { type?: string; name?: string } | null {
458
+ try {
459
+ return JSON.parse(readFileSync(join(dir, ".jfl", "config.json"), "utf-8"))
460
+ } catch {
461
+ return null
462
+ }
463
+ }
464
+
465
+ async function findGTMs(): Promise<GTM[]> {
466
+ // First try OpenClaw registry
467
+ try {
468
+ const gtms = await openclawJSON("gtm-list")
469
+ if (Array.isArray(gtms) && gtms.length > 0) {
470
+ return gtms.map((g: any) => ({ name: g.name, path: g.path }))
471
+ }
472
+ } catch { /* fall through to filesystem scan */ }
473
+
474
+ // Fallback: scan filesystem for type:"gtm" directories
475
+ const gtms: GTM[] = []
476
+ const searchPaths = [
477
+ join(homedir(), "CascadeProjects"),
478
+ join(homedir(), "Projects"),
479
+ join(homedir(), "code"),
480
+ ]
481
+
482
+ for (const basePath of searchPaths) {
483
+ if (!existsSync(basePath)) continue
484
+ try {
485
+ const entries = readdirSync(basePath, { withFileTypes: true })
486
+ for (const entry of entries) {
487
+ if (!entry.isDirectory()) continue
488
+ const candidate = join(basePath, entry.name)
489
+ const config = readJflConfig(candidate)
490
+ if (!config) continue
491
+
492
+ // Only include GTMs, not services
493
+ if (config.type === "gtm") {
494
+ gtms.push({ name: config.name || entry.name, path: candidate })
495
+ } else if (!config.type && existsSync(join(candidate, "knowledge"))) {
496
+ // Legacy: no type field but has knowledge/ → probably a GTM
497
+ gtms.push({ name: config.name || entry.name, path: candidate })
498
+ }
499
+ // type:"service" → skip entirely
500
+ }
501
+ } catch { /* skip */ }
502
+ }
503
+
504
+ return gtms
505
+ }
506
+
507
+ // ============================================================================
508
+ // LIFECYCLE HOOKS (Clawdbot calls these automatically)
509
+ // All hooks check activation state. When dormant, they're no-ops.
510
+ // ============================================================================
511
+
421
512
  /**
422
- * Git sync (runs in session worktree)
513
+ * session_start fires when Clawdbot session begins.
514
+ * Does NOT auto-activate. Just checks if JFL is available and notes it.
515
+ * User must run /jfl to activate.
423
516
  */
424
- async function runGitSync(session: SessionData) {
425
- try {
426
- // Session exec automatically runs session-sync.sh before command
427
- const { stdout } = await execAsync(
428
- `cd ${session.gtmPath} && jfl session exec "${session.sessionId}" "git status --short"`
429
- )
517
+ export async function onSessionStart(event: { agentId: string; platform: string; threadId: string }) {
518
+ const existing = getState(event.threadId)
430
519
 
431
- return {
432
- text: `✓ Synced ${session.gtmName}\n\nSession-sync runs automatically before each command.\n\n${stdout || "Working tree clean"}`
520
+ // If already activated from a previous session, re-inject context
521
+ if (existing?.activated && existing?.gtmPath) {
522
+ const agent = existing.agentId || event.agentId || `clawd-${event.platform}`
523
+
524
+ try {
525
+ // Try to resume or start a new session
526
+ const session = await openclawJSON(`session-start -a ${agent} -g "${existing.gtmPath}"`)
527
+ setState(event.threadId, { ...existing, sessionBranch: session.session_id })
528
+
529
+ return {
530
+ prependContext: [
531
+ `<jfl-session>`,
532
+ `GTM: ${existing.gtmName} (${existing.gtmPath})`,
533
+ `Session: ${session.session_id}`,
534
+ `Use /jfl for status. Use /context <query> to search.`,
535
+ `</jfl-session>`,
536
+ ].join("\n"),
537
+ }
538
+ } catch {
539
+ // Session start failed but we're still "activated" for the GTM
540
+ return {
541
+ prependContext: `<jfl-session>GTM: ${existing.gtmName} (session start failed, commands still work)</jfl-session>`,
542
+ }
433
543
  }
434
- } catch (error: any) {
435
- return {
436
- text: `❌ Sync failed\n\n${error.message}`
544
+ }
545
+
546
+ // Not activated — stay quiet. Don't inject anything.
547
+ // Just check if JFL is available for the welcome message later
548
+ const hasJFL = await ensureJFL()
549
+ if (hasJFL) {
550
+ const gtms = await findGTMs()
551
+ if (gtms.length > 0) {
552
+ return {
553
+ prependContext: `<jfl-available>JFL is installed with ${gtms.length} project(s) available. User can activate with /jfl.</jfl-available>`,
554
+ }
437
555
  }
438
556
  }
557
+
558
+ return { prependContext: "" }
439
559
  }
440
560
 
441
561
  /**
442
- * Git status (runs in session worktree)
562
+ * before_turn fires before each agent response.
563
+ * Only injects context when JFL is active.
443
564
  */
444
- async function runGitStatus(session: SessionData) {
565
+ export async function onBeforeTurn(event: { agentId: string; threadId: string; message?: string }) {
566
+ const s = getState(event.threadId)
567
+ if (!s?.activated || !s?.sessionBranch) return { prependContext: "" }
568
+
569
+ let contextBlock = ""
445
570
  try {
446
- // Use session exec to check status in isolated worktree
447
- const { stdout } = await execAsync(
448
- `cd ${session.gtmPath} && jfl session exec "${session.sessionId}" "git status --short && echo '---' && git log --oneline -5"`
449
- )
571
+ const query = event.message?.slice(0, 100) || ""
572
+ if (query.length > 10) {
573
+ const items = await openclawJSON(`context -q "${query.replace(/"/g, '\\"')}"`)
574
+ if (Array.isArray(items) && items.length > 0) {
575
+ const relevant = items.slice(0, 3).map((i: any) =>
576
+ `- [${i.type}] ${i.title}: ${(i.content || "").slice(0, 150)}`
577
+ ).join("\n")
578
+ contextBlock = `<jfl-context>\nRelevant context:\n${relevant}\n</jfl-context>`
579
+ }
580
+ }
581
+ } catch { /* non-fatal */ }
450
582
 
451
- return {
452
- text: `📊 Status: ${session.gtmName}\nSession: ${session.sessionId}\n\n\`\`\`\n${stdout}\n\`\`\``,
453
- parseMode: "Markdown"
583
+ return { prependContext: contextBlock }
584
+ }
585
+
586
+ /**
587
+ * after_turn — fires after each agent response.
588
+ * Only runs heartbeat + capture when JFL is active.
589
+ */
590
+ export async function onAfterTurn(event: {
591
+ agentId: string
592
+ threadId: string
593
+ response?: string
594
+ detectedIntent?: string
595
+ }) {
596
+ const s = getState(event.threadId)
597
+ if (!s?.activated || !s?.sessionBranch) return
598
+
599
+ // Heartbeat (auto-commit)
600
+ try {
601
+ await openclaw("heartbeat --json")
602
+ } catch { /* non-fatal */ }
603
+
604
+ // Auto-capture decisions/completions
605
+ if (event.detectedIntent && event.response) {
606
+ const intentMap: Record<string, string> = {
607
+ decision: "decision",
608
+ completed: "feature",
609
+ fixed: "fix",
610
+ learned: "discovery",
454
611
  }
455
- } catch (error: any) {
456
- return {
457
- text: `❌ Status failed\n\n${error.message}`
612
+ const type = intentMap[event.detectedIntent]
613
+ if (type) {
614
+ const title = event.response.slice(0, 80).replace(/\n/g, " ")
615
+ try {
616
+ await openclaw(`journal --type ${type} --title "${title.replace(/"/g, '\\"')}" --summary "${title.replace(/"/g, '\\"')}"`)
617
+ } catch { /* non-fatal */ }
458
618
  }
459
619
  }
460
620
  }
461
621
 
462
622
  /**
463
- * Format CLI output for Telegram
464
- * Optimized for mobile viewing
623
+ * session_end fires when Clawdbot session ends.
624
+ * Only cleans up if JFL was active.
465
625
  */
466
- function formatForTelegram(output: string): string {
467
- return output
468
- // Strip ANSI color codes
469
- .replace(/\x1b\[[0-9;]*m/g, "")
470
- // Convert long separator lines to short ones (mobile-friendly)
471
- .replace(/[━─]{20,}/g, "━━━━━━━━━")
472
- // Keep single box-drawing characters as-is (they render fine)
473
- .replace(/[┌┐└┘├┤┬┴┼]/g, "")
474
- // Convert vertical bars
475
- .replace(/[│┃]/g, "")
476
- // Add spacing around emoji headers for readability
477
- .replace(/^(📊|👥|🎨|✍️|🔄|📝)/gm, "\n$1")
478
- // Preserve emoji and structure
479
- .trim()
626
+ export async function onSessionEnd(event: { agentId: string; threadId: string }) {
627
+ const s = getState(event.threadId)
628
+ if (!s?.activated || !s?.sessionBranch) return
629
+
630
+ try {
631
+ await openclawJSON("session-end --sync")
632
+ } catch { /* never block shutdown */ }
633
+
634
+ // Keep activated state but clear session
635
+ setState(event.threadId, { ...s, sessionBranch: null })
636
+ }
637
+
638
+ // ============================================================================
639
+ // Util
640
+ // ============================================================================
641
+
642
+ function stripAnsi(str: string): string {
643
+ return str.replace(/\x1b\[[0-9;]*m/g, "")
480
644
  }
481
645
 
482
646
  export default {
647
+ // Interactive (user-triggered)
483
648
  onBoot,
484
649
  onCallback,
485
- onCommand
650
+ onCommand,
651
+ // Lifecycle (Clawdbot-triggered, gated by activation)
652
+ onSessionStart,
653
+ onBeforeTurn,
654
+ onAfterTurn,
655
+ onSessionEnd,
486
656
  }