runlist 0.0.0 → 0.94.2

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.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1112 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +296 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/public-inspection.mjs +19 -0
  136. package/src/query.mjs +977 -0
  137. package/src/record-action.mjs +11 -0
  138. package/src/record-create.mjs +82 -0
  139. package/src/reference-planner.mjs +388 -0
  140. package/src/reference-values.mjs +11 -0
  141. package/src/rename.mjs +107 -0
  142. package/src/render.mjs +678 -0
  143. package/src/roadmap.mjs +184 -0
  144. package/src/runlist.mjs +944 -0
  145. package/src/section.mjs +225 -0
  146. package/src/semantic-search-client.mjs +58 -0
  147. package/src/ship.mjs +166 -0
  148. package/src/show.mjs +101 -0
  149. package/src/skill-drift.mjs +98 -0
  150. package/src/source-editor.mjs +491 -0
  151. package/src/stages.mjs +73 -0
  152. package/src/state-migration.mjs +123 -0
  153. package/src/stats.mjs +200 -0
  154. package/src/status-metadata.mjs +87 -0
  155. package/src/statuses.mjs +721 -0
  156. package/src/summary.mjs +74 -0
  157. package/src/surfaces.mjs +28 -0
  158. package/src/sync-status.mjs +316 -0
  159. package/src/template-store.mjs +62 -0
  160. package/src/update.mjs +338 -0
  161. package/src/use.mjs +58 -0
  162. package/src/util.mjs +337 -0
  163. package/src/validate.mjs +795 -0
  164. package/src/watch.mjs +56 -0
  165. package/src/xref.mjs +406 -0
  166. package/src/yardstick-command.mjs +178 -0
  167. package/src/yardstick.mjs +112 -0
@@ -0,0 +1,222 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { bold, dim, green, yellow } from './color.mjs';
6
+ import {
7
+ CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_ID, claudeMarketplaceRefusalHint, installOpencodePlugin, opencodeDetected,
8
+ opencodeStatus, planClaudeInstall, removeOpencodePlugin,
9
+ } from './host-integration.mjs';
10
+ import { LEGACY_PLUGIN_ID, readInstalledPlugin, readInstalledPluginRecords } from './update.mjs';
11
+ import { executableName, which } from './util.mjs';
12
+ import { codexStatus, installCodexPlugin } from './codex-integration.mjs';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
16
+
17
+ const HOSTS = ['claude', 'codex', 'opencode'];
18
+ const ALIASES = { 'claude-code': 'claude', claudecode: 'claude' };
19
+
20
+ function flagValue(argv, name) {
21
+ const i = argv.indexOf(name);
22
+ return i >= 0 ? argv[i + 1] : undefined;
23
+ }
24
+
25
+ function hostStates() {
26
+ const plugin = readInstalledPlugin();
27
+ return {
28
+ claude: {
29
+ installed: Boolean(plugin),
30
+ version: plugin?.version ?? null,
31
+ id: plugin?.id ?? CLAUDE_PLUGIN_ID,
32
+ marketplace: plugin?.marketplace ?? null,
33
+ // false = record present, marketplace gone: Claude shows "failed to load".
34
+ marketplaceRegistered: plugin ? plugin.marketplaceRegistered : null,
35
+ },
36
+ opencode: { ...opencodeStatus({ version: pkg.version }), detected: opencodeDetected() },
37
+ codex: codexStatus({ version: pkg.version }),
38
+ };
39
+ }
40
+
41
+ function reportStatus(json) {
42
+ const states = hostStates();
43
+ if (json) {
44
+ process.stdout.write(JSON.stringify({ cli: pkg.version, hosts: states }, null, 2) + '\n');
45
+ return;
46
+ }
47
+ const { claude, codex, opencode } = states;
48
+ process.stdout.write(`${bold('runlist host integrations')} ${dim(`CLI ${pkg.version}`)}\n\n`);
49
+
50
+ const claudeBroken = claude.installed && claude.marketplaceRegistered === false;
51
+ const claudeState = !claude.installed ? yellow('not installed')
52
+ : claudeBroken ? yellow(`${claude.version ?? 'installed'} — marketplace "${claude.marketplace}" not registered, plugin fails to load`)
53
+ : green(claude.version ?? 'installed');
54
+ process.stdout.write(` claude ${claudeState}\n`);
55
+ process.stdout.write(` ${dim(claude.installed ? claude.id : 'plugin: SessionStart primer, PreToolUse guard, workflow skill')}\n`);
56
+
57
+ const codexState = codex.foreign ? yellow('unmanaged directory present')
58
+ : !codex.exists ? yellow('not installed')
59
+ : codex.stale ? yellow(`${codex.version} — behind CLI ${pkg.version}`)
60
+ : green(codex.version);
61
+ process.stdout.write(` codex ${codexState}\n`);
62
+ process.stdout.write(` ${dim(codex.plugin)}\n`);
63
+
64
+ const ocState = opencode.foreign ? yellow('unmanaged file present')
65
+ : !opencode.exists ? yellow('not installed')
66
+ : opencode.stale ? yellow(`${opencode.version} — behind CLI ${pkg.version}`)
67
+ : green(opencode.version);
68
+ process.stdout.write(` opencode ${ocState}\n`);
69
+ process.stdout.write(` ${dim(opencode.path)}\n`);
70
+
71
+ const todo = [];
72
+ if (!claude.installed || claudeBroken) todo.push('runlist install claude');
73
+ if (!codex.exists || codex.stale) todo.push('runlist install codex');
74
+ if (!opencode.exists || opencode.stale) todo.push('runlist install opencode');
75
+ if (todo.length) {
76
+ process.stdout.write('\n');
77
+ for (const cmd of todo) process.stdout.write(`Run ${bold(cmd)}\n`);
78
+ }
79
+ if (!opencode.exists) {
80
+ process.stdout.write(dim('\nWithout the OpenCode integration, every session in one OpenCode process\n'));
81
+ process.stdout.write(dim('shares one identity — so a session can release another session\'s plan —\n'));
82
+ process.stdout.write(dim('and no session-start primer runs.\n'));
83
+ }
84
+ }
85
+
86
+ function installCodex(argv, dryRun, json) {
87
+ if (argv.includes('--remove')) {
88
+ process.stderr.write('Codex removal is managed by `codex plugin remove`; the personal marketplace source is left available.\n');
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ const result = installCodexPlugin({ version: pkg.version, dryRun });
93
+ if (result.action === 'refused') {
94
+ if (json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
95
+ else process.stderr.write(`refused: ${result.reason}\n`);
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ if (!json) process.stdout.write(`${dryRun ? '[dry-run] ' : ''}${result.action} codex integration (${pkg.version}) at ${result.plugin}\n`);
100
+ if (dryRun) {
101
+ if (json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
102
+ return;
103
+ }
104
+ const selector = `runlist-codex@${result.marketplaceName}`;
105
+ if (!which('codex')) {
106
+ if (json) process.stdout.write(JSON.stringify({ ...result, pluginAdd: 'codex-cli-unavailable' }, null, 2) + '\n');
107
+ else process.stdout.write(`Install in Codex with: codex plugin add ${selector}\n`);
108
+ return;
109
+ }
110
+ const added = spawnSync(executableName('codex'), ['plugin', 'add', selector], {
111
+ stdio: json ? 'pipe' : 'inherit', encoding: json ? 'utf8' : undefined,
112
+ shell: process.platform === 'win32',
113
+ });
114
+ if (json) process.stdout.write(JSON.stringify({ ...result, pluginAdd: added.status === 0 ? 'installed' : 'failed', pluginAddExit: added.status }, null, 2) + '\n');
115
+ if (added.status !== 0) {
116
+ process.stderr.write(`Codex plugin source is ready; finish installation with: codex plugin add ${selector}\n`);
117
+ process.exitCode = 1;
118
+ } else if (!json) {
119
+ process.stdout.write('Start a new Codex thread and review/trust the plugin hooks to enable the primer and guard.\n');
120
+ }
121
+ }
122
+
123
+ function installClaude(argv, dryRun, json) {
124
+ const remove = argv.includes('--remove');
125
+ const steps = planClaudeInstall({ installed: readInstalledPlugin(), legacyInstalled: readInstalledPluginRecords({ id: LEGACY_PLUGIN_ID }), hasClaude: which('claude'), remove });
126
+ if (json) {
127
+ process.stdout.write(JSON.stringify({ host: 'claude', dryRun, steps }, null, 2) + '\n');
128
+ return;
129
+ }
130
+ let ran = false;
131
+ for (const step of steps) {
132
+ if (step.kind === 'skip') { process.stdout.write(`${dim('skip:')} ${step.reason}\n`); continue; }
133
+ if (step.kind === 'refusal') {
134
+ process.stdout.write(yellow(`${step.reason}\n`));
135
+ if (!dryRun) process.exitCode = 1;
136
+ continue;
137
+ }
138
+ if (step.kind === 'manual') {
139
+ process.stdout.write(`${yellow(step.reason ?? 'claude CLI not on PATH')} — run these from a Claude Code session:\n`);
140
+ for (const line of step.lines) process.stdout.write(` ${bold(line)}\n`);
141
+ continue;
142
+ }
143
+ if (step.reason) process.stdout.write(`${step.reason}\n`);
144
+ if (dryRun) { process.stdout.write(dim(`[dry-run] Would run: ${step.cmd.join(' ')}${step.cwd ? ` (in ${step.cwd})` : ''}\n`)); continue; }
145
+ process.stdout.write(dim(`$ ${step.cmd.join(' ')}${step.cwd ? ` (in ${step.cwd})` : ''}\n`));
146
+ const result = spawnSync(executableName(step.cmd[0]), step.cmd.slice(1), {
147
+ stdio: 'inherit', shell: process.platform === 'win32',
148
+ ...(step.cwd ? { cwd: step.cwd } : {}),
149
+ });
150
+ ran = true;
151
+ if (result.status !== 0) {
152
+ process.stdout.write(yellow(`(claude exited ${result.status ?? '?'})\n`));
153
+ // Each later step here depends on this one (the plugin resolves through
154
+ // the marketplace), so unlike `update` there is nothing independent left.
155
+ if (step.step === 'marketplace' || step.cmd.includes('marketplace')) {
156
+ for (const line of claudeMarketplaceRefusalHint(readInstalledPlugin()?.marketplace)) process.stdout.write(yellow(`${line}\n`));
157
+ }
158
+ process.exitCode = 1;
159
+ return;
160
+ }
161
+ }
162
+ if (ran && !remove && !process.exitCode) process.stdout.write(green('\n✓ restart Claude Code (or /reload-plugins) to apply.\n'));
163
+ }
164
+
165
+ function installOpencode(argv, dryRun, json) {
166
+ const force = argv.includes('--force');
167
+ const dir = flagValue(argv, '--path');
168
+ const result = argv.includes('--remove')
169
+ ? removeOpencodePlugin({ dryRun, force, dir })
170
+ : installOpencodePlugin({ version: pkg.version, dryRun, force, dir });
171
+
172
+ if (json) {
173
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
174
+ if (result.action === 'refused') process.exitCode = 1;
175
+ return result;
176
+ }
177
+
178
+ const prefix = dryRun ? dim('[dry-run] ') : '';
179
+ switch (result.action) {
180
+ case 'refused':
181
+ process.stderr.write(`${result.path}\n refused: ${result.reason}\n Pass --force to overwrite it.\n`);
182
+ process.exitCode = 1;
183
+ return result;
184
+ case 'current':
185
+ process.stdout.write(`${green('✓')} opencode integration already current (${result.version})\n${dim(result.path)}\n`);
186
+ return result;
187
+ case 'absent':
188
+ process.stdout.write(`${dim('nothing to remove:')} ${result.path}\n`);
189
+ return result;
190
+ case 'removed':
191
+ process.stdout.write(`${prefix}removed ${result.path}\n`);
192
+ process.stdout.write(dim('Restart OpenCode to apply. Sessions fall back to a process-scoped identity.\n'));
193
+ return result;
194
+ default:
195
+ process.stdout.write(`${prefix}${green('✓')} ${result.action} opencode integration (${pkg.version})\n${dim(result.path)}\n`);
196
+ if (dryRun) return result;
197
+ process.stdout.write('\nRestart OpenCode to apply. New sessions then get:\n');
198
+ process.stdout.write(` ${dim('·')} a per-session ownership identity (one session can no longer release another's plan)\n`);
199
+ process.stdout.write(` ${dim('·')} the ${bold('runlist hud')} primer at session start, like Claude Code's SessionStart hook\n`);
200
+ process.stdout.write(dim('\nPlans already in-session were claimed under the old process-scoped identity;\n'));
201
+ process.stdout.write(dim('close them before restarting, or reclaim with --force afterwards.\n'));
202
+ return result;
203
+ }
204
+ }
205
+
206
+ export function runInstall(argv, _config, opts = {}) {
207
+ const json = argv.includes('--json');
208
+ const requested = argv.find(a => !a.startsWith('-'));
209
+
210
+ if (!requested) { reportStatus(json); return; }
211
+ const host = ALIASES[requested] ?? requested;
212
+ if (!HOSTS.includes(host)) {
213
+ process.stderr.write(`Unknown host "${requested}". Known: ${HOSTS.join(', ')}\n`);
214
+ process.exitCode = 1;
215
+ return;
216
+ }
217
+
218
+ const dryRun = Boolean(opts.dryRun);
219
+ if (host === 'claude') { installClaude(argv, dryRun, json); return; }
220
+ if (host === 'codex') { installCodex(argv, dryRun, json); return; }
221
+ installOpencode(argv, dryRun, json);
222
+ }
@@ -0,0 +1,116 @@
1
+ import { existsSync } from 'node:fs';
2
+ import {
3
+ readJournalEntries,
4
+ journalFilePath,
5
+ isJournalEnabled,
6
+ } from './journal.mjs';
7
+ import { dim, green, red } from './color.mjs';
8
+
9
+ function parseArgs(argv) {
10
+ const opts = { tail: null, errorsOnly: false, sessionFilter: null, since: null, byCommand: false, helpTopics: false, asJson: false };
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const a = argv[i];
13
+ if (a === '--tail') {
14
+ const n = parseInt(argv[++i], 10);
15
+ opts.tail = Number.isFinite(n) && n > 0 ? n : 20;
16
+ } else if (a === '--errors') opts.errorsOnly = true;
17
+ else if (a === '--session' && argv[i + 1]) opts.sessionFilter = argv[++i];
18
+ else if (a === '--since' && argv[i + 1]) opts.since = argv[++i];
19
+ else if (a === '--by-command') opts.byCommand = true;
20
+ else if (a === '--help-topics') opts.helpTopics = true;
21
+ else if (a === '--json') opts.asJson = true;
22
+ }
23
+ // Default to last 20 when no view flag is given.
24
+ if (opts.tail === null && !opts.errorsOnly && !opts.sessionFilter
25
+ && !opts.since && !opts.byCommand && !opts.helpTopics) {
26
+ opts.tail = 20;
27
+ }
28
+ return opts;
29
+ }
30
+
31
+ export function runJournal(argv, config) {
32
+ const file = journalFilePath(config);
33
+ const retained = readJournalEntries(config);
34
+ if (!retained.length && !existsSync(file)) {
35
+ if (!isJournalEnabled(config)) {
36
+ process.stderr.write(
37
+ 'Journal is opt-in. Enable with `RUNLIST_JOURNAL=1` (env) or `journal: true` (in runlist.config.mjs).\n',
38
+ );
39
+ return;
40
+ }
41
+ process.stderr.write(`No journal entries yet at ${file}.\n`);
42
+ return;
43
+ }
44
+
45
+ const opts = parseArgs(argv);
46
+ let entries = retained;
47
+ if (opts.errorsOnly) entries = entries.filter(e => e.exit !== 0);
48
+ if (opts.sessionFilter) entries = entries.filter(e => e.sid === opts.sessionFilter);
49
+ if (opts.since) entries = entries.filter(e => typeof e.ts === 'string' && e.ts >= opts.since);
50
+
51
+ if (opts.helpTopics) {
52
+ if (opts.tail) entries = entries.slice(-opts.tail);
53
+ const help = entries.filter(e => e.helpTopic);
54
+ const topics = [...new Set(help.map(e => e.helpTopic))].sort().map(topic => {
55
+ const rows = help.filter(e => e.helpTopic === topic);
56
+ return { topic, total: rows.length, sessions: new Set(rows.map(e => e.sid).filter(Boolean)).size, errors: rows.filter(e => e.exit !== 0).length };
57
+ });
58
+ const summary = {
59
+ totalInvocations: entries.length,
60
+ observedInvocations: entries.filter(e => e.outcome != null).length,
61
+ legacyInvocations: entries.filter(e => e.outcome == null).length,
62
+ helpInvocations: help.length,
63
+ sessions: new Set(entries.map(e => e.sid).filter(Boolean)).size,
64
+ versions: [...new Set(entries.map(e => e.v).filter(Boolean))].sort(),
65
+ first: entries[0]?.ts ?? null, last: entries.at(-1)?.ts ?? null,
66
+ coverage: 'Retained opt-in invocations; passive context, HUD, and dry runs are excluded. Legacy entries may predate help capture.',
67
+ topics,
68
+ };
69
+ if (opts.asJson) process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
70
+ else {
71
+ process.stdout.write(`${summary.helpInvocations} help calls / ${summary.totalInvocations} retained invocations; ${summary.sessions} sessions.\n${summary.coverage}\n`);
72
+ for (const row of topics) process.stdout.write(`${row.topic.padEnd(20)} ${row.total} help calls, ${row.sessions} sessions, ${row.errors} errors\n`);
73
+ }
74
+ return;
75
+ }
76
+
77
+ if (opts.byCommand) {
78
+ const groups = new Map();
79
+ for (const e of entries) {
80
+ const cmd = (e.argv && e.argv[0]) || '(none)';
81
+ if (!groups.has(cmd)) groups.set(cmd, []);
82
+ groups.get(cmd).push(e);
83
+ }
84
+ const rows = [...groups.entries()].map(([cmd, list]) => {
85
+ const total = list.length;
86
+ const errors = list.filter(e => e.exit !== 0).length;
87
+ const times = list.map(e => e.ms ?? 0).sort((a, b) => a - b);
88
+ const median = times.length ? times[Math.floor(times.length / 2)] : 0;
89
+ return { cmd, total, errors, median };
90
+ }).sort((a, b) => b.total - a.total);
91
+
92
+ if (opts.asJson) {
93
+ process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
94
+ return;
95
+ }
96
+ for (const r of rows) {
97
+ const errPart = r.errors > 0 ? ` ${red(`${r.errors} err`)}` : '';
98
+ process.stdout.write(`${r.cmd.padEnd(20)} ${String(r.total).padStart(4)}× median ${r.median}ms${errPart}\n`);
99
+ }
100
+ return;
101
+ }
102
+
103
+ if (opts.tail) entries = entries.slice(-opts.tail);
104
+
105
+ if (opts.asJson) {
106
+ process.stdout.write(JSON.stringify(entries, null, 2) + '\n');
107
+ return;
108
+ }
109
+
110
+ for (const e of entries) {
111
+ const argvStr = Array.isArray(e.argv) ? e.argv.join(' ') : '';
112
+ const exitPart = e.exit === 0 ? green('ok') : red(`exit ${e.exit}`);
113
+ const errPart = e.err ? ` ${dim(`(${e.err})`)}` : '';
114
+ process.stdout.write(`[${e.ts}] ${argvStr} (${exitPart}, ${e.ms ?? '?'}ms)${errPart}\n`);
115
+ }
116
+ }