@crouton-kit/humanloop 0.1.4 → 0.3.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/dist/api.d.ts +35 -0
- package/dist/api.js +127 -0
- package/dist/cli.js +858 -183
- package/dist/editor/review.d.ts +24 -0
- package/dist/editor/review.js +425 -0
- package/dist/inbox/convention.d.ts +17 -0
- package/dist/inbox/convention.js +87 -0
- package/dist/inbox/deck-schema.d.ts +42 -0
- package/dist/inbox/deck-schema.js +110 -0
- package/dist/inbox/scan.d.ts +2 -0
- package/dist/inbox/scan.js +62 -0
- package/dist/inbox/tui.d.ts +9 -0
- package/dist/inbox/tui.js +158 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +13 -0
- package/dist/render/termrender.d.ts +36 -0
- package/dist/render/termrender.js +271 -0
- package/dist/render/version.d.ts +1 -0
- package/dist/render/version.js +1 -0
- package/dist/scripts/install-renderer.d.ts +2 -0
- package/dist/scripts/install-renderer.js +16 -0
- package/dist/surfaces/display.d.ts +5 -0
- package/dist/surfaces/display.js +19 -0
- package/dist/tui/app.d.ts +24 -1
- package/dist/tui/app.js +48 -113
- package/dist/tui/input.js +67 -8
- package/dist/tui/render.js +34 -51
- package/dist/tui/tmux.d.ts +4 -6
- package/dist/tui/tmux.js +6 -4
- package/dist/types.d.ts +71 -0
- package/dist/visuals/generate.js +2 -27
- package/package.json +4 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve, sep } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { checkMarkdown } from '../render/termrender.js';
|
|
5
|
+
// ── zod v4 building blocks ────────────────────────────────────────────────────
|
|
6
|
+
// v4 notes: .nonempty() → .min(1); error messages use {error: 'string'} per check.
|
|
7
|
+
export const interactionOptionSchema = z.object({
|
|
8
|
+
id: z.string().min(1),
|
|
9
|
+
label: z.string().min(1),
|
|
10
|
+
description: z.string().optional(),
|
|
11
|
+
shortcut: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
const interactionSchema = z.object({
|
|
14
|
+
id: z.string().regex(/^[A-Za-z0-9_-]+$/, { error: 'interaction id must match /^[A-Za-z0-9_-]+$/' }).min(1).max(64),
|
|
15
|
+
title: z.string().min(1, { error: 'title must be non-empty' }),
|
|
16
|
+
subtitle: z.string().min(1, { error: 'subtitle must be non-empty when present' }).optional(),
|
|
17
|
+
body: z.string().optional(),
|
|
18
|
+
bodyPath: z.string().optional(),
|
|
19
|
+
options: z.array(interactionOptionSchema),
|
|
20
|
+
multiSelect: z.boolean().optional(),
|
|
21
|
+
allowFreetext: z.boolean().optional(),
|
|
22
|
+
freetextLabel: z.string().optional(),
|
|
23
|
+
kind: z.enum(['notify', 'validation', 'decision', 'context', 'error']).optional(),
|
|
24
|
+
});
|
|
25
|
+
const deckSourceSchema = z.object({
|
|
26
|
+
sessionName: z.string().optional(),
|
|
27
|
+
askedBy: z.string().optional(),
|
|
28
|
+
blockedSince: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
export const deckSchema = z.object({
|
|
31
|
+
title: z.string().optional(),
|
|
32
|
+
source: deckSourceSchema.optional(),
|
|
33
|
+
interactions: z.array(interactionSchema).min(1, { error: 'interactions[] must be non-empty' }),
|
|
34
|
+
}).superRefine((input, ctx) => {
|
|
35
|
+
const seen = new Map();
|
|
36
|
+
for (let i = 0; i < input.interactions.length; i++) {
|
|
37
|
+
const interaction = input.interactions[i];
|
|
38
|
+
if (interaction.body !== undefined && interaction.bodyPath !== undefined) {
|
|
39
|
+
ctx.addIssue({
|
|
40
|
+
code: 'custom',
|
|
41
|
+
message: 'body and bodyPath are mutually exclusive',
|
|
42
|
+
path: ['interactions', i],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const prev = seen.get(interaction.id);
|
|
46
|
+
if (prev !== undefined) {
|
|
47
|
+
ctx.addIssue({
|
|
48
|
+
code: 'custom',
|
|
49
|
+
message: `duplicate interaction id "${interaction.id}" at indices ${prev} and ${i}`,
|
|
50
|
+
path: ['interactions', i, 'id'],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
seen.set(interaction.id, i);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// ── C2 bodyPath defense + inlining ────────────────────────────────────────────
|
|
57
|
+
export function inlineBodyPath(deckPath, bodyPath) {
|
|
58
|
+
const deckDir = dirname(deckPath);
|
|
59
|
+
const joined = resolve(deckDir, bodyPath);
|
|
60
|
+
// STEP 1: existence + lstat BEFORE realpath to catch symlinks and directories.
|
|
61
|
+
if (!existsSync(joined)) {
|
|
62
|
+
throw new Error(`bodyPath does not exist: '${bodyPath}' (resolved against deck dir '${deckDir}'). bodyPath is interpreted relative to the deck JSON's directory; place the body file there and use a relative path (e.g. "completion-summary.md").`);
|
|
63
|
+
}
|
|
64
|
+
const stat = lstatSync(joined);
|
|
65
|
+
if (!stat.isFile()) {
|
|
66
|
+
// Catches symlinks, directories, FIFOs — lstat does not follow symlinks.
|
|
67
|
+
throw new Error(`bodyPath must be a regular file (not a symlink, directory, or special file): ${bodyPath}`);
|
|
68
|
+
}
|
|
69
|
+
// STEP 2: realpath both sides, prefix-check (defense-in-depth for .. traversal).
|
|
70
|
+
// realpathSync is safe here: lstat already confirmed the path exists.
|
|
71
|
+
const realResolved = realpathSync(joined);
|
|
72
|
+
const realDeckDir = realpathSync(deckDir);
|
|
73
|
+
const prefix = realDeckDir + sep;
|
|
74
|
+
if (realResolved !== realDeckDir && !realResolved.startsWith(prefix)) {
|
|
75
|
+
throw new Error(`bodyPath '${bodyPath}' escapes the deck's directory ('${realDeckDir}'). bodyPath is resolved relative to the deck JSON file and must stay inside its directory (no '..', absolute paths pointing elsewhere, or symlinks out). Fix: write the deck JSON next to the body file (e.g. both inside $SISYPHUS_SESSION_DIR/context/) and use a relative path like "completion-summary.md".`);
|
|
76
|
+
}
|
|
77
|
+
// STEP 3: read. lstat confirmed regular file; realpath confirmed in-tree.
|
|
78
|
+
return readFileSync(joined, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
// ── public entry points ───────────────────────────────────────────────────────
|
|
81
|
+
export function parseDeck(deckPath) {
|
|
82
|
+
const raw = readFileSync(deckPath, 'utf-8');
|
|
83
|
+
let json;
|
|
84
|
+
try {
|
|
85
|
+
json = JSON.parse(raw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error('deck is not valid JSON');
|
|
89
|
+
}
|
|
90
|
+
const parsed = deckSchema.parse(json);
|
|
91
|
+
const inlinedInteractions = parsed.interactions.map(interaction => {
|
|
92
|
+
let body = interaction.body;
|
|
93
|
+
if (interaction.bodyPath !== undefined) {
|
|
94
|
+
body = inlineBodyPath(deckPath, interaction.bodyPath);
|
|
95
|
+
}
|
|
96
|
+
if (body !== undefined) {
|
|
97
|
+
const check = checkMarkdown(body);
|
|
98
|
+
if (!check.ok) {
|
|
99
|
+
throw new Error(check.error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Drop bodyPath from persisted decisions.json (recipe §1.8).
|
|
103
|
+
const { bodyPath: _drop, ...rest } = interaction;
|
|
104
|
+
return body !== undefined ? { ...rest, body } : { ...rest };
|
|
105
|
+
});
|
|
106
|
+
return { ...parsed, interactions: inlinedInteractions };
|
|
107
|
+
}
|
|
108
|
+
export function validateDeck(parsed) {
|
|
109
|
+
return deckSchema.parse(parsed);
|
|
110
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'fs';
|
|
2
|
+
import { resolve, basename } from 'path';
|
|
3
|
+
import { deckPath, isResolved, isClaimed, readJson } from './convention.js';
|
|
4
|
+
// ── scanInbox ─────────────────────────────────────────────────────────────────
|
|
5
|
+
export function scanInbox(roots) {
|
|
6
|
+
const items = [];
|
|
7
|
+
for (const root of roots) {
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = readdirSync(root);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// root doesn't exist or isn't readable — skip silently
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const dir = resolve(root, entry);
|
|
18
|
+
try {
|
|
19
|
+
const stat = statSync(dir);
|
|
20
|
+
if (!stat.isDirectory())
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// Skip resolved or actively claimed dirs
|
|
27
|
+
if (isResolved(dir) || isClaimed(dir))
|
|
28
|
+
continue;
|
|
29
|
+
const dp = deckPath(dir);
|
|
30
|
+
const deck = readJson(dp);
|
|
31
|
+
if (deck === null)
|
|
32
|
+
continue;
|
|
33
|
+
// Derive blockedSince: prefer deck.source.blockedSince, fall back to mtime
|
|
34
|
+
let blockedSince;
|
|
35
|
+
if (deck.source?.blockedSince !== undefined) {
|
|
36
|
+
blockedSince = deck.source.blockedSince;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
try {
|
|
40
|
+
blockedSince = new Date(statSync(dp).mtime).toISOString();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
blockedSince = new Date().toISOString();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const firstInteraction = deck.interactions[0];
|
|
47
|
+
const item = {
|
|
48
|
+
dir,
|
|
49
|
+
id: firstInteraction?.id ?? basename(dir),
|
|
50
|
+
title: deck.title ?? firstInteraction?.title,
|
|
51
|
+
subtitle: firstInteraction?.subtitle,
|
|
52
|
+
kind: firstInteraction?.kind,
|
|
53
|
+
source: deck.source,
|
|
54
|
+
blockedSince,
|
|
55
|
+
};
|
|
56
|
+
items.push(item);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Sort ascending by blockedSince (ISO string compare is monotonic)
|
|
60
|
+
items.sort((a, b) => (a.blockedSince < b.blockedSince ? -1 : a.blockedSince > b.blockedSince ? 1 : 0));
|
|
61
|
+
return items;
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InboxItem } from '../types.js';
|
|
2
|
+
export declare const KIND_ICON: Record<string, string>;
|
|
3
|
+
export declare const KIND_COLOR: Record<string, string>;
|
|
4
|
+
export declare function formatTimeAgo(iso: string): string;
|
|
5
|
+
export declare function buildInboxLines(items: InboxItem[], width: number, selectedIndex: number): string[];
|
|
6
|
+
export declare function pickFromInbox(items: InboxItem[], opts: {
|
|
7
|
+
cols: number;
|
|
8
|
+
rows: number;
|
|
9
|
+
}): Promise<InboxItem | null>;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import stringWidth from 'string-width';
|
|
2
|
+
import { setupTerminal, restoreTerminal, parseKeypress, getTerminalSize, } from '../tui/terminal.js';
|
|
3
|
+
import { diffFrame } from '../tui/render.js';
|
|
4
|
+
// ── ANSI helpers (local to this module) ──────────────────────────────────────
|
|
5
|
+
const ESC = '\x1b[';
|
|
6
|
+
const RESET = `${ESC}0m`;
|
|
7
|
+
const BOLD = `${ESC}1m`;
|
|
8
|
+
const DIM = `${ESC}2m`;
|
|
9
|
+
const ITALIC = `${ESC}3m`;
|
|
10
|
+
const CYAN = `${ESC}36m`;
|
|
11
|
+
const RED = `${ESC}31m`;
|
|
12
|
+
const GRAY = `${ESC}90m`;
|
|
13
|
+
const YELLOW = `${ESC}33m`;
|
|
14
|
+
function ansiColor(text, color) {
|
|
15
|
+
switch (color) {
|
|
16
|
+
case 'gray': return `${GRAY}${text}${RESET}`;
|
|
17
|
+
case 'cyan': return `${CYAN}${text}${RESET}`;
|
|
18
|
+
case 'red': return `${RED}${text}${RESET}`;
|
|
19
|
+
case 'yellow': return `${YELLOW}${text}${RESET}`;
|
|
20
|
+
default: return text;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ── Row model (ported verbatim from sisyphus cross-session-inbox.ts:8-21) ────
|
|
24
|
+
export const KIND_ICON = {
|
|
25
|
+
notify: '✉',
|
|
26
|
+
validation: '✓',
|
|
27
|
+
decision: '◆',
|
|
28
|
+
context: '✎',
|
|
29
|
+
error: '⚠',
|
|
30
|
+
};
|
|
31
|
+
export const KIND_COLOR = {
|
|
32
|
+
notify: 'gray',
|
|
33
|
+
validation: 'cyan',
|
|
34
|
+
decision: 'cyan',
|
|
35
|
+
context: 'cyan',
|
|
36
|
+
error: 'red',
|
|
37
|
+
};
|
|
38
|
+
// ── formatTimeAgo (ported from sisyphus src/tui/lib/format.ts:5-12) ──────────
|
|
39
|
+
export function formatTimeAgo(iso) {
|
|
40
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
41
|
+
const minutes = Math.floor(diff / 60000);
|
|
42
|
+
const hours = Math.floor(minutes / 60);
|
|
43
|
+
if (hours > 0)
|
|
44
|
+
return `${hours}h ago`;
|
|
45
|
+
if (minutes > 0)
|
|
46
|
+
return `${minutes}m ago`;
|
|
47
|
+
return 'just now';
|
|
48
|
+
}
|
|
49
|
+
// ── truncate (ported from sisyphus src/tui/lib/format.ts:19-37) ──────────────
|
|
50
|
+
function truncate(text, max) {
|
|
51
|
+
const clean = text.replace(/\n/g, ' ').replace(/✅/g, '✓').replace(/❌/g, '✗').replace(/\p{Emoji_Presentation}/gu, '');
|
|
52
|
+
if (max < 4)
|
|
53
|
+
return clean.slice(0, max);
|
|
54
|
+
const w = stringWidth(clean);
|
|
55
|
+
if (w <= max)
|
|
56
|
+
return clean;
|
|
57
|
+
let result = clean;
|
|
58
|
+
while (stringWidth(result) > max - 1 && result.length > 0) {
|
|
59
|
+
const cut = result.lastIndexOf(' ', result.length - 2);
|
|
60
|
+
if (cut > max * 0.4) {
|
|
61
|
+
result = result.slice(0, cut);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
result = result.slice(0, result.length - 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result + '…';
|
|
68
|
+
}
|
|
69
|
+
// ── buildInboxLines ───────────────────────────────────────────────────────────
|
|
70
|
+
export function buildInboxLines(items, width, selectedIndex) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
if (items.length === 0) {
|
|
73
|
+
lines.push(` ${DIM}${ITALIC}No pending interactions${RESET}`);
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
lines.push(` ${BOLD}${items.length} pending${RESET}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
const contentWidth = width - 4;
|
|
79
|
+
for (let i = 0; i < items.length; i++) {
|
|
80
|
+
const item = items[i];
|
|
81
|
+
const kindKey = item.kind ?? '';
|
|
82
|
+
const icon = kindKey in KIND_ICON ? KIND_ICON[kindKey] : '·';
|
|
83
|
+
const iconColor = kindKey in KIND_COLOR ? KIND_COLOR[kindKey] : 'cyan';
|
|
84
|
+
const sourceLabel = item.source?.sessionName ?? item.source?.askedBy ?? '';
|
|
85
|
+
const titleText = item.title ?? `(${item.id.slice(0, 8)})`;
|
|
86
|
+
const blocked = formatTimeAgo(item.blockedSince);
|
|
87
|
+
const cursor = i === selectedIndex ? `${CYAN}▸${RESET} ` : ' ';
|
|
88
|
+
const maxTitle = Math.max(10, contentWidth - sourceLabel.length - blocked.length - 8);
|
|
89
|
+
let row = cursor;
|
|
90
|
+
row += ansiColor(icon, iconColor);
|
|
91
|
+
if (sourceLabel) {
|
|
92
|
+
row += ` ${ansiColor(sourceLabel, 'yellow')}`;
|
|
93
|
+
row += ` ${DIM}·${RESET} `;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
row += ' ';
|
|
97
|
+
}
|
|
98
|
+
row += `${BOLD}${truncate(titleText, maxTitle)}${RESET}`;
|
|
99
|
+
row += ` ${DIM}${blocked}${RESET}`;
|
|
100
|
+
lines.push(row);
|
|
101
|
+
if (item.subtitle) {
|
|
102
|
+
lines.push(` ${DIM}${truncate(item.subtitle, contentWidth - 6)}${RESET}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
// ── pickFromInbox ─────────────────────────────────────────────────────────────
|
|
108
|
+
export function pickFromInbox(items, opts) {
|
|
109
|
+
if (items.length === 0)
|
|
110
|
+
return Promise.resolve(null);
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
let selectedIndex = 0;
|
|
113
|
+
let prevFrame = [];
|
|
114
|
+
let onData;
|
|
115
|
+
const flush = () => {
|
|
116
|
+
const { cols: currentCols, rows: currentRows } = getTerminalSize();
|
|
117
|
+
const lines = buildInboxLines(items, currentCols, selectedIndex);
|
|
118
|
+
const { writes, nextPrevFrame } = diffFrame(prevFrame, lines, currentRows);
|
|
119
|
+
process.stdout.write('\x1b[?2026h');
|
|
120
|
+
for (const w of writes)
|
|
121
|
+
process.stdout.write(w);
|
|
122
|
+
process.stdout.write('\x1b[?2026l');
|
|
123
|
+
prevFrame = nextPrevFrame;
|
|
124
|
+
};
|
|
125
|
+
const done = (result) => {
|
|
126
|
+
restoreTerminal();
|
|
127
|
+
process.stdin.removeListener('data', onData);
|
|
128
|
+
process.stdout.removeListener('resize', flush);
|
|
129
|
+
resolve(result);
|
|
130
|
+
};
|
|
131
|
+
setupTerminal();
|
|
132
|
+
flush();
|
|
133
|
+
onData = (data) => {
|
|
134
|
+
const { input, key } = parseKeypress(data);
|
|
135
|
+
if (key.downArrow || input === 'j') {
|
|
136
|
+
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
|
|
137
|
+
flush();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (key.upArrow || input === 'k') {
|
|
141
|
+
selectedIndex = Math.max(selectedIndex - 1, 0);
|
|
142
|
+
flush();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.return) {
|
|
146
|
+
const selected = items[selectedIndex];
|
|
147
|
+
done(selected ?? null);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (key.escape || (key.ctrl && input === 'c') || input === 'q') {
|
|
151
|
+
done(null);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
process.stdin.on('data', onData);
|
|
156
|
+
process.stdout.on('resize', flush);
|
|
157
|
+
});
|
|
158
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export { mountPanel, validateInput, launchTui } from './tui/app.js';
|
|
2
2
|
export { defaultGenerateVisual } from './visuals/generate.js';
|
|
3
|
-
export
|
|
3
|
+
export { launchReview } from './editor/review.js';
|
|
4
|
+
export { launchReview as review } from './editor/review.js';
|
|
5
|
+
export type { ReviewOptions } from './editor/review.js';
|
|
6
|
+
export { ask, approve, notify, inbox } from './api.js';
|
|
7
|
+
export { display } from './surfaces/display.js';
|
|
8
|
+
export { scanInbox } from './inbox/scan.js';
|
|
9
|
+
export { renderMarkdown, checkMarkdown, ensureRenderer, isRendererReady, } from './render/termrender.js';
|
|
10
|
+
export { parseDeck, validateDeck, deckSchema } from './inbox/deck-schema.js';
|
|
11
|
+
export { deckPath, responsePath, progressPath, visualsDir, interactionState, isResolved, isClaimed, atomicWriteJson, readJson, writeResponse, writeProgress, clearProgress, } from './inbox/convention.js';
|
|
12
|
+
export type { InteractionState } from './inbox/convention.js';
|
|
13
|
+
export type { Interaction, InteractionOption, InteractionResponse, InteractionKind, Deck, DeckSource, MountedPanel, MountedPanelOpts, GenerateVisual, VisualBlock, FeedbackComment, FeedbackResult, ResolutionEnvelope, InboxItem, DisplayOpts, } from './types.js';
|
|
4
14
|
export type { Key } from './tui/terminal.js';
|
|
5
15
|
export type { ConversationMessage } from './conversation/reader.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
export { mountPanel, validateInput, launchTui } from './tui/app.js';
|
|
2
2
|
export { defaultGenerateVisual } from './visuals/generate.js';
|
|
3
|
+
export { launchReview } from './editor/review.js';
|
|
4
|
+
export { launchReview as review } from './editor/review.js';
|
|
5
|
+
// Interaction-layer surface (SDK).
|
|
6
|
+
export { ask, approve, notify, inbox } from './api.js';
|
|
7
|
+
export { display } from './surfaces/display.js';
|
|
8
|
+
export { scanInbox } from './inbox/scan.js';
|
|
9
|
+
// Renderer binding — the sole org-wide termrender caller. Consumers
|
|
10
|
+
// (sisyphus md-render / ask-schema) route markdown through these.
|
|
11
|
+
export { renderMarkdown, checkMarkdown, ensureRenderer, isRendererReady, } from './render/termrender.js';
|
|
12
|
+
// Canonical deck schema + parsing/validation (consumers stop forking it).
|
|
13
|
+
export { parseDeck, validateDeck, deckSchema } from './inbox/deck-schema.js';
|
|
14
|
+
// Interaction-directory convention helpers (§B) — names humanloop owns.
|
|
15
|
+
export { deckPath, responsePath, progressPath, visualsDir, interactionState, isResolved, isClaimed, atomicWriteJson, readJson, writeResponse, writeProgress, clearProgress, } from './inbox/convention.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memoized, self-healing. Ensures the pinned termrender binary exists inside
|
|
3
|
+
* the humanloop-managed venv; (re)provisions it via `uv` when missing or the
|
|
4
|
+
* version drifts from the pin. Runs at most once per process. Single
|
|
5
|
+
* degradation path: `uv` absent → one stderr remediation line + plaintext
|
|
6
|
+
* fallback. win32 → plaintext (no renderer).
|
|
7
|
+
*
|
|
8
|
+
* Invoked at postinstall AND lazily on the first render/check/display call,
|
|
9
|
+
* so `npm ci --ignore-scripts` consumers still self-heal on first use.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ensureRenderer(): void;
|
|
12
|
+
/** Cheap predicate — true when the pinned managed binary is present and correct. Does not install. */
|
|
13
|
+
export declare function isRendererReady(): boolean;
|
|
14
|
+
/** Render markdown to terminal lines via the pinned binary; plaintext fallback. */
|
|
15
|
+
export declare function renderMarkdown(md: string, width: number): string[];
|
|
16
|
+
/** Validate markdown via `termrender doc check`. */
|
|
17
|
+
export declare function checkMarkdown(md: string): {
|
|
18
|
+
ok: true;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export interface DisplayInPaneOpts {
|
|
24
|
+
/** Pass watch so the pane live-updates on file edits. Default true. */
|
|
25
|
+
watch?: boolean;
|
|
26
|
+
/** Open in a new tmux window instead of splitting the current one. */
|
|
27
|
+
newWindow?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Spawn termrender into a live tmux pane. The pane-budget policy (whether to
|
|
31
|
+
* split vs open a new window) is decided by the caller (`src/surfaces/
|
|
32
|
+
* display.ts`); this is the thin managed-binary spawn it delegates to.
|
|
33
|
+
*/
|
|
34
|
+
export declare function displayInPane(path: string, opts?: DisplayInPaneOpts): {
|
|
35
|
+
paneId?: string;
|
|
36
|
+
};
|