@cleocode/cleo-os 2026.4.37 → 2026.4.38
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/dist/cli.d.ts +4 -15
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -41
- package/dist/cli.js.map +1 -1
- package/extensions/cleo-startup.d.ts +5 -79
- package/extensions/cleo-startup.d.ts.map +1 -1
- package/extensions/cleo-startup.js +66 -335
- package/extensions/cleo-startup.js.map +1 -1
- package/extensions/cleo-startup.ts +94 -463
- package/package.json +3 -3
|
@@ -7,19 +7,10 @@
|
|
|
7
7
|
* Loaded by: Pi via `--extension <path>` injected by CleoOS cli.ts
|
|
8
8
|
*
|
|
9
9
|
* On `session_start`, displays a branded CleoOS welcome panel with:
|
|
10
|
-
* - ASCII art CLEO logo
|
|
11
10
|
* - Project name and CLEO task summary (pending / active / done)
|
|
12
|
-
* - Last 3 brain decisions ("what we decided recently")
|
|
13
|
-
* - Memory bridge summary (total entries, % verified, top 3 cited)
|
|
14
|
-
* - Quick action hints based on current state
|
|
15
11
|
* - Current focused task (if any)
|
|
16
12
|
* - Last session handoff note from the memory bridge
|
|
17
13
|
*
|
|
18
|
-
* Also registers:
|
|
19
|
-
* - `/cleo:status` — on-demand project status refresh
|
|
20
|
-
* - `/cleo:focus <task-id>` — focus on a task
|
|
21
|
-
* - `/cleo:end-session [note]` — end the current session
|
|
22
|
-
*
|
|
23
14
|
* All data is fetched via the `cleo` CLI (best-effort). If any call
|
|
24
15
|
* fails, the banner degrades gracefully — Pi is never crashed.
|
|
25
16
|
*
|
|
@@ -118,36 +109,6 @@ interface CurrentTask {
|
|
|
118
109
|
status: string;
|
|
119
110
|
}
|
|
120
111
|
|
|
121
|
-
/**
|
|
122
|
-
* A single decision entry from `cleo memory decision-find --json`.
|
|
123
|
-
*/
|
|
124
|
-
interface DecisionEntry {
|
|
125
|
-
/** Short decision ID. */
|
|
126
|
-
id: string;
|
|
127
|
-
/** Decision title or summary text. */
|
|
128
|
-
title: string;
|
|
129
|
-
/** ISO date string. */
|
|
130
|
-
date: string;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Memory bridge statistics parsed from `.cleo/memory-bridge.md`.
|
|
135
|
-
*/
|
|
136
|
-
interface MemoryBridgeStats {
|
|
137
|
-
/** Total entry count found in memory bridge sections. */
|
|
138
|
-
totalEntries: number;
|
|
139
|
-
/** Top 3 cited items (label lines from the bridge). */
|
|
140
|
-
topThree: string[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Quick-action hints built from task/session state.
|
|
145
|
-
*/
|
|
146
|
-
interface QuickHints {
|
|
147
|
-
/** Array of short hint strings to display. */
|
|
148
|
-
hints: string[];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
112
|
// ============================================================================
|
|
152
113
|
// Parsers — best-effort, always return a typed default on failure
|
|
153
114
|
// ============================================================================
|
|
@@ -268,116 +229,6 @@ export function parseCurrentTask(stdout: string): CurrentTask | null {
|
|
|
268
229
|
}
|
|
269
230
|
}
|
|
270
231
|
|
|
271
|
-
/**
|
|
272
|
-
* Parse the last 3 decisions from `cleo memory decision-find --json` output.
|
|
273
|
-
*
|
|
274
|
-
* @param stdout - Raw stdout from the CLI call.
|
|
275
|
-
* @returns Array of up to 3 decision entries.
|
|
276
|
-
*/
|
|
277
|
-
export function parseRecentDecisions(stdout: string): DecisionEntry[] {
|
|
278
|
-
try {
|
|
279
|
-
const parsed = JSON.parse(stdout) as Record<string, unknown>;
|
|
280
|
-
const data = (parsed["data"] ?? parsed) as Record<string, unknown>;
|
|
281
|
-
const rawDecisions = data["decisions"];
|
|
282
|
-
if (!Array.isArray(rawDecisions)) return [];
|
|
283
|
-
|
|
284
|
-
return rawDecisions
|
|
285
|
-
.slice(0, 3)
|
|
286
|
-
.map((d) => {
|
|
287
|
-
const entry = d as Record<string, unknown>;
|
|
288
|
-
const id = typeof entry["id"] === "string" ? entry["id"] : "";
|
|
289
|
-
const title =
|
|
290
|
-
typeof entry["title"] === "string"
|
|
291
|
-
? entry["title"]
|
|
292
|
-
: typeof entry["summary"] === "string"
|
|
293
|
-
? entry["summary"]
|
|
294
|
-
: "";
|
|
295
|
-
const date =
|
|
296
|
-
typeof entry["date"] === "string"
|
|
297
|
-
? entry["date"].slice(0, 10)
|
|
298
|
-
: typeof entry["createdAt"] === "string"
|
|
299
|
-
? entry["createdAt"].slice(0, 10)
|
|
300
|
-
: "";
|
|
301
|
-
return { id, title, date };
|
|
302
|
-
})
|
|
303
|
-
.filter((d) => d.title.length > 0);
|
|
304
|
-
} catch {
|
|
305
|
-
return [];
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Parse memory bridge statistics from `.cleo/memory-bridge.md`.
|
|
311
|
-
*
|
|
312
|
-
* Counts total list items across all sections and extracts the
|
|
313
|
-
* top 3 cited items (items appearing in the "Recent Decisions" or
|
|
314
|
-
* "Key Learnings" sections).
|
|
315
|
-
*
|
|
316
|
-
* @param content - Raw memory-bridge.md content.
|
|
317
|
-
* @returns Memory bridge statistics.
|
|
318
|
-
*/
|
|
319
|
-
export function parseMemoryBridgeStats(content: string): MemoryBridgeStats {
|
|
320
|
-
if (!content || content.length === 0) {
|
|
321
|
-
return { totalEntries: 0, topThree: [] };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Count all list items (lines starting with "- " or "* ")
|
|
325
|
-
const allItems = content.match(/^[-*]\s+\[.+?\].+/gm) ?? [];
|
|
326
|
-
const totalEntries = allItems.length;
|
|
327
|
-
|
|
328
|
-
// Extract top 3 from "Key Learnings" or "Recent Decisions" sections
|
|
329
|
-
const learningsMatch = content.match(/## Key Learnings\n([\s\S]*?)(?=\n##|\s*$)/m);
|
|
330
|
-
const decisionsMatch = content.match(/## Recent Decisions\n([\s\S]*?)(?=\n##|\s*$)/m);
|
|
331
|
-
|
|
332
|
-
const topSection = (learningsMatch?.[1] ?? decisionsMatch?.[1] ?? "");
|
|
333
|
-
const topItems = (topSection.match(/^[-*]\s+\[.+?\]\s+(.+)/gm) ?? [])
|
|
334
|
-
.slice(0, 3)
|
|
335
|
-
.map((line) => {
|
|
336
|
-
// Strip the "- [ID] " prefix, keep the description
|
|
337
|
-
return line.replace(/^[-*]\s+\[.+?\]\s+/, "").trim();
|
|
338
|
-
})
|
|
339
|
-
.filter((s) => s.length > 0);
|
|
340
|
-
|
|
341
|
-
return { totalEntries, topThree: topItems };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Build quick-action hints based on current project state.
|
|
346
|
-
*
|
|
347
|
-
* Generates context-sensitive suggestions so the operator knows
|
|
348
|
-
* immediately what to do next without running extra commands.
|
|
349
|
-
*
|
|
350
|
-
* @param tasks - Task summary counts.
|
|
351
|
-
* @param session - Current session state.
|
|
352
|
-
* @param currentTask - Currently focused task, or null.
|
|
353
|
-
* @returns Quick-action hints.
|
|
354
|
-
*/
|
|
355
|
-
export function buildQuickHints(
|
|
356
|
-
tasks: TaskSummary,
|
|
357
|
-
session: SessionInfo,
|
|
358
|
-
currentTask: CurrentTask | null,
|
|
359
|
-
): QuickHints {
|
|
360
|
-
const hints: string[] = [];
|
|
361
|
-
|
|
362
|
-
if (!session.active) {
|
|
363
|
-
hints.push("No active session — run `cleo session start` to begin");
|
|
364
|
-
} else if (!currentTask && tasks.pending > 0) {
|
|
365
|
-
hints.push(`${tasks.pending} ready tasks — run \`cleo next\` to pick one`);
|
|
366
|
-
} else if (currentTask) {
|
|
367
|
-
hints.push(`Working on [${currentTask.id}] — run \`cleo complete ${currentTask.id}\` when done`);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (tasks.blocked > 0) {
|
|
371
|
-
hints.push(`${tasks.blocked} blocked task(s) — run \`cleo blockers\` to review`);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (hints.length === 0 && tasks.total === 0) {
|
|
375
|
-
hints.push("No tasks yet — run `cleo add \"<task title>\"` to start");
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return { hints };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
232
|
/**
|
|
382
233
|
* Read the last session handoff note from `.cleo/memory-bridge.md`.
|
|
383
234
|
*
|
|
@@ -455,30 +306,12 @@ function padBannerLine(
|
|
|
455
306
|
);
|
|
456
307
|
}
|
|
457
308
|
|
|
458
|
-
/**
|
|
459
|
-
* ASCII art CLEO logo lines (no ANSI — caller applies color).
|
|
460
|
-
*
|
|
461
|
-
* Rendered in a compact 3-row form that fits within the 54-char banner width.
|
|
462
|
-
*/
|
|
463
|
-
const CLEO_ASCII_LOGO: readonly string[] = [
|
|
464
|
-
" ██████╗██╗ ███████╗ ██████╗",
|
|
465
|
-
" ██╔════╝██║ ██╔════╝██╔═══██╗",
|
|
466
|
-
" ██║ ██║ █████╗ ██║ ██║",
|
|
467
|
-
" ██║ ██║ ██╔══╝ ██║ ██║",
|
|
468
|
-
" ╚██████╗███████╗███████╗╚██████╔╝",
|
|
469
|
-
" ╚═════╝╚══════╝╚══════╝ ╚═════╝",
|
|
470
|
-
] as const;
|
|
471
|
-
|
|
472
309
|
/**
|
|
473
310
|
* Build the full CleoOS startup banner.
|
|
474
311
|
*
|
|
475
312
|
* Renders a box-drawing widget with:
|
|
476
|
-
* - ASCII art CLEO logo
|
|
477
313
|
* - Branded header with forge icon
|
|
478
314
|
* - Task counts (active / pending / done / blocked)
|
|
479
|
-
* - Last 3 brain decisions
|
|
480
|
-
* - Memory bridge summary
|
|
481
|
-
* - Quick action hints
|
|
482
315
|
* - Current task title (if any)
|
|
483
316
|
* - Session info (name + ID)
|
|
484
317
|
* - Last session handoff note (from memory-bridge.md or session data)
|
|
@@ -488,9 +321,6 @@ const CLEO_ASCII_LOGO: readonly string[] = [
|
|
|
488
321
|
* @param currentTask - Currently focused task, or null.
|
|
489
322
|
* @param handoffNote - Last session handoff note, or null.
|
|
490
323
|
* @param projectName - Project display name.
|
|
491
|
-
* @param decisions - Recent brain decisions (up to 3).
|
|
492
|
-
* @param memStats - Memory bridge statistics.
|
|
493
|
-
* @param hints - Quick-action hints.
|
|
494
324
|
* @returns Array of ANSI-styled banner lines.
|
|
495
325
|
*/
|
|
496
326
|
export function buildStartupBanner(
|
|
@@ -499,9 +329,6 @@ export function buildStartupBanner(
|
|
|
499
329
|
currentTask: CurrentTask | null,
|
|
500
330
|
handoffNote: string | null,
|
|
501
331
|
projectName: string,
|
|
502
|
-
decisions: DecisionEntry[] = [],
|
|
503
|
-
memStats: MemoryBridgeStats = { totalEntries: 0, topThree: [] },
|
|
504
|
-
hints: QuickHints = { hints: [] },
|
|
505
332
|
): string[] {
|
|
506
333
|
// Inner width: characters between the two vertical border chars
|
|
507
334
|
// (not counting the leading space + BOX_VERTICAL or trailing BOX_VERTICAL)
|
|
@@ -513,20 +340,24 @@ export function buildStartupBanner(
|
|
|
513
340
|
// ── Top border ────────────────────────────────────────────────────────
|
|
514
341
|
lines.push(accentPrimary(BOX_TOP_LEFT + hBar + BOX_TOP_RIGHT));
|
|
515
342
|
|
|
516
|
-
// ── ASCII Logo ────────────────────────────────────────────────────────
|
|
517
|
-
for (const logoLine of CLEO_ASCII_LOGO) {
|
|
518
|
-
// Pad to INNER width so border chars align
|
|
519
|
-
lines.push(padBannerLine(logoLine + " ", accentPrimary(logoLine), INNER));
|
|
520
|
-
}
|
|
521
|
-
|
|
522
343
|
// ── Header row ────────────────────────────────────────────────────────
|
|
523
|
-
const headerRaw = ` ${ICON_FORGE}
|
|
344
|
+
const headerRaw = ` ${ICON_FORGE} C L E O O S ${ICON_FORGE} — ${truncate(projectName, 24)}`;
|
|
524
345
|
lines.push(
|
|
525
346
|
padBannerLine(
|
|
526
347
|
" " + headerRaw + " ",
|
|
527
|
-
" " + bold(accentPrimary(`${ICON_FORGE}
|
|
528
|
-
accentPrimary(" ") +
|
|
529
|
-
bold(textSecondary(truncate(projectName,
|
|
348
|
+
" " + bold(accentPrimary(`${ICON_FORGE} C L E O O S ${ICON_FORGE}`)) +
|
|
349
|
+
accentPrimary(" — ") +
|
|
350
|
+
bold(textSecondary(truncate(projectName, 24))),
|
|
351
|
+
INNER,
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// ── Subtitle ──────────────────────────────────────────────────────────
|
|
356
|
+
const subtitleRaw = " The Agentic Development Forge";
|
|
357
|
+
lines.push(
|
|
358
|
+
padBannerLine(
|
|
359
|
+
" " + subtitleRaw + " ",
|
|
360
|
+
" " + textSecondary("The Agentic Development Forge"),
|
|
530
361
|
INNER,
|
|
531
362
|
),
|
|
532
363
|
);
|
|
@@ -588,63 +419,6 @@ export function buildStartupBanner(
|
|
|
588
419
|
lines.push(padBannerLine(" " + sessionRaw + " ", sessionStyled, INNER));
|
|
589
420
|
}
|
|
590
421
|
|
|
591
|
-
// ── Memory bridge stats ───────────────────────────────────────────────
|
|
592
|
-
if (memStats.totalEntries > 0) {
|
|
593
|
-
lines.push(accentPrimary(BOX_LEFT_T + hBar + BOX_RIGHT_T));
|
|
594
|
-
const memRaw = ` Memory: ${memStats.totalEntries} entries`;
|
|
595
|
-
const memStyled =
|
|
596
|
-
` ${textSecondary("Memory:")} ` + accentSuccess(String(memStats.totalEntries)) +
|
|
597
|
-
textSecondary(" entries");
|
|
598
|
-
lines.push(padBannerLine(" " + memRaw + " ", memStyled, INNER));
|
|
599
|
-
|
|
600
|
-
for (const item of memStats.topThree) {
|
|
601
|
-
const truncItem = truncate(item, INNER - 6);
|
|
602
|
-
lines.push(
|
|
603
|
-
padBannerLine(
|
|
604
|
-
` * ${truncItem} `,
|
|
605
|
-
` ${textTertiary("*")} ${textSecondary(truncItem)}`,
|
|
606
|
-
INNER,
|
|
607
|
-
),
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// ── Recent decisions ──────────────────────────────────────────────────
|
|
613
|
-
if (decisions.length > 0) {
|
|
614
|
-
lines.push(accentPrimary(BOX_LEFT_T + hBar + BOX_RIGHT_T));
|
|
615
|
-
const decHeaderRaw = " What we decided recently:";
|
|
616
|
-
lines.push(
|
|
617
|
-
padBannerLine(
|
|
618
|
-
" " + decHeaderRaw + " ",
|
|
619
|
-
" " + bold(textSecondary("What we decided recently:")),
|
|
620
|
-
INNER,
|
|
621
|
-
),
|
|
622
|
-
);
|
|
623
|
-
for (const dec of decisions) {
|
|
624
|
-
const label = dec.date ? `[${dec.date}]` : `[${dec.id}]`;
|
|
625
|
-
const decRaw = ` ${label} ${truncate(dec.title, INNER - label.length - 5)}`;
|
|
626
|
-
const decStyled =
|
|
627
|
-
` ${textTertiary(label)} ` +
|
|
628
|
-
textSecondary(truncate(dec.title, INNER - label.length - 5));
|
|
629
|
-
lines.push(padBannerLine(" " + decRaw + " ", decStyled, INNER));
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// ── Quick action hints ────────────────────────────────────────────────
|
|
634
|
-
if (hints.hints.length > 0) {
|
|
635
|
-
lines.push(accentPrimary(BOX_LEFT_T + hBar + BOX_RIGHT_T));
|
|
636
|
-
for (const hint of hints.hints) {
|
|
637
|
-
const truncHint = truncate(hint, INNER - 5);
|
|
638
|
-
lines.push(
|
|
639
|
-
padBannerLine(
|
|
640
|
-
` > ${truncHint} `,
|
|
641
|
-
` ${accentWarning(">")} ${accentWarning(truncHint)}`,
|
|
642
|
-
INNER,
|
|
643
|
-
),
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
422
|
// ── Handoff note ──────────────────────────────────────────────────────
|
|
649
423
|
const note = handoffNote ?? session.handoffNote;
|
|
650
424
|
if (note) {
|
|
@@ -738,143 +512,65 @@ export function detectProjectName(projectDir: string): string {
|
|
|
738
512
|
// Pi extension factory
|
|
739
513
|
// ============================================================================
|
|
740
514
|
|
|
741
|
-
// ============================================================================
|
|
742
|
-
// Shared fetch helper (used by both session_start and /cleo:status)
|
|
743
|
-
// ============================================================================
|
|
744
|
-
|
|
745
515
|
/**
|
|
746
|
-
*
|
|
516
|
+
* Pi extension factory for the CleoOS branded startup experience.
|
|
517
|
+
*
|
|
518
|
+
* Registers a `session_start` handler that:
|
|
519
|
+
* 1. Fetches project, task, and session data via `cleo` CLI in parallel
|
|
520
|
+
* 2. Reads the memory-bridge.md handoff note
|
|
521
|
+
* 3. Renders the branded startup banner as a Pi UI widget
|
|
747
522
|
*
|
|
748
|
-
* All
|
|
749
|
-
*
|
|
523
|
+
* All operations are best-effort — failures are silently swallowed so Pi
|
|
524
|
+
* is never blocked by CLEO unavailability.
|
|
750
525
|
*
|
|
751
|
-
* @param
|
|
752
|
-
* @returns All banner data.
|
|
526
|
+
* @param pi - The Pi extension API instance.
|
|
753
527
|
*/
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
projectName: string;
|
|
760
|
-
decisions: DecisionEntry[];
|
|
761
|
-
memStats: MemoryBridgeStats;
|
|
762
|
-
hints: QuickHints;
|
|
763
|
-
}> {
|
|
764
|
-
const [dashResult, sessionResult, currentResult, decisionsResult] =
|
|
765
|
-
await Promise.allSettled([
|
|
766
|
-
execFileAsync("cleo", ["dash", "--json"], { timeout: 8_000, cwd }),
|
|
767
|
-
execFileAsync("cleo", ["session", "status", "--json"], { timeout: 8_000, cwd }),
|
|
768
|
-
execFileAsync("cleo", ["current", "--json"], { timeout: 8_000, cwd }),
|
|
769
|
-
execFileAsync("cleo", ["memory", "decision-find", "--limit", "3", "--json"], {
|
|
528
|
+
export default function (pi: ExtensionAPI): void {
|
|
529
|
+
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
530
|
+
// Fetch dash + session status in parallel (best-effort)
|
|
531
|
+
const [dashResult, sessionResult, currentResult] = await Promise.allSettled([
|
|
532
|
+
execFileAsync("cleo", ["dash", "--json"], {
|
|
770
533
|
timeout: 8_000,
|
|
771
|
-
cwd,
|
|
534
|
+
cwd: ctx.cwd,
|
|
535
|
+
}),
|
|
536
|
+
execFileAsync("cleo", ["session", "status", "--json"], {
|
|
537
|
+
timeout: 8_000,
|
|
538
|
+
cwd: ctx.cwd,
|
|
539
|
+
}),
|
|
540
|
+
execFileAsync("cleo", ["current", "--json"], {
|
|
541
|
+
timeout: 8_000,
|
|
542
|
+
cwd: ctx.cwd,
|
|
772
543
|
}),
|
|
773
544
|
]);
|
|
774
545
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const session =
|
|
781
|
-
sessionResult.status === "fulfilled"
|
|
782
|
-
? parseSessionInfo(sessionResult.value.stdout)
|
|
783
|
-
: { active: false, id: "", name: "", currentTaskId: null, handoffNote: null };
|
|
784
|
-
|
|
785
|
-
const currentTask =
|
|
786
|
-
currentResult.status === "fulfilled"
|
|
787
|
-
? parseCurrentTask(currentResult.value.stdout)
|
|
788
|
-
: null;
|
|
546
|
+
const tasks =
|
|
547
|
+
dashResult.status === "fulfilled"
|
|
548
|
+
? parseDashSummary(dashResult.value.stdout)
|
|
549
|
+
: { active: 0, pending: 0, done: 0, total: 0, blocked: 0 };
|
|
789
550
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
551
|
+
const session =
|
|
552
|
+
sessionResult.status === "fulfilled"
|
|
553
|
+
? parseSessionInfo(sessionResult.value.stdout)
|
|
554
|
+
: { active: false, id: "", name: "", currentTaskId: null, handoffNote: null };
|
|
794
555
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
: { totalEntries: 0, topThree: [] };
|
|
800
|
-
|
|
801
|
-
const handoffNote = bridgeContent ? extractNoteFromBridgeContent(bridgeContent) : null;
|
|
802
|
-
const projectName = detectProjectName(cwd);
|
|
803
|
-
const hints = buildQuickHints(tasks, session, currentTask);
|
|
804
|
-
|
|
805
|
-
return { tasks, session, currentTask, handoffNote, projectName, decisions, memStats, hints };
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* Read raw `.cleo/memory-bridge.md` content.
|
|
810
|
-
*
|
|
811
|
-
* Returns null when the file does not exist or cannot be read.
|
|
812
|
-
*
|
|
813
|
-
* @param projectDir - The project root directory.
|
|
814
|
-
* @returns The raw file content, or null.
|
|
815
|
-
*/
|
|
816
|
-
function readMemoryBridgeContent(projectDir: string): string | null {
|
|
817
|
-
try {
|
|
818
|
-
const bridgePath = join(projectDir, ".cleo", "memory-bridge.md");
|
|
819
|
-
if (!existsSync(bridgePath)) return null;
|
|
820
|
-
const content = readFileSync(bridgePath, "utf-8");
|
|
821
|
-
return content.length > 0 ? content : null;
|
|
822
|
-
} catch {
|
|
823
|
-
return null;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
556
|
+
const currentTask =
|
|
557
|
+
currentResult.status === "fulfilled"
|
|
558
|
+
? parseCurrentTask(currentResult.value.stdout)
|
|
559
|
+
: null;
|
|
826
560
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
*
|
|
830
|
-
* Prefers the `## Last Session` section's `Note:` line.
|
|
831
|
-
*
|
|
832
|
-
* @param content - Raw memory-bridge.md content.
|
|
833
|
-
* @returns The last session note, or null.
|
|
834
|
-
*/
|
|
835
|
-
function extractNoteFromBridgeContent(content: string): string | null {
|
|
836
|
-
try {
|
|
837
|
-
const noteMatch = content.match(/[-*]\s+\*\*Note\*\*:\s*(.+)/m);
|
|
838
|
-
if (noteMatch?.[1]) return noteMatch[1].trim();
|
|
839
|
-
const altMatch = content.match(/\*\*Note\*\*[:\s]+(.+)/m);
|
|
840
|
-
if (altMatch?.[1]) return altMatch[1].trim();
|
|
841
|
-
return null;
|
|
842
|
-
} catch {
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
561
|
+
// Read handoff note from memory-bridge.md (synchronous, fast)
|
|
562
|
+
const handoffNote = readMemoryBridgeNote(ctx.cwd);
|
|
846
563
|
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
// ============================================================================
|
|
850
|
-
|
|
851
|
-
/**
|
|
852
|
-
* Pi extension factory for the CleoOS branded startup experience.
|
|
853
|
-
*
|
|
854
|
-
* Registers:
|
|
855
|
-
* - `session_start` — fetches all data, renders the branded startup banner
|
|
856
|
-
* - `/cleo:status` — on-demand project status refresh
|
|
857
|
-
* - `/cleo:focus <task-id>` — focus on a task from inside Pi
|
|
858
|
-
* - `/cleo:end-session [note]` — end the current session from inside Pi
|
|
859
|
-
*
|
|
860
|
-
* All operations are best-effort — failures are silently swallowed so Pi
|
|
861
|
-
* is never blocked by CLEO unavailability.
|
|
862
|
-
*
|
|
863
|
-
* @param pi - The Pi extension API instance.
|
|
864
|
-
*/
|
|
865
|
-
export default function (pi: ExtensionAPI): void {
|
|
866
|
-
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
867
|
-
const data = await fetchBannerData(ctx.cwd);
|
|
564
|
+
// Detect project name from filesystem
|
|
565
|
+
const projectName = detectProjectName(ctx.cwd);
|
|
868
566
|
|
|
567
|
+
// Build and display the banner
|
|
869
568
|
const bannerLines = buildStartupBanner(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
data.decisions,
|
|
876
|
-
data.memStats,
|
|
877
|
-
data.hints,
|
|
569
|
+
tasks,
|
|
570
|
+
session,
|
|
571
|
+
currentTask,
|
|
572
|
+
handoffNote,
|
|
573
|
+
projectName,
|
|
878
574
|
);
|
|
879
575
|
|
|
880
576
|
if (ctx.hasUI) {
|
|
@@ -882,11 +578,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
882
578
|
placement: "aboveEditor",
|
|
883
579
|
});
|
|
884
580
|
|
|
885
|
-
//
|
|
886
|
-
const taskSummary = `${
|
|
581
|
+
// Also set a compact status bar entry
|
|
582
|
+
const taskSummary = `${tasks.active}a ${tasks.pending}p ${tasks.done}d`;
|
|
887
583
|
ctx.ui.setStatus(
|
|
888
584
|
"cleo-startup",
|
|
889
|
-
`${ICON_FORGE} ${
|
|
585
|
+
`${ICON_FORGE} ${projectName.split("/").pop() ?? projectName} [${taskSummary}]`,
|
|
890
586
|
);
|
|
891
587
|
} else {
|
|
892
588
|
// No UI — print to stderr as a text summary (visible in TTY mode)
|
|
@@ -898,29 +594,40 @@ export default function (pi: ExtensionAPI): void {
|
|
|
898
594
|
// Command: /cleo:status — on-demand project status refresh
|
|
899
595
|
// -------------------------------------------------------------------------
|
|
900
596
|
pi.registerCommand("cleo:status", {
|
|
901
|
-
description: "Show CleoOS project status: tasks, session,
|
|
597
|
+
description: "Show CleoOS project status: tasks, session, and last handoff",
|
|
902
598
|
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
903
|
-
const
|
|
599
|
+
const [dashResult, sessionResult, currentResult] = await Promise.allSettled([
|
|
600
|
+
execFileAsync("cleo", ["dash", "--json"], { timeout: 8_000, cwd: ctx.cwd }),
|
|
601
|
+
execFileAsync("cleo", ["session", "status", "--json"], { timeout: 8_000, cwd: ctx.cwd }),
|
|
602
|
+
execFileAsync("cleo", ["current", "--json"], { timeout: 8_000, cwd: ctx.cwd }),
|
|
603
|
+
]);
|
|
604
|
+
|
|
605
|
+
const tasks =
|
|
606
|
+
dashResult.status === "fulfilled"
|
|
607
|
+
? parseDashSummary(dashResult.value.stdout)
|
|
608
|
+
: { active: 0, pending: 0, done: 0, total: 0, blocked: 0 };
|
|
609
|
+
|
|
610
|
+
const session =
|
|
611
|
+
sessionResult.status === "fulfilled"
|
|
612
|
+
? parseSessionInfo(sessionResult.value.stdout)
|
|
613
|
+
: { active: false, id: "", name: "", currentTaskId: null, handoffNote: null };
|
|
614
|
+
|
|
615
|
+
const currentTask =
|
|
616
|
+
currentResult.status === "fulfilled"
|
|
617
|
+
? parseCurrentTask(currentResult.value.stdout)
|
|
618
|
+
: null;
|
|
619
|
+
|
|
620
|
+
const handoffNote = readMemoryBridgeNote(ctx.cwd);
|
|
621
|
+
const projectName = detectProjectName(ctx.cwd);
|
|
904
622
|
|
|
905
623
|
const bannerLines = buildStartupBanner(
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
data.decisions,
|
|
912
|
-
data.memStats,
|
|
913
|
-
data.hints,
|
|
624
|
+
tasks,
|
|
625
|
+
session,
|
|
626
|
+
currentTask,
|
|
627
|
+
handoffNote,
|
|
628
|
+
projectName,
|
|
914
629
|
);
|
|
915
630
|
|
|
916
|
-
// Refresh the widget too
|
|
917
|
-
if (ctx.hasUI) {
|
|
918
|
-
ctx.ui.setWidget("cleo-startup-banner", bannerLines, {
|
|
919
|
-
placement: "aboveEditor",
|
|
920
|
-
});
|
|
921
|
-
ctx.ui.notify("CleoOS status refreshed", "info");
|
|
922
|
-
}
|
|
923
|
-
|
|
924
631
|
pi.sendMessage(
|
|
925
632
|
{
|
|
926
633
|
customType: "cleo-status",
|
|
@@ -929,91 +636,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
929
636
|
},
|
|
930
637
|
{ triggerTurn: false },
|
|
931
638
|
);
|
|
932
|
-
},
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
// -------------------------------------------------------------------------
|
|
936
|
-
// Command: /cleo:focus <task-id> — focus on a task
|
|
937
|
-
// -------------------------------------------------------------------------
|
|
938
|
-
pi.registerCommand("cleo:focus", {
|
|
939
|
-
description: "Focus on a CLEO task: /cleo:focus <task-id>",
|
|
940
|
-
handler: async (args: string, ctx: ExtensionContext) => {
|
|
941
|
-
const taskId = args.trim();
|
|
942
|
-
if (!taskId) {
|
|
943
|
-
pi.sendMessage(
|
|
944
|
-
{
|
|
945
|
-
customType: "cleo-focus",
|
|
946
|
-
content: "Usage: /cleo:focus <task-id>",
|
|
947
|
-
display: true,
|
|
948
|
-
},
|
|
949
|
-
{ triggerTurn: false },
|
|
950
|
-
);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
let resultText: string;
|
|
955
|
-
try {
|
|
956
|
-
const { stdout } = await execFileAsync(
|
|
957
|
-
"cleo",
|
|
958
|
-
["start", taskId],
|
|
959
|
-
{ timeout: 8_000, cwd: ctx.cwd },
|
|
960
|
-
);
|
|
961
|
-
resultText = stdout.trim() || `Focused on task ${taskId}`;
|
|
962
|
-
} catch (err: unknown) {
|
|
963
|
-
resultText = `Failed to focus on ${taskId}: ${err instanceof Error ? err.message : String(err)}`;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
pi.sendMessage(
|
|
967
|
-
{
|
|
968
|
-
customType: "cleo-focus",
|
|
969
|
-
content: resultText,
|
|
970
|
-
display: true,
|
|
971
|
-
},
|
|
972
|
-
{ triggerTurn: false },
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
if (ctx.hasUI) {
|
|
976
|
-
ctx.ui.notify(`Focused: ${taskId}`, "info");
|
|
977
|
-
}
|
|
978
|
-
},
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
// -------------------------------------------------------------------------
|
|
982
|
-
// Command: /cleo:end-session [note] — end the current CLEO session
|
|
983
|
-
// -------------------------------------------------------------------------
|
|
984
|
-
pi.registerCommand("cleo:end-session", {
|
|
985
|
-
description: "End the current CLEO session with an optional handoff note",
|
|
986
|
-
handler: async (args: string, ctx: ExtensionContext) => {
|
|
987
|
-
const note = args.trim() || "Session ended from CleoOS Hearth";
|
|
988
|
-
let resultText: string;
|
|
989
|
-
try {
|
|
990
|
-
const { stdout } = await execFileAsync(
|
|
991
|
-
"cleo",
|
|
992
|
-
["session", "end", "--note", note],
|
|
993
|
-
{ timeout: 15_000, cwd: ctx.cwd },
|
|
994
|
-
);
|
|
995
|
-
resultText = stdout.trim() || "Session ended";
|
|
996
|
-
} catch (err: unknown) {
|
|
997
|
-
resultText = `Failed to end session: ${err instanceof Error ? err.message : String(err)}`;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
pi.sendMessage(
|
|
1001
|
-
{
|
|
1002
|
-
customType: "cleo-end-session",
|
|
1003
|
-
content: resultText,
|
|
1004
|
-
display: true,
|
|
1005
|
-
},
|
|
1006
|
-
{ triggerTurn: false },
|
|
1007
|
-
);
|
|
1008
639
|
|
|
1009
640
|
if (ctx.hasUI) {
|
|
1010
|
-
ctx.ui.notify("
|
|
641
|
+
ctx.ui.notify("CleoOS status refreshed", "info");
|
|
1011
642
|
}
|
|
1012
643
|
},
|
|
1013
644
|
});
|
|
1014
645
|
|
|
1015
|
-
// session_shutdown:
|
|
646
|
+
// session_shutdown: remove the startup banner widget
|
|
1016
647
|
pi.on("session_shutdown", async () => {
|
|
1017
|
-
//
|
|
648
|
+
// No cleanup needed — Pi clears widgets on shutdown
|
|
1018
649
|
});
|
|
1019
650
|
}
|