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