codehere 0.5.0-alpha.4 → 0.5.0-alpha.5
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/application/use-cases/generate-final-report.js +22 -6
- package/dist/application/use-cases/generate-thread-title.js +120 -0
- package/dist/index.js +18 -0
- package/dist/infrastructure/orchestration/aider-adapter.js +59 -0
- package/dist/infrastructure/orchestration/codex-adapter.js +76 -0
- package/dist/infrastructure/orchestration/conductor-adapter.js +11 -1
- package/dist/infrastructure/orchestration/gemini-adapter.js +58 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +62 -0
- package/dist/infrastructure/orchestration/session-manager.js +20 -0
- package/dist/infrastructure/protocols/mcp-server-impl.js +97 -4
- package/dist/infrastructure/protocols/ui-page.js +503 -1
- package/dist/infrastructure/storage/cross-tool-agents.js +251 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +159 -0
- package/dist/infrastructure/storage/project-thread-reader.js +140 -0
- package/dist/infrastructure/storage/project-thread-title-backfill.js +182 -0
- package/package.json +4 -2
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title backfill — v0.6.0 follow-up to M9.
|
|
3
|
+
*
|
|
4
|
+
* The M9 hidden Title agent (generate-thread-title.ts) only fires on the FIRST
|
|
5
|
+
* task entry written to a project's thread.jsonl. Projects seeded by the v0.6
|
|
6
|
+
* migration (project-thread-migration.ts) bypass that path — they get a
|
|
7
|
+
* project-meta header but no Title agent invocation. Result: 5/7 projects on
|
|
8
|
+
* a typical dogfood machine show "(untitled)" in the /threads switcher even
|
|
9
|
+
* though their first task carries enough signal to title from.
|
|
10
|
+
*
|
|
11
|
+
* This backfill closes that gap. For every project with title=null and at
|
|
12
|
+
* least one task entry, it fires the Title agent on the first task's prompt +
|
|
13
|
+
* cwd and appends a `{_meta: 'project-update', title: ...}` line. Reader
|
|
14
|
+
* (project-thread-reader.ts) takes the latest project-update title.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: skips projects that already have a title (or that have NO tasks
|
|
17
|
+
* to title from). Safe to run on every daemon boot. Best-effort: any LLM
|
|
18
|
+
* failure (provider missing, timeout, rate limit) skips that project silently.
|
|
19
|
+
*
|
|
20
|
+
* Cost budget: ~1 LLM call per untitled project on first run after this lands.
|
|
21
|
+
* Subsequent boots are no-ops since titles persist.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, appendFileSync, statSync } from 'fs';
|
|
24
|
+
import { join } from 'path';
|
|
25
|
+
import { homedir } from 'os';
|
|
26
|
+
import { generateThreadTitle } from '../../application/use-cases/generate-thread-title.js';
|
|
27
|
+
const DEFAULT_PROJECTS_DIR = join(homedir(), '.codehere', 'projects');
|
|
28
|
+
function scanThread(slug, file) {
|
|
29
|
+
let title = null;
|
|
30
|
+
let firstPrompt = null;
|
|
31
|
+
let firstCwd = null;
|
|
32
|
+
let firstStartedAt = Number.MAX_SAFE_INTEGER;
|
|
33
|
+
let content;
|
|
34
|
+
try {
|
|
35
|
+
content = readFileSync(file, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { slug, title, firstPrompt, firstCwd };
|
|
39
|
+
}
|
|
40
|
+
for (const line of content.split('\n')) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (!trimmed)
|
|
43
|
+
continue;
|
|
44
|
+
let rec;
|
|
45
|
+
try {
|
|
46
|
+
rec = JSON.parse(trimmed);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (rec._meta === 'project') {
|
|
52
|
+
if (typeof rec.title === 'string')
|
|
53
|
+
title = rec.title;
|
|
54
|
+
}
|
|
55
|
+
else if (rec._meta === 'project-update') {
|
|
56
|
+
if (typeof rec.title === 'string')
|
|
57
|
+
title = rec.title;
|
|
58
|
+
}
|
|
59
|
+
else if (rec.type === 'task') {
|
|
60
|
+
const startedAt = typeof rec.startedAt === 'number' ? rec.startedAt : Number.MAX_SAFE_INTEGER;
|
|
61
|
+
if (startedAt < firstStartedAt) {
|
|
62
|
+
firstStartedAt = startedAt;
|
|
63
|
+
if (typeof rec.prompt === 'string')
|
|
64
|
+
firstPrompt = rec.prompt;
|
|
65
|
+
if (typeof rec.cwd === 'string')
|
|
66
|
+
firstCwd = rec.cwd;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { slug, title, firstPrompt, firstCwd };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Backfill missing titles. Returns counts; never throws (best-effort per project).
|
|
74
|
+
*
|
|
75
|
+
* Disable via CODEHERE_DISABLE_TITLE_AGENT=1 (same env var that gates M9 itself —
|
|
76
|
+
* keeps the toggle surface minimal).
|
|
77
|
+
*/
|
|
78
|
+
export async function backfillThreadTitles(options) {
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
const result = {
|
|
81
|
+
scanned: 0,
|
|
82
|
+
alreadyTitled: 0,
|
|
83
|
+
noTasks: 0,
|
|
84
|
+
titlesGenerated: 0,
|
|
85
|
+
titleAgentFailed: 0,
|
|
86
|
+
durationMs: 0,
|
|
87
|
+
};
|
|
88
|
+
if (process.env.CODEHERE_DISABLE_TITLE_AGENT === '1') {
|
|
89
|
+
result.durationMs = Date.now() - start;
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
const root = options?.projectsDir ?? DEFAULT_PROJECTS_DIR;
|
|
93
|
+
if (!existsSync(root)) {
|
|
94
|
+
result.durationMs = Date.now() - start;
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
let stat;
|
|
98
|
+
try {
|
|
99
|
+
stat = statSync(root);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
result.durationMs = Date.now() - start;
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
if (!stat.isDirectory()) {
|
|
106
|
+
result.durationMs = Date.now() - start;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
let slugs;
|
|
110
|
+
try {
|
|
111
|
+
slugs = readdirSync(root);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
result.durationMs = Date.now() - start;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
const cap = options?.maxProjects ?? Number.MAX_SAFE_INTEGER;
|
|
118
|
+
let backfilledCount = 0;
|
|
119
|
+
for (const slug of slugs) {
|
|
120
|
+
if (backfilledCount >= cap)
|
|
121
|
+
break;
|
|
122
|
+
const projectDir = join(root, slug);
|
|
123
|
+
let s;
|
|
124
|
+
try {
|
|
125
|
+
s = statSync(projectDir);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!s.isDirectory())
|
|
131
|
+
continue;
|
|
132
|
+
const file = join(projectDir, 'thread.jsonl');
|
|
133
|
+
if (!existsSync(file))
|
|
134
|
+
continue;
|
|
135
|
+
result.scanned++;
|
|
136
|
+
const scanned = scanThread(slug, file);
|
|
137
|
+
if (scanned.title) {
|
|
138
|
+
result.alreadyTitled++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!scanned.firstPrompt || !scanned.firstCwd) {
|
|
142
|
+
// Nothing to title from — skip. (Migration-seeded headers without task
|
|
143
|
+
// entries fall here, as do projects where the first task lost its prompt
|
|
144
|
+
// somehow.)
|
|
145
|
+
result.noTasks++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
let title = null;
|
|
149
|
+
try {
|
|
150
|
+
title = await generateThreadTitle(scanned.firstPrompt, scanned.firstCwd);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Title agent should never throw (it returns null on failure), but guard
|
|
154
|
+
// anyway so one bad call doesn't sink the whole backfill.
|
|
155
|
+
title = null;
|
|
156
|
+
}
|
|
157
|
+
if (!title) {
|
|
158
|
+
result.titleAgentFailed++;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const updateLine = JSON.stringify({
|
|
163
|
+
_meta: 'project-update',
|
|
164
|
+
slug,
|
|
165
|
+
title,
|
|
166
|
+
cwd: scanned.firstCwd,
|
|
167
|
+
backfilledAt: Date.now(),
|
|
168
|
+
updatedAt: Date.now(),
|
|
169
|
+
});
|
|
170
|
+
appendFileSync(file, updateLine + '\n', 'utf-8');
|
|
171
|
+
result.titlesGenerated++;
|
|
172
|
+
backfilledCount++;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Append failure is logged via counter but doesn't sink the loop.
|
|
176
|
+
result.titleAgentFailed++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
result.durationMs = Date.now() - start;
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=project-thread-title-backfill.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.5",
|
|
4
4
|
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, OpenCode, Gemini, and any user-configured CLI agent from one local web UI — every AI action traced, every file scored, every spawn warmed with project memory.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
"cli-table3": "^0.6.5",
|
|
66
66
|
"commander": "^11.1.0",
|
|
67
67
|
"dotenv": "^16.3.1",
|
|
68
|
-
"
|
|
68
|
+
"gray-matter": "^4.0.3",
|
|
69
|
+
"ora": "^7.0.1",
|
|
70
|
+
"yaml": "^2.8.4"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
73
|
"@eslint/js": "^10.0.1",
|