jfl 0.6.2 → 0.6.3
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/package.json
CHANGED
|
@@ -14,6 +14,12 @@ import { execSync } from "child_process"
|
|
|
14
14
|
import type { PiContext, PiTheme } from "./types.js"
|
|
15
15
|
import { hudRenderCall, hudRenderResult } from "./tool-renderers.js"
|
|
16
16
|
|
|
17
|
+
function clipLine(text: string, maxW: number): string {
|
|
18
|
+
const stripped = text.replace(/\x1b\[[0-9;]*m/g, "")
|
|
19
|
+
if (stripped.length <= maxW) return text
|
|
20
|
+
return text.slice(0, maxW - 1) + "…"
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
let projectRoot = ""
|
|
18
24
|
|
|
19
25
|
function getDaysToLaunch(root: string): string | null {
|
|
@@ -51,6 +57,22 @@ function getProjectPhase(root: string): string {
|
|
|
51
57
|
return "unknown"
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
function getLastJournalTitle(root: string): string | null {
|
|
61
|
+
try {
|
|
62
|
+
const journalDir = join(root, ".jfl", "journal")
|
|
63
|
+
if (!existsSync(journalDir)) return null
|
|
64
|
+
const files = execSync(`ls -t "${journalDir}"/*.jsonl 2>/dev/null | head -1`, {
|
|
65
|
+
cwd: root, timeout: 3000, encoding: "utf-8",
|
|
66
|
+
}).trim()
|
|
67
|
+
if (!files) return null
|
|
68
|
+
const content = readFileSync(files, "utf-8").trim()
|
|
69
|
+
const lastLine = content.split("\n").pop()
|
|
70
|
+
if (!lastLine) return null
|
|
71
|
+
const entry = JSON.parse(lastLine)
|
|
72
|
+
return entry.title ?? null
|
|
73
|
+
} catch { return null }
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
function buildHudLines(root: string): string[] {
|
|
55
77
|
const configPath = join(root, ".jfl", "config.json")
|
|
56
78
|
let projectName = root.split("/").pop() ?? "JFL"
|
|
@@ -66,41 +88,30 @@ function buildHudLines(root: string): string[] {
|
|
|
66
88
|
|
|
67
89
|
const days = getDaysToLaunch(root)
|
|
68
90
|
const phase = getProjectPhase(root)
|
|
91
|
+
const lastWork = getLastJournalTitle(root)
|
|
69
92
|
|
|
70
93
|
const lines = [
|
|
71
|
-
`◆ ${projectName}
|
|
94
|
+
`◆ ${projectName}`,
|
|
95
|
+
phase !== "unknown" ? ` ${phase}` : "",
|
|
72
96
|
days ? ` ${days}` : "",
|
|
73
|
-
|
|
97
|
+
lastWork ? ` Last: ${lastWork.slice(0, 60)}` : "",
|
|
74
98
|
].filter(Boolean)
|
|
75
99
|
|
|
76
|
-
try {
|
|
77
|
-
const crmOutput = execSync("./crm list --compact 2>/dev/null | head -3", {
|
|
78
|
-
cwd: root,
|
|
79
|
-
timeout: 5000,
|
|
80
|
-
encoding: "utf-8",
|
|
81
|
-
}).trim()
|
|
82
|
-
if (crmOutput) {
|
|
83
|
-
lines.push(" Pipeline:", ...crmOutput.split("\n").map(l => ` ${l}`))
|
|
84
|
-
}
|
|
85
|
-
} catch {}
|
|
86
|
-
|
|
87
100
|
return lines
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
export function setupHudTool(ctx: PiContext): void {
|
|
91
104
|
projectRoot = ctx.session.projectRoot
|
|
92
105
|
|
|
93
|
-
// Themed widget above editor
|
|
94
106
|
ctx.ui.setWidget("jfl-hud", (_tui: any, theme: PiTheme) => {
|
|
95
107
|
const lines = buildHudLines(projectRoot)
|
|
96
108
|
const themed = lines.map((line, i) => {
|
|
97
|
-
if (i === 0) return theme.fg("accent", line)
|
|
98
|
-
if (line.includes("
|
|
99
|
-
if (line.includes("Pipeline")) return theme.fg("accent", line)
|
|
109
|
+
if (i === 0) return theme.fg("accent", theme.bold(line))
|
|
110
|
+
if (line.includes("Last:")) return theme.fg("dim", line)
|
|
100
111
|
return theme.fg("muted", line)
|
|
101
112
|
})
|
|
102
113
|
return {
|
|
103
|
-
render: () => themed,
|
|
114
|
+
render: (width: number) => themed.map(l => clipLine(l, width)),
|
|
104
115
|
invalidate() {},
|
|
105
116
|
}
|
|
106
117
|
})
|
|
@@ -135,11 +146,10 @@ export async function updateHudWidget(ctx: PiContext): Promise<void> {
|
|
|
135
146
|
ctx.ui.setWidget("jfl-hud", (_tui: any, theme: PiTheme) => {
|
|
136
147
|
const lines = buildHudLines(projectRoot)
|
|
137
148
|
const themed = lines.map((line, i) => {
|
|
138
|
-
if (i === 0) return theme.fg("accent", line)
|
|
139
|
-
if (line.includes("
|
|
140
|
-
if (line.includes("Pipeline")) return theme.fg("accent", line)
|
|
149
|
+
if (i === 0) return theme.fg("accent", theme.bold(line))
|
|
150
|
+
if (line.includes("Last:")) return theme.fg("dim", line)
|
|
141
151
|
return theme.fg("muted", line)
|
|
142
152
|
})
|
|
143
|
-
return { render: () => themed, invalidate() {} }
|
|
153
|
+
return { render: (width: number) => themed.map(l => clipLine(l, width)), invalidate() {} }
|
|
144
154
|
})
|
|
145
155
|
}
|
|
@@ -317,24 +317,26 @@ export default async function jflExtension(pi: any): Promise<void> {
|
|
|
317
317
|
|
|
318
318
|
ctx.log(`JFL: ${projectName} — session ready`)
|
|
319
319
|
|
|
320
|
-
if (config.pi?.auto_start !== false && pi.
|
|
320
|
+
if (config.pi?.auto_start !== false && pi.sendMessage) {
|
|
321
321
|
setTimeout(() => {
|
|
322
|
-
pi.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
322
|
+
pi.sendMessage({
|
|
323
|
+
customType: "jfl-session-init",
|
|
324
|
+
content: [
|
|
325
|
+
`JFL session ready: "${projectName}" on branch ${ctx.session.branch}.`,
|
|
326
|
+
"",
|
|
327
|
+
"You have full project context injected in your system prompt (CLAUDE.md + recent journal + knowledge docs).",
|
|
328
|
+
"Tools available: jfl_context, jfl_memory_search, jfl_hud, jfl_journal.",
|
|
329
|
+
"",
|
|
330
|
+
"Greet the user naturally with a brief status:",
|
|
331
|
+
"- What was worked on recently (from your injected context)",
|
|
332
|
+
"- Current phase and any blockers",
|
|
333
|
+
"- A suggested next action",
|
|
334
|
+
"",
|
|
335
|
+
"Keep it to 3-5 lines. No setup noise. Just be ready to work.",
|
|
336
|
+
"Write journal entries as you work. Capture decisions immediately.",
|
|
337
|
+
].join("\n"),
|
|
338
|
+
display: false,
|
|
339
|
+
}, { triggerTurn: true, deliverAs: "steer" })
|
|
338
340
|
}, 500)
|
|
339
341
|
}
|
|
340
342
|
})
|
|
@@ -92,41 +92,38 @@ export function contextRenderCall(args: Record<string, any>, theme: PiTheme): an
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export function contextRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
95
|
-
const MAX_W = 140
|
|
96
95
|
const raw = extractText(result)
|
|
97
|
-
if (raw === "No relevant context found." || raw === "No context available.") {
|
|
98
|
-
return { render: () => safeRender([theme.fg("dim", "No
|
|
96
|
+
if (raw === "No relevant context found." || raw === "No context available." || raw === "Context Hub unavailable.") {
|
|
97
|
+
return { render: () => safeRender([theme.fg("dim", "No context available")]), invalidate() {} }
|
|
99
98
|
}
|
|
100
99
|
|
|
101
100
|
const sections = raw.split(/---\n/).filter(Boolean)
|
|
102
|
-
const
|
|
101
|
+
const lineCount = raw.split("\n").length
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
103
|
+
// Collapsed: one-line summary
|
|
104
|
+
if (!opts.expanded) {
|
|
105
|
+
const summary = theme.fg("success", "✓ ") + theme.fg("muted", `${sections.length} items, ${lineCount} lines loaded (Ctrl+O to expand)`)
|
|
106
|
+
return { render: () => safeRender([summary]), invalidate() {} }
|
|
107
|
+
}
|
|
108
108
|
|
|
109
|
+
// Expanded: show sections with truncation
|
|
110
|
+
const lines: string[] = []
|
|
111
|
+
for (const section of sections) {
|
|
112
|
+
const firstLine = section.trim().split("\n")[0] ?? ""
|
|
109
113
|
if (firstLine.startsWith("[")) {
|
|
110
114
|
const typeMatch = firstLine.match(/^\[(\w+)\]\s*(.*)/)
|
|
111
115
|
if (typeMatch) {
|
|
112
116
|
const typeColor = typeMatch[1] === "decision" ? "warning" : typeMatch[1] === "feature" ? "success" : "muted"
|
|
113
|
-
lines.push(truncLine(`${theme.fg(typeColor, `[${typeMatch[1]}]`)} ${theme.fg("text", typeMatch[2])}
|
|
117
|
+
lines.push(truncLine(`${theme.fg(typeColor, `[${typeMatch[1]}]`)} ${theme.fg("text", typeMatch[2])}`))
|
|
114
118
|
} else {
|
|
115
|
-
lines.push(truncLine(theme.fg("text", firstLine)
|
|
119
|
+
lines.push(truncLine(theme.fg("text", firstLine)))
|
|
116
120
|
}
|
|
117
121
|
} else {
|
|
118
|
-
lines.push(truncLine(theme.fg("text", firstLine)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (opts.expanded) {
|
|
122
|
-
const rest = section.split("\n").slice(1)
|
|
123
|
-
for (const l of rest) lines.push(truncLine(theme.fg("muted", l), MAX_W))
|
|
124
|
-
lines.push("")
|
|
122
|
+
lines.push(truncLine(theme.fg("text", firstLine)))
|
|
125
123
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
lines.push(theme.fg("dim", `... ${sections.length - 3} more results (Ctrl+O)`))
|
|
124
|
+
const rest = section.trim().split("\n").slice(1)
|
|
125
|
+
for (const l of rest) lines.push(truncLine(theme.fg("muted", l)))
|
|
126
|
+
lines.push("")
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
return { render: () => safeRender(lines), invalidate() {} }
|
|
@@ -177,27 +174,24 @@ export function memoryRenderCall(args: Record<string, any>, theme: PiTheme): any
|
|
|
177
174
|
export function memoryRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
|
|
178
175
|
const raw = extractText(result)
|
|
179
176
|
if (raw.includes("unavailable") || raw.includes("No memories")) {
|
|
180
|
-
return { render: () => safeRender([theme.fg("dim",
|
|
177
|
+
return { render: () => safeRender([theme.fg("dim", "No memories found")]), invalidate() {} }
|
|
181
178
|
}
|
|
182
179
|
|
|
183
180
|
const entries = raw.split(/\n\n---\n\n/).filter(Boolean)
|
|
184
|
-
const lines: string[] = []
|
|
185
181
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
if (header) lines.push(theme.fg("accent", header))
|
|
191
|
-
if (opts.expanded) {
|
|
192
|
-
for (const l of body) lines.push(theme.fg("muted", l))
|
|
193
|
-
lines.push("")
|
|
194
|
-
} else if (body.length > 0) {
|
|
195
|
-
lines.push(theme.fg("dim", body[0].slice(0, 80) + (body[0].length > 80 ? "…" : "")))
|
|
196
|
-
}
|
|
182
|
+
// Collapsed: one-line summary
|
|
183
|
+
if (!opts.expanded) {
|
|
184
|
+
const summary = theme.fg("success", "✓ ") + theme.fg("muted", `${entries.length} memories found (Ctrl+O to expand)`)
|
|
185
|
+
return { render: () => safeRender([summary]), invalidate() {} }
|
|
197
186
|
}
|
|
198
187
|
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
// Expanded: show entries
|
|
189
|
+
const lines: string[] = []
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const [header, ...body] = entry.trim().split("\n")
|
|
192
|
+
if (header) lines.push(truncLine(theme.fg("accent", header)))
|
|
193
|
+
for (const l of body) lines.push(truncLine(theme.fg("muted", l)))
|
|
194
|
+
lines.push("")
|
|
201
195
|
}
|
|
202
196
|
|
|
203
197
|
return { render: () => safeRender(lines), invalidate() {} }
|