@gobing-ai/knowledge-kit 0.0.12 → 0.0.14
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/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +120 -26
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22187 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
- package/plugins/generations/core-facts-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
- package/plugins/generations/daily-article-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
- package/plugins/generations/episode-plan-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +24862 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +286 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +23055 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +24399 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +32 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
- package/plugins/publishings/podcast-pub/plugin.json +8 -2
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* sp:daily-summary — Daily Summary Report Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates structured markdown summaries from:
|
|
6
|
+
* - Token usage data (via ccusage CLI)
|
|
7
|
+
* - Git history (commits, changes)
|
|
8
|
+
* - User annotations (learnings, issues, pending)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, mkdirSync, readlinkSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
14
|
+
import { logger } from './logger';
|
|
15
|
+
|
|
16
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface CliOptions {
|
|
19
|
+
date: string;
|
|
20
|
+
dryRun: boolean;
|
|
21
|
+
outputPath?: string;
|
|
22
|
+
skipGit: boolean;
|
|
23
|
+
skipCcusage: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CcusageData {
|
|
27
|
+
daily?: Array<{
|
|
28
|
+
date: string;
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
cacheCreationTokens: number;
|
|
32
|
+
cacheReadTokens: number;
|
|
33
|
+
totalTokens: number;
|
|
34
|
+
totalCost: number;
|
|
35
|
+
modelsUsed: string[];
|
|
36
|
+
}>;
|
|
37
|
+
totals: {
|
|
38
|
+
inputTokens: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
cacheCreationTokens: number;
|
|
41
|
+
cacheReadTokens: number;
|
|
42
|
+
totalTokens: number;
|
|
43
|
+
totalCost: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface GitCommit {
|
|
48
|
+
hash: string;
|
|
49
|
+
date: string;
|
|
50
|
+
message: string;
|
|
51
|
+
filesChanged: number;
|
|
52
|
+
insertions: number;
|
|
53
|
+
deletions: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UserAnnotations {
|
|
57
|
+
learnings: string;
|
|
58
|
+
issuesFixed: string;
|
|
59
|
+
pending: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface HistoryLoopFinding {
|
|
63
|
+
toolName: string;
|
|
64
|
+
argsDigest: string;
|
|
65
|
+
repeats: number;
|
|
66
|
+
sessionId: string;
|
|
67
|
+
fromSeq?: number;
|
|
68
|
+
toSeq?: number;
|
|
69
|
+
wastedTokens: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HistoryHealthSummary {
|
|
73
|
+
toolCalls: number;
|
|
74
|
+
toolErrors: number;
|
|
75
|
+
errorRatePct: number;
|
|
76
|
+
loops: HistoryLoopFinding[];
|
|
77
|
+
redundantCalls: number;
|
|
78
|
+
wastedTokens: number;
|
|
79
|
+
remediationProposals: Array<{
|
|
80
|
+
key: string;
|
|
81
|
+
title: string;
|
|
82
|
+
command: string;
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DailySummary {
|
|
87
|
+
date: string;
|
|
88
|
+
platforms: string[];
|
|
89
|
+
tokenUsage?: {
|
|
90
|
+
inputTokens: number;
|
|
91
|
+
outputTokens: number;
|
|
92
|
+
cacheTokens: number;
|
|
93
|
+
totalTokens: number;
|
|
94
|
+
costUsd: number;
|
|
95
|
+
};
|
|
96
|
+
gitActivity?: {
|
|
97
|
+
commitCount: number;
|
|
98
|
+
filesChanged: number;
|
|
99
|
+
insertions: number;
|
|
100
|
+
deletions: number;
|
|
101
|
+
};
|
|
102
|
+
commits: GitCommit[];
|
|
103
|
+
annotations: UserAnnotations;
|
|
104
|
+
/** Health metrics and loop findings from Spur history analytics. */
|
|
105
|
+
historyHealth?: HistoryHealthSummary;
|
|
106
|
+
/** Path to the newest history report artifact (R7), resolved from the
|
|
107
|
+
* `.spur/reports/history/latest.json` pointer. Omitted when no report exists. */
|
|
108
|
+
historyReportPath?: string;
|
|
109
|
+
generatedAt: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const DAILY_DIR = 'docs/daily';
|
|
115
|
+
const DEFAULT_DATE = 'today';
|
|
116
|
+
|
|
117
|
+
// ─── CLI Argument Parsing ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function parseArgs(argv: string[] = process.argv.slice(2)): CliOptions {
|
|
120
|
+
const args = argv;
|
|
121
|
+
const options: CliOptions = {
|
|
122
|
+
date: DEFAULT_DATE,
|
|
123
|
+
dryRun: false,
|
|
124
|
+
skipGit: false,
|
|
125
|
+
skipCcusage: false,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < args.length; i++) {
|
|
129
|
+
const arg = args[i];
|
|
130
|
+
if (arg === '--date' && i + 1 < args.length) {
|
|
131
|
+
options.date = args[++i];
|
|
132
|
+
} else if (arg === '--dry-run') {
|
|
133
|
+
options.dryRun = true;
|
|
134
|
+
} else if (arg === '--output' && i + 1 < args.length) {
|
|
135
|
+
options.outputPath = args[++i];
|
|
136
|
+
} else if (arg === '--no-git') {
|
|
137
|
+
options.skipGit = true;
|
|
138
|
+
} else if (arg === '--no-ccusage') {
|
|
139
|
+
options.skipCcusage = true;
|
|
140
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
141
|
+
printUsage();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resolve date
|
|
147
|
+
if (options.date === 'today') {
|
|
148
|
+
options.date = todayLocal();
|
|
149
|
+
} else if (options.date === 'yesterday') {
|
|
150
|
+
options.date = yesterdayLocal();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return options;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function printUsage(): void {
|
|
157
|
+
console.log(`
|
|
158
|
+
sp:daily-summary — Generate daily summary reports
|
|
159
|
+
|
|
160
|
+
Usage: daily-summary.ts [options]
|
|
161
|
+
|
|
162
|
+
Options:
|
|
163
|
+
--date YYYY-MM-DD Date for summary (default: today, also: yesterday)
|
|
164
|
+
--dry-run Show summary without writing file
|
|
165
|
+
--output <path> Write to custom path
|
|
166
|
+
--no-git Skip git history collection
|
|
167
|
+
--no-ccusage Skip token usage collection
|
|
168
|
+
--help, -h Show this help
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
daily-summary.ts # Today's summary
|
|
172
|
+
daily-summary.ts --date yesterday # Yesterday's summary
|
|
173
|
+
daily-summary.ts --dry-run # Preview without writing
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Date Helpers ─────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
// ─── Date Helpers ─────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/** Return today's date as YYYY-MM-DD in the system (git) timezone. */
|
|
182
|
+
export function todayLocal(): string {
|
|
183
|
+
// Spawn 'date' to get system timezone date, since JS runtime
|
|
184
|
+
// may have a different TZ (e.g. bun test forces UTC).
|
|
185
|
+
const proc = spawnSync('date', ['+%Y-%m-%d'], { encoding: 'utf8' });
|
|
186
|
+
return (proc.stdout ?? '').trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Return yesterday's date as YYYY-MM-DD in the system (git) timezone. */
|
|
190
|
+
export function yesterdayLocal(): string {
|
|
191
|
+
// Use portable epoch math via date command
|
|
192
|
+
const epochProc = spawnSync('date', ['+%s'], { encoding: 'utf8' });
|
|
193
|
+
const epoch = parseInt((epochProc.stdout ?? '').trim(), 10);
|
|
194
|
+
const yesterdayEpoch = epoch - 86400;
|
|
195
|
+
// Try BSD -r first, then GNU -d @
|
|
196
|
+
let proc = spawnSync('date', ['-r', String(yesterdayEpoch), '+%Y-%m-%d'], { encoding: 'utf8' });
|
|
197
|
+
if ((proc.status ?? 1) !== 0) {
|
|
198
|
+
proc = spawnSync('date', ['-d', `@${yesterdayEpoch}`, '+%Y-%m-%d'], { encoding: 'utf8' });
|
|
199
|
+
}
|
|
200
|
+
return (proc.stdout ?? '').trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getDateRange(dateStr: string): { start: string; end: string } {
|
|
204
|
+
// Date is YYYY-MM-DD format
|
|
205
|
+
const date = new Date(`${dateStr}T00:00:00`);
|
|
206
|
+
const start = `${dateStr} 00:00:00`;
|
|
207
|
+
const endDate = new Date(date);
|
|
208
|
+
endDate.setDate(endDate.getDate() + 1);
|
|
209
|
+
const end = `${endDate.toISOString().slice(0, 10)} 00:00:00`;
|
|
210
|
+
return { start, end };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Subprocess Spawner ──────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
export interface ProcessSpawnResult {
|
|
216
|
+
stdout: string;
|
|
217
|
+
stderr: string;
|
|
218
|
+
exitCode: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type ProcessSpawner = (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => Promise<ProcessSpawnResult>;
|
|
222
|
+
|
|
223
|
+
export const defaultProcessSpawner: ProcessSpawner = (cmd, args, env) => {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
try {
|
|
226
|
+
const proc = spawn(cmd, args, {
|
|
227
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
228
|
+
env: env ?? process.env,
|
|
229
|
+
});
|
|
230
|
+
const stdoutChunks: Buffer[] = [];
|
|
231
|
+
const stderrChunks: Buffer[] = [];
|
|
232
|
+
proc.stdout.on('data', (chunk) => stdoutChunks.push(Buffer.from(chunk)));
|
|
233
|
+
proc.stderr.on('data', (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
234
|
+
proc.on('close', (exitCode) => {
|
|
235
|
+
resolve({
|
|
236
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
237
|
+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
|
238
|
+
exitCode: exitCode ?? 0,
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
proc.on('error', (err) => {
|
|
242
|
+
reject(err);
|
|
243
|
+
});
|
|
244
|
+
} catch (err) {
|
|
245
|
+
reject(err);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let processSpawner: ProcessSpawner = defaultProcessSpawner;
|
|
251
|
+
|
|
252
|
+
export function setProcessSpawner(next?: ProcessSpawner): void {
|
|
253
|
+
processSpawner = next ?? defaultProcessSpawner;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Ccusage Integration ─────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
export async function getCcusageData(date: string): Promise<CcusageData | null> {
|
|
259
|
+
try {
|
|
260
|
+
// Check if ccusage is available
|
|
261
|
+
const env = { ...process.env };
|
|
262
|
+
const ccusageCheck = await processSpawner('ccusage', ['--version'], env);
|
|
263
|
+
if (ccusageCheck.exitCode !== 0) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Get daily data for the date
|
|
268
|
+
const since = `${date}T00:00:00`;
|
|
269
|
+
const until = `${date}T23:59:59`;
|
|
270
|
+
|
|
271
|
+
const proc = await processSpawner('ccusage', ['daily', '--since', since, '--until', until, '--json'], env);
|
|
272
|
+
|
|
273
|
+
if (proc.exitCode !== 0) {
|
|
274
|
+
logger.warn(`ccusage error: ${proc.stderr}`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const data = JSON.parse(proc.stdout) as CcusageData;
|
|
279
|
+
return data;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
logger.warn(`Failed to get ccusage data: ${error}`);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Spur History Health Integration ──────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
export async function getSpurHistoryHealth(
|
|
289
|
+
date: string,
|
|
290
|
+
dbPath = '.spur/spur.db',
|
|
291
|
+
): Promise<HistoryHealthSummary | null> {
|
|
292
|
+
try {
|
|
293
|
+
const resolvedPath = resolve(process.cwd(), dbPath);
|
|
294
|
+
if (!existsSync(resolvedPath)) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const { Database } = await import('bun:sqlite');
|
|
299
|
+
const db = new Database(resolvedPath, { readonly: true });
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
// 1. Query execution loop findings
|
|
303
|
+
const loopTable = db
|
|
304
|
+
.query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
305
|
+
.get('history_board_loop_findings');
|
|
306
|
+
|
|
307
|
+
let loops: HistoryLoopFinding[] = [];
|
|
308
|
+
if (loopTable) {
|
|
309
|
+
const rows = db
|
|
310
|
+
.query<
|
|
311
|
+
{
|
|
312
|
+
tool_name: string;
|
|
313
|
+
args_digest: string;
|
|
314
|
+
repeats: number;
|
|
315
|
+
session_id: string;
|
|
316
|
+
first_seq: number;
|
|
317
|
+
last_seq: number;
|
|
318
|
+
started_at: string | null;
|
|
319
|
+
},
|
|
320
|
+
[string]
|
|
321
|
+
>(
|
|
322
|
+
`SELECT tool_name, args_digest, repeats, session_id, first_seq, last_seq, started_at
|
|
323
|
+
FROM history_board_loop_findings
|
|
324
|
+
WHERE started_at IS NULL OR started_at LIKE ?
|
|
325
|
+
ORDER BY repeats DESC
|
|
326
|
+
LIMIT 20`,
|
|
327
|
+
)
|
|
328
|
+
.all(`${date}%`);
|
|
329
|
+
|
|
330
|
+
loops = rows.map((r) => ({
|
|
331
|
+
toolName: r.tool_name,
|
|
332
|
+
argsDigest: r.args_digest || 'repeated execution',
|
|
333
|
+
repeats: r.repeats,
|
|
334
|
+
sessionId: r.session_id,
|
|
335
|
+
fromSeq: r.first_seq,
|
|
336
|
+
toSeq: r.last_seq,
|
|
337
|
+
wastedTokens: r.repeats * 250,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2. Query tool calls and errors
|
|
342
|
+
let toolCalls = 0;
|
|
343
|
+
let toolErrors = 0;
|
|
344
|
+
const toolTable = db
|
|
345
|
+
.query<{ name: string }, [string]>("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
346
|
+
.get('history_board_tool_5m');
|
|
347
|
+
|
|
348
|
+
if (toolTable) {
|
|
349
|
+
const stats = db
|
|
350
|
+
.query<{ calls: number | null; errors: number | null }, [string]>(
|
|
351
|
+
`SELECT SUM(calls) AS calls, SUM(errors) AS errors
|
|
352
|
+
FROM history_board_tool_5m
|
|
353
|
+
WHERE bucket_start LIKE ?`,
|
|
354
|
+
)
|
|
355
|
+
.get(`${date}%`);
|
|
356
|
+
|
|
357
|
+
toolCalls = stats?.calls ?? 0;
|
|
358
|
+
toolErrors = stats?.errors ?? 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const redundantCalls = loops.reduce((acc, l) => acc + Math.max(0, l.repeats - 1), 0);
|
|
362
|
+
const wastedTokens = loops.reduce((acc, l) => acc + l.wastedTokens, 0);
|
|
363
|
+
const errorRatePct = toolCalls > 0 ? (toolErrors / toolCalls) * 100 : 0;
|
|
364
|
+
|
|
365
|
+
// 3. Generate auto-healing remediation proposals
|
|
366
|
+
const remediationProposals: Array<{ key: string; title: string; command: string }> = [];
|
|
367
|
+
|
|
368
|
+
for (const lp of loops.slice(0, 5)) {
|
|
369
|
+
const cleanTool = lp.toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
370
|
+
const key = `repetition:${cleanTool}:${lp.argsDigest.slice(0, 16)}`;
|
|
371
|
+
const title = `Break execution loop in ${lp.toolName} (${lp.repeats} repeats)`;
|
|
372
|
+
const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
|
|
373
|
+
remediationProposals.push({ key, title, command });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (toolCalls > 0 && errorRatePct > 10) {
|
|
377
|
+
const key = 'reliability:tooling:high-error-rate';
|
|
378
|
+
const title = `Investigate high tool error rate (${errorRatePct.toFixed(1)}%)`;
|
|
379
|
+
const command = `spur task create "<title>" --feature <id> && spur task update <wbs> --section Plan --from-file <path>`;
|
|
380
|
+
remediationProposals.push({ key, title, command });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
toolCalls,
|
|
385
|
+
toolErrors,
|
|
386
|
+
errorRatePct,
|
|
387
|
+
loops,
|
|
388
|
+
redundantCalls,
|
|
389
|
+
wastedTokens,
|
|
390
|
+
remediationProposals,
|
|
391
|
+
};
|
|
392
|
+
} finally {
|
|
393
|
+
db.close();
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── Git Integration ─────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
export async function getGitCommits(date: string): Promise<GitCommit[]> {
|
|
403
|
+
try {
|
|
404
|
+
const { start, end } = getDateRange(date);
|
|
405
|
+
|
|
406
|
+
const proc = await processSpawner('git', [
|
|
407
|
+
'log',
|
|
408
|
+
'--since',
|
|
409
|
+
start,
|
|
410
|
+
'--until',
|
|
411
|
+
end,
|
|
412
|
+
'--pretty=format:%H|%ad|%s',
|
|
413
|
+
'--date=iso',
|
|
414
|
+
'--numstat',
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
if (proc.exitCode !== 0) {
|
|
418
|
+
logger.warn('Failed to get git commits');
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const commits: GitCommit[] = [];
|
|
423
|
+
const lines = proc.stdout.trim().split('\n');
|
|
424
|
+
|
|
425
|
+
let currentCommit: Partial<GitCommit> | null = null;
|
|
426
|
+
|
|
427
|
+
for (const line of lines) {
|
|
428
|
+
if (!line.trim()) continue;
|
|
429
|
+
|
|
430
|
+
// Check if this is a commit line (contains | separator)
|
|
431
|
+
if (line.includes('|')) {
|
|
432
|
+
const parts = line.split('|');
|
|
433
|
+
if (parts.length >= 3) {
|
|
434
|
+
// Save previous commit if exists
|
|
435
|
+
if (currentCommit?.hash) {
|
|
436
|
+
commits.push(currentCommit as GitCommit);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
currentCommit = {
|
|
440
|
+
hash: parts[0],
|
|
441
|
+
date: parts[1],
|
|
442
|
+
message: parts[2],
|
|
443
|
+
filesChanged: 0,
|
|
444
|
+
insertions: 0,
|
|
445
|
+
deletions: 0,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
} else if (currentCommit && line.includes('\t')) {
|
|
449
|
+
// This is a numstat line (files changed)
|
|
450
|
+
const parts = line.split('\t');
|
|
451
|
+
if (parts.length >= 3) {
|
|
452
|
+
const insertions = parseInt(parts[0], 10) || 0;
|
|
453
|
+
const deletions = parseInt(parts[1], 10) || 0;
|
|
454
|
+
currentCommit.filesChanged = (currentCommit.filesChanged ?? 0) + 1;
|
|
455
|
+
currentCommit.insertions = (currentCommit.insertions ?? 0) + insertions;
|
|
456
|
+
currentCommit.deletions = (currentCommit.deletions ?? 0) + deletions;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Don't forget the last commit
|
|
462
|
+
if (currentCommit?.hash) {
|
|
463
|
+
commits.push(currentCommit as GitCommit);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return commits;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
logger.warn(`Failed to get git commits: ${error}`);
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ─── User Input ─────────────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
export async function promptUser(): Promise<UserAnnotations> {
|
|
476
|
+
if (process.env.SP_DAILY_SUMMARY_NO_PROMPT === '1') {
|
|
477
|
+
return { learnings: '', issuesFixed: '', pending: '' };
|
|
478
|
+
}
|
|
479
|
+
if (process.env.RD3_DAILY_SUMMARY_NO_PROMPT === '1') {
|
|
480
|
+
logger.warn('[deprecate] RD3_DAILY_SUMMARY_NO_PROMPT is deprecated; use SP_DAILY_SUMMARY_NO_PROMPT');
|
|
481
|
+
return { learnings: '', issuesFixed: '', pending: '' };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
console.log(`\n📊 Daily Summary — ${todayLocal()}`);
|
|
485
|
+
console.log('═'.repeat(50));
|
|
486
|
+
console.log('\nPlease provide the following (press Enter to skip):\n');
|
|
487
|
+
|
|
488
|
+
if (!process.stdin.isTTY) {
|
|
489
|
+
const chunks: Buffer[] = [];
|
|
490
|
+
for await (const chunk of process.stdin) {
|
|
491
|
+
chunks.push(chunk as Buffer);
|
|
492
|
+
}
|
|
493
|
+
const buffered = Buffer.concat(chunks).toString('utf-8');
|
|
494
|
+
const [learnings = '', issuesFixed = '', pending = ''] = buffered.split('\n');
|
|
495
|
+
return {
|
|
496
|
+
learnings: learnings.trim(),
|
|
497
|
+
issuesFixed: issuesFixed.trim(),
|
|
498
|
+
pending: pending.trim(),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const readline = await import('node:readline');
|
|
503
|
+
const rl = readline.createInterface({
|
|
504
|
+
input: process.stdin,
|
|
505
|
+
output: process.stdout,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const question = (prompt: string): Promise<string> =>
|
|
509
|
+
new Promise((resolve) => {
|
|
510
|
+
rl.question(prompt, (answer) => {
|
|
511
|
+
resolve(answer.trim());
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const learnings = await question('1. What did you learn today? (optional)\n > ');
|
|
516
|
+
const issuesFixed = await question('\n2. What issues did you fix? (optional)\n > ');
|
|
517
|
+
const pending = await question("\n3. What's pending for tomorrow? (optional)\n > ");
|
|
518
|
+
|
|
519
|
+
rl.close();
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
learnings,
|
|
523
|
+
issuesFixed,
|
|
524
|
+
pending,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ─── Markdown Generation ─────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
export function generateMarkdown(summary: DailySummary): string {
|
|
531
|
+
const lines: string[] = [];
|
|
532
|
+
|
|
533
|
+
// Header
|
|
534
|
+
lines.push(`# Daily Summary — ${summary.date}`);
|
|
535
|
+
lines.push('');
|
|
536
|
+
lines.push(`**Generated:** ${summary.generatedAt}`);
|
|
537
|
+
lines.push('');
|
|
538
|
+
|
|
539
|
+
// Meta section
|
|
540
|
+
lines.push('## Meta');
|
|
541
|
+
lines.push('');
|
|
542
|
+
lines.push(`- **Date:** ${summary.date}`);
|
|
543
|
+
lines.push(`- **Platforms:** ${summary.platforms.join(', ') || 'unknown'}`);
|
|
544
|
+
lines.push('');
|
|
545
|
+
|
|
546
|
+
// Token Usage
|
|
547
|
+
if (summary.tokenUsage) {
|
|
548
|
+
const tu = summary.tokenUsage;
|
|
549
|
+
lines.push('## Token Usage');
|
|
550
|
+
lines.push('');
|
|
551
|
+
lines.push(`| Metric | Value |`);
|
|
552
|
+
lines.push(`|--------|-------|`);
|
|
553
|
+
lines.push(`| Input Tokens | ${tu.inputTokens.toLocaleString()} |`);
|
|
554
|
+
lines.push(`| Output Tokens | ${tu.outputTokens.toLocaleString()} |`);
|
|
555
|
+
lines.push(`| Cache Tokens | ${tu.cacheTokens.toLocaleString()} |`);
|
|
556
|
+
lines.push(`| Total Tokens | ${tu.totalTokens.toLocaleString()} |`);
|
|
557
|
+
lines.push(`| Estimated Cost | $${tu.costUsd.toFixed(4)} |`);
|
|
558
|
+
lines.push('');
|
|
559
|
+
|
|
560
|
+
// Calculate cache hit rate
|
|
561
|
+
if (tu.inputTokens > 0) {
|
|
562
|
+
const cacheHitRate = (tu.cacheTokens / (tu.inputTokens + tu.cacheTokens)) * 100;
|
|
563
|
+
lines.push(`**Cache Hit Rate:** ${cacheHitRate.toFixed(1)}%`);
|
|
564
|
+
lines.push('');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Git Activity
|
|
569
|
+
if (summary.gitActivity) {
|
|
570
|
+
const ga = summary.gitActivity;
|
|
571
|
+
lines.push('## Git Activity');
|
|
572
|
+
lines.push('');
|
|
573
|
+
lines.push(`| Metric | Value |`);
|
|
574
|
+
lines.push(`|--------|-------|`);
|
|
575
|
+
lines.push(`| Commits | ${ga.commitCount} |`);
|
|
576
|
+
lines.push(`| Files Changed | ${ga.filesChanged} |`);
|
|
577
|
+
lines.push(`| Insertions | +${ga.insertions} |`);
|
|
578
|
+
lines.push(`| Deletions | -${ga.deletions} |`);
|
|
579
|
+
lines.push('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Commits
|
|
583
|
+
if (summary.commits.length > 0) {
|
|
584
|
+
lines.push('## Commits');
|
|
585
|
+
lines.push('');
|
|
586
|
+
for (const commit of summary.commits.slice(0, 10)) {
|
|
587
|
+
const shortHash = commit.hash.slice(0, 7);
|
|
588
|
+
lines.push(`- \`${shortHash}\` ${commit.message}`);
|
|
589
|
+
}
|
|
590
|
+
if (summary.commits.length > 10) {
|
|
591
|
+
lines.push(`- ... and ${summary.commits.length - 10} more commits`);
|
|
592
|
+
}
|
|
593
|
+
lines.push('');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Annotations
|
|
597
|
+
const { learnings, issuesFixed, pending } = summary.annotations;
|
|
598
|
+
|
|
599
|
+
if (learnings) {
|
|
600
|
+
lines.push('## Learnings');
|
|
601
|
+
lines.push('');
|
|
602
|
+
lines.push(learnings);
|
|
603
|
+
lines.push('');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (issuesFixed) {
|
|
607
|
+
lines.push('## Issues Fixed');
|
|
608
|
+
lines.push('');
|
|
609
|
+
lines.push(issuesFixed);
|
|
610
|
+
lines.push('');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (pending) {
|
|
614
|
+
lines.push('## Pending');
|
|
615
|
+
lines.push('');
|
|
616
|
+
lines.push(pending);
|
|
617
|
+
lines.push('');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Execution Loops & Health Findings
|
|
621
|
+
if (summary.historyHealth) {
|
|
622
|
+
const hh = summary.historyHealth;
|
|
623
|
+
lines.push('## Execution Loops & Health Findings');
|
|
624
|
+
lines.push('');
|
|
625
|
+
|
|
626
|
+
if (hh.loops.length === 0 && hh.toolCalls === 0) {
|
|
627
|
+
lines.push('- **Status:** ✅ Clean — No execution loops or tool calls recorded for this date.');
|
|
628
|
+
lines.push('');
|
|
629
|
+
} else {
|
|
630
|
+
lines.push('| Metric | Value |');
|
|
631
|
+
lines.push('|--------|-------|');
|
|
632
|
+
lines.push(`| Tool Invocations | ${hh.toolCalls.toLocaleString()} |`);
|
|
633
|
+
lines.push(`| Tool Errors | ${hh.toolErrors.toLocaleString()} (${hh.errorRatePct.toFixed(1)}%) |`);
|
|
634
|
+
lines.push(`| Detected Loops (Repeats ≥ 3) | ${hh.loops.length} |`);
|
|
635
|
+
lines.push(`| Redundant Invocations | ${hh.redundantCalls.toLocaleString()} |`);
|
|
636
|
+
lines.push(`| Estimated Wasted Tokens | ${hh.wastedTokens.toLocaleString()} |`);
|
|
637
|
+
lines.push('');
|
|
638
|
+
|
|
639
|
+
if (hh.loops.length > 0) {
|
|
640
|
+
lines.push('### Detected Execution Loops');
|
|
641
|
+
lines.push('');
|
|
642
|
+
for (const lp of hh.loops.slice(0, 10)) {
|
|
643
|
+
const seqInfo = lp.fromSeq && lp.toSeq ? ` (steps #${lp.fromSeq} → #${lp.toSeq})` : '';
|
|
644
|
+
const argsHint =
|
|
645
|
+
lp.argsDigest === '74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b'
|
|
646
|
+
? 'empty/unrecorded arguments'
|
|
647
|
+
: lp.argsDigest.length > 28
|
|
648
|
+
? `${lp.argsDigest.slice(0, 24)}...`
|
|
649
|
+
: lp.argsDigest;
|
|
650
|
+
lines.push(
|
|
651
|
+
`- \`${lp.toolName || 'unknown'}\` × **${lp.repeats} repeats** in session \`${lp.sessionId}\`${seqInfo}`,
|
|
652
|
+
);
|
|
653
|
+
lines.push(` - *Args hint:* \`${argsHint}\` (~${lp.wastedTokens.toLocaleString()} wasted tokens)`);
|
|
654
|
+
}
|
|
655
|
+
lines.push('');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (hh.remediationProposals.length > 0) {
|
|
659
|
+
lines.push('### Auto-Healing Remediation Proposals');
|
|
660
|
+
lines.push('');
|
|
661
|
+
lines.push('To remediate root causes and prevent recurring token waste, execute:');
|
|
662
|
+
lines.push('');
|
|
663
|
+
lines.push('```bash');
|
|
664
|
+
for (const prop of hh.remediationProposals) {
|
|
665
|
+
lines.push(`# ${prop.title} [${prop.key}]`);
|
|
666
|
+
lines.push(prop.command);
|
|
667
|
+
lines.push('');
|
|
668
|
+
}
|
|
669
|
+
lines.push('```');
|
|
670
|
+
lines.push('');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// History report path (R7 — surfaces the newest nightly-run artifact).
|
|
676
|
+
if (summary.historyReportPath) {
|
|
677
|
+
lines.push('## History Report');
|
|
678
|
+
lines.push('');
|
|
679
|
+
lines.push(`- **Newest artifact:** ${summary.historyReportPath}`);
|
|
680
|
+
lines.push('');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Footer
|
|
684
|
+
lines.push('---');
|
|
685
|
+
lines.push('');
|
|
686
|
+
lines.push(`*Generated by sp:daily-summary at ${summary.generatedAt}*`);
|
|
687
|
+
|
|
688
|
+
return lines.join('\n');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ─── File Output ─────────────────────────────────────────────────────────────
|
|
692
|
+
|
|
693
|
+
export function ensureDir(path: string): void {
|
|
694
|
+
if (!existsSync(path)) {
|
|
695
|
+
mkdirSync(path, { recursive: true });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function writeSummary(markdown: string, options: CliOptions): string {
|
|
700
|
+
const filename = `summary_${options.date.replace(/-/g, '')}.md`;
|
|
701
|
+
const outputPath = options.outputPath || join(DAILY_DIR, filename);
|
|
702
|
+
|
|
703
|
+
ensureDir(join(outputPath, '..'));
|
|
704
|
+
|
|
705
|
+
writeFileSync(outputPath, markdown, 'utf-8');
|
|
706
|
+
|
|
707
|
+
return outputPath;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Resolve the newest history report artifact path by following the
|
|
714
|
+
* `.spur/reports/history/latest.json` symlink (task 0471 R7). Returns
|
|
715
|
+
* `undefined` when no report exists — the daily summary simply omits the
|
|
716
|
+
* section rather than failing.
|
|
717
|
+
*/
|
|
718
|
+
function resolveHistoryReportPath(): string | undefined {
|
|
719
|
+
const pointer = resolve(process.cwd(), '.spur', 'reports', 'history', 'latest.json');
|
|
720
|
+
if (!existsSync(pointer)) {
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
const target = readlinkSync(pointer);
|
|
725
|
+
const resolved = isAbsolute(target) ? target : resolve(dirname(pointer), target);
|
|
726
|
+
return existsSync(resolved) ? resolved : undefined;
|
|
727
|
+
} catch {
|
|
728
|
+
// Not a symlink or unreadable — treat as absent.
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export async function buildDailySummary(options: CliOptions): Promise<DailySummary> {
|
|
734
|
+
const platforms: string[] = [];
|
|
735
|
+
|
|
736
|
+
// Get token usage from ccusage
|
|
737
|
+
let tokenUsage: DailySummary['tokenUsage'];
|
|
738
|
+
|
|
739
|
+
if (!options.skipCcusage) {
|
|
740
|
+
const ccusageData = await getCcusageData(options.date);
|
|
741
|
+
if (ccusageData?.totals) {
|
|
742
|
+
const totals = ccusageData.totals;
|
|
743
|
+
tokenUsage = {
|
|
744
|
+
inputTokens: totals.inputTokens,
|
|
745
|
+
outputTokens: totals.outputTokens,
|
|
746
|
+
cacheTokens: totals.cacheCreationTokens + totals.cacheReadTokens,
|
|
747
|
+
totalTokens: totals.totalTokens,
|
|
748
|
+
costUsd: totals.totalCost,
|
|
749
|
+
};
|
|
750
|
+
platforms.push('Claude Code');
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Get git history
|
|
755
|
+
let gitActivity: DailySummary['gitActivity'];
|
|
756
|
+
let commits: GitCommit[] = [];
|
|
757
|
+
|
|
758
|
+
if (!options.skipGit) {
|
|
759
|
+
commits = await getGitCommits(options.date);
|
|
760
|
+
if (commits.length > 0) {
|
|
761
|
+
gitActivity = commits.reduce(
|
|
762
|
+
(acc, commit) => ({
|
|
763
|
+
commitCount: acc.commitCount + 1,
|
|
764
|
+
filesChanged: acc.filesChanged + (commit.filesChanged || 0),
|
|
765
|
+
insertions: acc.insertions + (commit.insertions || 0),
|
|
766
|
+
deletions: acc.deletions + (commit.deletions || 0),
|
|
767
|
+
}),
|
|
768
|
+
{ commitCount: 0, filesChanged: 0, insertions: 0, deletions: 0 },
|
|
769
|
+
);
|
|
770
|
+
platforms.push('Git');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// R7 — surface the newest history report artifact path (if present).
|
|
775
|
+
const historyReportPath = resolveHistoryReportPath();
|
|
776
|
+
|
|
777
|
+
// Get user annotations
|
|
778
|
+
const annotations = await promptUser();
|
|
779
|
+
|
|
780
|
+
const result: DailySummary = {
|
|
781
|
+
date: options.date,
|
|
782
|
+
platforms,
|
|
783
|
+
commits,
|
|
784
|
+
annotations,
|
|
785
|
+
historyReportPath,
|
|
786
|
+
generatedAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
if (tokenUsage !== undefined) {
|
|
790
|
+
result.tokenUsage = tokenUsage;
|
|
791
|
+
}
|
|
792
|
+
if (gitActivity !== undefined) {
|
|
793
|
+
result.gitActivity = gitActivity;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Query Spur history health (loops, tool errors, and auto-healing proposals)
|
|
797
|
+
const historyHealth = await getSpurHistoryHealth(options.date);
|
|
798
|
+
if (historyHealth && (historyHealth.loops.length > 0 || historyHealth.toolCalls > 0)) {
|
|
799
|
+
result.historyHealth = historyHealth;
|
|
800
|
+
platforms.push('Spur History');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export async function main(): Promise<void> {
|
|
807
|
+
const options = parseArgs();
|
|
808
|
+
|
|
809
|
+
logger.info(`Generating daily summary for ${options.date}...`);
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
const summary = await buildDailySummary(options);
|
|
813
|
+
const markdown = generateMarkdown(summary);
|
|
814
|
+
|
|
815
|
+
if (options.dryRun) {
|
|
816
|
+
console.log(`\n${markdown}\n`);
|
|
817
|
+
logger.info('(dry-run) Summary not written to file');
|
|
818
|
+
} else {
|
|
819
|
+
const outputPath = writeSummary(markdown, options);
|
|
820
|
+
console.log(`\n${markdown}\n`);
|
|
821
|
+
console.log(`\n✅ Summary written to: ${outputPath}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Print summary stats
|
|
825
|
+
console.log('\n📊 Summary Statistics:');
|
|
826
|
+
console.log(` Date: ${summary.date}`);
|
|
827
|
+
console.log(` Platforms: ${summary.platforms.join(', ') || 'none'}`);
|
|
828
|
+
if (summary.tokenUsage) {
|
|
829
|
+
console.log(` Tokens: ${summary.tokenUsage.totalTokens.toLocaleString()}`);
|
|
830
|
+
console.log(` Cost: $${summary.tokenUsage.costUsd.toFixed(4)}`);
|
|
831
|
+
}
|
|
832
|
+
if (summary.gitActivity) {
|
|
833
|
+
console.log(` Commits: ${summary.gitActivity.commitCount}`);
|
|
834
|
+
}
|
|
835
|
+
} catch (error) {
|
|
836
|
+
logger.error(`Failed to generate summary: ${error}`);
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (import.meta.main) {
|
|
842
|
+
main().catch((error) => {
|
|
843
|
+
logger.error(`Daily summary failed: ${error}`);
|
|
844
|
+
process.exit(1);
|
|
845
|
+
});
|
|
846
|
+
}
|