agenthud 0.11.3 → 0.12.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/README.md +58 -16
- package/dist/index.js +1 -1
- package/dist/{main-PCPOGJO6.js → main-CRTF5EHN.js} +910 -305
- package/package.json +1 -1
|
@@ -1,15 +1,306 @@
|
|
|
1
1
|
// src/main.ts
|
|
2
|
-
import { existsSync as
|
|
2
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, realpathSync, rmSync } from "fs";
|
|
3
3
|
import { homedir as homedir5 } from "os";
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join7 } from "path";
|
|
5
5
|
import { createInterface as createInterface2 } from "readline";
|
|
6
6
|
import { render } from "ink";
|
|
7
7
|
import React from "react";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
|
-
import { readFileSync } from "fs";
|
|
11
|
-
import { dirname, join } from "path";
|
|
10
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
11
|
+
import { dirname, join as join2 } from "path";
|
|
12
12
|
import { fileURLToPath } from "url";
|
|
13
|
+
|
|
14
|
+
// src/config/globalConfig.ts
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
19
|
+
var CONFIG_PATH = join(homedir(), ".agenthud", "config.yaml");
|
|
20
|
+
var STATE_PATH = join(homedir(), ".agenthud", "state.yaml");
|
|
21
|
+
var DEFAULT_INCLUDE_TYPES = [
|
|
22
|
+
"user",
|
|
23
|
+
"response",
|
|
24
|
+
"bash",
|
|
25
|
+
"edit",
|
|
26
|
+
"thinking"
|
|
27
|
+
];
|
|
28
|
+
var ALLOWED_INCLUDE_TYPES = /* @__PURE__ */ new Set([
|
|
29
|
+
"user",
|
|
30
|
+
"response",
|
|
31
|
+
"bash",
|
|
32
|
+
"edit",
|
|
33
|
+
"thinking",
|
|
34
|
+
"read",
|
|
35
|
+
"glob",
|
|
36
|
+
"commit"
|
|
37
|
+
]);
|
|
38
|
+
var DEFAULT_GLOBAL_CONFIG = {
|
|
39
|
+
refreshIntervalMs: 2e3,
|
|
40
|
+
hiddenSessions: [],
|
|
41
|
+
hiddenSubAgents: [],
|
|
42
|
+
// [] means "show all"; conversation preset bundles assistant + user;
|
|
43
|
+
// commits-only preset filters down to git activity.
|
|
44
|
+
filterPresets: [[], ["response", "user"], ["commit"]],
|
|
45
|
+
hiddenProjects: [],
|
|
46
|
+
report: {
|
|
47
|
+
include: [...DEFAULT_INCLUDE_TYPES],
|
|
48
|
+
detailLimit: 120,
|
|
49
|
+
withGit: false,
|
|
50
|
+
format: "markdown"
|
|
51
|
+
},
|
|
52
|
+
summary: {}
|
|
53
|
+
};
|
|
54
|
+
var ALL_PRESET_KEYWORDS = /* @__PURE__ */ new Set(["all", "*", "any"]);
|
|
55
|
+
function normalizePreset(tokens) {
|
|
56
|
+
if (tokens.some((t) => ALL_PRESET_KEYWORDS.has(t.toLowerCase()))) return [];
|
|
57
|
+
return tokens;
|
|
58
|
+
}
|
|
59
|
+
function parseInterval(value) {
|
|
60
|
+
const match = value.match(/^(\d+)(s|m)$/);
|
|
61
|
+
if (!match) return null;
|
|
62
|
+
const n = parseInt(match[1], 10);
|
|
63
|
+
return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
|
|
64
|
+
}
|
|
65
|
+
function ensureAgenthudDir() {
|
|
66
|
+
const dir = join(homedir(), ".agenthud");
|
|
67
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
function writeDefaultConfig() {
|
|
70
|
+
ensureAgenthudDir();
|
|
71
|
+
const defaultYaml = `# AgentHUD user settings.
|
|
72
|
+
# App-managed state (hidden sessions/projects) lives in state.yaml.
|
|
73
|
+
|
|
74
|
+
# How often to poll for activity updates
|
|
75
|
+
refreshInterval: 2s
|
|
76
|
+
|
|
77
|
+
# Activity filter presets (cycle with 'f' key in viewer)
|
|
78
|
+
# Each list is one preset. Use "all" (or "*") to show everything.
|
|
79
|
+
# Types: response, user, bash, edit, thinking, read, glob, commit
|
|
80
|
+
filterPresets:
|
|
81
|
+
- ["all"]
|
|
82
|
+
- ["response", "user"]
|
|
83
|
+
- ["commit"]
|
|
84
|
+
|
|
85
|
+
# Defaults for \`agenthud report\` (CLI flags still win per-invocation).
|
|
86
|
+
# include: activity types to keep. Types: user, response, bash, edit,
|
|
87
|
+
# thinking, read, glob.
|
|
88
|
+
# detailLimit: max chars per activity detail (0 = unlimited).
|
|
89
|
+
# withGit: merge git commits from each session's project.
|
|
90
|
+
# format: markdown | json.
|
|
91
|
+
report:
|
|
92
|
+
include: [user, response, bash, edit, thinking]
|
|
93
|
+
detailLimit: 120
|
|
94
|
+
withGit: false
|
|
95
|
+
format: markdown
|
|
96
|
+
|
|
97
|
+
# Defaults for \`agenthud summary\`. Any field omitted here is inherited
|
|
98
|
+
# from \`report\` above. \`model\` is summary-specific and passed to
|
|
99
|
+
# \`claude --model\` (e.g. sonnet, haiku, or a full model id).
|
|
100
|
+
summary:
|
|
101
|
+
withGit: true
|
|
102
|
+
detailLimit: 0
|
|
103
|
+
# model: sonnet
|
|
104
|
+
`;
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(CONFIG_PATH, defaultYaml, "utf-8");
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function writeState(state) {
|
|
111
|
+
ensureAgenthudDir();
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(STATE_PATH, stringifyYaml(state), "utf-8");
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function rewriteConfigWithoutHideFields(raw) {
|
|
118
|
+
const cleaned = {};
|
|
119
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
120
|
+
if (k === "hiddenSessions" || k === "hiddenSubAgents" || k === "hiddenProjects")
|
|
121
|
+
continue;
|
|
122
|
+
cleaned[k] = v;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
writeFileSync(CONFIG_PATH, stringifyYaml(cleaned), "utf-8");
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function loadGlobalConfig() {
|
|
130
|
+
const config = {
|
|
131
|
+
...DEFAULT_GLOBAL_CONFIG,
|
|
132
|
+
hiddenSessions: [...DEFAULT_GLOBAL_CONFIG.hiddenSessions],
|
|
133
|
+
hiddenSubAgents: [...DEFAULT_GLOBAL_CONFIG.hiddenSubAgents],
|
|
134
|
+
hiddenProjects: [...DEFAULT_GLOBAL_CONFIG.hiddenProjects],
|
|
135
|
+
filterPresets: DEFAULT_GLOBAL_CONFIG.filterPresets.map((p) => [...p]),
|
|
136
|
+
report: {
|
|
137
|
+
...DEFAULT_GLOBAL_CONFIG.report,
|
|
138
|
+
include: [...DEFAULT_GLOBAL_CONFIG.report.include]
|
|
139
|
+
},
|
|
140
|
+
summary: { ...DEFAULT_GLOBAL_CONFIG.summary }
|
|
141
|
+
};
|
|
142
|
+
let configRaw = {};
|
|
143
|
+
let configHadHideFields = false;
|
|
144
|
+
if (existsSync(CONFIG_PATH)) {
|
|
145
|
+
try {
|
|
146
|
+
const text = readFileSync(CONFIG_PATH, "utf-8");
|
|
147
|
+
configRaw = parseYaml(text) ?? {};
|
|
148
|
+
} catch {
|
|
149
|
+
configRaw = {};
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
writeDefaultConfig();
|
|
153
|
+
}
|
|
154
|
+
if (typeof configRaw.refreshInterval === "string") {
|
|
155
|
+
const ms = parseInterval(configRaw.refreshInterval);
|
|
156
|
+
if (ms !== null) config.refreshIntervalMs = ms;
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(configRaw.filterPresets)) {
|
|
159
|
+
const presets = configRaw.filterPresets.filter(Array.isArray).map((p) => {
|
|
160
|
+
const tokens = p.filter(
|
|
161
|
+
(t) => typeof t === "string"
|
|
162
|
+
);
|
|
163
|
+
return normalizePreset(tokens);
|
|
164
|
+
});
|
|
165
|
+
if (presets.length > 0) config.filterPresets = presets;
|
|
166
|
+
}
|
|
167
|
+
if (configRaw.report && typeof configRaw.report === "object") {
|
|
168
|
+
const r = configRaw.report;
|
|
169
|
+
if (Array.isArray(r.include)) {
|
|
170
|
+
const tokens = r.include.filter(
|
|
171
|
+
(t) => typeof t === "string"
|
|
172
|
+
);
|
|
173
|
+
const cleaned = tokens.filter((t) => ALLOWED_INCLUDE_TYPES.has(t));
|
|
174
|
+
if (cleaned.length > 0) config.report.include = cleaned;
|
|
175
|
+
}
|
|
176
|
+
if (typeof r.detailLimit === "number" && Number.isInteger(r.detailLimit) && r.detailLimit >= 0) {
|
|
177
|
+
config.report.detailLimit = r.detailLimit;
|
|
178
|
+
}
|
|
179
|
+
if (typeof r.withGit === "boolean") config.report.withGit = r.withGit;
|
|
180
|
+
if (r.format === "markdown" || r.format === "json") config.report.format = r.format;
|
|
181
|
+
}
|
|
182
|
+
if (configRaw.summary && typeof configRaw.summary === "object") {
|
|
183
|
+
const s = configRaw.summary;
|
|
184
|
+
if (Array.isArray(s.include)) {
|
|
185
|
+
const tokens = s.include.filter(
|
|
186
|
+
(t) => typeof t === "string"
|
|
187
|
+
);
|
|
188
|
+
const cleaned = tokens.filter((t) => ALLOWED_INCLUDE_TYPES.has(t));
|
|
189
|
+
if (cleaned.length > 0) config.summary.include = cleaned;
|
|
190
|
+
}
|
|
191
|
+
if (typeof s.detailLimit === "number" && Number.isInteger(s.detailLimit) && s.detailLimit >= 0) {
|
|
192
|
+
config.summary.detailLimit = s.detailLimit;
|
|
193
|
+
}
|
|
194
|
+
if (typeof s.withGit === "boolean") config.summary.withGit = s.withGit;
|
|
195
|
+
if (s.format === "markdown" || s.format === "json") config.summary.format = s.format;
|
|
196
|
+
if (typeof s.model === "string" && s.model.trim().length > 0) {
|
|
197
|
+
config.summary.model = s.model.trim();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const legacyHidden = {};
|
|
201
|
+
for (const key of [
|
|
202
|
+
"hiddenSessions",
|
|
203
|
+
"hiddenSubAgents",
|
|
204
|
+
"hiddenProjects"
|
|
205
|
+
]) {
|
|
206
|
+
if (Array.isArray(configRaw[key])) {
|
|
207
|
+
configHadHideFields = true;
|
|
208
|
+
legacyHidden[key] = configRaw[key].filter(
|
|
209
|
+
(s) => typeof s === "string"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let stateRaw = {};
|
|
214
|
+
if (existsSync(STATE_PATH)) {
|
|
215
|
+
try {
|
|
216
|
+
const text = readFileSync(STATE_PATH, "utf-8");
|
|
217
|
+
stateRaw = parseYaml(text) ?? {};
|
|
218
|
+
} catch {
|
|
219
|
+
stateRaw = {};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const key of [
|
|
223
|
+
"hiddenSessions",
|
|
224
|
+
"hiddenSubAgents",
|
|
225
|
+
"hiddenProjects"
|
|
226
|
+
]) {
|
|
227
|
+
if (Array.isArray(stateRaw[key])) {
|
|
228
|
+
config[key] = stateRaw[key].filter(
|
|
229
|
+
(s) => typeof s === "string"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (configHadHideFields) {
|
|
234
|
+
const merged = {
|
|
235
|
+
hiddenSessions: config.hiddenSessions.length > 0 ? config.hiddenSessions : legacyHidden.hiddenSessions ?? [],
|
|
236
|
+
hiddenSubAgents: config.hiddenSubAgents.length > 0 ? config.hiddenSubAgents : legacyHidden.hiddenSubAgents ?? [],
|
|
237
|
+
hiddenProjects: config.hiddenProjects.length > 0 ? config.hiddenProjects : legacyHidden.hiddenProjects ?? []
|
|
238
|
+
};
|
|
239
|
+
writeState(merged);
|
|
240
|
+
rewriteConfigWithoutHideFields(configRaw);
|
|
241
|
+
config.hiddenSessions = merged.hiddenSessions;
|
|
242
|
+
config.hiddenSubAgents = merged.hiddenSubAgents;
|
|
243
|
+
config.hiddenProjects = merged.hiddenProjects;
|
|
244
|
+
}
|
|
245
|
+
return config;
|
|
246
|
+
}
|
|
247
|
+
function updateState(updates) {
|
|
248
|
+
let state = {
|
|
249
|
+
hiddenSessions: [],
|
|
250
|
+
hiddenSubAgents: [],
|
|
251
|
+
hiddenProjects: []
|
|
252
|
+
};
|
|
253
|
+
if (existsSync(STATE_PATH)) {
|
|
254
|
+
try {
|
|
255
|
+
const text = readFileSync(STATE_PATH, "utf-8");
|
|
256
|
+
const raw = parseYaml(text) ?? {};
|
|
257
|
+
for (const key of [
|
|
258
|
+
"hiddenSessions",
|
|
259
|
+
"hiddenSubAgents",
|
|
260
|
+
"hiddenProjects"
|
|
261
|
+
]) {
|
|
262
|
+
if (Array.isArray(raw[key])) {
|
|
263
|
+
state[key] = raw[key].filter(
|
|
264
|
+
(s) => typeof s === "string"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const key of [
|
|
272
|
+
"hiddenSessions",
|
|
273
|
+
"hiddenSubAgents",
|
|
274
|
+
"hiddenProjects"
|
|
275
|
+
]) {
|
|
276
|
+
if (updates[key] !== void 0) {
|
|
277
|
+
state[key] = updates[key];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
writeState(state);
|
|
281
|
+
}
|
|
282
|
+
function hideSession(id) {
|
|
283
|
+
const config = loadGlobalConfig();
|
|
284
|
+
if (config.hiddenSessions.includes(id)) return;
|
|
285
|
+
updateState({ hiddenSessions: [...config.hiddenSessions, id] });
|
|
286
|
+
}
|
|
287
|
+
function hideSubAgent(id) {
|
|
288
|
+
const config = loadGlobalConfig();
|
|
289
|
+
if (config.hiddenSubAgents.includes(id)) return;
|
|
290
|
+
updateState({ hiddenSubAgents: [...config.hiddenSubAgents, id] });
|
|
291
|
+
}
|
|
292
|
+
function hideProject(name) {
|
|
293
|
+
const config = loadGlobalConfig();
|
|
294
|
+
if (config.hiddenProjects.includes(name)) return;
|
|
295
|
+
updateState({ hiddenProjects: [...config.hiddenProjects, name] });
|
|
296
|
+
}
|
|
297
|
+
function hasProjectLevelConfig() {
|
|
298
|
+
const candidate = join(process.cwd(), ".agenthud", "config.yaml");
|
|
299
|
+
if (candidate === join(homedir(), ".agenthud", "config.yaml")) return false;
|
|
300
|
+
return existsSync(candidate);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/cli.ts
|
|
13
304
|
var ALL_TYPES = [
|
|
14
305
|
"response",
|
|
15
306
|
"bash",
|
|
@@ -19,7 +310,6 @@ var ALL_TYPES = [
|
|
|
19
310
|
"glob",
|
|
20
311
|
"user"
|
|
21
312
|
];
|
|
22
|
-
var DEFAULT_TYPES = ["user", "response", "bash", "edit", "thinking"];
|
|
23
313
|
var KNOWN_WATCH_FLAGS = /* @__PURE__ */ new Set([
|
|
24
314
|
"-w",
|
|
25
315
|
"--watch",
|
|
@@ -46,7 +336,15 @@ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
|
|
|
46
336
|
"--force",
|
|
47
337
|
"--model",
|
|
48
338
|
"-y",
|
|
49
|
-
"--yes"
|
|
339
|
+
"--yes",
|
|
340
|
+
"--include",
|
|
341
|
+
"--format",
|
|
342
|
+
"--detail-limit",
|
|
343
|
+
"--with-git",
|
|
344
|
+
"-o",
|
|
345
|
+
"--open",
|
|
346
|
+
"-I",
|
|
347
|
+
"--open-index"
|
|
50
348
|
]);
|
|
51
349
|
var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["watch", "report", "summary"]);
|
|
52
350
|
function getHelp() {
|
|
@@ -81,7 +379,8 @@ Commands:
|
|
|
81
379
|
project into the timeline
|
|
82
380
|
|
|
83
381
|
summary [--date DATE | --last Nd | --from DATE --to DATE]
|
|
84
|
-
[--
|
|
382
|
+
[--include TYPES] [--detail-limit N] [--with-git]
|
|
383
|
+
[--prompt TEXT] [--force] [--model NAME] [-y] [-o]
|
|
85
384
|
Generate an LLM summary via the claude
|
|
86
385
|
CLI. A single day produces a daily
|
|
87
386
|
summary; a date range produces a
|
|
@@ -91,12 +390,27 @@ Commands:
|
|
|
91
390
|
(e.g. --last 7d)
|
|
92
391
|
--from YYYY-MM-DD Range start (use with --to)
|
|
93
392
|
--to YYYY-MM-DD Range end (use with --from)
|
|
393
|
+
--include TYPES Activity types fed to the LLM
|
|
394
|
+
(same shape as report's --include)
|
|
395
|
+
--detail-limit N Max chars per activity detail in the
|
|
396
|
+
LLM payload (0 = unlimited)
|
|
397
|
+
--with-git Merge git commits into the LLM payload
|
|
94
398
|
--prompt TEXT Override prompt for this run (daily only)
|
|
95
399
|
--force Regenerate even if cached
|
|
96
400
|
--model NAME Pass --model to claude (e.g. "sonnet",
|
|
97
401
|
"haiku", or a full model id)
|
|
98
402
|
-y, --yes Skip confirmation prompts for new daily
|
|
99
403
|
summaries
|
|
404
|
+
-o, --open Launch the resulting summary in your OS
|
|
405
|
+
default app once it's written (or read
|
|
406
|
+
back from cache).
|
|
407
|
+
-I, --open-index Launch ~/.agenthud/summaries/index.md
|
|
408
|
+
in your OS default app. Combinable with
|
|
409
|
+
-o (e.g. -oI opens both).
|
|
410
|
+
|
|
411
|
+
Defaults for report and summary live under \`report:\` and \`summary:\`
|
|
412
|
+
in ~/.agenthud/config.yaml. Flags override config values per-run; the
|
|
413
|
+
effective values are printed to stderr at the start of each run.
|
|
100
414
|
|
|
101
415
|
Global options:
|
|
102
416
|
-V, --version Show version number
|
|
@@ -112,13 +426,10 @@ Config: ~/.agenthud/config.yaml
|
|
|
112
426
|
function getVersion() {
|
|
113
427
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
114
428
|
const packageJson = JSON.parse(
|
|
115
|
-
|
|
429
|
+
readFileSync2(join2(__dirname2, "..", "package.json"), "utf-8")
|
|
116
430
|
);
|
|
117
431
|
return packageJson.version;
|
|
118
432
|
}
|
|
119
|
-
function clearScreen() {
|
|
120
|
-
console.clear();
|
|
121
|
-
}
|
|
122
433
|
function parseLocalMidnight(dateStr) {
|
|
123
434
|
if (dateStr === "today") {
|
|
124
435
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -141,11 +452,24 @@ function parseLocalMidnight(dateStr) {
|
|
|
141
452
|
if (Number.isNaN(date.getTime())) return null;
|
|
142
453
|
return date;
|
|
143
454
|
}
|
|
455
|
+
function formatEffectiveOptionsLine(command, fields) {
|
|
456
|
+
const parts = [];
|
|
457
|
+
parts.push(`include=[${fields.include.join(",")}]`);
|
|
458
|
+
if (fields.detailLimit !== void 0) {
|
|
459
|
+
parts.push(
|
|
460
|
+
`detail-limit=${fields.detailLimit === 0 ? "\u221E" : fields.detailLimit}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
parts.push(`with-git=${fields.withGit ? "on" : "off"}`);
|
|
464
|
+
if (fields.format) parts.push(`format=${fields.format}`);
|
|
465
|
+
if (fields.model) parts.push(`model=${fields.model}`);
|
|
466
|
+
return `${command} \u2192 ${parts.join(" ")}`;
|
|
467
|
+
}
|
|
144
468
|
function todayLocalMidnight() {
|
|
145
469
|
const now = /* @__PURE__ */ new Date();
|
|
146
470
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
147
471
|
}
|
|
148
|
-
function parseArgs(args) {
|
|
472
|
+
function parseArgs(args, config) {
|
|
149
473
|
if (args[0] === "watch") args = args.slice(1);
|
|
150
474
|
if (args.includes("--help") || args.includes("-h")) {
|
|
151
475
|
return { mode: "watch", command: "help" };
|
|
@@ -159,7 +483,7 @@ function parseArgs(args) {
|
|
|
159
483
|
if (args[0] === "report") {
|
|
160
484
|
const rest = args.slice(1);
|
|
161
485
|
let reportDate = todayLocalMidnight();
|
|
162
|
-
let reportInclude =
|
|
486
|
+
let reportInclude = config?.report.include ?? DEFAULT_INCLUDE_TYPES;
|
|
163
487
|
let reportError;
|
|
164
488
|
const FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
165
489
|
"--date",
|
|
@@ -207,7 +531,7 @@ function parseArgs(args) {
|
|
|
207
531
|
}
|
|
208
532
|
}
|
|
209
533
|
}
|
|
210
|
-
let reportFormat = "markdown";
|
|
534
|
+
let reportFormat = config?.report.format ?? "markdown";
|
|
211
535
|
const formatIdx = rest.indexOf("--format");
|
|
212
536
|
if (formatIdx !== -1) {
|
|
213
537
|
const fmt = rest[formatIdx + 1];
|
|
@@ -219,7 +543,7 @@ function parseArgs(args) {
|
|
|
219
543
|
reportError = "Invalid format: missing value for --format.";
|
|
220
544
|
}
|
|
221
545
|
}
|
|
222
|
-
let reportDetailLimit;
|
|
546
|
+
let reportDetailLimit = config?.report.detailLimit;
|
|
223
547
|
const detailLimitIdx = rest.indexOf("--detail-limit");
|
|
224
548
|
if (detailLimitIdx !== -1) {
|
|
225
549
|
const val = rest[detailLimitIdx + 1];
|
|
@@ -230,7 +554,7 @@ function parseArgs(args) {
|
|
|
230
554
|
reportDetailLimit = n;
|
|
231
555
|
}
|
|
232
556
|
}
|
|
233
|
-
const reportWithGit = rest.includes("--with-git");
|
|
557
|
+
const reportWithGit = rest.includes("--with-git") || (config?.report.withGit ?? false);
|
|
234
558
|
return {
|
|
235
559
|
mode: "report",
|
|
236
560
|
reportDate,
|
|
@@ -257,7 +581,10 @@ function parseArgs(args) {
|
|
|
257
581
|
"--from",
|
|
258
582
|
"--to",
|
|
259
583
|
"--prompt",
|
|
260
|
-
"--model"
|
|
584
|
+
"--model",
|
|
585
|
+
"--include",
|
|
586
|
+
"--format",
|
|
587
|
+
"--detail-limit"
|
|
261
588
|
]);
|
|
262
589
|
for (let i = 0; i < rest.length; i++) {
|
|
263
590
|
const arg = rest[i];
|
|
@@ -360,234 +687,86 @@ function parseArgs(args) {
|
|
|
360
687
|
}
|
|
361
688
|
if (rest.includes("--force")) summaryForce = true;
|
|
362
689
|
if (rest.includes("-y") || rest.includes("--yes")) summaryAssumeYes = true;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (arg.startsWith("-") && !KNOWN_WATCH_FLAGS.has(arg)) {
|
|
383
|
-
return {
|
|
384
|
-
mode: "watch",
|
|
385
|
-
error: `Unknown option: "${arg}". Run agenthud --help for usage.`
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
return args.includes("--cwd") ? { mode: "watch", scopeToCwd: true } : { mode: "watch" };
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// src/config/globalConfig.ts
|
|
393
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
394
|
-
import { homedir } from "os";
|
|
395
|
-
import { join as join2 } from "path";
|
|
396
|
-
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
397
|
-
var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
|
|
398
|
-
var STATE_PATH = join2(homedir(), ".agenthud", "state.yaml");
|
|
399
|
-
var DEFAULT_GLOBAL_CONFIG = {
|
|
400
|
-
refreshIntervalMs: 2e3,
|
|
401
|
-
hiddenSessions: [],
|
|
402
|
-
hiddenSubAgents: [],
|
|
403
|
-
// [] means "show all"; conversation preset bundles assistant + user;
|
|
404
|
-
// commits-only preset filters down to git activity.
|
|
405
|
-
filterPresets: [[], ["response", "user"], ["commit"]],
|
|
406
|
-
hiddenProjects: []
|
|
407
|
-
};
|
|
408
|
-
var ALL_PRESET_KEYWORDS = /* @__PURE__ */ new Set(["all", "*", "any"]);
|
|
409
|
-
function normalizePreset(tokens) {
|
|
410
|
-
if (tokens.some((t) => ALL_PRESET_KEYWORDS.has(t.toLowerCase()))) return [];
|
|
411
|
-
return tokens;
|
|
412
|
-
}
|
|
413
|
-
function parseInterval(value) {
|
|
414
|
-
const match = value.match(/^(\d+)(s|m)$/);
|
|
415
|
-
if (!match) return null;
|
|
416
|
-
const n = parseInt(match[1], 10);
|
|
417
|
-
return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
|
|
418
|
-
}
|
|
419
|
-
function ensureAgenthudDir() {
|
|
420
|
-
const dir = join2(homedir(), ".agenthud");
|
|
421
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
422
|
-
}
|
|
423
|
-
function writeDefaultConfig() {
|
|
424
|
-
ensureAgenthudDir();
|
|
425
|
-
const defaultYaml = `# AgentHUD user settings.
|
|
426
|
-
# App-managed state (hidden sessions/projects) lives in state.yaml.
|
|
427
|
-
|
|
428
|
-
# How often to poll for activity updates
|
|
429
|
-
refreshInterval: 2s
|
|
430
|
-
|
|
431
|
-
# Activity filter presets (cycle with 'f' key in viewer)
|
|
432
|
-
# Each list is one preset. Use "all" (or "*") to show everything.
|
|
433
|
-
# Types: response, user, bash, edit, thinking, read, glob, commit
|
|
434
|
-
filterPresets:
|
|
435
|
-
- ["all"]
|
|
436
|
-
- ["response", "user"]
|
|
437
|
-
- ["commit"]
|
|
438
|
-
`;
|
|
439
|
-
try {
|
|
440
|
-
writeFileSync(CONFIG_PATH, defaultYaml, "utf-8");
|
|
441
|
-
} catch {
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
function writeState(state) {
|
|
445
|
-
ensureAgenthudDir();
|
|
446
|
-
try {
|
|
447
|
-
writeFileSync(STATE_PATH, stringifyYaml(state), "utf-8");
|
|
448
|
-
} catch {
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
function rewriteConfigWithoutHideFields(raw) {
|
|
452
|
-
const cleaned = {};
|
|
453
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
454
|
-
if (k === "hiddenSessions" || k === "hiddenSubAgents" || k === "hiddenProjects")
|
|
455
|
-
continue;
|
|
456
|
-
cleaned[k] = v;
|
|
457
|
-
}
|
|
458
|
-
try {
|
|
459
|
-
writeFileSync(CONFIG_PATH, stringifyYaml(cleaned), "utf-8");
|
|
460
|
-
} catch {
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
function loadGlobalConfig() {
|
|
464
|
-
const config = { ...DEFAULT_GLOBAL_CONFIG };
|
|
465
|
-
let configRaw = {};
|
|
466
|
-
let configHadHideFields = false;
|
|
467
|
-
if (existsSync(CONFIG_PATH)) {
|
|
468
|
-
try {
|
|
469
|
-
const text = readFileSync2(CONFIG_PATH, "utf-8");
|
|
470
|
-
configRaw = parseYaml(text) ?? {};
|
|
471
|
-
} catch {
|
|
472
|
-
configRaw = {};
|
|
473
|
-
}
|
|
474
|
-
} else {
|
|
475
|
-
writeDefaultConfig();
|
|
476
|
-
}
|
|
477
|
-
if (typeof configRaw.refreshInterval === "string") {
|
|
478
|
-
const ms = parseInterval(configRaw.refreshInterval);
|
|
479
|
-
if (ms !== null) config.refreshIntervalMs = ms;
|
|
480
|
-
}
|
|
481
|
-
if (Array.isArray(configRaw.filterPresets)) {
|
|
482
|
-
const presets = configRaw.filterPresets.filter(Array.isArray).map((p) => {
|
|
483
|
-
const tokens = p.filter(
|
|
484
|
-
(t) => typeof t === "string"
|
|
485
|
-
);
|
|
486
|
-
return normalizePreset(tokens);
|
|
487
|
-
});
|
|
488
|
-
if (presets.length > 0) config.filterPresets = presets;
|
|
489
|
-
}
|
|
490
|
-
const legacyHidden = {};
|
|
491
|
-
for (const key of [
|
|
492
|
-
"hiddenSessions",
|
|
493
|
-
"hiddenSubAgents",
|
|
494
|
-
"hiddenProjects"
|
|
495
|
-
]) {
|
|
496
|
-
if (Array.isArray(configRaw[key])) {
|
|
497
|
-
configHadHideFields = true;
|
|
498
|
-
legacyHidden[key] = configRaw[key].filter(
|
|
499
|
-
(s) => typeof s === "string"
|
|
500
|
-
);
|
|
690
|
+
const summaryOpen = rest.includes("--open") || rest.includes("-o") || void 0;
|
|
691
|
+
const summaryOpenIndex = rest.includes("--open-index") || rest.includes("-I") || void 0;
|
|
692
|
+
let summaryInclude = config?.summary.include ?? config?.report.include ?? DEFAULT_INCLUDE_TYPES;
|
|
693
|
+
const summaryIncludeIdx = rest.indexOf("--include");
|
|
694
|
+
if (summaryIncludeIdx !== -1) {
|
|
695
|
+
const includeStr = rest[summaryIncludeIdx + 1];
|
|
696
|
+
if (!includeStr) {
|
|
697
|
+
summaryError = summaryError ?? "Invalid --include: missing value.";
|
|
698
|
+
} else if (includeStr === "all") {
|
|
699
|
+
summaryInclude = ALL_TYPES;
|
|
700
|
+
} else {
|
|
701
|
+
const tokens = includeStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
702
|
+
const unknown = tokens.filter((t) => !ALL_TYPES.includes(t));
|
|
703
|
+
if (unknown.length > 0) {
|
|
704
|
+
summaryError = summaryError ?? `Unknown --include type${unknown.length > 1 ? "s" : ""}: ${unknown.map((u) => `"${u}"`).join(", ")}. Valid types: ${ALL_TYPES.join(", ")} (or "all").`;
|
|
705
|
+
} else {
|
|
706
|
+
summaryInclude = tokens;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
501
709
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
710
|
+
let summaryFormat = config?.summary.format ?? config?.report.format ?? "markdown";
|
|
711
|
+
const summaryFormatIdx = rest.indexOf("--format");
|
|
712
|
+
if (summaryFormatIdx !== -1) {
|
|
713
|
+
const fmt = rest[summaryFormatIdx + 1];
|
|
714
|
+
if (fmt === "json" || fmt === "markdown") {
|
|
715
|
+
summaryFormat = fmt;
|
|
716
|
+
} else if (fmt) {
|
|
717
|
+
summaryError = summaryError ?? `Invalid format: "${fmt}". Use "markdown" or "json".`;
|
|
718
|
+
} else {
|
|
719
|
+
summaryError = summaryError ?? "Invalid format: missing value for --format.";
|
|
720
|
+
}
|
|
510
721
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
722
|
+
let summaryDetailLimit = config?.summary.detailLimit ?? config?.report.detailLimit;
|
|
723
|
+
const summaryDetailLimitIdx = rest.indexOf("--detail-limit");
|
|
724
|
+
if (summaryDetailLimitIdx !== -1) {
|
|
725
|
+
const val = rest[summaryDetailLimitIdx + 1];
|
|
726
|
+
const n = Number(val);
|
|
727
|
+
if (!val || Number.isNaN(n) || n < 0 || !Number.isInteger(n)) {
|
|
728
|
+
summaryError = summaryError ?? `Invalid --detail-limit: "${val}". Must be a non-negative integer.`;
|
|
729
|
+
} else {
|
|
730
|
+
summaryDetailLimit = n;
|
|
731
|
+
}
|
|
521
732
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
733
|
+
const summaryWithGit = rest.includes("--with-git") || (config?.summary.withGit ?? config?.report.withGit ?? false);
|
|
734
|
+
if (summaryModel === void 0 && config?.summary.model) {
|
|
735
|
+
summaryModel = config.summary.model;
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
mode: "summary",
|
|
739
|
+
summaryDate,
|
|
740
|
+
summaryFrom,
|
|
741
|
+
summaryTo,
|
|
742
|
+
summaryPrompt,
|
|
743
|
+
summaryForce,
|
|
744
|
+
summaryAssumeYes,
|
|
745
|
+
summaryModel,
|
|
746
|
+
summaryInclude,
|
|
747
|
+
summaryFormat,
|
|
748
|
+
summaryDetailLimit,
|
|
749
|
+
summaryWithGit,
|
|
750
|
+
summaryOpen,
|
|
751
|
+
summaryOpenIndex,
|
|
752
|
+
summaryError
|
|
528
753
|
};
|
|
529
|
-
writeState(merged);
|
|
530
|
-
rewriteConfigWithoutHideFields(configRaw);
|
|
531
|
-
config.hiddenSessions = merged.hiddenSessions;
|
|
532
|
-
config.hiddenSubAgents = merged.hiddenSubAgents;
|
|
533
|
-
config.hiddenProjects = merged.hiddenProjects;
|
|
534
754
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
hiddenSubAgents: [],
|
|
541
|
-
hiddenProjects: []
|
|
542
|
-
};
|
|
543
|
-
if (existsSync(STATE_PATH)) {
|
|
544
|
-
try {
|
|
545
|
-
const text = readFileSync2(STATE_PATH, "utf-8");
|
|
546
|
-
const raw = parseYaml(text) ?? {};
|
|
547
|
-
for (const key of [
|
|
548
|
-
"hiddenSessions",
|
|
549
|
-
"hiddenSubAgents",
|
|
550
|
-
"hiddenProjects"
|
|
551
|
-
]) {
|
|
552
|
-
if (Array.isArray(raw[key])) {
|
|
553
|
-
state[key] = raw[key].filter(
|
|
554
|
-
(s) => typeof s === "string"
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
} catch {
|
|
559
|
-
}
|
|
755
|
+
if (args[0] && !args[0].startsWith("-") && !KNOWN_SUBCOMMANDS.has(args[0])) {
|
|
756
|
+
return {
|
|
757
|
+
mode: "watch",
|
|
758
|
+
error: `Unknown command: "${args[0]}". Run agenthud --help for usage.`
|
|
759
|
+
};
|
|
560
760
|
}
|
|
561
|
-
for (const
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
state[key] = updates[key];
|
|
761
|
+
for (const arg of args) {
|
|
762
|
+
if (arg.startsWith("-") && !KNOWN_WATCH_FLAGS.has(arg)) {
|
|
763
|
+
return {
|
|
764
|
+
mode: "watch",
|
|
765
|
+
error: `Unknown option: "${arg}". Run agenthud --help for usage.`
|
|
766
|
+
};
|
|
568
767
|
}
|
|
569
768
|
}
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
function hideSession(id) {
|
|
573
|
-
const config = loadGlobalConfig();
|
|
574
|
-
if (config.hiddenSessions.includes(id)) return;
|
|
575
|
-
updateState({ hiddenSessions: [...config.hiddenSessions, id] });
|
|
576
|
-
}
|
|
577
|
-
function hideSubAgent(id) {
|
|
578
|
-
const config = loadGlobalConfig();
|
|
579
|
-
if (config.hiddenSubAgents.includes(id)) return;
|
|
580
|
-
updateState({ hiddenSubAgents: [...config.hiddenSubAgents, id] });
|
|
581
|
-
}
|
|
582
|
-
function hideProject(name) {
|
|
583
|
-
const config = loadGlobalConfig();
|
|
584
|
-
if (config.hiddenProjects.includes(name)) return;
|
|
585
|
-
updateState({ hiddenProjects: [...config.hiddenProjects, name] });
|
|
586
|
-
}
|
|
587
|
-
function hasProjectLevelConfig() {
|
|
588
|
-
const candidate = join2(process.cwd(), ".agenthud", "config.yaml");
|
|
589
|
-
if (candidate === join2(homedir(), ".agenthud", "config.yaml")) return false;
|
|
590
|
-
return existsSync(candidate);
|
|
769
|
+
return args.includes("--cwd") ? { mode: "watch", scopeToCwd: true } : { mode: "watch" };
|
|
591
770
|
}
|
|
592
771
|
|
|
593
772
|
// src/data/reportGenerator.ts
|
|
@@ -1484,56 +1663,365 @@ function discoverSessions(config, options2) {
|
|
|
1484
1663
|
}
|
|
1485
1664
|
|
|
1486
1665
|
// src/data/summaryRunner.ts
|
|
1487
|
-
import { spawn } from "child_process";
|
|
1666
|
+
import { spawn as spawn2 } from "child_process";
|
|
1488
1667
|
import {
|
|
1489
1668
|
copyFileSync,
|
|
1490
1669
|
createWriteStream,
|
|
1491
|
-
existsSync as
|
|
1670
|
+
existsSync as existsSync5,
|
|
1492
1671
|
mkdirSync as mkdirSync2,
|
|
1493
|
-
readFileSync as
|
|
1672
|
+
readFileSync as readFileSync6,
|
|
1494
1673
|
unlinkSync
|
|
1495
1674
|
} from "fs";
|
|
1496
1675
|
import { homedir as homedir3 } from "os";
|
|
1497
|
-
import { dirname as dirname2, join as
|
|
1676
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1498
1677
|
import { createInterface } from "readline";
|
|
1499
1678
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1679
|
+
|
|
1680
|
+
// src/utils/openInDefaultApp.ts
|
|
1681
|
+
import { spawn } from "child_process";
|
|
1682
|
+
function buildOpenCommand(platform, path) {
|
|
1683
|
+
switch (platform) {
|
|
1684
|
+
case "darwin":
|
|
1685
|
+
return { command: "open", args: [path] };
|
|
1686
|
+
case "linux":
|
|
1687
|
+
return { command: "xdg-open", args: [path] };
|
|
1688
|
+
case "win32":
|
|
1689
|
+
return { command: "cmd", args: ["/c", "start", "", path] };
|
|
1690
|
+
default:
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
function openInDefaultApp(path) {
|
|
1695
|
+
const cmd = buildOpenCommand(process.platform, path);
|
|
1696
|
+
if (!cmd) {
|
|
1697
|
+
process.stderr.write(
|
|
1698
|
+
`agenthud: --open: no known opener for platform "${process.platform}"
|
|
1699
|
+
`
|
|
1700
|
+
);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
try {
|
|
1704
|
+
const child = spawn(cmd.command, cmd.args, {
|
|
1705
|
+
detached: true,
|
|
1706
|
+
stdio: "ignore"
|
|
1707
|
+
});
|
|
1708
|
+
child.on("error", (err) => {
|
|
1709
|
+
process.stderr.write(`agenthud: --open failed: ${err.message}
|
|
1710
|
+
`);
|
|
1711
|
+
});
|
|
1712
|
+
child.unref();
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1715
|
+
process.stderr.write(`agenthud: --open failed: ${msg}
|
|
1716
|
+
`);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/data/summariesIndex.ts
|
|
1721
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1722
|
+
import { join as join4 } from "path";
|
|
1723
|
+
var INDEX_HEADER_MARKER = "<!-- agenthud-summaries-index -->";
|
|
1724
|
+
var BACKLINK_START_MARKER = "<!-- agenthud-backlinks-start -->";
|
|
1725
|
+
var BACKLINK_END_MARKER = "<!-- agenthud-backlinks-end -->";
|
|
1726
|
+
var DAILY_RE = /^(\d{4})-(\d{2})-(\d{2})\.md$/;
|
|
1727
|
+
var RANGE_RE = /^range-(\d{4})-(\d{2})-(\d{2})_(\d{4})-(\d{2})-(\d{2})\.md$/;
|
|
1728
|
+
function makeLocalDate(y, m, d) {
|
|
1729
|
+
const date = new Date(y, m - 1, d);
|
|
1730
|
+
if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1733
|
+
return date;
|
|
1734
|
+
}
|
|
1735
|
+
function parseSummaryFilename(name) {
|
|
1736
|
+
const range = RANGE_RE.exec(name);
|
|
1737
|
+
if (range) {
|
|
1738
|
+
const from = makeLocalDate(
|
|
1739
|
+
Number(range[1]),
|
|
1740
|
+
Number(range[2]),
|
|
1741
|
+
Number(range[3])
|
|
1742
|
+
);
|
|
1743
|
+
const to = makeLocalDate(
|
|
1744
|
+
Number(range[4]),
|
|
1745
|
+
Number(range[5]),
|
|
1746
|
+
Number(range[6])
|
|
1747
|
+
);
|
|
1748
|
+
if (!from || !to) return null;
|
|
1749
|
+
return { kind: "range", from, to, filename: name };
|
|
1750
|
+
}
|
|
1751
|
+
const daily = DAILY_RE.exec(name);
|
|
1752
|
+
if (daily) {
|
|
1753
|
+
const date = makeLocalDate(
|
|
1754
|
+
Number(daily[1]),
|
|
1755
|
+
Number(daily[2]),
|
|
1756
|
+
Number(daily[3])
|
|
1757
|
+
);
|
|
1758
|
+
if (!date) return null;
|
|
1759
|
+
return { kind: "daily", date, filename: name };
|
|
1760
|
+
}
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
function listSummaries(dir) {
|
|
1764
|
+
if (!existsSync4(dir)) return [];
|
|
1765
|
+
let names;
|
|
1766
|
+
try {
|
|
1767
|
+
names = readdirSync2(dir);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return [];
|
|
1770
|
+
}
|
|
1771
|
+
const entries = names.map((n) => parseSummaryFilename(n)).filter((e) => e !== null);
|
|
1772
|
+
const anchor = (e) => e.kind === "range" ? e.from : e.date;
|
|
1773
|
+
entries.sort((a, b) => {
|
|
1774
|
+
const da = anchor(a);
|
|
1775
|
+
const db = anchor(b);
|
|
1776
|
+
if (da.getFullYear() !== db.getFullYear()) {
|
|
1777
|
+
return db.getFullYear() - da.getFullYear();
|
|
1778
|
+
}
|
|
1779
|
+
if (da.getMonth() !== db.getMonth()) {
|
|
1780
|
+
return db.getMonth() - da.getMonth();
|
|
1781
|
+
}
|
|
1782
|
+
if (a.kind !== b.kind) return a.kind === "range" ? -1 : 1;
|
|
1783
|
+
return db.getTime() - da.getTime();
|
|
1784
|
+
});
|
|
1785
|
+
return entries;
|
|
1786
|
+
}
|
|
1787
|
+
var MONTHS = [
|
|
1788
|
+
"January",
|
|
1789
|
+
"February",
|
|
1790
|
+
"March",
|
|
1791
|
+
"April",
|
|
1792
|
+
"May",
|
|
1793
|
+
"June",
|
|
1794
|
+
"July",
|
|
1795
|
+
"August",
|
|
1796
|
+
"September",
|
|
1797
|
+
"October",
|
|
1798
|
+
"November",
|
|
1799
|
+
"December"
|
|
1800
|
+
];
|
|
1801
|
+
var WEEKDAYS_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1802
|
+
var WEEKDAYS_LONG = [
|
|
1803
|
+
"Sunday",
|
|
1804
|
+
"Monday",
|
|
1805
|
+
"Tuesday",
|
|
1806
|
+
"Wednesday",
|
|
1807
|
+
"Thursday",
|
|
1808
|
+
"Friday",
|
|
1809
|
+
"Saturday"
|
|
1810
|
+
];
|
|
1811
|
+
function formatDateKey(d) {
|
|
1812
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1813
|
+
}
|
|
1814
|
+
function formatDateLabel(d) {
|
|
1815
|
+
return `${formatDateKey(d)} (${WEEKDAYS_SHORT[d.getDay()]})`;
|
|
1816
|
+
}
|
|
1817
|
+
function entryAnchor(e) {
|
|
1818
|
+
return e.kind === "range" ? e.from : e.date;
|
|
1819
|
+
}
|
|
1820
|
+
function buildIndexMarkdown(entries, snippets) {
|
|
1821
|
+
const lines = [INDEX_HEADER_MARKER, "", "# AgentHUD summaries", ""];
|
|
1822
|
+
if (entries.length === 0) {
|
|
1823
|
+
lines.push("_No summaries yet. Run `agenthud summary` to create one._");
|
|
1824
|
+
return `${lines.join("\n")}
|
|
1825
|
+
`;
|
|
1826
|
+
}
|
|
1827
|
+
const byYear = /* @__PURE__ */ new Map();
|
|
1828
|
+
for (const entry of entries) {
|
|
1829
|
+
const a = entryAnchor(entry);
|
|
1830
|
+
const y = a.getFullYear();
|
|
1831
|
+
const m = a.getMonth();
|
|
1832
|
+
const months = byYear.get(y) ?? /* @__PURE__ */ new Map();
|
|
1833
|
+
const bucket = months.get(m) ?? [];
|
|
1834
|
+
bucket.push(entry);
|
|
1835
|
+
months.set(m, bucket);
|
|
1836
|
+
byYear.set(y, months);
|
|
1837
|
+
}
|
|
1838
|
+
for (const [year, months] of byYear) {
|
|
1839
|
+
lines.push(`## ${year}`, "");
|
|
1840
|
+
for (const [month, bucket] of months) {
|
|
1841
|
+
lines.push(`### ${MONTHS[month]}`);
|
|
1842
|
+
for (const entry of bucket) {
|
|
1843
|
+
const snippet = snippets?.get(entry.filename);
|
|
1844
|
+
const trail = snippet ? ` \u2014 ${snippet}` : "";
|
|
1845
|
+
if (entry.kind === "range") {
|
|
1846
|
+
const fromIso = formatDateKey(entry.from);
|
|
1847
|
+
const toIso = formatDateKey(entry.to);
|
|
1848
|
+
lines.push(
|
|
1849
|
+
`- [Range: ${fromIso} \u2192 ${toIso}](./${entry.filename}) \xB7 weekly${trail}`
|
|
1850
|
+
);
|
|
1851
|
+
} else {
|
|
1852
|
+
lines.push(
|
|
1853
|
+
`- [${formatDateLabel(entry.date)}](./${entry.filename})${trail}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
lines.push("");
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return `${lines.join("\n")}
|
|
1861
|
+
`;
|
|
1862
|
+
}
|
|
1863
|
+
function buildTitleLine(entry) {
|
|
1864
|
+
if (entry.kind === "range") {
|
|
1865
|
+
return `# Range: ${formatDateKey(entry.from)} \u2192 ${formatDateKey(entry.to)}`;
|
|
1866
|
+
}
|
|
1867
|
+
const iso = formatDateKey(entry.date);
|
|
1868
|
+
const weekday = WEEKDAYS_LONG[entry.date.getDay()];
|
|
1869
|
+
return `# ${iso} (${weekday})`;
|
|
1870
|
+
}
|
|
1871
|
+
function buildHeaderBlock(currentFilename, entries) {
|
|
1872
|
+
const parts = ["[\u2190 all summaries](./index.md)"];
|
|
1873
|
+
const me = entries.find((e) => e.filename === currentFilename);
|
|
1874
|
+
if (me && me.kind === "daily") {
|
|
1875
|
+
const dailies = entries.filter((e) => e.kind === "daily").sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
1876
|
+
const idx = dailies.findIndex((e) => e.filename === currentFilename);
|
|
1877
|
+
if (idx > 0) {
|
|
1878
|
+
const prev = dailies[idx - 1];
|
|
1879
|
+
parts.push(
|
|
1880
|
+
`[\u2190 ${formatDateLabel(prev.date)}](./${prev.filename})`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
if (idx >= 0 && idx < dailies.length - 1) {
|
|
1884
|
+
const next = dailies[idx + 1];
|
|
1885
|
+
parts.push(
|
|
1886
|
+
`[${formatDateLabel(next.date)} \u2192](./${next.filename})`
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
const backlinkLine = parts.join(" \xB7 ");
|
|
1891
|
+
const title = me ? buildTitleLine(me) : "";
|
|
1892
|
+
return `${BACKLINK_START_MARKER}
|
|
1893
|
+
${backlinkLine}
|
|
1894
|
+
|
|
1895
|
+
${title}
|
|
1896
|
+
${BACKLINK_END_MARKER}
|
|
1897
|
+
|
|
1898
|
+
`;
|
|
1899
|
+
}
|
|
1900
|
+
function stripExistingHeaderBlock(content) {
|
|
1901
|
+
if (!content.startsWith(BACKLINK_START_MARKER)) return content;
|
|
1902
|
+
const endIdx = content.indexOf(BACKLINK_END_MARKER);
|
|
1903
|
+
if (endIdx === -1) return content;
|
|
1904
|
+
let cut = endIdx + BACKLINK_END_MARKER.length;
|
|
1905
|
+
while (cut < content.length && (content[cut] === "\n" || content[cut] === "\r")) {
|
|
1906
|
+
cut++;
|
|
1907
|
+
}
|
|
1908
|
+
return content.slice(cut);
|
|
1909
|
+
}
|
|
1910
|
+
function prependHeaderBlock(content, block) {
|
|
1911
|
+
return block + stripExistingHeaderBlock(content);
|
|
1912
|
+
}
|
|
1913
|
+
function extractContextSnippet(content, maxChars = 200) {
|
|
1914
|
+
const body = stripExistingHeaderBlock(content);
|
|
1915
|
+
for (const raw of body.split("\n")) {
|
|
1916
|
+
const line = raw.trim();
|
|
1917
|
+
if (!line) continue;
|
|
1918
|
+
if (line.startsWith("#")) continue;
|
|
1919
|
+
if (line.startsWith("<!--")) continue;
|
|
1920
|
+
if (line.length <= maxChars) return line;
|
|
1921
|
+
return `${line.slice(0, maxChars - 1).trimEnd()}\u2026`;
|
|
1922
|
+
}
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
function regenerateIndex(summariesDir2) {
|
|
1926
|
+
const entries = listSummaries(summariesDir2);
|
|
1927
|
+
const snippets = /* @__PURE__ */ new Map();
|
|
1928
|
+
for (const entry of entries) {
|
|
1929
|
+
const path = join4(summariesDir2, entry.filename);
|
|
1930
|
+
try {
|
|
1931
|
+
const content = readFileSync5(path, "utf-8");
|
|
1932
|
+
const snippet = extractContextSnippet(content);
|
|
1933
|
+
if (snippet) snippets.set(entry.filename, snippet);
|
|
1934
|
+
const block = buildHeaderBlock(entry.filename, entries);
|
|
1935
|
+
const updated = prependHeaderBlock(content, block);
|
|
1936
|
+
if (updated !== content) {
|
|
1937
|
+
writeFileSync2(path, updated, "utf-8");
|
|
1938
|
+
}
|
|
1939
|
+
} catch {
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
const indexPath = join4(summariesDir2, "index.md");
|
|
1943
|
+
try {
|
|
1944
|
+
writeFileSync2(
|
|
1945
|
+
indexPath,
|
|
1946
|
+
buildIndexMarkdown(entries, snippets),
|
|
1947
|
+
"utf-8"
|
|
1948
|
+
);
|
|
1949
|
+
} catch (err) {
|
|
1950
|
+
process.stderr.write(
|
|
1951
|
+
`warning: cannot write summaries index (${err.message})
|
|
1952
|
+
`
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/utils/stderrTicker.ts
|
|
1958
|
+
function formatTickerLine(label, elapsedSeconds, dots) {
|
|
1959
|
+
const tail = ".".repeat(dots % 4).padEnd(3, " ");
|
|
1960
|
+
return `${label}${tail} ${elapsedSeconds}s`;
|
|
1961
|
+
}
|
|
1962
|
+
function startStderrTicker(label, options2 = {}) {
|
|
1963
|
+
const intervalMs = options2.intervalMs ?? 500;
|
|
1964
|
+
const stream = options2.stream ?? process.stderr;
|
|
1965
|
+
const start = Date.now();
|
|
1966
|
+
let ticks = 0;
|
|
1967
|
+
const id = setInterval(() => {
|
|
1968
|
+
ticks++;
|
|
1969
|
+
const elapsed = Math.floor((Date.now() - start) / 1e3);
|
|
1970
|
+
stream.write(`\r${formatTickerLine(label, elapsed, ticks)}`);
|
|
1971
|
+
}, intervalMs);
|
|
1972
|
+
return function stop() {
|
|
1973
|
+
clearInterval(id);
|
|
1974
|
+
if (ticks > 0) stream.write("\r\x1B[K");
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/data/summaryRunner.ts
|
|
1500
1979
|
function agenthudHomeDir() {
|
|
1501
|
-
const dir =
|
|
1502
|
-
if (!
|
|
1980
|
+
const dir = join5(homedir3(), ".agenthud");
|
|
1981
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
1503
1982
|
return dir;
|
|
1504
1983
|
}
|
|
1505
1984
|
function summariesDir() {
|
|
1506
|
-
const dir =
|
|
1507
|
-
if (!
|
|
1985
|
+
const dir = join5(agenthudHomeDir(), "summaries");
|
|
1986
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
1508
1987
|
return dir;
|
|
1509
1988
|
}
|
|
1510
1989
|
function promptFilename(kind) {
|
|
1511
1990
|
return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
|
|
1512
1991
|
}
|
|
1513
1992
|
function userPromptPath(kind) {
|
|
1514
|
-
return
|
|
1993
|
+
return join5(homedir3(), ".agenthud", promptFilename(kind));
|
|
1994
|
+
}
|
|
1995
|
+
function formatPromptSource(kind, override) {
|
|
1996
|
+
if (kind === "daily" && override) {
|
|
1997
|
+
return "<inline> (from --prompt)";
|
|
1998
|
+
}
|
|
1999
|
+
const home = homedir3();
|
|
2000
|
+
const path = userPromptPath(kind);
|
|
2001
|
+
const abbreviated = path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
2002
|
+
return abbreviated.replace(/\\/g, "/");
|
|
1515
2003
|
}
|
|
1516
2004
|
function templatePath(kind) {
|
|
1517
2005
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1518
|
-
return
|
|
2006
|
+
return join5(here, "templates", promptFilename(kind));
|
|
1519
2007
|
}
|
|
1520
2008
|
function dateKey(d) {
|
|
1521
2009
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1522
2010
|
}
|
|
1523
2011
|
function dailyCachePath(date) {
|
|
1524
|
-
return
|
|
2012
|
+
return join5(summariesDir(), `${dateKey(date)}.md`);
|
|
1525
2013
|
}
|
|
1526
2014
|
function rangeCachePath(from, to) {
|
|
1527
|
-
return
|
|
2015
|
+
return join5(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
|
|
1528
2016
|
}
|
|
1529
2017
|
function isSameLocalDay2(a, b) {
|
|
1530
2018
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
1531
2019
|
}
|
|
1532
2020
|
function ensureUserPromptFile(kind) {
|
|
1533
2021
|
const p = userPromptPath(kind);
|
|
1534
|
-
if (
|
|
2022
|
+
if (existsSync5(p)) return;
|
|
1535
2023
|
const dir = dirname2(p);
|
|
1536
|
-
if (!
|
|
2024
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
1537
2025
|
try {
|
|
1538
2026
|
copyFileSync(templatePath(kind), p);
|
|
1539
2027
|
} catch {
|
|
@@ -1542,14 +2030,14 @@ function ensureUserPromptFile(kind) {
|
|
|
1542
2030
|
function resolvePrompt(kind, override) {
|
|
1543
2031
|
if (override) return override;
|
|
1544
2032
|
const p = userPromptPath(kind);
|
|
1545
|
-
if (
|
|
2033
|
+
if (existsSync5(p)) {
|
|
1546
2034
|
try {
|
|
1547
|
-
return
|
|
2035
|
+
return readFileSync6(p, "utf-8");
|
|
1548
2036
|
} catch {
|
|
1549
2037
|
}
|
|
1550
2038
|
}
|
|
1551
2039
|
try {
|
|
1552
|
-
return
|
|
2040
|
+
return readFileSync6(templatePath(kind), "utf-8");
|
|
1553
2041
|
} catch {
|
|
1554
2042
|
return "Summarize the input below.";
|
|
1555
2043
|
}
|
|
@@ -1604,7 +2092,7 @@ function spawnClaude(opts) {
|
|
|
1604
2092
|
];
|
|
1605
2093
|
if (opts.model) args.push("--model", opts.model);
|
|
1606
2094
|
args.push(opts.prompt);
|
|
1607
|
-
const proc =
|
|
2095
|
+
const proc = spawn2(
|
|
1608
2096
|
"claude",
|
|
1609
2097
|
args,
|
|
1610
2098
|
{
|
|
@@ -1725,11 +2213,11 @@ async function generateDailySummary(opts) {
|
|
|
1725
2213
|
const isToday = isSameLocalDay2(opts.date, opts.today);
|
|
1726
2214
|
const cached = dailyCachePath(opts.date);
|
|
1727
2215
|
const dateLabel = dateKey(opts.date);
|
|
1728
|
-
if (!isToday && !opts.force &&
|
|
2216
|
+
if (!isToday && !opts.force && existsSync5(cached)) {
|
|
1729
2217
|
try {
|
|
1730
|
-
const content =
|
|
2218
|
+
const content = readFileSync6(cached, "utf-8");
|
|
1731
2219
|
if (opts.announce) {
|
|
1732
|
-
process.stderr.write(`
|
|
2220
|
+
process.stderr.write(`cached summary from ${cached}
|
|
1733
2221
|
`);
|
|
1734
2222
|
}
|
|
1735
2223
|
if (opts.streamToStdout) {
|
|
@@ -1747,7 +2235,7 @@ async function generateDailySummary(opts) {
|
|
|
1747
2235
|
}
|
|
1748
2236
|
}
|
|
1749
2237
|
if (opts.announce) {
|
|
1750
|
-
process.stderr.write(`
|
|
2238
|
+
process.stderr.write(`scanning sessions for ${dateLabel}...
|
|
1751
2239
|
`);
|
|
1752
2240
|
}
|
|
1753
2241
|
const config = loadGlobalConfig();
|
|
@@ -1758,10 +2246,13 @@ async function generateDailySummary(opts) {
|
|
|
1758
2246
|
];
|
|
1759
2247
|
const reportMarkdown = generateReport(flatSessions, {
|
|
1760
2248
|
date: opts.date,
|
|
1761
|
-
include:
|
|
2249
|
+
include: opts.include,
|
|
2250
|
+
// The LLM only ingests markdown — the format option on summary is
|
|
2251
|
+
// for the report-extraction *export* surface (post-LLM), not for
|
|
2252
|
+
// the payload itself, so this stays fixed.
|
|
1762
2253
|
format: "markdown",
|
|
1763
|
-
detailLimit:
|
|
1764
|
-
withGit:
|
|
2254
|
+
detailLimit: opts.detailLimit,
|
|
2255
|
+
withGit: opts.withGit
|
|
1765
2256
|
});
|
|
1766
2257
|
const reportBytes = Buffer.byteLength(reportMarkdown, "utf-8");
|
|
1767
2258
|
const estimatedTokens = Math.ceil(reportBytes / 4);
|
|
@@ -1776,7 +2267,7 @@ async function generateDailySummary(opts) {
|
|
|
1776
2267
|
).length;
|
|
1777
2268
|
const sizeKb = (reportBytes / 1024).toFixed(1);
|
|
1778
2269
|
process.stderr.write(
|
|
1779
|
-
`
|
|
2270
|
+
`input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB \u2248 ${estimatedTokens.toLocaleString()} tokens)
|
|
1780
2271
|
`
|
|
1781
2272
|
);
|
|
1782
2273
|
}
|
|
@@ -1815,13 +2306,15 @@ async function generateDailySummary(opts) {
|
|
|
1815
2306
|
}
|
|
1816
2307
|
}
|
|
1817
2308
|
}
|
|
1818
|
-
|
|
2309
|
+
const showTicker = opts.announce && !opts.streamToStdout;
|
|
2310
|
+
if (opts.announce && !showTicker) {
|
|
1819
2311
|
process.stderr.write(
|
|
1820
|
-
`
|
|
2312
|
+
`sending to claude (this may take a minute)...
|
|
1821
2313
|
|
|
1822
2314
|
`
|
|
1823
2315
|
);
|
|
1824
2316
|
}
|
|
2317
|
+
const stopTicker = showTicker ? startStderrTicker("sending to claude") : null;
|
|
1825
2318
|
const prompt = resolvePrompt("daily", opts.promptOverride);
|
|
1826
2319
|
const result = await spawnClaude({
|
|
1827
2320
|
prompt,
|
|
@@ -1830,12 +2323,13 @@ async function generateDailySummary(opts) {
|
|
|
1830
2323
|
streamToStdout: opts.streamToStdout,
|
|
1831
2324
|
model: opts.model
|
|
1832
2325
|
});
|
|
2326
|
+
if (stopTicker) stopTicker();
|
|
1833
2327
|
if (opts.announce && result.code === 0) {
|
|
1834
2328
|
process.stderr.write("\n");
|
|
1835
|
-
process.stderr.write(`
|
|
2329
|
+
process.stderr.write(`saved to ${cached}
|
|
1836
2330
|
`);
|
|
1837
2331
|
if (result.usage) {
|
|
1838
|
-
process.stderr.write(
|
|
2332
|
+
process.stderr.write(`${formatUsage(result.usage)}
|
|
1839
2333
|
`);
|
|
1840
2334
|
}
|
|
1841
2335
|
}
|
|
@@ -1853,10 +2347,25 @@ async function runSummary(options2) {
|
|
|
1853
2347
|
today: options2.today,
|
|
1854
2348
|
force: options2.force,
|
|
1855
2349
|
promptOverride: options2.prompt,
|
|
1856
|
-
streamToStdout:
|
|
2350
|
+
streamToStdout: !options2.open,
|
|
1857
2351
|
announce: true,
|
|
1858
|
-
model: options2.model
|
|
2352
|
+
model: options2.model,
|
|
2353
|
+
include: options2.include,
|
|
2354
|
+
detailLimit: options2.detailLimit,
|
|
2355
|
+
withGit: options2.withGit
|
|
1859
2356
|
});
|
|
2357
|
+
if (res.code === 0 && !res.skipped) {
|
|
2358
|
+
try {
|
|
2359
|
+
regenerateIndex(summariesDir());
|
|
2360
|
+
} catch {
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (options2.open && res.code === 0 && !res.skipped) {
|
|
2364
|
+
openInDefaultApp(dailyCachePath(options2.date));
|
|
2365
|
+
}
|
|
2366
|
+
if (options2.openIndex && res.code === 0 && !res.skipped) {
|
|
2367
|
+
openInDefaultApp(join5(summariesDir(), "index.md"));
|
|
2368
|
+
}
|
|
1860
2369
|
return res.code;
|
|
1861
2370
|
}
|
|
1862
2371
|
async function runRangeSummary(options2) {
|
|
@@ -1870,16 +2379,26 @@ async function runRangeSummary(options2) {
|
|
|
1870
2379
|
options2.force,
|
|
1871
2380
|
dates,
|
|
1872
2381
|
options2.today,
|
|
1873
|
-
|
|
2382
|
+
existsSync5(rangeCache)
|
|
1874
2383
|
)) {
|
|
1875
2384
|
try {
|
|
1876
|
-
const content =
|
|
2385
|
+
const content = readFileSync6(rangeCache, "utf-8");
|
|
1877
2386
|
process.stderr.write(
|
|
1878
|
-
`
|
|
2387
|
+
`cached range summary from ${rangeCache}
|
|
1879
2388
|
`
|
|
1880
2389
|
);
|
|
1881
|
-
|
|
1882
|
-
|
|
2390
|
+
if (!options2.open) {
|
|
2391
|
+
process.stdout.write(content);
|
|
2392
|
+
if (!content.endsWith("\n")) process.stdout.write("\n");
|
|
2393
|
+
}
|
|
2394
|
+
try {
|
|
2395
|
+
regenerateIndex(summariesDir());
|
|
2396
|
+
} catch {
|
|
2397
|
+
}
|
|
2398
|
+
if (options2.open) openInDefaultApp(rangeCache);
|
|
2399
|
+
if (options2.openIndex) {
|
|
2400
|
+
openInDefaultApp(join5(summariesDir(), "index.md"));
|
|
2401
|
+
}
|
|
1883
2402
|
return 0;
|
|
1884
2403
|
} catch {
|
|
1885
2404
|
}
|
|
@@ -1888,15 +2407,15 @@ async function runRangeSummary(options2) {
|
|
|
1888
2407
|
let missingCount = 0;
|
|
1889
2408
|
for (const d of dates) {
|
|
1890
2409
|
const isToday = isSameLocalDay2(d, options2.today);
|
|
1891
|
-
if (!isToday &&
|
|
2410
|
+
if (!isToday && existsSync5(dailyCachePath(d))) cachedCount++;
|
|
1892
2411
|
else missingCount++;
|
|
1893
2412
|
}
|
|
1894
2413
|
process.stderr.write(
|
|
1895
|
-
`
|
|
2414
|
+
`range ${fromLabel} \u2192 ${toLabel} (${dates.length} days)
|
|
1896
2415
|
`
|
|
1897
2416
|
);
|
|
1898
2417
|
process.stderr.write(
|
|
1899
|
-
|
|
2418
|
+
`${cachedCount} cached, ${missingCount} to generate
|
|
1900
2419
|
`
|
|
1901
2420
|
);
|
|
1902
2421
|
const dailyMarkdowns = [];
|
|
@@ -1905,9 +2424,9 @@ async function runRangeSummary(options2) {
|
|
|
1905
2424
|
const label = dateKey(d);
|
|
1906
2425
|
const isToday = isSameLocalDay2(d, options2.today);
|
|
1907
2426
|
process.stderr.write(`
|
|
1908
|
-
|
|
2427
|
+
--- ${label} ---
|
|
1909
2428
|
`);
|
|
1910
|
-
const willPrompt = !options2.assumeYes && (isToday || !
|
|
2429
|
+
const willPrompt = !options2.assumeYes && (isToday || !existsSync5(dailyCachePath(d)));
|
|
1911
2430
|
const confirmer = willPrompt ? async () => {
|
|
1912
2431
|
const hint = isToday ? " (today \u2014 regenerated every time)" : "";
|
|
1913
2432
|
return ask(`Generate this summary${hint}? [Y/n] `, true);
|
|
@@ -1920,7 +2439,10 @@ agenthud: --- ${label} ---
|
|
|
1920
2439
|
announce: true,
|
|
1921
2440
|
confirmBeforeSpawn: confirmer,
|
|
1922
2441
|
assumeYes: options2.assumeYes,
|
|
1923
|
-
model: options2.model
|
|
2442
|
+
model: options2.model,
|
|
2443
|
+
include: options2.include,
|
|
2444
|
+
detailLimit: options2.detailLimit,
|
|
2445
|
+
withGit: options2.withGit
|
|
1924
2446
|
});
|
|
1925
2447
|
if (res.skipped) {
|
|
1926
2448
|
process.stderr.write(`agenthud: ${label} \u2014 skipped by user.
|
|
@@ -1937,7 +2459,7 @@ agenthud: --- ${label} ---
|
|
|
1937
2459
|
}
|
|
1938
2460
|
const text = res.markdown.trim();
|
|
1939
2461
|
if (text.length === 0 || /^no activity found/i.test(text)) {
|
|
1940
|
-
process.stderr.write(
|
|
2462
|
+
process.stderr.write(`${label} has no activity \u2014 skipping.
|
|
1941
2463
|
`);
|
|
1942
2464
|
continue;
|
|
1943
2465
|
}
|
|
@@ -1952,37 +2474,51 @@ agenthud: --- ${label} ---
|
|
|
1952
2474
|
${markdown}`).join("\n\n---\n\n");
|
|
1953
2475
|
process.stderr.write(
|
|
1954
2476
|
`
|
|
1955
|
-
|
|
2477
|
+
combining ${dailyMarkdowns.length} daily summaries into range summary...
|
|
1956
2478
|
`
|
|
1957
2479
|
);
|
|
1958
|
-
|
|
1959
|
-
|
|
2480
|
+
const metaStreams = !options2.open;
|
|
2481
|
+
if (!metaStreams) {
|
|
2482
|
+
} else {
|
|
2483
|
+
process.stderr.write(
|
|
2484
|
+
`sending to claude (this may take a minute)...
|
|
1960
2485
|
|
|
1961
2486
|
`
|
|
1962
|
-
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
const stopMetaTicker = metaStreams ? null : startStderrTicker("sending to claude");
|
|
1963
2490
|
const metaPrompt = resolvePrompt("range");
|
|
1964
2491
|
const metaResult = await spawnClaude({
|
|
1965
2492
|
prompt: metaPrompt,
|
|
1966
2493
|
stdin: metaInput,
|
|
1967
2494
|
cachePath: rangeCache,
|
|
1968
|
-
streamToStdout:
|
|
2495
|
+
streamToStdout: metaStreams,
|
|
1969
2496
|
model: options2.model
|
|
1970
2497
|
});
|
|
2498
|
+
if (stopMetaTicker) stopMetaTicker();
|
|
1971
2499
|
if (metaResult.code !== 0) {
|
|
1972
2500
|
return metaResult.code;
|
|
1973
2501
|
}
|
|
1974
2502
|
process.stderr.write("\n");
|
|
1975
|
-
process.stderr.write(`
|
|
2503
|
+
process.stderr.write(`saved to ${rangeCache}
|
|
1976
2504
|
`);
|
|
1977
2505
|
if (metaResult.usage) {
|
|
1978
|
-
process.stderr.write(
|
|
2506
|
+
process.stderr.write(`${formatUsage(metaResult.usage)}
|
|
1979
2507
|
`);
|
|
1980
2508
|
}
|
|
2509
|
+
try {
|
|
2510
|
+
regenerateIndex(summariesDir());
|
|
2511
|
+
} catch {
|
|
2512
|
+
}
|
|
2513
|
+
if (options2.open) openInDefaultApp(rangeCache);
|
|
2514
|
+
if (options2.openIndex) {
|
|
2515
|
+
openInDefaultApp(join5(summariesDir(), "index.md"));
|
|
2516
|
+
}
|
|
1981
2517
|
return 0;
|
|
1982
2518
|
}
|
|
1983
2519
|
|
|
1984
2520
|
// src/ui/App.tsx
|
|
1985
|
-
import { existsSync as
|
|
2521
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
1986
2522
|
import { basename as basename3 } from "path";
|
|
1987
2523
|
import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
|
|
1988
2524
|
import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
@@ -2419,6 +2955,7 @@ var SECTIONS = [
|
|
|
2419
2955
|
title: "Project tree",
|
|
2420
2956
|
rows: [
|
|
2421
2957
|
["\u2191 \u2193 / k j", "Move selection"],
|
|
2958
|
+
["\u2190", "Jump to parent (sub-agent \u2192 session, session \u2192 project)"],
|
|
2422
2959
|
["PgUp / Ctrl+B", "Page up"],
|
|
2423
2960
|
["PgDn / Ctrl+F", "Page down"],
|
|
2424
2961
|
["\u21B5", "Expand/collapse project, session, or summary"],
|
|
@@ -2568,6 +3105,7 @@ function useHotkeys({
|
|
|
2568
3105
|
onHelpScroll,
|
|
2569
3106
|
onHelpScrollToTop,
|
|
2570
3107
|
onToggleTracking,
|
|
3108
|
+
onJumpToParent,
|
|
2571
3109
|
filterLabel,
|
|
2572
3110
|
trackingOn = false
|
|
2573
3111
|
}) {
|
|
@@ -2687,6 +3225,10 @@ function useHotkeys({
|
|
|
2687
3225
|
onHide();
|
|
2688
3226
|
return;
|
|
2689
3227
|
}
|
|
3228
|
+
if (key.leftArrow && onJumpToParent) {
|
|
3229
|
+
onJumpToParent();
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
2690
3232
|
if (key.upArrow || input === "k") {
|
|
2691
3233
|
onScrollUp();
|
|
2692
3234
|
return;
|
|
@@ -2720,6 +3262,7 @@ function useHotkeys({
|
|
|
2720
3262
|
...trackingItems,
|
|
2721
3263
|
"Tab: viewer",
|
|
2722
3264
|
"\u2191\u2193/jk: select",
|
|
3265
|
+
"\u2190: parent",
|
|
2723
3266
|
"PgUp/Dn: page",
|
|
2724
3267
|
"\u21B5: expand",
|
|
2725
3268
|
"h: hide",
|
|
@@ -3197,7 +3740,8 @@ function appendSubAgentRows(result, session, expandedIds) {
|
|
|
3197
3740
|
const sessionExpandedKey = `__expanded-session-${session.id}`;
|
|
3198
3741
|
const sessionHidden = isCold ? !expandedIds.has(sessionExpandedKey) : expandedIds.has(sessionCollapsedKey);
|
|
3199
3742
|
if (sessionHidden) return;
|
|
3200
|
-
|
|
3743
|
+
const subAgentsFullyExpanded = expandedIds.has(session.id) || isCold && expandedIds.has(sessionExpandedKey);
|
|
3744
|
+
if (subAgentsFullyExpanded) {
|
|
3201
3745
|
result.push(...session.subAgents);
|
|
3202
3746
|
} else {
|
|
3203
3747
|
result.push(
|
|
@@ -3325,6 +3869,26 @@ function collectAllIds(tree) {
|
|
|
3325
3869
|
}
|
|
3326
3870
|
return ids;
|
|
3327
3871
|
}
|
|
3872
|
+
function findParentTarget(currentId, tree, flat) {
|
|
3873
|
+
if (currentId.startsWith("__sub-") && currentId.endsWith("__")) {
|
|
3874
|
+
return currentId.slice("__sub-".length, -"__".length);
|
|
3875
|
+
}
|
|
3876
|
+
const allProjects = [...tree.projects, ...tree.coldProjects];
|
|
3877
|
+
const allSessions = allProjects.flatMap((p) => p.sessions);
|
|
3878
|
+
for (const session of allSessions) {
|
|
3879
|
+
if (session.subAgents.some((sa) => sa.id === currentId)) {
|
|
3880
|
+
return session.id;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
for (const project of allProjects) {
|
|
3884
|
+
if (project.sessions.some((s) => s.id === currentId)) {
|
|
3885
|
+
return `__proj-${project.name}__`;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
const idx = flat.findIndex((row) => row.id === currentId);
|
|
3889
|
+
if (idx <= 0) return currentId;
|
|
3890
|
+
return flat[idx - 1]?.id ?? currentId;
|
|
3891
|
+
}
|
|
3328
3892
|
function initialSelectedId(tree) {
|
|
3329
3893
|
const firstProject = tree.projects[0];
|
|
3330
3894
|
if (firstProject) return `__proj-${firstProject.name}__`;
|
|
@@ -3499,7 +4063,7 @@ function App({
|
|
|
3499
4063
|
useEffect3(() => {
|
|
3500
4064
|
if (!isWatchMode) return;
|
|
3501
4065
|
const projectsDir = getProjectsDir();
|
|
3502
|
-
const usePolling = process.platform === "linux" || !
|
|
4066
|
+
const usePolling = process.platform === "linux" || !existsSync6(projectsDir);
|
|
3503
4067
|
if (usePolling) {
|
|
3504
4068
|
const timer = setInterval(
|
|
3505
4069
|
() => refreshRef.current(),
|
|
@@ -3593,6 +4157,13 @@ function App({
|
|
|
3593
4157
|
});
|
|
3594
4158
|
},
|
|
3595
4159
|
trackingOn: tracking,
|
|
4160
|
+
onJumpToParent: () => {
|
|
4161
|
+
if (focus !== "tree") return;
|
|
4162
|
+
stopTracking();
|
|
4163
|
+
if (!selectedId) return;
|
|
4164
|
+
const target = findParentTarget(selectedId, sessionTree, allFlat);
|
|
4165
|
+
if (target && target !== selectedId) setSelectedId(target);
|
|
4166
|
+
},
|
|
3596
4167
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3597
4168
|
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3598
4169
|
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
@@ -4034,15 +4605,16 @@ function installAltScreenCleanup() {
|
|
|
4034
4605
|
}
|
|
4035
4606
|
|
|
4036
4607
|
// src/utils/legacyConfig.ts
|
|
4037
|
-
import { join as
|
|
4608
|
+
import { join as join6, resolve } from "path";
|
|
4038
4609
|
function isLegacyProjectConfig(cwd, home) {
|
|
4039
|
-
const legacy = resolve(
|
|
4040
|
-
const global = resolve(
|
|
4610
|
+
const legacy = resolve(join6(cwd, ".agenthud", "config.yaml"));
|
|
4611
|
+
const global = resolve(join6(home, ".agenthud", "config.yaml"));
|
|
4041
4612
|
return legacy !== global;
|
|
4042
4613
|
}
|
|
4043
4614
|
|
|
4044
4615
|
// src/main.ts
|
|
4045
|
-
var
|
|
4616
|
+
var globalConfig = loadGlobalConfig();
|
|
4617
|
+
var options = parseArgs(process.argv.slice(2), globalConfig);
|
|
4046
4618
|
if (options.error) {
|
|
4047
4619
|
process.stderr.write(`agenthud: ${options.error}
|
|
4048
4620
|
`);
|
|
@@ -4056,8 +4628,8 @@ if (options.command === "version") {
|
|
|
4056
4628
|
console.log(getVersion());
|
|
4057
4629
|
process.exit(0);
|
|
4058
4630
|
}
|
|
4059
|
-
var legacyConfig =
|
|
4060
|
-
if (isLegacyProjectConfig(process.cwd(), homedir5()) &&
|
|
4631
|
+
var legacyConfig = join7(process.cwd(), ".agenthud", "config.yaml");
|
|
4632
|
+
if (isLegacyProjectConfig(process.cwd(), homedir5()) && existsSync7(legacyConfig)) {
|
|
4061
4633
|
console.log(
|
|
4062
4634
|
"The project-level config file (.agenthud/config.yaml) is no longer supported."
|
|
4063
4635
|
);
|
|
@@ -4083,8 +4655,16 @@ if (options.mode === "report") {
|
|
|
4083
4655
|
`);
|
|
4084
4656
|
process.exit(1);
|
|
4085
4657
|
}
|
|
4086
|
-
|
|
4087
|
-
|
|
4658
|
+
process.stderr.write(
|
|
4659
|
+
`${formatEffectiveOptionsLine("report", {
|
|
4660
|
+
include: options.reportInclude,
|
|
4661
|
+
detailLimit: options.reportDetailLimit,
|
|
4662
|
+
withGit: options.reportWithGit ?? false,
|
|
4663
|
+
format: options.reportFormat
|
|
4664
|
+
})}
|
|
4665
|
+
`
|
|
4666
|
+
);
|
|
4667
|
+
const tree = discoverSessions(globalConfig);
|
|
4088
4668
|
const flatSessions = [
|
|
4089
4669
|
...tree.projects.flatMap((p) => p.sessions),
|
|
4090
4670
|
...tree.coldProjects.flatMap((p) => p.sessions)
|
|
@@ -4106,6 +4686,23 @@ if (options.mode === "summary") {
|
|
|
4106
4686
|
`);
|
|
4107
4687
|
process.exit(1);
|
|
4108
4688
|
}
|
|
4689
|
+
process.stderr.write(
|
|
4690
|
+
`${formatEffectiveOptionsLine("summary", {
|
|
4691
|
+
include: options.summaryInclude,
|
|
4692
|
+
detailLimit: options.summaryDetailLimit,
|
|
4693
|
+
withGit: options.summaryWithGit ?? false,
|
|
4694
|
+
model: options.summaryModel
|
|
4695
|
+
})}
|
|
4696
|
+
`
|
|
4697
|
+
);
|
|
4698
|
+
const isRangeMode = !!(options.summaryFrom && options.summaryTo);
|
|
4699
|
+
process.stderr.write(
|
|
4700
|
+
`prompt = ${formatPromptSource(
|
|
4701
|
+
isRangeMode ? "range" : "daily",
|
|
4702
|
+
options.summaryPrompt
|
|
4703
|
+
)}
|
|
4704
|
+
`
|
|
4705
|
+
);
|
|
4109
4706
|
const today = /* @__PURE__ */ new Date();
|
|
4110
4707
|
if (options.summaryFrom && options.summaryTo) {
|
|
4111
4708
|
const exitCode2 = await runRangeSummary({
|
|
@@ -4114,7 +4711,12 @@ if (options.mode === "summary") {
|
|
|
4114
4711
|
today,
|
|
4115
4712
|
force: options.summaryForce ?? false,
|
|
4116
4713
|
assumeYes: options.summaryAssumeYes ?? false,
|
|
4117
|
-
model: options.summaryModel
|
|
4714
|
+
model: options.summaryModel,
|
|
4715
|
+
include: options.summaryInclude,
|
|
4716
|
+
detailLimit: options.summaryDetailLimit,
|
|
4717
|
+
withGit: options.summaryWithGit,
|
|
4718
|
+
open: options.summaryOpen,
|
|
4719
|
+
openIndex: options.summaryOpenIndex
|
|
4118
4720
|
});
|
|
4119
4721
|
process.exit(exitCode2);
|
|
4120
4722
|
}
|
|
@@ -4123,7 +4725,12 @@ if (options.mode === "summary") {
|
|
|
4123
4725
|
prompt: options.summaryPrompt,
|
|
4124
4726
|
force: options.summaryForce ?? false,
|
|
4125
4727
|
today,
|
|
4126
|
-
model: options.summaryModel
|
|
4728
|
+
model: options.summaryModel,
|
|
4729
|
+
include: options.summaryInclude,
|
|
4730
|
+
detailLimit: options.summaryDetailLimit,
|
|
4731
|
+
withGit: options.summaryWithGit,
|
|
4732
|
+
open: options.summaryOpen,
|
|
4733
|
+
openIndex: options.summaryOpenIndex
|
|
4127
4734
|
});
|
|
4128
4735
|
process.exit(exitCode);
|
|
4129
4736
|
}
|
|
@@ -4132,7 +4739,7 @@ if (options.scopeToCwd) {
|
|
|
4132
4739
|
const projectsDir = getProjectsDir();
|
|
4133
4740
|
let registered = [];
|
|
4134
4741
|
try {
|
|
4135
|
-
registered =
|
|
4742
|
+
registered = readdirSync3(projectsDir).map(decodeProjectPath);
|
|
4136
4743
|
} catch {
|
|
4137
4744
|
}
|
|
4138
4745
|
const safeReal = (p) => {
|
|
@@ -4153,13 +4760,11 @@ if (options.scopeToCwd) {
|
|
|
4153
4760
|
process.exit(1);
|
|
4154
4761
|
}
|
|
4155
4762
|
scopeToProject = match;
|
|
4156
|
-
process.stderr.write(`
|
|
4763
|
+
process.stderr.write(`scope = ${match}
|
|
4157
4764
|
`);
|
|
4158
4765
|
}
|
|
4159
4766
|
if (options.mode === "watch") {
|
|
4160
4767
|
installAltScreenCleanup();
|
|
4161
4768
|
enterAltScreen();
|
|
4162
|
-
} else {
|
|
4163
|
-
if (options.mode === "once") clearScreen();
|
|
4164
4769
|
}
|
|
4165
4770
|
render(React.createElement(App, { mode: options.mode, scopeToProject }));
|