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
|
@@ -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
|