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.
- package/README.md +443 -145
- 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 +491 -321
- 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/context-hub.d.ts +5 -0
- package/dist/commands/context-hub.d.ts.map +1 -1
- package/dist/commands/context-hub.js +394 -28
- package/dist/commands/context-hub.js.map +1 -1
- 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/init.d.ts.map +1 -1
- package/dist/commands/init.js +278 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +32 -33
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory.d.ts +38 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +229 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/migrate-services.d.ts +8 -0
- package/dist/commands/migrate-services.d.ts.map +1 -0
- package/dist/commands/migrate-services.js +182 -0
- package/dist/commands/migrate-services.js.map +1 -0
- package/dist/commands/onboard.d.ts +24 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +663 -0
- package/dist/commands/onboard.js.map +1 -0
- 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/orchestrate.d.ts +14 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +270 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/profile.d.ts +46 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +498 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/repair.d.ts.map +1 -1
- package/dist/commands/repair.js +37 -0
- package/dist/commands/repair.js.map +1 -1
- package/dist/commands/service-agent.d.ts +16 -0
- package/dist/commands/service-agent.d.ts.map +1 -0
- package/dist/commands/service-agent.js +375 -0
- package/dist/commands/service-agent.js.map +1 -0
- package/dist/commands/service-manager.d.ts +12 -0
- package/dist/commands/service-manager.d.ts.map +1 -0
- package/dist/commands/service-manager.js +967 -0
- package/dist/commands/service-manager.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-scan.d.ts +13 -0
- package/dist/commands/services-scan.d.ts.map +1 -0
- package/dist/commands/services-scan.js +251 -0
- package/dist/commands/services-scan.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 +19 -0
- package/dist/commands/services.d.ts.map +1 -0
- package/dist/commands/services.js +742 -0
- package/dist/commands/services.js.map +1 -0
- package/dist/commands/session.d.ts +5 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +68 -586
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +17 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +75 -21
- package/dist/commands/update.js.map +1 -1
- 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/commands/voice.d.ts +0 -1
- package/dist/commands/voice.d.ts.map +1 -1
- package/dist/commands/voice.js +16 -15
- package/dist/commands/voice.js.map +1 -1
- package/dist/index.js +395 -141
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-generator.d.ts +26 -0
- package/dist/lib/agent-generator.d.ts.map +1 -0
- package/dist/lib/agent-generator.js +331 -0
- package/dist/lib/agent-generator.js.map +1 -0
- package/dist/lib/memory-db.d.ts +102 -0
- package/dist/lib/memory-db.d.ts.map +1 -0
- package/dist/lib/memory-db.js +313 -0
- package/dist/lib/memory-db.js.map +1 -0
- package/dist/lib/memory-indexer.d.ts +47 -0
- package/dist/lib/memory-indexer.d.ts.map +1 -0
- package/dist/lib/memory-indexer.js +215 -0
- package/dist/lib/memory-indexer.js.map +1 -0
- package/dist/lib/memory-search.d.ts +41 -0
- package/dist/lib/memory-search.d.ts.map +1 -0
- package/dist/lib/memory-search.js +246 -0
- package/dist/lib/memory-search.js.map +1 -0
- 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-dependencies.d.ts +44 -0
- package/dist/lib/service-dependencies.d.ts.map +1 -0
- package/dist/lib/service-dependencies.js +314 -0
- package/dist/lib/service-dependencies.js.map +1 -0
- package/dist/lib/service-detector.d.ts +61 -0
- package/dist/lib/service-detector.d.ts.map +1 -0
- package/dist/lib/service-detector.js +521 -0
- package/dist/lib/service-detector.js.map +1 -0
- 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 +103 -0
- package/dist/lib/service-mcp-base.d.ts.map +1 -0
- package/dist/lib/service-mcp-base.js +263 -0
- package/dist/lib/service-mcp-base.js.map +1 -0
- package/dist/lib/service-utils.d.ts +103 -0
- package/dist/lib/service-utils.d.ts.map +1 -0
- package/dist/lib/service-utils.js +368 -0
- package/dist/lib/service-utils.js.map +1 -0
- package/dist/lib/skill-generator.d.ts +21 -0
- package/dist/lib/skill-generator.d.ts.map +1 -0
- package/dist/lib/skill-generator.js +253 -0
- package/dist/lib/skill-generator.js.map +1 -0
- package/dist/lib/stratus-client.d.ts +100 -0
- package/dist/lib/stratus-client.d.ts.map +1 -0
- package/dist/lib/stratus-client.js +255 -0
- package/dist/lib/stratus-client.js.map +1 -0
- package/dist/mcp/context-hub-mcp.js +135 -53
- package/dist/mcp/context-hub-mcp.js.map +1 -1
- package/dist/mcp/service-mcp-server.d.ts +12 -0
- package/dist/mcp/service-mcp-server.d.ts.map +1 -0
- package/dist/mcp/service-mcp-server.js +434 -0
- package/dist/mcp/service-mcp-server.js.map +1 -0
- 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.d.ts +13 -0
- package/dist/mcp/service-registry-mcp.d.ts.map +1 -0
- package/dist/mcp/service-registry-mcp.js +330 -0
- package/dist/mcp/service-registry-mcp.js.map +1 -0
- package/dist/ui/banner.js +1 -1
- package/dist/ui/banner.js.map +1 -1
- package/dist/ui/context-hub-logs.d.ts +10 -0
- package/dist/ui/context-hub-logs.d.ts.map +1 -0
- package/dist/ui/context-hub-logs.js +175 -0
- package/dist/ui/context-hub-logs.js.map +1 -0
- package/dist/ui/service-dashboard.d.ts +11 -0
- package/dist/ui/service-dashboard.d.ts.map +1 -0
- package/dist/ui/service-dashboard.js +357 -0
- package/dist/ui/service-dashboard.js.map +1 -0
- package/dist/ui/services-manager.d.ts +11 -0
- package/dist/ui/services-manager.d.ts.map +1 -0
- package/dist/ui/services-manager.js +507 -0
- package/dist/ui/services-manager.js.map +1 -0
- package/dist/utils/auth-guard.d.ts.map +1 -1
- package/dist/utils/auth-guard.js +8 -9
- package/dist/utils/auth-guard.js.map +1 -1
- package/dist/utils/claude-md-generator.d.ts +10 -0
- package/dist/utils/claude-md-generator.d.ts.map +1 -0
- package/dist/utils/claude-md-generator.js +215 -0
- package/dist/utils/claude-md-generator.js.map +1 -0
- package/dist/utils/ensure-context-hub.d.ts +20 -0
- package/dist/utils/ensure-context-hub.d.ts.map +1 -0
- package/dist/utils/ensure-context-hub.js +65 -0
- package/dist/utils/ensure-context-hub.js.map +1 -0
- package/dist/utils/ensure-project.d.ts.map +1 -1
- package/dist/utils/ensure-project.js +3 -4
- package/dist/utils/ensure-project.js.map +1 -1
- package/dist/utils/jfl-config.d.ts +19 -0
- package/dist/utils/jfl-config.d.ts.map +1 -0
- package/dist/utils/jfl-config.js +112 -0
- package/dist/utils/jfl-config.js.map +1 -0
- package/dist/utils/jfl-migration.d.ts +29 -0
- package/dist/utils/jfl-migration.d.ts.map +1 -0
- package/dist/utils/jfl-migration.js +142 -0
- package/dist/utils/jfl-migration.js.map +1 -0
- package/dist/utils/jfl-paths.d.ts +55 -0
- package/dist/utils/jfl-paths.d.ts.map +1 -0
- package/dist/utils/jfl-paths.js +120 -0
- package/dist/utils/jfl-paths.js.map +1 -0
- package/dist/utils/settings-validator.d.ts +73 -0
- package/dist/utils/settings-validator.d.ts.map +1 -0
- package/dist/utils/settings-validator.js +222 -0
- package/dist/utils/settings-validator.js.map +1 -0
- package/package.json +19 -3
- package/scripts/commit-gtm.sh +56 -0
- package/scripts/commit-product.sh +68 -0
- package/scripts/context-query.sh +45 -0
- package/scripts/session/auto-commit.sh +297 -0
- package/scripts/session/jfl-doctor.sh +707 -0
- package/scripts/session/session-cleanup.sh +268 -0
- package/scripts/session/session-end.sh +198 -0
- package/scripts/session/session-init.sh +350 -0
- package/scripts/session/session-init.sh.backup +292 -0
- package/scripts/session/session-sync.sh +167 -0
- package/scripts/session/test-context-preservation.sh +160 -0
- package/scripts/session/test-critical-infrastructure.sh +293 -0
- package/scripts/session/test-experience-level.sh +336 -0
- package/scripts/session/test-session-cleanup.sh +268 -0
- package/scripts/session/test-session-sync.sh +320 -0
- package/scripts/voice-start.sh +36 -8
- package/scripts/where-am-i.sh +78 -0
- package/template/.claude/service-settings.json +32 -0
- package/template/.claude/settings.json +14 -1
- package/template/.claude/skills/end/SKILL.md +1780 -0
- package/template/.jfl/config.json +2 -1
- package/template/CLAUDE.md +1039 -134
- 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 +58 -6
- package/template/scripts/session/jfl-doctor.sh +137 -17
- package/template/scripts/session/session-cleanup.sh +268 -0
- package/template/scripts/session/session-end.sh +4 -0
- package/template/scripts/session/session-init.sh +253 -66
- 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/template/scripts/session/auto-merge.sh +0 -325
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,626 @@ 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)
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// OpenClaw helpers
|
|
76
|
+
// ============================================================================
|
|
114
77
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
join(homedir(), "Projects"),
|
|
151
|
-
join(homedir(), "code"),
|
|
152
|
-
]
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// Welcome / Intro (shown on first use or install)
|
|
111
|
+
// ============================================================================
|
|
153
112
|
|
|
154
|
-
|
|
155
|
-
if (!existsSync(basePath)) continue
|
|
113
|
+
const WELCOME_MESSAGE = `JFL - Just Fucking Launch
|
|
156
114
|
|
|
157
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
continue // Skip product repos
|
|
170
|
-
}
|
|
129
|
+
To get started, use /jfl to pick a project.`
|
|
171
130
|
|
|
172
|
-
|
|
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
|
-
|
|
181
|
-
|
|
133
|
+
I'll now:
|
|
134
|
+
- Search context before each response
|
|
135
|
+
- Capture decisions automatically
|
|
136
|
+
- Auto-commit work periodically
|
|
182
137
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
sessionId: string
|
|
192
|
-
platform: string
|
|
193
|
-
}
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Boot (on /jfl command)
|
|
146
|
+
// ============================================================================
|
|
194
147
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
170
|
+
// Single GTM → auto-activate
|
|
171
|
+
if (gtms.length === 1) {
|
|
172
|
+
return await activateGTM(gtms[0].path, ctx)
|
|
173
|
+
}
|
|
223
174
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
setSession(ctx.threadId, {
|
|
208
|
+
setState(ctx.threadId, {
|
|
244
209
|
gtmPath,
|
|
245
210
|
gtmName,
|
|
246
|
-
|
|
247
|
-
|
|
211
|
+
agentId: agent,
|
|
212
|
+
sessionBranch: session.session_id,
|
|
213
|
+
activated: true,
|
|
248
214
|
})
|
|
249
215
|
|
|
250
|
-
|
|
251
|
-
|
|
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: "
|
|
254
|
-
{ text: "
|
|
256
|
+
{ text: "Dashboard", callbackData: "cmd:hud" },
|
|
257
|
+
{ text: "Search Context", callbackData: "cmd:context" },
|
|
255
258
|
],
|
|
256
259
|
[
|
|
257
|
-
{ text: "
|
|
258
|
-
{ text: "
|
|
260
|
+
{ text: "Switch Project", callbackData: "cmd:switch" },
|
|
261
|
+
{ text: "End Session", callbackData: "cmd:end" },
|
|
259
262
|
],
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
]
|
|
264
|
-
]
|
|
263
|
+
],
|
|
264
|
+
}
|
|
265
|
+
}
|
|
265
266
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
async function handleCommand(cmd: string, ctx: Context) {
|
|
285
|
-
const session = getSession(ctx.threadId)
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Commands
|
|
315
|
+
// ============================================================================
|
|
286
316
|
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
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:
|
|
350
|
+
return { text: `Unknown command: /${cmd}` }
|
|
312
351
|
}
|
|
313
352
|
}
|
|
314
353
|
|
|
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
|
-
)
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Runners
|
|
356
|
+
// ============================================================================
|
|
327
357
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
366
|
-
|
|
401
|
+
// ============================================================================
|
|
402
|
+
// Menus
|
|
403
|
+
// ============================================================================
|
|
404
|
+
|
|
367
405
|
function showCRMMenu() {
|
|
368
406
|
return {
|
|
369
|
-
text: "
|
|
407
|
+
text: "CRM\n\nWhat do you want to do?",
|
|
370
408
|
buttons: [
|
|
371
409
|
[
|
|
372
|
-
{ text: "
|
|
373
|
-
{ text: "
|
|
410
|
+
{ text: "Pipeline", callbackData: "crm:list" },
|
|
411
|
+
{ text: "Stale Deals", callbackData: "crm:stale" },
|
|
374
412
|
],
|
|
375
413
|
[
|
|
376
|
-
{ text: "
|
|
377
|
-
{ text: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
439
|
+
text: "Content Creator\n\nWhat do you want to create?",
|
|
408
440
|
buttons: [
|
|
409
441
|
[
|
|
410
|
-
{ text: "
|
|
411
|
-
{ text: "
|
|
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
|
-
*
|
|
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
|
|
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
|
-
)
|
|
517
|
+
export async function onSessionStart(event: { agentId: string; platform: string; threadId: string }) {
|
|
518
|
+
const existing = getState(event.threadId)
|
|
430
519
|
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
*
|
|
562
|
+
* before_turn — fires before each agent response.
|
|
563
|
+
* Only injects context when JFL is active.
|
|
443
564
|
*/
|
|
444
|
-
async function
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
*
|
|
464
|
-
*
|
|
623
|
+
* session_end — fires when Clawdbot session ends.
|
|
624
|
+
* Only cleans up if JFL was active.
|
|
465
625
|
*/
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
}
|