@crouton-kit/humanloop 0.3.38 → 0.4.0
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 +3 -2
- package/dist/api.js +2 -2
- package/dist/browser/server.d.ts +15 -5
- package/dist/browser/server.js +110 -14
- package/dist/cli.js +0 -0
- package/dist/editor/roundtrip.d.ts +5 -0
- package/dist/editor/roundtrip.js +36 -0
- package/dist/inbox/completion.js +1 -27
- package/dist/inbox/controller.d.ts +32 -3
- package/dist/inbox/controller.js +427 -35
- package/dist/inbox/convention.d.ts +9 -2
- package/dist/inbox/convention.js +51 -3
- package/dist/inbox/deck-adapter.d.ts +10 -2
- package/dist/inbox/deck-adapter.js +23 -12
- package/dist/inbox/deck-schema.d.ts +2 -0
- package/dist/inbox/deck-schema.js +4 -4
- package/dist/inbox/followup.d.ts +57 -0
- package/dist/inbox/followup.js +117 -0
- package/dist/inbox/registry.d.ts +4 -0
- package/dist/inbox/registry.js +9 -4
- package/dist/inbox/tickets.d.ts +5 -0
- package/dist/inbox/tickets.js +25 -32
- package/dist/inbox/tui.d.ts +1 -1
- package/dist/inbox/tui.js +58 -25
- package/dist/inbox/visual.d.ts +130 -0
- package/dist/inbox/visual.js +747 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/tui/app.d.ts +3 -6
- package/dist/tui/app.js +130 -97
- package/dist/tui/input.d.ts +5 -1
- package/dist/tui/input.js +41 -15
- package/dist/tui/log.d.ts +2 -0
- package/dist/tui/log.js +53 -0
- package/dist/tui/render.js +44 -27
- package/dist/tui/terminal.d.ts +1 -0
- package/dist/tui/terminal.js +5 -0
- package/dist/tui/tmux.js +44 -22
- package/dist/types.d.ts +71 -7
- package/dist/web/assets/{index-DhbBiRqS.js → index-YFwgZZMg.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +6 -3
- package/dist/conversation/reader.d.ts +0 -6
- package/dist/conversation/reader.js +0 -58
- package/dist/visuals/generate.d.ts +0 -9
- package/dist/visuals/generate.js +0 -79
package/dist/web/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>humanloop</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-YFwgZZMg.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-BxJZgs8l.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouton-kit/humanloop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Human-in-the-loop decision TUI — agents write questions, humans answer them",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.19.0"
|
|
7
|
+
},
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "dist/index.js",
|
|
7
10
|
"types": "dist/index.d.ts",
|
|
@@ -26,13 +29,13 @@
|
|
|
26
29
|
],
|
|
27
30
|
"scripts": {
|
|
28
31
|
"build": "tsc && npm run build --workspace=web",
|
|
32
|
+
"postbuild": "chmod +x dist/cli.js",
|
|
29
33
|
"dev": "tsx src/cli.ts",
|
|
30
34
|
"link": "npm link",
|
|
31
35
|
"postinstall": "node dist/scripts/install-renderer.js || true",
|
|
32
|
-
"test": "
|
|
36
|
+
"test": "node scripts/run-tests.mjs"
|
|
33
37
|
},
|
|
34
38
|
"dependencies": {
|
|
35
|
-
"@r-cli/sdk": "^1.3.0",
|
|
36
39
|
"commander": "^13.0.0",
|
|
37
40
|
"string-width": "^7.0.0",
|
|
38
41
|
"ws": "^8.21.0",
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { homedir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { existsSync } from 'fs';
|
|
5
|
-
const CLAUDE_DB_PATH = join(homedir(), '.claude', '__store.db');
|
|
6
|
-
export function readConversation(sessionId) {
|
|
7
|
-
if (!existsSync(CLAUDE_DB_PATH)) {
|
|
8
|
-
throw new Error(`Claude database not found at ${CLAUDE_DB_PATH}`);
|
|
9
|
-
}
|
|
10
|
-
const query = `
|
|
11
|
-
SELECT bm.message_type,
|
|
12
|
-
COALESCE(um.message, am.message) AS content
|
|
13
|
-
FROM base_messages bm
|
|
14
|
-
LEFT JOIN user_messages um ON bm.uuid = um.uuid
|
|
15
|
-
LEFT JOIN assistant_messages am ON bm.uuid = am.uuid
|
|
16
|
-
WHERE bm.session_id = '${sessionId.replace(/'/g, "''")}'
|
|
17
|
-
ORDER BY bm.timestamp ASC;
|
|
18
|
-
`;
|
|
19
|
-
const raw = execSync(`sqlite3 -json "${CLAUDE_DB_PATH}" "${query.replace(/"/g, '\\"')}"`, {
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
maxBuffer: 50 * 1024 * 1024,
|
|
22
|
-
});
|
|
23
|
-
if (!raw.trim())
|
|
24
|
-
return [];
|
|
25
|
-
const rows = JSON.parse(raw);
|
|
26
|
-
const messages = [];
|
|
27
|
-
for (const row of rows) {
|
|
28
|
-
if (!row.content)
|
|
29
|
-
continue;
|
|
30
|
-
if (row.message_type === 'user' || row.message_type === 'assistant') {
|
|
31
|
-
messages.push({
|
|
32
|
-
role: row.message_type,
|
|
33
|
-
content: row.content,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return messages;
|
|
38
|
-
}
|
|
39
|
-
export function findRecentSessionId(cwd) {
|
|
40
|
-
if (!existsSync(CLAUDE_DB_PATH))
|
|
41
|
-
return null;
|
|
42
|
-
const whereClause = cwd
|
|
43
|
-
? `WHERE cwd = '${cwd.replace(/'/g, "''")}'`
|
|
44
|
-
: '';
|
|
45
|
-
const query = `SELECT DISTINCT session_id FROM base_messages ${whereClause} ORDER BY timestamp DESC LIMIT 1;`;
|
|
46
|
-
try {
|
|
47
|
-
const raw = execSync(`sqlite3 -json "${CLAUDE_DB_PATH}" "${query.replace(/"/g, '\\"')}"`, {
|
|
48
|
-
encoding: 'utf8',
|
|
49
|
-
});
|
|
50
|
-
if (!raw.trim())
|
|
51
|
-
return null;
|
|
52
|
-
const rows = JSON.parse(raw);
|
|
53
|
-
return rows[0]?.session_id ?? null;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
package/dist/visuals/generate.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { query } from '@r-cli/sdk';
|
|
2
|
-
import { renderMarkdown } from '../render/termrender.js';
|
|
3
|
-
const VISUAL_SYSTEM_PROMPT = `You are re-grounding a decision-maker in the moment before they answer the question below. They were deep in this problem but got pulled away; in the next 20 seconds they need to remember *what is actually in play* — the current state, the files, the constraint this decision lives inside — so the question stops feeling cold and they can answer with confidence.
|
|
4
|
-
|
|
5
|
-
Write from the conversation history you're given. Lead with what *is*: the real files, functions, data, and constraints that ground this decision, named concretely (as \`path/to/file.ts:123\` so they can jump straight there). Reconstruct just enough of how they arrived here to make the question legible — no more.
|
|
6
|
-
|
|
7
|
-
Keep it tight: usually a short paragraph or a few bullets, 30 lines hard cap. Say less when less is true. Choose whatever shape carries the meaning fastest — prose by default, a list when enumerating, a table only to compare several things across the same dimensions, a small diagram when the structure itself is the point. You're trusted to pick; don't force a format.
|
|
8
|
-
|
|
9
|
-
The one rule that matters: **only reference files, identifiers, and facts that actually appear in the conversation.** Never invent a plausible-looking path or name. If the conversation is thin, write a short honest briefing about what little is grounded — that beats confident fabrication every time.
|
|
10
|
-
|
|
11
|
-
And stay in your lane: don't restate the question, don't recommend an option or tell them how to decide, don't lay out tradeoffs or sketch alternatives. They own the decision; you only reconstruct the ground it stands on.
|
|
12
|
-
|
|
13
|
-
Formatting: plain markdown renders (**bold**, *italic*, \`code\`, bullets, and GFM tables). These termrender directives are available if one genuinely helps — anything you draw with box-drawing/ASCII must sit inside a :::panel or it will be reflowed and destroyed:
|
|
14
|
-
:::panel{title="T" color="cyan"} bordered box (red|green|yellow|blue|magenta|cyan|white|gray)
|
|
15
|
-
:::tree{color="c"} indented hierarchy (2-space indent = nesting)
|
|
16
|
-
:::callout{type="info|warning|error|success"} status callout
|
|
17
|
-
::::columns / :::col{width="50%"} side-by-side (outer fence needs strictly more colons)
|
|
18
|
-
Never wrap the whole output in a code fence.`;
|
|
19
|
-
async function callHaiku(prompt, systemPrompt) {
|
|
20
|
-
try {
|
|
21
|
-
const session = await query({
|
|
22
|
-
prompt,
|
|
23
|
-
options: {
|
|
24
|
-
model: 'haiku',
|
|
25
|
-
maxTurns: 1,
|
|
26
|
-
systemPrompt,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
let text = '';
|
|
30
|
-
for await (const msg of session) {
|
|
31
|
-
if (msg.type === 'assistant' && msg.message?.content) {
|
|
32
|
-
for (const block of msg.message.content) {
|
|
33
|
-
if (block.type === 'text')
|
|
34
|
-
text += block.text;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return text.trim() || null;
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
process.stderr.write(`[hl] Haiku call failed: ${err instanceof Error ? err.message : err}\n`);
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// defaultGenerateVisual matches the GenerateVisual contract for use with
|
|
46
|
-
// mountPanel. Width is read from process.stdout.columns so callers that
|
|
47
|
-
// embed humanloop in a sub-region should supply their own closure that bakes
|
|
48
|
-
// in the correct width.
|
|
49
|
-
// Cap on how much conversation we hand the generator. Recency is what grounds
|
|
50
|
-
// the decision in front of the human, so when history is long we keep the tail
|
|
51
|
-
// (most recent messages) rather than the head.
|
|
52
|
-
const MAX_CONTEXT_CHARS = 24000;
|
|
53
|
-
export async function defaultGenerateVisual(interaction, conversationContext) {
|
|
54
|
-
const width = Math.max(40, Math.min((process.stdout.columns || 80) - 4, 76));
|
|
55
|
-
const optionsSummary = interaction.options.length > 0
|
|
56
|
-
? `\nOptions: ${interaction.options.map((o) => o.label).join(' | ')}`
|
|
57
|
-
: '';
|
|
58
|
-
const subtitleLine = interaction.subtitle ? `\nContext: ${interaction.subtitle}` : '';
|
|
59
|
-
// The body is the richest statement of what's being asked — hand it to the
|
|
60
|
-
// generator so the briefing grounds in the actual question, not just its title.
|
|
61
|
-
const bodyLine = interaction.body ? `\nDetail:\n${interaction.body}` : '';
|
|
62
|
-
const questionText = `Title: "${interaction.title}"${subtitleLine}${bodyLine}${optionsSummary}`;
|
|
63
|
-
const trimmedContext = conversationContext.length > MAX_CONTEXT_CHARS
|
|
64
|
-
? `…(earlier conversation trimmed)…\n\n${conversationContext.slice(-MAX_CONTEXT_CHARS)}`
|
|
65
|
-
: conversationContext;
|
|
66
|
-
const prompt = trimmedContext
|
|
67
|
-
? `Here is the conversation so far:\n\n${trimmedContext}\n\n---\n\nThe human is about to answer this decision. Re-ground them in what's in play:\n\n${questionText}`
|
|
68
|
-
: `The human is about to answer this decision. Re-ground them in what's in play:\n\n${questionText}`;
|
|
69
|
-
const result = await callHaiku(prompt, VISUAL_SYSTEM_PROMPT);
|
|
70
|
-
if (result) {
|
|
71
|
-
const markdown = result
|
|
72
|
-
.replace(/^```[\w]*\n?/gm, '')
|
|
73
|
-
.replace(/^```\s*$/gm, '')
|
|
74
|
-
.trim();
|
|
75
|
-
const ansi = renderMarkdown(markdown, width).join('\n');
|
|
76
|
-
return { ok: true, ansi, markdown };
|
|
77
|
-
}
|
|
78
|
-
return { ok: false, error: 'haiku returned no output' };
|
|
79
|
-
}
|