jfl 0.1.1 → 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 (112) hide show
  1. package/README.md +77 -7
  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/gtm-process-update.d.ts +10 -0
  13. package/dist/commands/gtm-process-update.d.ts.map +1 -0
  14. package/dist/commands/gtm-process-update.js +101 -0
  15. package/dist/commands/gtm-process-update.js.map +1 -0
  16. package/dist/commands/onboard.d.ts.map +1 -1
  17. package/dist/commands/onboard.js +203 -15
  18. package/dist/commands/onboard.js.map +1 -1
  19. package/dist/commands/openclaw.d.ts +56 -0
  20. package/dist/commands/openclaw.d.ts.map +1 -0
  21. package/dist/commands/openclaw.js +700 -0
  22. package/dist/commands/openclaw.js.map +1 -0
  23. package/dist/commands/service-validate.d.ts +12 -0
  24. package/dist/commands/service-validate.d.ts.map +1 -0
  25. package/dist/commands/service-validate.js +611 -0
  26. package/dist/commands/service-validate.js.map +1 -0
  27. package/dist/commands/services-create.d.ts +15 -0
  28. package/dist/commands/services-create.d.ts.map +1 -0
  29. package/dist/commands/services-create.js +1452 -0
  30. package/dist/commands/services-create.js.map +1 -0
  31. package/dist/commands/services-sync-agents.d.ts +23 -0
  32. package/dist/commands/services-sync-agents.d.ts.map +1 -0
  33. package/dist/commands/services-sync-agents.js +207 -0
  34. package/dist/commands/services-sync-agents.js.map +1 -0
  35. package/dist/commands/services.d.ts +7 -1
  36. package/dist/commands/services.d.ts.map +1 -1
  37. package/dist/commands/services.js +347 -22
  38. package/dist/commands/services.js.map +1 -1
  39. package/dist/commands/update.js +0 -0
  40. package/dist/commands/validate-settings.d.ts +37 -0
  41. package/dist/commands/validate-settings.d.ts.map +1 -0
  42. package/dist/commands/validate-settings.js +197 -0
  43. package/dist/commands/validate-settings.js.map +1 -0
  44. package/dist/index.js +155 -60
  45. package/dist/index.js.map +1 -1
  46. package/dist/lib/agent-generator.d.ts.map +1 -1
  47. package/dist/lib/agent-generator.js +94 -1
  48. package/dist/lib/agent-generator.js.map +1 -1
  49. package/dist/lib/openclaw-registry.d.ts +48 -0
  50. package/dist/lib/openclaw-registry.d.ts.map +1 -0
  51. package/dist/lib/openclaw-registry.js +181 -0
  52. package/dist/lib/openclaw-registry.js.map +1 -0
  53. package/dist/lib/openclaw-sdk.d.ts +107 -0
  54. package/dist/lib/openclaw-sdk.d.ts.map +1 -0
  55. package/dist/lib/openclaw-sdk.js +208 -0
  56. package/dist/lib/openclaw-sdk.js.map +1 -0
  57. package/dist/lib/peer-agent-generator.d.ts +44 -0
  58. package/dist/lib/peer-agent-generator.d.ts.map +1 -0
  59. package/dist/lib/peer-agent-generator.js +286 -0
  60. package/dist/lib/peer-agent-generator.js.map +1 -0
  61. package/dist/lib/service-detector.d.ts +1 -1
  62. package/dist/lib/service-detector.d.ts.map +1 -1
  63. package/dist/lib/service-detector.js +118 -5
  64. package/dist/lib/service-detector.js.map +1 -1
  65. package/dist/lib/service-gtm.d.ts +157 -0
  66. package/dist/lib/service-gtm.d.ts.map +1 -0
  67. package/dist/lib/service-gtm.js +786 -0
  68. package/dist/lib/service-gtm.js.map +1 -0
  69. package/dist/lib/service-mcp-base.d.ts +10 -1
  70. package/dist/lib/service-mcp-base.d.ts.map +1 -1
  71. package/dist/lib/service-mcp-base.js +20 -1
  72. package/dist/lib/service-mcp-base.js.map +1 -1
  73. package/dist/mcp/service-peer-mcp.d.ts +36 -0
  74. package/dist/mcp/service-peer-mcp.d.ts.map +1 -0
  75. package/dist/mcp/service-peer-mcp.js +220 -0
  76. package/dist/mcp/service-peer-mcp.js.map +1 -0
  77. package/dist/mcp/service-registry-mcp.js +0 -0
  78. package/dist/utils/settings-validator.d.ts +4 -1
  79. package/dist/utils/settings-validator.d.ts.map +1 -1
  80. package/dist/utils/settings-validator.js +25 -1
  81. package/dist/utils/settings-validator.js.map +1 -1
  82. package/package.json +2 -1
  83. package/template/.claude/service-settings.json +32 -0
  84. package/template/.claude/settings.json +10 -0
  85. package/template/.claude/skills/end/SKILL.md +1780 -0
  86. package/template/.jfl/config.json +2 -1
  87. package/template/.mcp.json +1 -7
  88. package/template/CLAUDE.md +1042 -248
  89. package/template/CLAUDE.md.bak +1187 -0
  90. package/template/scripts/commit-gtm.sh +56 -0
  91. package/template/scripts/commit-product.sh +68 -0
  92. package/template/scripts/migrate-to-branch-sessions.sh +201 -0
  93. package/template/scripts/session/auto-commit.sh +4 -3
  94. package/template/scripts/session/jfl-doctor.sh +222 -83
  95. package/template/scripts/session/session-cleanup.sh +109 -21
  96. package/template/scripts/session/session-end.sh +26 -13
  97. package/template/scripts/session/session-init.sh +280 -98
  98. package/template/scripts/session/test-critical-infrastructure.sh +293 -0
  99. package/template/scripts/session/test-experience-level.sh +336 -0
  100. package/template/scripts/session/test-session-cleanup.sh +268 -0
  101. package/template/scripts/session/test-session-sync.sh +320 -0
  102. package/template/scripts/where-am-i.sh +78 -0
  103. package/template/templates/service-agent/.claude/settings.json +32 -0
  104. package/template/templates/service-agent/CLAUDE.md +334 -0
  105. package/template/templates/service-agent/knowledge/ARCHITECTURE.md +115 -0
  106. package/template/templates/service-agent/knowledge/DEPLOYMENT.md +199 -0
  107. package/template/templates/service-agent/knowledge/RUNBOOK.md +412 -0
  108. package/template/templates/service-agent/knowledge/SERVICE_SPEC.md +77 -0
  109. package/dist/commands/session-mgmt.d.ts +0 -33
  110. package/dist/commands/session-mgmt.d.ts.map +0 -1
  111. package/dist/commands/session-mgmt.js +0 -404
  112. package/dist/commands/session-mgmt.js.map +0 -1
@@ -0,0 +1,700 @@
1
+ /**
2
+ * OpenClaw CLI Commands
3
+ *
4
+ * Runtime-agnostic agent plugin protocol for JFL.
5
+ * Provides session management, context, journaling, multi-GTM, and service tagging.
6
+ *
7
+ * @purpose CLI command tree for OpenClaw agent protocol
8
+ * @spec specs/OPENCLAW_SPEC.md
9
+ */
10
+ import chalk from "chalk";
11
+ import ora from "ora";
12
+ import { execSync } from "child_process";
13
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "fs";
14
+ import { join, basename, resolve } from "path";
15
+ import axios from "axios";
16
+ import { ensureAgent, listAgents, registerGtm, getActiveGtm, switchGtm, listGtms, updateSession, clearSession, getRegistryPath, } from "../lib/openclaw-registry.js";
17
+ import { findProjectRoot, isInJFLProject } from "../utils/jfl-config.js";
18
+ const CONTEXT_HUB_URL = process.env.CONTEXT_HUB_URL || "http://localhost:4242";
19
+ // ============================================================================
20
+ // Helpers
21
+ // ============================================================================
22
+ function jsonOutput(data) {
23
+ console.log(JSON.stringify(data, null, 2));
24
+ }
25
+ function errorOutput(code, message, suggestion) {
26
+ return { error: true, code, message, suggestion };
27
+ }
28
+ function getAuthToken(gtmPath) {
29
+ const tokenPath = join(gtmPath, ".jfl", "context-hub.token");
30
+ if (!existsSync(tokenPath))
31
+ return null;
32
+ return readFileSync(tokenPath, "utf-8").trim();
33
+ }
34
+ function getCurrentBranch() {
35
+ try {
36
+ return execSync("git branch --show-current", {
37
+ encoding: "utf-8",
38
+ stdio: ["pipe", "pipe", "pipe"],
39
+ }).trim();
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function resolveGtmPath(agentId, gtmPathArg) {
46
+ // Explicit path takes priority
47
+ if (gtmPathArg) {
48
+ const resolved = resolve(gtmPathArg);
49
+ if (isInJFLProject(resolved))
50
+ return resolved;
51
+ return null;
52
+ }
53
+ // Try agent registry
54
+ if (agentId) {
55
+ const gtm = getActiveGtm(agentId);
56
+ if (gtm && existsSync(gtm.path))
57
+ return gtm.path;
58
+ }
59
+ // Try current directory
60
+ const projectRoot = findProjectRoot();
61
+ if (projectRoot)
62
+ return projectRoot;
63
+ return null;
64
+ }
65
+ function generateSessionName(agentId) {
66
+ const date = new Date();
67
+ const dateStr = date.toISOString().slice(0, 10).replace(/-/g, "");
68
+ const timeStr = date.toISOString().slice(11, 16).replace(/:/g, "");
69
+ const randomId = Math.random().toString(16).slice(2, 8);
70
+ return `session-${agentId}-${dateStr}-${timeStr}-${randomId}`;
71
+ }
72
+ // ============================================================================
73
+ // session-start
74
+ // ============================================================================
75
+ export async function sessionStartCommand(options) {
76
+ const { agent, gtm, json } = options;
77
+ if (!agent) {
78
+ if (json)
79
+ return jsonOutput(errorOutput("MISSING_AGENT", "Agent name required", "Use --agent <name>"));
80
+ console.log(chalk.red("Error: --agent <name> is required"));
81
+ return;
82
+ }
83
+ const spinner = json ? null : ora("Starting session...").start();
84
+ // Ensure agent exists in registry
85
+ const agentEntry = ensureAgent(agent);
86
+ // Resolve GTM path
87
+ const gtmPath = resolveGtmPath(agent, gtm);
88
+ if (!gtmPath) {
89
+ if (spinner)
90
+ spinner.fail("No GTM workspace found");
91
+ if (json)
92
+ return jsonOutput(errorOutput("GTM_NOT_FOUND", "No GTM workspace found", "Use --gtm <path> or register with: jfl openclaw register --gtm <path>"));
93
+ console.log(chalk.gray("Use --gtm <path> or register with: jfl openclaw register --gtm <path>"));
94
+ return;
95
+ }
96
+ // Check for existing session
97
+ if (agentEntry.session) {
98
+ if (spinner)
99
+ spinner.warn("Agent already has an active session");
100
+ if (json)
101
+ return jsonOutput(errorOutput("SESSION_ALREADY_ACTIVE", `Session ${agentEntry.session.branch} already active`, "End it first: jfl openclaw session-end"));
102
+ console.log(chalk.yellow(`Active session: ${agentEntry.session.branch}`));
103
+ console.log(chalk.gray("End it first: jfl openclaw session-end"));
104
+ return;
105
+ }
106
+ // Register GTM if not already registered
107
+ const gtmName = readGtmName(gtmPath);
108
+ registerGtm(agent, gtmPath, gtmName, true);
109
+ // Create session branch
110
+ const sessionName = generateSessionName(agent);
111
+ try {
112
+ execSync(`git -C "${gtmPath}" checkout -b "${sessionName}" 2>&1`, {
113
+ encoding: "utf-8",
114
+ stdio: ["pipe", "pipe", "pipe"],
115
+ });
116
+ }
117
+ catch (err) {
118
+ // Branch might already exist or we're in detached HEAD
119
+ if (spinner)
120
+ spinner.fail("Failed to create session branch");
121
+ if (json)
122
+ return jsonOutput(errorOutput("SESSION_START_FAILED", `Could not create branch: ${err.message}`));
123
+ return;
124
+ }
125
+ // Ensure journal directory
126
+ mkdirSync(join(gtmPath, ".jfl", "journal"), { recursive: true });
127
+ // Save session info
128
+ const sessionInfo = {
129
+ branch: sessionName,
130
+ started_at: new Date().toISOString(),
131
+ worktree: null,
132
+ };
133
+ updateSession(agent, sessionInfo);
134
+ // Write session branch file
135
+ writeFileSync(join(gtmPath, ".jfl", "current-session-branch.txt"), sessionName);
136
+ // Start auto-commit if available
137
+ let autoCommitRunning = false;
138
+ const autoCommitScript = join(gtmPath, "scripts", "session", "auto-commit.sh");
139
+ if (existsSync(autoCommitScript)) {
140
+ try {
141
+ execSync(`bash "${autoCommitScript}" start >> "${join(gtmPath, ".jfl", "logs", "auto-commit.log")}" 2>&1 &`, {
142
+ cwd: gtmPath,
143
+ stdio: ["pipe", "pipe", "pipe"],
144
+ });
145
+ autoCommitRunning = true;
146
+ }
147
+ catch {
148
+ // Non-fatal
149
+ }
150
+ }
151
+ // Check Context Hub health
152
+ let hubHealthy = false;
153
+ try {
154
+ const resp = await axios.get(`${CONTEXT_HUB_URL}/api/health`, { timeout: 3000 });
155
+ hubHealthy = resp.data?.status === "ok";
156
+ }
157
+ catch {
158
+ // Hub not running - try to start it
159
+ try {
160
+ execSync("jfl context-hub ensure", { cwd: gtmPath, stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
161
+ hubHealthy = true;
162
+ }
163
+ catch {
164
+ // Non-fatal
165
+ }
166
+ }
167
+ if (spinner)
168
+ spinner.succeed(`Session started: ${sessionName}`);
169
+ const result = {
170
+ session_id: sessionName,
171
+ branch: sessionName,
172
+ gtm_path: gtmPath,
173
+ gtm_name: gtmName,
174
+ context_hub: { url: CONTEXT_HUB_URL, healthy: hubHealthy },
175
+ auto_commit: { running: autoCommitRunning, interval: 120 },
176
+ };
177
+ if (json)
178
+ return jsonOutput(result);
179
+ console.log(chalk.gray(` Branch: ${sessionName}`));
180
+ console.log(chalk.gray(` GTM: ${gtmName} (${gtmPath})`));
181
+ console.log(chalk.gray(` Context Hub: ${hubHealthy ? chalk.green("healthy") : chalk.yellow("unreachable")}`));
182
+ console.log(chalk.gray(` Auto-commit: ${autoCommitRunning ? chalk.green("running") : chalk.yellow("not started")}`));
183
+ }
184
+ // ============================================================================
185
+ // session-end
186
+ // ============================================================================
187
+ export async function sessionEndCommand(options) {
188
+ const { sync, json } = options;
189
+ const spinner = json ? null : ora("Ending session...").start();
190
+ // Find active agent session
191
+ const agents = listAgents();
192
+ const activeAgent = agents.find((a) => a.session !== null);
193
+ if (!activeAgent) {
194
+ if (spinner)
195
+ spinner.fail("No active session found");
196
+ if (json)
197
+ return jsonOutput(errorOutput("NO_ACTIVE_SESSION", "No agent has an active session"));
198
+ return;
199
+ }
200
+ const gtmPath = resolveGtmPath(activeAgent.id);
201
+ if (!gtmPath) {
202
+ if (spinner)
203
+ spinner.fail("GTM workspace not found");
204
+ if (json)
205
+ return jsonOutput(errorOutput("GTM_NOT_FOUND", "Could not resolve GTM path"));
206
+ return;
207
+ }
208
+ // Run session cleanup script if available
209
+ const cleanupScript = join(gtmPath, "scripts", "session", "session-cleanup.sh");
210
+ if (existsSync(cleanupScript)) {
211
+ try {
212
+ execSync(`bash "${cleanupScript}"`, {
213
+ cwd: gtmPath,
214
+ encoding: "utf-8",
215
+ timeout: 60000,
216
+ stdio: ["pipe", "pipe", "pipe"],
217
+ });
218
+ }
219
+ catch (err) {
220
+ // Cleanup scripts should not block session end
221
+ if (!json)
222
+ console.log(chalk.yellow(` Cleanup warning: ${err.message}`));
223
+ }
224
+ }
225
+ else {
226
+ // Manual cleanup: commit, merge
227
+ try {
228
+ execSync(`git -C "${gtmPath}" add -A && git -C "${gtmPath}" commit -m "session: end ${new Date().toISOString()}" 2>&1 || true`, {
229
+ encoding: "utf-8",
230
+ stdio: ["pipe", "pipe", "pipe"],
231
+ });
232
+ }
233
+ catch {
234
+ // Non-fatal
235
+ }
236
+ }
237
+ // Sync to GTM if requested and this is a service
238
+ if (sync) {
239
+ try {
240
+ execSync("jfl services sync 2>&1", {
241
+ cwd: gtmPath,
242
+ encoding: "utf-8",
243
+ timeout: 30000,
244
+ stdio: ["pipe", "pipe", "pipe"],
245
+ });
246
+ }
247
+ catch {
248
+ // Non-fatal
249
+ }
250
+ }
251
+ // Clear session in registry
252
+ clearSession(activeAgent.id);
253
+ if (spinner)
254
+ spinner.succeed("Session ended");
255
+ if (json) {
256
+ return jsonOutput({
257
+ agent: activeAgent.id,
258
+ session: activeAgent.session?.branch,
259
+ merged: true,
260
+ synced: !!sync,
261
+ });
262
+ }
263
+ console.log(chalk.gray(` Agent: ${activeAgent.id}`));
264
+ console.log(chalk.gray(` Session: ${activeAgent.session?.branch}`));
265
+ }
266
+ // ============================================================================
267
+ // heartbeat
268
+ // ============================================================================
269
+ export async function heartbeatCommand(options) {
270
+ const { json } = options;
271
+ const agents = listAgents();
272
+ const activeAgent = agents.find((a) => a.session !== null);
273
+ if (!activeAgent || !activeAgent.session) {
274
+ if (json)
275
+ return jsonOutput(errorOutput("NO_ACTIVE_SESSION", "No active session"));
276
+ console.log(chalk.yellow("No active session"));
277
+ return;
278
+ }
279
+ const gtmPath = resolveGtmPath(activeAgent.id);
280
+ // Auto-commit
281
+ let uncommittedChanges = false;
282
+ if (gtmPath) {
283
+ try {
284
+ const status = execSync(`git -C "${gtmPath}" status --porcelain`, {
285
+ encoding: "utf-8",
286
+ stdio: ["pipe", "pipe", "pipe"],
287
+ }).trim();
288
+ uncommittedChanges = status.length > 0;
289
+ if (uncommittedChanges) {
290
+ execSync(`git -C "${gtmPath}" add knowledge/ content/ suggestions/ CLAUDE.md .jfl/ 2>/dev/null; git -C "${gtmPath}" commit -m "auto: heartbeat ${new Date().toISOString().slice(0, 19)}" 2>/dev/null || true`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
291
+ }
292
+ }
293
+ catch {
294
+ // Non-fatal
295
+ }
296
+ }
297
+ // Check Context Hub
298
+ let hubHealthy = false;
299
+ try {
300
+ const resp = await axios.get(`${CONTEXT_HUB_URL}/api/health`, { timeout: 3000 });
301
+ hubHealthy = resp.data?.status === "ok";
302
+ }
303
+ catch {
304
+ // Hub down
305
+ }
306
+ // Calculate duration
307
+ const startedAt = activeAgent.session.started_at;
308
+ const durationSeconds = Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000);
309
+ // Get last commit time
310
+ let lastCommit = null;
311
+ if (gtmPath) {
312
+ try {
313
+ lastCommit = execSync(`git -C "${gtmPath}" log -1 --format=%cI`, {
314
+ encoding: "utf-8",
315
+ stdio: ["pipe", "pipe", "pipe"],
316
+ }).trim();
317
+ }
318
+ catch {
319
+ // Non-fatal
320
+ }
321
+ }
322
+ const result = {
323
+ healthy: hubHealthy && !uncommittedChanges,
324
+ context_hub: hubHealthy,
325
+ uncommitted_changes: uncommittedChanges,
326
+ last_commit: lastCommit,
327
+ session_duration_seconds: durationSeconds,
328
+ };
329
+ if (json)
330
+ return jsonOutput(result);
331
+ console.log(chalk.bold(result.healthy ? chalk.green("Healthy") : chalk.yellow("Degraded")));
332
+ console.log(chalk.gray(` Context Hub: ${hubHealthy ? "ok" : "unreachable"}`));
333
+ console.log(chalk.gray(` Uncommitted: ${uncommittedChanges ? "yes" : "no"}`));
334
+ console.log(chalk.gray(` Duration: ${Math.floor(durationSeconds / 60)}m`));
335
+ }
336
+ // ============================================================================
337
+ // context
338
+ // ============================================================================
339
+ export async function contextCommand(options) {
340
+ const { query, taskType, json } = options;
341
+ const spinner = json ? null : ora("Fetching context...").start();
342
+ // Find GTM path for auth token
343
+ const agents = listAgents();
344
+ const activeAgent = agents.find((a) => a.session !== null);
345
+ const gtmPath = resolveGtmPath(activeAgent?.id);
346
+ const headers = { "Content-Type": "application/json" };
347
+ if (gtmPath) {
348
+ const token = getAuthToken(gtmPath);
349
+ if (token)
350
+ headers["Authorization"] = `Bearer ${token}`;
351
+ }
352
+ try {
353
+ const resp = await axios.post(`${CONTEXT_HUB_URL}/api/context`, { query, taskType, maxItems: 30 }, { headers, timeout: 10000 });
354
+ const items = resp.data?.items || resp.data || [];
355
+ if (spinner)
356
+ spinner.succeed(`Got ${items.length} context items`);
357
+ if (json)
358
+ return jsonOutput(items);
359
+ if (items.length === 0) {
360
+ console.log(chalk.gray(" No context items found"));
361
+ return;
362
+ }
363
+ for (const item of items) {
364
+ console.log(chalk.cyan(` [${item.source || item.type}] `) + chalk.bold(item.title));
365
+ if (item.content) {
366
+ const preview = item.content.slice(0, 120).replace(/\n/g, " ");
367
+ console.log(chalk.gray(` ${preview}${item.content.length > 120 ? "..." : ""}`));
368
+ }
369
+ }
370
+ }
371
+ catch (err) {
372
+ if (spinner)
373
+ spinner.fail("Context Hub unreachable");
374
+ if (json)
375
+ return jsonOutput(errorOutput("CONTEXT_HUB_UNREACHABLE", "Context Hub not responding", "Run: jfl context-hub start"));
376
+ console.log(chalk.gray("Run: jfl context-hub start"));
377
+ }
378
+ }
379
+ // ============================================================================
380
+ // journal
381
+ // ============================================================================
382
+ export async function journalCommand(options) {
383
+ const { type, title, summary, detail, files, json } = options;
384
+ if (!type || !title || !summary) {
385
+ if (json)
386
+ return jsonOutput(errorOutput("MISSING_FIELDS", "Required: --type, --title, --summary"));
387
+ console.log(chalk.red("Required: --type, --title, --summary"));
388
+ return;
389
+ }
390
+ // Find session and GTM
391
+ const agents = listAgents();
392
+ const activeAgent = agents.find((a) => a.session !== null);
393
+ const session = activeAgent?.session?.branch || getCurrentBranch() || "unknown";
394
+ const gtmPath = resolveGtmPath(activeAgent?.id) || process.cwd();
395
+ const journalDir = join(gtmPath, ".jfl", "journal");
396
+ mkdirSync(journalDir, { recursive: true });
397
+ const entry = {
398
+ v: 1,
399
+ ts: new Date().toISOString(),
400
+ session,
401
+ type,
402
+ status: "complete",
403
+ title,
404
+ summary,
405
+ ...(detail && { detail }),
406
+ ...(files && { files: files.split(",").map((f) => f.trim()) }),
407
+ };
408
+ const journalFile = join(journalDir, `${session}.jsonl`);
409
+ appendFileSync(journalFile, JSON.stringify(entry) + "\n");
410
+ if (json)
411
+ return jsonOutput({ written: true, file: journalFile, entry });
412
+ console.log(chalk.green(`Journal entry written: ${title}`));
413
+ console.log(chalk.gray(` Type: ${type} | Session: ${session}`));
414
+ console.log(chalk.gray(` File: ${journalFile}`));
415
+ }
416
+ // ============================================================================
417
+ // status
418
+ // ============================================================================
419
+ export async function statusCommand(options) {
420
+ const { json } = options;
421
+ const agents = listAgents();
422
+ const activeAgent = agents.find((a) => a.session !== null);
423
+ // Check Context Hub
424
+ let hubHealthy = false;
425
+ try {
426
+ const resp = await axios.get(`${CONTEXT_HUB_URL}/api/health`, { timeout: 3000 });
427
+ hubHealthy = resp.data?.status === "ok";
428
+ }
429
+ catch {
430
+ // Down
431
+ }
432
+ const gtmPath = resolveGtmPath(activeAgent?.id);
433
+ const status = {
434
+ agent: activeAgent?.id || null,
435
+ session: activeAgent?.session || null,
436
+ gtm_path: gtmPath,
437
+ gtm_name: gtmPath ? readGtmName(gtmPath) : null,
438
+ context_hub: { url: CONTEXT_HUB_URL, healthy: hubHealthy },
439
+ registry_path: getRegistryPath(),
440
+ registered_agents: agents.map((a) => ({
441
+ id: a.id,
442
+ runtime: a.runtime,
443
+ active: !!a.session,
444
+ gtm_count: a.registered_gtms.length,
445
+ })),
446
+ };
447
+ if (json)
448
+ return jsonOutput(status);
449
+ console.log(chalk.bold("\nOpenClaw Status\n"));
450
+ if (activeAgent?.session) {
451
+ console.log(chalk.green(" Active Session"));
452
+ console.log(chalk.gray(` Agent: ${activeAgent.id}`));
453
+ console.log(chalk.gray(` Branch: ${activeAgent.session.branch}`));
454
+ console.log(chalk.gray(` Started: ${activeAgent.session.started_at}`));
455
+ }
456
+ else {
457
+ console.log(chalk.gray(" No active session"));
458
+ }
459
+ console.log();
460
+ console.log(chalk.gray(` GTM: ${status.gtm_name || "none"} (${gtmPath || "not found"})`));
461
+ console.log(chalk.gray(` Context Hub: ${hubHealthy ? chalk.green("healthy") : chalk.yellow("unreachable")}`));
462
+ console.log(chalk.gray(` Agents: ${agents.length} registered`));
463
+ console.log(chalk.gray(` Registry: ${getRegistryPath()}`));
464
+ console.log();
465
+ }
466
+ // ============================================================================
467
+ // gtm-list
468
+ // ============================================================================
469
+ export async function gtmListCommand(options) {
470
+ const { json } = options;
471
+ // Find agent - use first agent with GTMs or show all
472
+ const agents = listAgents();
473
+ const activeAgent = agents.find((a) => a.session !== null) || agents[0];
474
+ if (!activeAgent) {
475
+ if (json)
476
+ return jsonOutput([]);
477
+ console.log(chalk.gray("No agents registered. Use: jfl openclaw register --gtm <path>"));
478
+ return;
479
+ }
480
+ const gtms = listGtms(activeAgent.id);
481
+ if (json)
482
+ return jsonOutput(gtms);
483
+ if (gtms.length === 0) {
484
+ console.log(chalk.gray("No GTMs registered for agent. Use: jfl openclaw register --gtm <path>"));
485
+ return;
486
+ }
487
+ console.log(chalk.bold(`\nGTM Workspaces (${activeAgent.id})\n`));
488
+ for (const g of gtms) {
489
+ const marker = g.default ? chalk.green(" (default)") : "";
490
+ const active = activeAgent.active_gtm === g.id ? chalk.cyan(" [active]") : "";
491
+ console.log(` ${chalk.bold(g.name)}${marker}${active}`);
492
+ console.log(chalk.gray(` ID: ${g.id} | Path: ${g.path}`));
493
+ }
494
+ console.log();
495
+ }
496
+ // ============================================================================
497
+ // gtm-switch
498
+ // ============================================================================
499
+ export async function gtmSwitchCommand(gtmId, options) {
500
+ const { json } = options;
501
+ if (!gtmId) {
502
+ if (json)
503
+ return jsonOutput(errorOutput("MISSING_GTM_ID", "GTM ID required"));
504
+ console.log(chalk.red("Error: GTM ID required"));
505
+ return;
506
+ }
507
+ const agents = listAgents();
508
+ const activeAgent = agents.find((a) => a.session !== null) || agents[0];
509
+ if (!activeAgent) {
510
+ if (json)
511
+ return jsonOutput(errorOutput("AGENT_NOT_REGISTERED", "No agents registered"));
512
+ console.log(chalk.red("No agents registered"));
513
+ return;
514
+ }
515
+ try {
516
+ const gtm = switchGtm(activeAgent.id, gtmId);
517
+ if (json)
518
+ return jsonOutput({ switched: true, gtm });
519
+ console.log(chalk.green(`Switched to GTM: ${gtm.name}`));
520
+ console.log(chalk.gray(` Path: ${gtm.path}`));
521
+ }
522
+ catch (err) {
523
+ if (json)
524
+ return jsonOutput(errorOutput("GTM_NOT_FOUND", err.message));
525
+ console.log(chalk.red(err.message));
526
+ }
527
+ }
528
+ // ============================================================================
529
+ // gtm-create
530
+ // ============================================================================
531
+ export async function gtmCreateCommand(name, options) {
532
+ const { path: targetPath, json } = options;
533
+ if (!name) {
534
+ if (json)
535
+ return jsonOutput(errorOutput("MISSING_NAME", "GTM name required"));
536
+ console.log(chalk.red("Error: GTM name required"));
537
+ return;
538
+ }
539
+ const spinner = json ? null : ora(`Creating GTM workspace: ${name}`).start();
540
+ const gtmDir = targetPath || join(process.cwd(), name.toLowerCase().replace(/\s+/g, "-"));
541
+ try {
542
+ // Use jfl init to create the workspace
543
+ execSync(`jfl init -n "${name}"`, {
544
+ cwd: resolve(targetPath || process.cwd()),
545
+ encoding: "utf-8",
546
+ timeout: 30000,
547
+ stdio: ["pipe", "pipe", "pipe"],
548
+ });
549
+ // Register with current agent if one exists
550
+ const agents = listAgents();
551
+ const activeAgent = agents.find((a) => a.session !== null) || agents[0];
552
+ if (activeAgent) {
553
+ registerGtm(activeAgent.id, gtmDir, name);
554
+ }
555
+ if (spinner)
556
+ spinner.succeed(`GTM workspace created: ${name}`);
557
+ if (json)
558
+ return jsonOutput({ created: true, name, path: gtmDir });
559
+ console.log(chalk.gray(` Path: ${gtmDir}`));
560
+ }
561
+ catch (err) {
562
+ if (spinner)
563
+ spinner.fail("Failed to create GTM workspace");
564
+ if (json)
565
+ return jsonOutput(errorOutput("CREATE_FAILED", err.message));
566
+ console.log(chalk.gray(err.message));
567
+ }
568
+ }
569
+ // ============================================================================
570
+ // register
571
+ // ============================================================================
572
+ export async function registerCommand(options) {
573
+ const { agent, gtm, json } = options;
574
+ if (!gtm) {
575
+ if (json)
576
+ return jsonOutput(errorOutput("MISSING_GTM", "GTM path required. Use --gtm <path>"));
577
+ console.log(chalk.red("Error: --gtm <path> required"));
578
+ return;
579
+ }
580
+ const gtmPath = resolve(gtm);
581
+ if (!isInJFLProject(gtmPath)) {
582
+ if (json)
583
+ return jsonOutput(errorOutput("GTM_NOT_FOUND", `Not a JFL project: ${gtmPath}`));
584
+ console.log(chalk.red(`Not a JFL project: ${gtmPath}`));
585
+ return;
586
+ }
587
+ // Detect agent from manifest or use provided name
588
+ let agentId = agent || "default";
589
+ let runtime = "custom";
590
+ // Check for openclaw.plugin.json in current directory
591
+ const manifestPath = join(process.cwd(), "openclaw.plugin.json");
592
+ if (existsSync(manifestPath)) {
593
+ try {
594
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
595
+ agentId = manifest.id || agentId;
596
+ runtime = manifest.runtime?.type || runtime;
597
+ }
598
+ catch {
599
+ // Use defaults
600
+ }
601
+ }
602
+ const agentEntry = ensureAgent(agentId, runtime);
603
+ const gtmName = readGtmName(gtmPath);
604
+ const registration = registerGtm(agentId, gtmPath, gtmName, true);
605
+ // Also register in GTM's config
606
+ try {
607
+ const gtmConfigPath = join(gtmPath, ".jfl", "config.json");
608
+ if (existsSync(gtmConfigPath)) {
609
+ const gtmConfig = JSON.parse(readFileSync(gtmConfigPath, "utf-8"));
610
+ if (!gtmConfig.openclaw_agents)
611
+ gtmConfig.openclaw_agents = [];
612
+ const existing = gtmConfig.openclaw_agents.find((a) => a.id === agentId);
613
+ if (!existing) {
614
+ gtmConfig.openclaw_agents.push({
615
+ id: agentId,
616
+ runtime,
617
+ registered_at: new Date().toISOString(),
618
+ });
619
+ writeFileSync(gtmConfigPath, JSON.stringify(gtmConfig, null, 2) + "\n");
620
+ }
621
+ }
622
+ }
623
+ catch {
624
+ // Non-fatal
625
+ }
626
+ if (json)
627
+ return jsonOutput({ registered: true, agent: agentId, gtm: registration });
628
+ console.log(chalk.green(`Registered agent "${agentId}" with GTM "${gtmName}"`));
629
+ console.log(chalk.gray(` GTM path: ${gtmPath}`));
630
+ console.log(chalk.gray(` GTM ID: ${registration.id}`));
631
+ }
632
+ // ============================================================================
633
+ // tag
634
+ // ============================================================================
635
+ export async function tagCommand(service, message, options) {
636
+ const { json } = options;
637
+ if (!service || !message) {
638
+ if (json)
639
+ return jsonOutput(errorOutput("MISSING_ARGS", "Usage: jfl openclaw tag <service> <message>"));
640
+ console.log(chalk.red("Usage: jfl openclaw tag <service> <message>"));
641
+ return;
642
+ }
643
+ const agents = listAgents();
644
+ const activeAgent = agents.find((a) => a.session !== null);
645
+ const session = activeAgent?.session?.branch || getCurrentBranch() || "unknown";
646
+ const gtmPath = resolveGtmPath(activeAgent?.id) || process.cwd();
647
+ const event = {
648
+ ts: new Date().toISOString(),
649
+ source: `openclaw:${activeAgent?.id || "unknown"}`,
650
+ target: service,
651
+ type: "tag",
652
+ message,
653
+ session,
654
+ };
655
+ // Append to service events
656
+ const eventsFile = join(gtmPath, ".jfl", "service-events.jsonl");
657
+ appendFileSync(eventsFile, JSON.stringify(event) + "\n");
658
+ // Create inbox trigger if directory exists
659
+ const inboxDir = join(gtmPath, ".jfl", "inbox", service);
660
+ if (existsSync(join(gtmPath, ".jfl", "inbox"))) {
661
+ mkdirSync(inboxDir, { recursive: true });
662
+ const triggerFile = join(inboxDir, `${Date.now()}.json`);
663
+ writeFileSync(triggerFile, JSON.stringify(event, null, 2) + "\n");
664
+ }
665
+ if (json)
666
+ return jsonOutput({ sent: true, event });
667
+ console.log(chalk.green(`Tagged ${service}: ${message}`));
668
+ console.log(chalk.gray(` Event logged to ${eventsFile}`));
669
+ }
670
+ // ============================================================================
671
+ // Utility
672
+ // ============================================================================
673
+ function readGtmName(gtmPath) {
674
+ try {
675
+ const configPath = join(gtmPath, ".jfl", "config.json");
676
+ if (existsSync(configPath)) {
677
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
678
+ if (config.name)
679
+ return config.name;
680
+ }
681
+ }
682
+ catch {
683
+ // Fall through
684
+ }
685
+ // Try knowledge/VISION.md first heading
686
+ try {
687
+ const visionPath = join(gtmPath, "knowledge", "VISION.md");
688
+ if (existsSync(visionPath)) {
689
+ const content = readFileSync(visionPath, "utf-8");
690
+ const match = content.match(/^#\s+(.+)/m);
691
+ if (match)
692
+ return match[1].trim();
693
+ }
694
+ }
695
+ catch {
696
+ // Fall through
697
+ }
698
+ return basename(gtmPath);
699
+ }
700
+ //# sourceMappingURL=openclaw.js.map