agenthud 0.11.4 → 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-WAT4DY4T.js → main-CRTF5EHN.js} +910 -300
- 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,7 +426,7 @@ 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
|
}
|
|
@@ -138,11 +452,24 @@ function parseLocalMidnight(dateStr) {
|
|
|
138
452
|
if (Number.isNaN(date.getTime())) return null;
|
|
139
453
|
return date;
|
|
140
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
|
+
}
|
|
141
468
|
function todayLocalMidnight() {
|
|
142
469
|
const now = /* @__PURE__ */ new Date();
|
|
143
470
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
144
471
|
}
|
|
145
|
-
function parseArgs(args) {
|
|
472
|
+
function parseArgs(args, config) {
|
|
146
473
|
if (args[0] === "watch") args = args.slice(1);
|
|
147
474
|
if (args.includes("--help") || args.includes("-h")) {
|
|
148
475
|
return { mode: "watch", command: "help" };
|
|
@@ -156,7 +483,7 @@ function parseArgs(args) {
|
|
|
156
483
|
if (args[0] === "report") {
|
|
157
484
|
const rest = args.slice(1);
|
|
158
485
|
let reportDate = todayLocalMidnight();
|
|
159
|
-
let reportInclude =
|
|
486
|
+
let reportInclude = config?.report.include ?? DEFAULT_INCLUDE_TYPES;
|
|
160
487
|
let reportError;
|
|
161
488
|
const FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
162
489
|
"--date",
|
|
@@ -204,7 +531,7 @@ function parseArgs(args) {
|
|
|
204
531
|
}
|
|
205
532
|
}
|
|
206
533
|
}
|
|
207
|
-
let reportFormat = "markdown";
|
|
534
|
+
let reportFormat = config?.report.format ?? "markdown";
|
|
208
535
|
const formatIdx = rest.indexOf("--format");
|
|
209
536
|
if (formatIdx !== -1) {
|
|
210
537
|
const fmt = rest[formatIdx + 1];
|
|
@@ -216,7 +543,7 @@ function parseArgs(args) {
|
|
|
216
543
|
reportError = "Invalid format: missing value for --format.";
|
|
217
544
|
}
|
|
218
545
|
}
|
|
219
|
-
let reportDetailLimit;
|
|
546
|
+
let reportDetailLimit = config?.report.detailLimit;
|
|
220
547
|
const detailLimitIdx = rest.indexOf("--detail-limit");
|
|
221
548
|
if (detailLimitIdx !== -1) {
|
|
222
549
|
const val = rest[detailLimitIdx + 1];
|
|
@@ -227,7 +554,7 @@ function parseArgs(args) {
|
|
|
227
554
|
reportDetailLimit = n;
|
|
228
555
|
}
|
|
229
556
|
}
|
|
230
|
-
const reportWithGit = rest.includes("--with-git");
|
|
557
|
+
const reportWithGit = rest.includes("--with-git") || (config?.report.withGit ?? false);
|
|
231
558
|
return {
|
|
232
559
|
mode: "report",
|
|
233
560
|
reportDate,
|
|
@@ -254,7 +581,10 @@ function parseArgs(args) {
|
|
|
254
581
|
"--from",
|
|
255
582
|
"--to",
|
|
256
583
|
"--prompt",
|
|
257
|
-
"--model"
|
|
584
|
+
"--model",
|
|
585
|
+
"--include",
|
|
586
|
+
"--format",
|
|
587
|
+
"--detail-limit"
|
|
258
588
|
]);
|
|
259
589
|
for (let i = 0; i < rest.length; i++) {
|
|
260
590
|
const arg = rest[i];
|
|
@@ -357,234 +687,86 @@ function parseArgs(args) {
|
|
|
357
687
|
}
|
|
358
688
|
if (rest.includes("--force")) summaryForce = true;
|
|
359
689
|
if (rest.includes("-y") || rest.includes("--yes")) summaryAssumeYes = true;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (arg.startsWith("-") && !KNOWN_WATCH_FLAGS.has(arg)) {
|
|
380
|
-
return {
|
|
381
|
-
mode: "watch",
|
|
382
|
-
error: `Unknown option: "${arg}". Run agenthud --help for usage.`
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return args.includes("--cwd") ? { mode: "watch", scopeToCwd: true } : { mode: "watch" };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// src/config/globalConfig.ts
|
|
390
|
-
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
391
|
-
import { homedir } from "os";
|
|
392
|
-
import { join as join2 } from "path";
|
|
393
|
-
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
394
|
-
var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
|
|
395
|
-
var STATE_PATH = join2(homedir(), ".agenthud", "state.yaml");
|
|
396
|
-
var DEFAULT_GLOBAL_CONFIG = {
|
|
397
|
-
refreshIntervalMs: 2e3,
|
|
398
|
-
hiddenSessions: [],
|
|
399
|
-
hiddenSubAgents: [],
|
|
400
|
-
// [] means "show all"; conversation preset bundles assistant + user;
|
|
401
|
-
// commits-only preset filters down to git activity.
|
|
402
|
-
filterPresets: [[], ["response", "user"], ["commit"]],
|
|
403
|
-
hiddenProjects: []
|
|
404
|
-
};
|
|
405
|
-
var ALL_PRESET_KEYWORDS = /* @__PURE__ */ new Set(["all", "*", "any"]);
|
|
406
|
-
function normalizePreset(tokens) {
|
|
407
|
-
if (tokens.some((t) => ALL_PRESET_KEYWORDS.has(t.toLowerCase()))) return [];
|
|
408
|
-
return tokens;
|
|
409
|
-
}
|
|
410
|
-
function parseInterval(value) {
|
|
411
|
-
const match = value.match(/^(\d+)(s|m)$/);
|
|
412
|
-
if (!match) return null;
|
|
413
|
-
const n = parseInt(match[1], 10);
|
|
414
|
-
return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
|
|
415
|
-
}
|
|
416
|
-
function ensureAgenthudDir() {
|
|
417
|
-
const dir = join2(homedir(), ".agenthud");
|
|
418
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
419
|
-
}
|
|
420
|
-
function writeDefaultConfig() {
|
|
421
|
-
ensureAgenthudDir();
|
|
422
|
-
const defaultYaml = `# AgentHUD user settings.
|
|
423
|
-
# App-managed state (hidden sessions/projects) lives in state.yaml.
|
|
424
|
-
|
|
425
|
-
# How often to poll for activity updates
|
|
426
|
-
refreshInterval: 2s
|
|
427
|
-
|
|
428
|
-
# Activity filter presets (cycle with 'f' key in viewer)
|
|
429
|
-
# Each list is one preset. Use "all" (or "*") to show everything.
|
|
430
|
-
# Types: response, user, bash, edit, thinking, read, glob, commit
|
|
431
|
-
filterPresets:
|
|
432
|
-
- ["all"]
|
|
433
|
-
- ["response", "user"]
|
|
434
|
-
- ["commit"]
|
|
435
|
-
`;
|
|
436
|
-
try {
|
|
437
|
-
writeFileSync(CONFIG_PATH, defaultYaml, "utf-8");
|
|
438
|
-
} catch {
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
function writeState(state) {
|
|
442
|
-
ensureAgenthudDir();
|
|
443
|
-
try {
|
|
444
|
-
writeFileSync(STATE_PATH, stringifyYaml(state), "utf-8");
|
|
445
|
-
} catch {
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
function rewriteConfigWithoutHideFields(raw) {
|
|
449
|
-
const cleaned = {};
|
|
450
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
451
|
-
if (k === "hiddenSessions" || k === "hiddenSubAgents" || k === "hiddenProjects")
|
|
452
|
-
continue;
|
|
453
|
-
cleaned[k] = v;
|
|
454
|
-
}
|
|
455
|
-
try {
|
|
456
|
-
writeFileSync(CONFIG_PATH, stringifyYaml(cleaned), "utf-8");
|
|
457
|
-
} catch {
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
function loadGlobalConfig() {
|
|
461
|
-
const config = { ...DEFAULT_GLOBAL_CONFIG };
|
|
462
|
-
let configRaw = {};
|
|
463
|
-
let configHadHideFields = false;
|
|
464
|
-
if (existsSync(CONFIG_PATH)) {
|
|
465
|
-
try {
|
|
466
|
-
const text = readFileSync2(CONFIG_PATH, "utf-8");
|
|
467
|
-
configRaw = parseYaml(text) ?? {};
|
|
468
|
-
} catch {
|
|
469
|
-
configRaw = {};
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
writeDefaultConfig();
|
|
473
|
-
}
|
|
474
|
-
if (typeof configRaw.refreshInterval === "string") {
|
|
475
|
-
const ms = parseInterval(configRaw.refreshInterval);
|
|
476
|
-
if (ms !== null) config.refreshIntervalMs = ms;
|
|
477
|
-
}
|
|
478
|
-
if (Array.isArray(configRaw.filterPresets)) {
|
|
479
|
-
const presets = configRaw.filterPresets.filter(Array.isArray).map((p) => {
|
|
480
|
-
const tokens = p.filter(
|
|
481
|
-
(t) => typeof t === "string"
|
|
482
|
-
);
|
|
483
|
-
return normalizePreset(tokens);
|
|
484
|
-
});
|
|
485
|
-
if (presets.length > 0) config.filterPresets = presets;
|
|
486
|
-
}
|
|
487
|
-
const legacyHidden = {};
|
|
488
|
-
for (const key of [
|
|
489
|
-
"hiddenSessions",
|
|
490
|
-
"hiddenSubAgents",
|
|
491
|
-
"hiddenProjects"
|
|
492
|
-
]) {
|
|
493
|
-
if (Array.isArray(configRaw[key])) {
|
|
494
|
-
configHadHideFields = true;
|
|
495
|
-
legacyHidden[key] = configRaw[key].filter(
|
|
496
|
-
(s) => typeof s === "string"
|
|
497
|
-
);
|
|
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
|
+
}
|
|
498
709
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
+
}
|
|
507
721
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
+
}
|
|
518
732
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
525
753
|
};
|
|
526
|
-
writeState(merged);
|
|
527
|
-
rewriteConfigWithoutHideFields(configRaw);
|
|
528
|
-
config.hiddenSessions = merged.hiddenSessions;
|
|
529
|
-
config.hiddenSubAgents = merged.hiddenSubAgents;
|
|
530
|
-
config.hiddenProjects = merged.hiddenProjects;
|
|
531
754
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
hiddenSubAgents: [],
|
|
538
|
-
hiddenProjects: []
|
|
539
|
-
};
|
|
540
|
-
if (existsSync(STATE_PATH)) {
|
|
541
|
-
try {
|
|
542
|
-
const text = readFileSync2(STATE_PATH, "utf-8");
|
|
543
|
-
const raw = parseYaml(text) ?? {};
|
|
544
|
-
for (const key of [
|
|
545
|
-
"hiddenSessions",
|
|
546
|
-
"hiddenSubAgents",
|
|
547
|
-
"hiddenProjects"
|
|
548
|
-
]) {
|
|
549
|
-
if (Array.isArray(raw[key])) {
|
|
550
|
-
state[key] = raw[key].filter(
|
|
551
|
-
(s) => typeof s === "string"
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
} catch {
|
|
556
|
-
}
|
|
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
|
+
};
|
|
557
760
|
}
|
|
558
|
-
for (const
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
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
|
+
};
|
|
565
767
|
}
|
|
566
768
|
}
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
function hideSession(id) {
|
|
570
|
-
const config = loadGlobalConfig();
|
|
571
|
-
if (config.hiddenSessions.includes(id)) return;
|
|
572
|
-
updateState({ hiddenSessions: [...config.hiddenSessions, id] });
|
|
573
|
-
}
|
|
574
|
-
function hideSubAgent(id) {
|
|
575
|
-
const config = loadGlobalConfig();
|
|
576
|
-
if (config.hiddenSubAgents.includes(id)) return;
|
|
577
|
-
updateState({ hiddenSubAgents: [...config.hiddenSubAgents, id] });
|
|
578
|
-
}
|
|
579
|
-
function hideProject(name) {
|
|
580
|
-
const config = loadGlobalConfig();
|
|
581
|
-
if (config.hiddenProjects.includes(name)) return;
|
|
582
|
-
updateState({ hiddenProjects: [...config.hiddenProjects, name] });
|
|
583
|
-
}
|
|
584
|
-
function hasProjectLevelConfig() {
|
|
585
|
-
const candidate = join2(process.cwd(), ".agenthud", "config.yaml");
|
|
586
|
-
if (candidate === join2(homedir(), ".agenthud", "config.yaml")) return false;
|
|
587
|
-
return existsSync(candidate);
|
|
769
|
+
return args.includes("--cwd") ? { mode: "watch", scopeToCwd: true } : { mode: "watch" };
|
|
588
770
|
}
|
|
589
771
|
|
|
590
772
|
// src/data/reportGenerator.ts
|
|
@@ -1481,56 +1663,365 @@ function discoverSessions(config, options2) {
|
|
|
1481
1663
|
}
|
|
1482
1664
|
|
|
1483
1665
|
// src/data/summaryRunner.ts
|
|
1484
|
-
import { spawn } from "child_process";
|
|
1666
|
+
import { spawn as spawn2 } from "child_process";
|
|
1485
1667
|
import {
|
|
1486
1668
|
copyFileSync,
|
|
1487
1669
|
createWriteStream,
|
|
1488
|
-
existsSync as
|
|
1670
|
+
existsSync as existsSync5,
|
|
1489
1671
|
mkdirSync as mkdirSync2,
|
|
1490
|
-
readFileSync as
|
|
1672
|
+
readFileSync as readFileSync6,
|
|
1491
1673
|
unlinkSync
|
|
1492
1674
|
} from "fs";
|
|
1493
1675
|
import { homedir as homedir3 } from "os";
|
|
1494
|
-
import { dirname as dirname2, join as
|
|
1676
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
1495
1677
|
import { createInterface } from "readline";
|
|
1496
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
|
|
1497
1979
|
function agenthudHomeDir() {
|
|
1498
|
-
const dir =
|
|
1499
|
-
if (!
|
|
1980
|
+
const dir = join5(homedir3(), ".agenthud");
|
|
1981
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
1500
1982
|
return dir;
|
|
1501
1983
|
}
|
|
1502
1984
|
function summariesDir() {
|
|
1503
|
-
const dir =
|
|
1504
|
-
if (!
|
|
1985
|
+
const dir = join5(agenthudHomeDir(), "summaries");
|
|
1986
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
1505
1987
|
return dir;
|
|
1506
1988
|
}
|
|
1507
1989
|
function promptFilename(kind) {
|
|
1508
1990
|
return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
|
|
1509
1991
|
}
|
|
1510
1992
|
function userPromptPath(kind) {
|
|
1511
|
-
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, "/");
|
|
1512
2003
|
}
|
|
1513
2004
|
function templatePath(kind) {
|
|
1514
2005
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1515
|
-
return
|
|
2006
|
+
return join5(here, "templates", promptFilename(kind));
|
|
1516
2007
|
}
|
|
1517
2008
|
function dateKey(d) {
|
|
1518
2009
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1519
2010
|
}
|
|
1520
2011
|
function dailyCachePath(date) {
|
|
1521
|
-
return
|
|
2012
|
+
return join5(summariesDir(), `${dateKey(date)}.md`);
|
|
1522
2013
|
}
|
|
1523
2014
|
function rangeCachePath(from, to) {
|
|
1524
|
-
return
|
|
2015
|
+
return join5(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
|
|
1525
2016
|
}
|
|
1526
2017
|
function isSameLocalDay2(a, b) {
|
|
1527
2018
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
1528
2019
|
}
|
|
1529
2020
|
function ensureUserPromptFile(kind) {
|
|
1530
2021
|
const p = userPromptPath(kind);
|
|
1531
|
-
if (
|
|
2022
|
+
if (existsSync5(p)) return;
|
|
1532
2023
|
const dir = dirname2(p);
|
|
1533
|
-
if (!
|
|
2024
|
+
if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
|
|
1534
2025
|
try {
|
|
1535
2026
|
copyFileSync(templatePath(kind), p);
|
|
1536
2027
|
} catch {
|
|
@@ -1539,14 +2030,14 @@ function ensureUserPromptFile(kind) {
|
|
|
1539
2030
|
function resolvePrompt(kind, override) {
|
|
1540
2031
|
if (override) return override;
|
|
1541
2032
|
const p = userPromptPath(kind);
|
|
1542
|
-
if (
|
|
2033
|
+
if (existsSync5(p)) {
|
|
1543
2034
|
try {
|
|
1544
|
-
return
|
|
2035
|
+
return readFileSync6(p, "utf-8");
|
|
1545
2036
|
} catch {
|
|
1546
2037
|
}
|
|
1547
2038
|
}
|
|
1548
2039
|
try {
|
|
1549
|
-
return
|
|
2040
|
+
return readFileSync6(templatePath(kind), "utf-8");
|
|
1550
2041
|
} catch {
|
|
1551
2042
|
return "Summarize the input below.";
|
|
1552
2043
|
}
|
|
@@ -1601,7 +2092,7 @@ function spawnClaude(opts) {
|
|
|
1601
2092
|
];
|
|
1602
2093
|
if (opts.model) args.push("--model", opts.model);
|
|
1603
2094
|
args.push(opts.prompt);
|
|
1604
|
-
const proc =
|
|
2095
|
+
const proc = spawn2(
|
|
1605
2096
|
"claude",
|
|
1606
2097
|
args,
|
|
1607
2098
|
{
|
|
@@ -1722,11 +2213,11 @@ async function generateDailySummary(opts) {
|
|
|
1722
2213
|
const isToday = isSameLocalDay2(opts.date, opts.today);
|
|
1723
2214
|
const cached = dailyCachePath(opts.date);
|
|
1724
2215
|
const dateLabel = dateKey(opts.date);
|
|
1725
|
-
if (!isToday && !opts.force &&
|
|
2216
|
+
if (!isToday && !opts.force && existsSync5(cached)) {
|
|
1726
2217
|
try {
|
|
1727
|
-
const content =
|
|
2218
|
+
const content = readFileSync6(cached, "utf-8");
|
|
1728
2219
|
if (opts.announce) {
|
|
1729
|
-
process.stderr.write(`
|
|
2220
|
+
process.stderr.write(`cached summary from ${cached}
|
|
1730
2221
|
`);
|
|
1731
2222
|
}
|
|
1732
2223
|
if (opts.streamToStdout) {
|
|
@@ -1744,7 +2235,7 @@ async function generateDailySummary(opts) {
|
|
|
1744
2235
|
}
|
|
1745
2236
|
}
|
|
1746
2237
|
if (opts.announce) {
|
|
1747
|
-
process.stderr.write(`
|
|
2238
|
+
process.stderr.write(`scanning sessions for ${dateLabel}...
|
|
1748
2239
|
`);
|
|
1749
2240
|
}
|
|
1750
2241
|
const config = loadGlobalConfig();
|
|
@@ -1755,10 +2246,13 @@ async function generateDailySummary(opts) {
|
|
|
1755
2246
|
];
|
|
1756
2247
|
const reportMarkdown = generateReport(flatSessions, {
|
|
1757
2248
|
date: opts.date,
|
|
1758
|
-
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.
|
|
1759
2253
|
format: "markdown",
|
|
1760
|
-
detailLimit:
|
|
1761
|
-
withGit:
|
|
2254
|
+
detailLimit: opts.detailLimit,
|
|
2255
|
+
withGit: opts.withGit
|
|
1762
2256
|
});
|
|
1763
2257
|
const reportBytes = Buffer.byteLength(reportMarkdown, "utf-8");
|
|
1764
2258
|
const estimatedTokens = Math.ceil(reportBytes / 4);
|
|
@@ -1773,7 +2267,7 @@ async function generateDailySummary(opts) {
|
|
|
1773
2267
|
).length;
|
|
1774
2268
|
const sizeKb = (reportBytes / 1024).toFixed(1);
|
|
1775
2269
|
process.stderr.write(
|
|
1776
|
-
`
|
|
2270
|
+
`input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB \u2248 ${estimatedTokens.toLocaleString()} tokens)
|
|
1777
2271
|
`
|
|
1778
2272
|
);
|
|
1779
2273
|
}
|
|
@@ -1812,13 +2306,15 @@ async function generateDailySummary(opts) {
|
|
|
1812
2306
|
}
|
|
1813
2307
|
}
|
|
1814
2308
|
}
|
|
1815
|
-
|
|
2309
|
+
const showTicker = opts.announce && !opts.streamToStdout;
|
|
2310
|
+
if (opts.announce && !showTicker) {
|
|
1816
2311
|
process.stderr.write(
|
|
1817
|
-
`
|
|
2312
|
+
`sending to claude (this may take a minute)...
|
|
1818
2313
|
|
|
1819
2314
|
`
|
|
1820
2315
|
);
|
|
1821
2316
|
}
|
|
2317
|
+
const stopTicker = showTicker ? startStderrTicker("sending to claude") : null;
|
|
1822
2318
|
const prompt = resolvePrompt("daily", opts.promptOverride);
|
|
1823
2319
|
const result = await spawnClaude({
|
|
1824
2320
|
prompt,
|
|
@@ -1827,12 +2323,13 @@ async function generateDailySummary(opts) {
|
|
|
1827
2323
|
streamToStdout: opts.streamToStdout,
|
|
1828
2324
|
model: opts.model
|
|
1829
2325
|
});
|
|
2326
|
+
if (stopTicker) stopTicker();
|
|
1830
2327
|
if (opts.announce && result.code === 0) {
|
|
1831
2328
|
process.stderr.write("\n");
|
|
1832
|
-
process.stderr.write(`
|
|
2329
|
+
process.stderr.write(`saved to ${cached}
|
|
1833
2330
|
`);
|
|
1834
2331
|
if (result.usage) {
|
|
1835
|
-
process.stderr.write(
|
|
2332
|
+
process.stderr.write(`${formatUsage(result.usage)}
|
|
1836
2333
|
`);
|
|
1837
2334
|
}
|
|
1838
2335
|
}
|
|
@@ -1850,10 +2347,25 @@ async function runSummary(options2) {
|
|
|
1850
2347
|
today: options2.today,
|
|
1851
2348
|
force: options2.force,
|
|
1852
2349
|
promptOverride: options2.prompt,
|
|
1853
|
-
streamToStdout:
|
|
2350
|
+
streamToStdout: !options2.open,
|
|
1854
2351
|
announce: true,
|
|
1855
|
-
model: options2.model
|
|
2352
|
+
model: options2.model,
|
|
2353
|
+
include: options2.include,
|
|
2354
|
+
detailLimit: options2.detailLimit,
|
|
2355
|
+
withGit: options2.withGit
|
|
1856
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
|
+
}
|
|
1857
2369
|
return res.code;
|
|
1858
2370
|
}
|
|
1859
2371
|
async function runRangeSummary(options2) {
|
|
@@ -1867,16 +2379,26 @@ async function runRangeSummary(options2) {
|
|
|
1867
2379
|
options2.force,
|
|
1868
2380
|
dates,
|
|
1869
2381
|
options2.today,
|
|
1870
|
-
|
|
2382
|
+
existsSync5(rangeCache)
|
|
1871
2383
|
)) {
|
|
1872
2384
|
try {
|
|
1873
|
-
const content =
|
|
2385
|
+
const content = readFileSync6(rangeCache, "utf-8");
|
|
1874
2386
|
process.stderr.write(
|
|
1875
|
-
`
|
|
2387
|
+
`cached range summary from ${rangeCache}
|
|
1876
2388
|
`
|
|
1877
2389
|
);
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
+
}
|
|
1880
2402
|
return 0;
|
|
1881
2403
|
} catch {
|
|
1882
2404
|
}
|
|
@@ -1885,15 +2407,15 @@ async function runRangeSummary(options2) {
|
|
|
1885
2407
|
let missingCount = 0;
|
|
1886
2408
|
for (const d of dates) {
|
|
1887
2409
|
const isToday = isSameLocalDay2(d, options2.today);
|
|
1888
|
-
if (!isToday &&
|
|
2410
|
+
if (!isToday && existsSync5(dailyCachePath(d))) cachedCount++;
|
|
1889
2411
|
else missingCount++;
|
|
1890
2412
|
}
|
|
1891
2413
|
process.stderr.write(
|
|
1892
|
-
`
|
|
2414
|
+
`range ${fromLabel} \u2192 ${toLabel} (${dates.length} days)
|
|
1893
2415
|
`
|
|
1894
2416
|
);
|
|
1895
2417
|
process.stderr.write(
|
|
1896
|
-
|
|
2418
|
+
`${cachedCount} cached, ${missingCount} to generate
|
|
1897
2419
|
`
|
|
1898
2420
|
);
|
|
1899
2421
|
const dailyMarkdowns = [];
|
|
@@ -1902,9 +2424,9 @@ async function runRangeSummary(options2) {
|
|
|
1902
2424
|
const label = dateKey(d);
|
|
1903
2425
|
const isToday = isSameLocalDay2(d, options2.today);
|
|
1904
2426
|
process.stderr.write(`
|
|
1905
|
-
|
|
2427
|
+
--- ${label} ---
|
|
1906
2428
|
`);
|
|
1907
|
-
const willPrompt = !options2.assumeYes && (isToday || !
|
|
2429
|
+
const willPrompt = !options2.assumeYes && (isToday || !existsSync5(dailyCachePath(d)));
|
|
1908
2430
|
const confirmer = willPrompt ? async () => {
|
|
1909
2431
|
const hint = isToday ? " (today \u2014 regenerated every time)" : "";
|
|
1910
2432
|
return ask(`Generate this summary${hint}? [Y/n] `, true);
|
|
@@ -1917,7 +2439,10 @@ agenthud: --- ${label} ---
|
|
|
1917
2439
|
announce: true,
|
|
1918
2440
|
confirmBeforeSpawn: confirmer,
|
|
1919
2441
|
assumeYes: options2.assumeYes,
|
|
1920
|
-
model: options2.model
|
|
2442
|
+
model: options2.model,
|
|
2443
|
+
include: options2.include,
|
|
2444
|
+
detailLimit: options2.detailLimit,
|
|
2445
|
+
withGit: options2.withGit
|
|
1921
2446
|
});
|
|
1922
2447
|
if (res.skipped) {
|
|
1923
2448
|
process.stderr.write(`agenthud: ${label} \u2014 skipped by user.
|
|
@@ -1934,7 +2459,7 @@ agenthud: --- ${label} ---
|
|
|
1934
2459
|
}
|
|
1935
2460
|
const text = res.markdown.trim();
|
|
1936
2461
|
if (text.length === 0 || /^no activity found/i.test(text)) {
|
|
1937
|
-
process.stderr.write(
|
|
2462
|
+
process.stderr.write(`${label} has no activity \u2014 skipping.
|
|
1938
2463
|
`);
|
|
1939
2464
|
continue;
|
|
1940
2465
|
}
|
|
@@ -1949,37 +2474,51 @@ agenthud: --- ${label} ---
|
|
|
1949
2474
|
${markdown}`).join("\n\n---\n\n");
|
|
1950
2475
|
process.stderr.write(
|
|
1951
2476
|
`
|
|
1952
|
-
|
|
2477
|
+
combining ${dailyMarkdowns.length} daily summaries into range summary...
|
|
1953
2478
|
`
|
|
1954
2479
|
);
|
|
1955
|
-
|
|
1956
|
-
|
|
2480
|
+
const metaStreams = !options2.open;
|
|
2481
|
+
if (!metaStreams) {
|
|
2482
|
+
} else {
|
|
2483
|
+
process.stderr.write(
|
|
2484
|
+
`sending to claude (this may take a minute)...
|
|
1957
2485
|
|
|
1958
2486
|
`
|
|
1959
|
-
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
const stopMetaTicker = metaStreams ? null : startStderrTicker("sending to claude");
|
|
1960
2490
|
const metaPrompt = resolvePrompt("range");
|
|
1961
2491
|
const metaResult = await spawnClaude({
|
|
1962
2492
|
prompt: metaPrompt,
|
|
1963
2493
|
stdin: metaInput,
|
|
1964
2494
|
cachePath: rangeCache,
|
|
1965
|
-
streamToStdout:
|
|
2495
|
+
streamToStdout: metaStreams,
|
|
1966
2496
|
model: options2.model
|
|
1967
2497
|
});
|
|
2498
|
+
if (stopMetaTicker) stopMetaTicker();
|
|
1968
2499
|
if (metaResult.code !== 0) {
|
|
1969
2500
|
return metaResult.code;
|
|
1970
2501
|
}
|
|
1971
2502
|
process.stderr.write("\n");
|
|
1972
|
-
process.stderr.write(`
|
|
2503
|
+
process.stderr.write(`saved to ${rangeCache}
|
|
1973
2504
|
`);
|
|
1974
2505
|
if (metaResult.usage) {
|
|
1975
|
-
process.stderr.write(
|
|
2506
|
+
process.stderr.write(`${formatUsage(metaResult.usage)}
|
|
1976
2507
|
`);
|
|
1977
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
|
+
}
|
|
1978
2517
|
return 0;
|
|
1979
2518
|
}
|
|
1980
2519
|
|
|
1981
2520
|
// src/ui/App.tsx
|
|
1982
|
-
import { existsSync as
|
|
2521
|
+
import { existsSync as existsSync6, watch } from "fs";
|
|
1983
2522
|
import { basename as basename3 } from "path";
|
|
1984
2523
|
import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
|
|
1985
2524
|
import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
@@ -2416,6 +2955,7 @@ var SECTIONS = [
|
|
|
2416
2955
|
title: "Project tree",
|
|
2417
2956
|
rows: [
|
|
2418
2957
|
["\u2191 \u2193 / k j", "Move selection"],
|
|
2958
|
+
["\u2190", "Jump to parent (sub-agent \u2192 session, session \u2192 project)"],
|
|
2419
2959
|
["PgUp / Ctrl+B", "Page up"],
|
|
2420
2960
|
["PgDn / Ctrl+F", "Page down"],
|
|
2421
2961
|
["\u21B5", "Expand/collapse project, session, or summary"],
|
|
@@ -2565,6 +3105,7 @@ function useHotkeys({
|
|
|
2565
3105
|
onHelpScroll,
|
|
2566
3106
|
onHelpScrollToTop,
|
|
2567
3107
|
onToggleTracking,
|
|
3108
|
+
onJumpToParent,
|
|
2568
3109
|
filterLabel,
|
|
2569
3110
|
trackingOn = false
|
|
2570
3111
|
}) {
|
|
@@ -2684,6 +3225,10 @@ function useHotkeys({
|
|
|
2684
3225
|
onHide();
|
|
2685
3226
|
return;
|
|
2686
3227
|
}
|
|
3228
|
+
if (key.leftArrow && onJumpToParent) {
|
|
3229
|
+
onJumpToParent();
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
2687
3232
|
if (key.upArrow || input === "k") {
|
|
2688
3233
|
onScrollUp();
|
|
2689
3234
|
return;
|
|
@@ -2717,6 +3262,7 @@ function useHotkeys({
|
|
|
2717
3262
|
...trackingItems,
|
|
2718
3263
|
"Tab: viewer",
|
|
2719
3264
|
"\u2191\u2193/jk: select",
|
|
3265
|
+
"\u2190: parent",
|
|
2720
3266
|
"PgUp/Dn: page",
|
|
2721
3267
|
"\u21B5: expand",
|
|
2722
3268
|
"h: hide",
|
|
@@ -3194,7 +3740,8 @@ function appendSubAgentRows(result, session, expandedIds) {
|
|
|
3194
3740
|
const sessionExpandedKey = `__expanded-session-${session.id}`;
|
|
3195
3741
|
const sessionHidden = isCold ? !expandedIds.has(sessionExpandedKey) : expandedIds.has(sessionCollapsedKey);
|
|
3196
3742
|
if (sessionHidden) return;
|
|
3197
|
-
|
|
3743
|
+
const subAgentsFullyExpanded = expandedIds.has(session.id) || isCold && expandedIds.has(sessionExpandedKey);
|
|
3744
|
+
if (subAgentsFullyExpanded) {
|
|
3198
3745
|
result.push(...session.subAgents);
|
|
3199
3746
|
} else {
|
|
3200
3747
|
result.push(
|
|
@@ -3322,6 +3869,26 @@ function collectAllIds(tree) {
|
|
|
3322
3869
|
}
|
|
3323
3870
|
return ids;
|
|
3324
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
|
+
}
|
|
3325
3892
|
function initialSelectedId(tree) {
|
|
3326
3893
|
const firstProject = tree.projects[0];
|
|
3327
3894
|
if (firstProject) return `__proj-${firstProject.name}__`;
|
|
@@ -3496,7 +4063,7 @@ function App({
|
|
|
3496
4063
|
useEffect3(() => {
|
|
3497
4064
|
if (!isWatchMode) return;
|
|
3498
4065
|
const projectsDir = getProjectsDir();
|
|
3499
|
-
const usePolling = process.platform === "linux" || !
|
|
4066
|
+
const usePolling = process.platform === "linux" || !existsSync6(projectsDir);
|
|
3500
4067
|
if (usePolling) {
|
|
3501
4068
|
const timer = setInterval(
|
|
3502
4069
|
() => refreshRef.current(),
|
|
@@ -3590,6 +4157,13 @@ function App({
|
|
|
3590
4157
|
});
|
|
3591
4158
|
},
|
|
3592
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
|
+
},
|
|
3593
4167
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3594
4168
|
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3595
4169
|
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
@@ -4031,15 +4605,16 @@ function installAltScreenCleanup() {
|
|
|
4031
4605
|
}
|
|
4032
4606
|
|
|
4033
4607
|
// src/utils/legacyConfig.ts
|
|
4034
|
-
import { join as
|
|
4608
|
+
import { join as join6, resolve } from "path";
|
|
4035
4609
|
function isLegacyProjectConfig(cwd, home) {
|
|
4036
|
-
const legacy = resolve(
|
|
4037
|
-
const global = resolve(
|
|
4610
|
+
const legacy = resolve(join6(cwd, ".agenthud", "config.yaml"));
|
|
4611
|
+
const global = resolve(join6(home, ".agenthud", "config.yaml"));
|
|
4038
4612
|
return legacy !== global;
|
|
4039
4613
|
}
|
|
4040
4614
|
|
|
4041
4615
|
// src/main.ts
|
|
4042
|
-
var
|
|
4616
|
+
var globalConfig = loadGlobalConfig();
|
|
4617
|
+
var options = parseArgs(process.argv.slice(2), globalConfig);
|
|
4043
4618
|
if (options.error) {
|
|
4044
4619
|
process.stderr.write(`agenthud: ${options.error}
|
|
4045
4620
|
`);
|
|
@@ -4053,8 +4628,8 @@ if (options.command === "version") {
|
|
|
4053
4628
|
console.log(getVersion());
|
|
4054
4629
|
process.exit(0);
|
|
4055
4630
|
}
|
|
4056
|
-
var legacyConfig =
|
|
4057
|
-
if (isLegacyProjectConfig(process.cwd(), homedir5()) &&
|
|
4631
|
+
var legacyConfig = join7(process.cwd(), ".agenthud", "config.yaml");
|
|
4632
|
+
if (isLegacyProjectConfig(process.cwd(), homedir5()) && existsSync7(legacyConfig)) {
|
|
4058
4633
|
console.log(
|
|
4059
4634
|
"The project-level config file (.agenthud/config.yaml) is no longer supported."
|
|
4060
4635
|
);
|
|
@@ -4080,8 +4655,16 @@ if (options.mode === "report") {
|
|
|
4080
4655
|
`);
|
|
4081
4656
|
process.exit(1);
|
|
4082
4657
|
}
|
|
4083
|
-
|
|
4084
|
-
|
|
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);
|
|
4085
4668
|
const flatSessions = [
|
|
4086
4669
|
...tree.projects.flatMap((p) => p.sessions),
|
|
4087
4670
|
...tree.coldProjects.flatMap((p) => p.sessions)
|
|
@@ -4103,6 +4686,23 @@ if (options.mode === "summary") {
|
|
|
4103
4686
|
`);
|
|
4104
4687
|
process.exit(1);
|
|
4105
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
|
+
);
|
|
4106
4706
|
const today = /* @__PURE__ */ new Date();
|
|
4107
4707
|
if (options.summaryFrom && options.summaryTo) {
|
|
4108
4708
|
const exitCode2 = await runRangeSummary({
|
|
@@ -4111,7 +4711,12 @@ if (options.mode === "summary") {
|
|
|
4111
4711
|
today,
|
|
4112
4712
|
force: options.summaryForce ?? false,
|
|
4113
4713
|
assumeYes: options.summaryAssumeYes ?? false,
|
|
4114
|
-
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
|
|
4115
4720
|
});
|
|
4116
4721
|
process.exit(exitCode2);
|
|
4117
4722
|
}
|
|
@@ -4120,7 +4725,12 @@ if (options.mode === "summary") {
|
|
|
4120
4725
|
prompt: options.summaryPrompt,
|
|
4121
4726
|
force: options.summaryForce ?? false,
|
|
4122
4727
|
today,
|
|
4123
|
-
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
|
|
4124
4734
|
});
|
|
4125
4735
|
process.exit(exitCode);
|
|
4126
4736
|
}
|
|
@@ -4129,7 +4739,7 @@ if (options.scopeToCwd) {
|
|
|
4129
4739
|
const projectsDir = getProjectsDir();
|
|
4130
4740
|
let registered = [];
|
|
4131
4741
|
try {
|
|
4132
|
-
registered =
|
|
4742
|
+
registered = readdirSync3(projectsDir).map(decodeProjectPath);
|
|
4133
4743
|
} catch {
|
|
4134
4744
|
}
|
|
4135
4745
|
const safeReal = (p) => {
|
|
@@ -4150,7 +4760,7 @@ if (options.scopeToCwd) {
|
|
|
4150
4760
|
process.exit(1);
|
|
4151
4761
|
}
|
|
4152
4762
|
scopeToProject = match;
|
|
4153
|
-
process.stderr.write(`
|
|
4763
|
+
process.stderr.write(`scope = ${match}
|
|
4154
4764
|
`);
|
|
4155
4765
|
}
|
|
4156
4766
|
if (options.mode === "watch") {
|