jfl 0.6.0 → 0.6.2

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jfl",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Just Fucking Launch - CLI for AI-powered GTM and development",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,10 +12,29 @@ import type { PiTheme } from "./types.js"
12
12
 
13
13
  // ─── Shared helpers ──────────────────────────────────────────────────────────
14
14
 
15
- function truncLine(text: string, maxW: number): string {
16
- const stripped = text.replace(/\x1b\[[0-9;]*m/g, "")
17
- if (stripped.length <= maxW) return text
18
- return text.slice(0, maxW - 1) + ""
15
+ const MAX_LINE_W = 140
16
+
17
+ function visibleLen(text: string): number {
18
+ return text.replace(/\x1b\[[0-9;]*m/g, "").length
19
+ }
20
+
21
+ function truncLine(text: string, maxW: number = MAX_LINE_W): string {
22
+ if (visibleLen(text) <= maxW) return text
23
+ let visible = 0
24
+ let i = 0
25
+ while (i < text.length && visible < maxW - 1) {
26
+ if (text[i] === "\x1b" && text[i + 1] === "[") {
27
+ const end = text.indexOf("m", i)
28
+ if (end !== -1) { i = end + 1; continue }
29
+ }
30
+ visible++
31
+ i++
32
+ }
33
+ return text.slice(0, i) + "\x1b[0m…"
34
+ }
35
+
36
+ function safeRender(lines: string[]): string[] {
37
+ return lines.map(l => truncLine(l, MAX_LINE_W))
19
38
  }
20
39
 
21
40
  function wrapText(text: string, width: number): string[] {
@@ -41,7 +60,7 @@ function sectionBorder(theme: PiTheme, label: string, width: number): string {
41
60
 
42
61
  export function hudRenderCall(args: Record<string, any>, theme: PiTheme): any {
43
62
  const label = theme.fg("accent", "◆") + " " + theme.fg("toolTitle", theme.bold("HUD"))
44
- return { render: () => [label], invalidate() {} }
63
+ return { render: () => safeRender([label]), invalidate() {} }
45
64
  }
46
65
 
47
66
  export function hudRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
@@ -51,7 +70,7 @@ export function hudRenderResult(result: any, opts: { expanded: boolean }, theme:
51
70
  if (!opts.expanded && lines.length > 5) {
52
71
  const display = lines.slice(0, 5).map(l => theme.fg("toolOutput", l))
53
72
  display.push(theme.fg("dim", `... ${lines.length - 5} more lines (Ctrl+O)`))
54
- return { render: () => display, invalidate() {} }
73
+ return { render: () => safeRender(display), invalidate() {} }
55
74
  }
56
75
 
57
76
  const themed = lines.map(line => {
@@ -61,7 +80,7 @@ export function hudRenderResult(result: any, opts: { expanded: boolean }, theme:
61
80
  return theme.fg("toolOutput", line)
62
81
  })
63
82
 
64
- return { render: () => themed, invalidate() {} }
83
+ return { render: () => safeRender(themed), invalidate() {} }
65
84
  }
66
85
 
67
86
  // ─── Context Tool ───────────────────────────────────────────────────────────
@@ -69,14 +88,14 @@ export function hudRenderResult(result: any, opts: { expanded: boolean }, theme:
69
88
  export function contextRenderCall(args: Record<string, any>, theme: PiTheme): any {
70
89
  const query = args.query ?? ""
71
90
  const label = theme.fg("toolTitle", theme.bold("context ")) + theme.fg("accent", `"${query}"`)
72
- return { render: () => [label], invalidate() {} }
91
+ return { render: () => safeRender([label]), invalidate() {} }
73
92
  }
74
93
 
75
94
  export function contextRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
76
95
  const MAX_W = 140
77
96
  const raw = extractText(result)
78
97
  if (raw === "No relevant context found." || raw === "No context available.") {
79
- return { render: () => [theme.fg("dim", "No relevant context found")], invalidate() {} }
98
+ return { render: () => safeRender([theme.fg("dim", "No relevant context found")]), invalidate() {} }
80
99
  }
81
100
 
82
101
  const sections = raw.split(/---\n/).filter(Boolean)
@@ -110,7 +129,7 @@ export function contextRenderResult(result: any, opts: { expanded: boolean }, th
110
129
  lines.push(theme.fg("dim", `... ${sections.length - 3} more results (Ctrl+O)`))
111
130
  }
112
131
 
113
- return { render: () => lines, invalidate() {} }
132
+ return { render: () => safeRender(lines), invalidate() {} }
114
133
  }
115
134
 
116
135
  // ─── CRM Tool ───────────────────────────────────────────────────────────────
@@ -119,13 +138,13 @@ export function crmRenderCall(args: Record<string, any>, theme: PiTheme): any {
119
138
  const cmd = args.command ?? "list"
120
139
  const extra = args.args ? ` ${args.args}` : ""
121
140
  const label = theme.fg("toolTitle", theme.bold("crm ")) + theme.fg("accent", cmd) + theme.fg("dim", extra)
122
- return { render: () => [label], invalidate() {} }
141
+ return { render: () => safeRender([label]), invalidate() {} }
123
142
  }
124
143
 
125
144
  export function crmRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
126
145
  const raw = extractText(result)
127
146
  if (raw.startsWith("Error")) {
128
- return { render: () => [theme.fg("error", raw)], invalidate() {} }
147
+ return { render: () => safeRender([theme.fg("error", raw)]), invalidate() {} }
129
148
  }
130
149
 
131
150
  const lines = raw.split("\n").filter(Boolean)
@@ -140,10 +159,10 @@ export function crmRenderResult(result: any, opts: { expanded: boolean }, theme:
140
159
  if (!opts.expanded && themed.length > 8) {
141
160
  const display = themed.slice(0, 8)
142
161
  display.push(theme.fg("dim", `... ${themed.length - 8} more (Ctrl+O)`))
143
- return { render: () => display, invalidate() {} }
162
+ return { render: () => safeRender(display), invalidate() {} }
144
163
  }
145
164
 
146
- return { render: () => themed, invalidate() {} }
165
+ return { render: () => safeRender(themed), invalidate() {} }
147
166
  }
148
167
 
149
168
  // ─── Memory Search Tool ─────────────────────────────────────────────────────
@@ -152,13 +171,13 @@ export function memoryRenderCall(args: Record<string, any>, theme: PiTheme): any
152
171
  const query = args.query ?? ""
153
172
  const type = args.type && args.type !== "all" ? ` [${args.type}]` : ""
154
173
  const label = theme.fg("toolTitle", theme.bold("memory ")) + theme.fg("accent", `"${query}"`) + theme.fg("muted", type)
155
- return { render: () => [label], invalidate() {} }
174
+ return { render: () => safeRender([label]), invalidate() {} }
156
175
  }
157
176
 
158
177
  export function memoryRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
159
178
  const raw = extractText(result)
160
179
  if (raw.includes("unavailable") || raw.includes("No memories")) {
161
- return { render: () => [theme.fg("dim", raw)], invalidate() {} }
180
+ return { render: () => safeRender([theme.fg("dim", raw)]), invalidate() {} }
162
181
  }
163
182
 
164
183
  const entries = raw.split(/\n\n---\n\n/).filter(Boolean)
@@ -181,7 +200,7 @@ export function memoryRenderResult(result: any, opts: { expanded: boolean }, the
181
200
  lines.push(theme.fg("dim", `... ${entries.length - 4} more (Ctrl+O)`))
182
201
  }
183
202
 
184
- return { render: () => lines, invalidate() {} }
203
+ return { render: () => safeRender(lines), invalidate() {} }
185
204
  }
186
205
 
187
206
  // ─── Synopsis Tool ──────────────────────────────────────────────────────────
@@ -190,7 +209,7 @@ export function synopsisRenderCall(args: Record<string, any>, theme: PiTheme): a
190
209
  const hours = args.hours ?? 24
191
210
  const author = args.author ? ` by ${args.author}` : ""
192
211
  const label = theme.fg("toolTitle", theme.bold("synopsis ")) + theme.fg("accent", `${hours}h`) + theme.fg("muted", author)
193
- return { render: () => [label], invalidate() {} }
212
+ return { render: () => safeRender([label]), invalidate() {} }
194
213
  }
195
214
 
196
215
  export function synopsisRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
@@ -211,17 +230,17 @@ export function synopsisRenderResult(result: any, opts: { expanded: boolean }, t
211
230
  if (!opts.expanded && themed.length > 15) {
212
231
  const display = themed.slice(0, 15)
213
232
  display.push(theme.fg("dim", `... ${themed.length - 15} more lines (Ctrl+O)`))
214
- return { render: () => display, invalidate() {} }
233
+ return { render: () => safeRender(display), invalidate() {} }
215
234
  }
216
235
 
217
- return { render: () => themed, invalidate() {} }
236
+ return { render: () => safeRender(themed), invalidate() {} }
218
237
  }
219
238
 
220
239
  // ─── Eval Status Tool ───────────────────────────────────────────────────────
221
240
 
222
241
  export function evalStatusRenderCall(args: Record<string, any>, theme: PiTheme): any {
223
242
  const label = theme.fg("toolTitle", theme.bold("eval")) + " " + theme.fg("accent", "status")
224
- return { render: () => [label], invalidate() {} }
243
+ return { render: () => safeRender([label]), invalidate() {} }
225
244
  }
226
245
 
227
246
  export function evalStatusRenderResult(result: any, opts: { expanded: boolean }, theme: PiTheme): any {
@@ -239,7 +258,7 @@ export function evalStatusRenderResult(result: any, opts: { expanded: boolean },
239
258
  return theme.fg("toolOutput", line)
240
259
  })
241
260
 
242
- return { render: () => themed, invalidate() {} }
261
+ return { render: () => safeRender(themed), invalidate() {} }
243
262
  }
244
263
 
245
264
  // ─── Eval Compare Tool ──────────────────────────────────────────────────────
@@ -248,7 +267,7 @@ export function evalCompareRenderCall(args: Record<string, any>, theme: PiTheme)
248
267
  const a = args.a ?? "-2"
249
268
  const b = args.b ?? "-1"
250
269
  const label = theme.fg("toolTitle", theme.bold("eval")) + " " + theme.fg("accent", `compare ${a} ↔ ${b}`)
251
- return { render: () => [label], invalidate() {} }
270
+ return { render: () => safeRender([label]), invalidate() {} }
252
271
  }
253
272
 
254
273
  export function evalCompareRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
@@ -259,7 +278,7 @@ export function evalCompareRenderResult(result: any, _opts: { expanded: boolean
259
278
  if (/unchanged|same/i.test(line)) return theme.fg("dim", line)
260
279
  return theme.fg("toolOutput", line)
261
280
  })
262
- return { render: () => lines, invalidate() {} }
281
+ return { render: () => safeRender(lines), invalidate() {} }
263
282
  }
264
283
 
265
284
  // ─── Policy Score Tool ──────────────────────────────────────────────────────
@@ -268,7 +287,7 @@ export function policyScoreRenderCall(args: Record<string, any>, theme: PiTheme)
268
287
  const action = args.action_type ?? "?"
269
288
  const scope = args.scope ? `[${args.scope}]` : ""
270
289
  const label = theme.fg("toolTitle", theme.bold("policy ")) + theme.fg("accent", action) + " " + theme.fg("dim", scope)
271
- return { render: () => [label], invalidate() {} }
290
+ return { render: () => safeRender([label]), invalidate() {} }
272
291
  }
273
292
 
274
293
  export function policyScoreRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
@@ -279,7 +298,7 @@ export function policyScoreRenderResult(result: any, _opts: { expanded: boolean
279
298
  if (/delta|score|predict/i.test(line)) return theme.fg("accent", line)
280
299
  return theme.fg("toolOutput", line)
281
300
  })
282
- return { render: () => lines, invalidate() {} }
301
+ return { render: () => safeRender(lines), invalidate() {} }
283
302
  }
284
303
 
285
304
  // ─── Policy Rank Tool ───────────────────────────────────────────────────────
@@ -288,7 +307,7 @@ export function policyRankRenderCall(args: Record<string, any>, theme: PiTheme):
288
307
  let count = 0
289
308
  try { count = JSON.parse(args.actions ?? "[]").length } catch {}
290
309
  const label = theme.fg("toolTitle", theme.bold("policy ")) + theme.fg("accent", `rank ${count} actions`)
291
- return { render: () => [label], invalidate() {} }
310
+ return { render: () => safeRender([label]), invalidate() {} }
292
311
  }
293
312
 
294
313
  export function policyRankRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
@@ -299,7 +318,7 @@ export function policyRankRenderResult(result: any, _opts: { expanded: boolean }
299
318
  if (/delta|score/i.test(line)) return `${medal} ${theme.fg("accent", line)}`
300
319
  return `${medal} ${theme.fg("toolOutput", line)}`
301
320
  })
302
- return { render: () => themed, invalidate() {} }
321
+ return { render: () => safeRender(themed), invalidate() {} }
303
322
  }
304
323
 
305
324
  // ─── Training Buffer Tool ───────────────────────────────────────────────────
@@ -309,7 +328,7 @@ export function trainingBufferRenderCall(args: Record<string, any>, theme: PiThe
309
328
  const outcome = args.outcome ?? ""
310
329
  const icon = outcome === "improved" ? theme.fg("success", "↑") : outcome === "regressed" ? theme.fg("error", "↓") : theme.fg("dim", "→")
311
330
  const label = theme.fg("toolTitle", theme.bold("train ")) + `${icon} ${theme.fg("accent", action)}`
312
- return { render: () => [label], invalidate() {} }
331
+ return { render: () => safeRender([label]), invalidate() {} }
313
332
  }
314
333
 
315
334
  export function trainingBufferRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
@@ -317,7 +336,7 @@ export function trainingBufferRenderResult(result: any, _opts: { expanded: boole
317
336
  const line = raw.includes("recorded") || raw.includes("saved")
318
337
  ? theme.fg("success", "✓ ") + theme.fg("muted", raw)
319
338
  : theme.fg("toolOutput", raw)
320
- return { render: () => [line], invalidate() {} }
339
+ return { render: () => safeRender([line]), invalidate() {} }
321
340
  }
322
341
 
323
342
  // ─── Mine Tuples Tool ───────────────────────────────────────────────────────
@@ -326,7 +345,7 @@ export function mineTuplesRenderCall(args: Record<string, any>, theme: PiTheme):
326
345
  const source = args.source ?? "all"
327
346
  const write = args.write === "yes" ? theme.fg("warning", " (write)") : ""
328
347
  const label = theme.fg("toolTitle", theme.bold("mine ")) + theme.fg("accent", source) + write
329
- return { render: () => [label], invalidate() {} }
348
+ return { render: () => safeRender([label]), invalidate() {} }
330
349
  }
331
350
 
332
351
  export function mineTuplesRenderResult(result: any, _opts: { expanded: boolean }, theme: PiTheme): any {
@@ -336,7 +355,7 @@ export function mineTuplesRenderResult(result: any, _opts: { expanded: boolean }
336
355
  if (/error|failed/i.test(line)) return theme.fg("error", line)
337
356
  return theme.fg("toolOutput", line)
338
357
  })
339
- return { render: () => lines, invalidate() {} }
358
+ return { render: () => safeRender(lines), invalidate() {} }
340
359
  }
341
360
 
342
361
  // ─── Extract text helper ────────────────────────────────────────────────────