whydid 1.0.0
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 +232 -0
- package/dashboard/dist/android-chrome-192x192.png +0 -0
- package/dashboard/dist/android-chrome-512x512.png +0 -0
- package/dashboard/dist/apple-touch-icon.png +0 -0
- package/dashboard/dist/assets/index-DImKqodm.js +69 -0
- package/dashboard/dist/assets/index-mHXgqqkj.css +1 -0
- package/dashboard/dist/favicon-16x16.png +0 -0
- package/dashboard/dist/favicon-32x32.png +0 -0
- package/dashboard/dist/favicon.ico +0 -0
- package/dashboard/dist/index.html +24 -0
- package/dashboard/dist/logo-mark.svg +8 -0
- package/dashboard/dist/site.webmanifest +11 -0
- package/dist/adapters/claude-code.js +168 -0
- package/dist/adapters/hook-events.js +86 -0
- package/dist/adapters/sync-claude-code.js +85 -0
- package/dist/cli/ai-budget-warning.js +19 -0
- package/dist/cli/commands/agent-event.js +80 -0
- package/dist/cli/commands/checkpoint.js +36 -0
- package/dist/cli/commands/context.js +27 -0
- package/dist/cli/commands/daemon.js +139 -0
- package/dist/cli/commands/export.js +36 -0
- package/dist/cli/commands/init.js +63 -0
- package/dist/cli/commands/key.js +65 -0
- package/dist/cli/commands/license.js +57 -0
- package/dist/cli/commands/milestone.js +64 -0
- package/dist/cli/commands/prompt.js +75 -0
- package/dist/cli/commands/recap.js +48 -0
- package/dist/cli/commands/restore-fork.js +101 -0
- package/dist/cli/commands/status.js +26 -0
- package/dist/cli/commands/test.js +33 -0
- package/dist/cli/commands/timeline.js +23 -0
- package/dist/cli/commands/why.js +71 -0
- package/dist/cli/context.js +48 -0
- package/dist/cli/index.js +77 -0
- package/dist/cli/license-gate.js +26 -0
- package/dist/cli/preflight.js +28 -0
- package/dist/core/agent-context.js +46 -0
- package/dist/core/agent-hooks.js +68 -0
- package/dist/core/ai-budget.js +27 -0
- package/dist/core/chat-retrieval.js +36 -0
- package/dist/core/context-tree.js +135 -0
- package/dist/core/diff.js +30 -0
- package/dist/core/engine.js +321 -0
- package/dist/core/export.js +98 -0
- package/dist/core/file-history.js +31 -0
- package/dist/core/manifest.js +102 -0
- package/dist/core/recap.js +49 -0
- package/dist/core/test-runner.js +23 -0
- package/dist/daemon/listen.js +26 -0
- package/dist/daemon/pidfile.js +36 -0
- package/dist/daemon/server.js +554 -0
- package/dist/db/database.js +631 -0
- package/dist/db/schema.js +186 -0
- package/dist/git/git.js +22 -0
- package/dist/licensing/config.js +28 -0
- package/dist/licensing/license-client.js +45 -0
- package/dist/licensing/license-manager.js +89 -0
- package/dist/licensing/license-payload.js +12 -0
- package/dist/licensing/license-store.js +96 -0
- package/dist/licensing/machine-id.js +78 -0
- package/dist/licensing/secret-store.js +102 -0
- package/dist/licensing/verify-license.js +20 -0
- package/dist/llm/agent-prompt.js +36 -0
- package/dist/llm/annotate-checkpoint.js +67 -0
- package/dist/llm/annotate.js +11 -0
- package/dist/llm/anthropic-provider.js +97 -0
- package/dist/llm/chat.js +35 -0
- package/dist/llm/community-pricing.js +75 -0
- package/dist/llm/factory.js +17 -0
- package/dist/llm/http-error.js +25 -0
- package/dist/llm/noop-provider.js +19 -0
- package/dist/llm/openai-provider.js +85 -0
- package/dist/llm/pricing.js +55 -0
- package/dist/llm/prompt.js +103 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/schema.js +49 -0
- package/dist/llm/social-post.js +49 -0
- package/dist/objects/store.js +31 -0
- package/dist/shared/config.js +45 -0
- package/dist/shared/redaction.js +18 -0
- package/dist/shared/types.js +2 -0
- package/dist/watcher/ignore-rules.js +92 -0
- package/dist/watcher/watcher.js +249 -0
- package/package.json +55 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext } from "../context.js";
|
|
3
|
+
import { loadConfig } from "../../shared/config.js";
|
|
4
|
+
import { printAiBudgetWarning } from "../ai-budget-warning.js";
|
|
5
|
+
export function createStatusCommand() {
|
|
6
|
+
return new Command("status").description("Show current whydid status").action(() => {
|
|
7
|
+
const projectRoot = process.cwd();
|
|
8
|
+
const ctx = requireEngineContext(projectRoot);
|
|
9
|
+
const projectRow = ctx.db.raw.prepare(`SELECT * FROM projects ORDER BY created_at ASC LIMIT 1`).get();
|
|
10
|
+
if (!projectRow) {
|
|
11
|
+
console.log("whydid is not initialized here. Run `whydid init` first.");
|
|
12
|
+
ctx.db.close();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const head = ctx.db.getHeadState(projectRow.id);
|
|
16
|
+
const changes = head ? ctx.db.getFileChanges(head.id) : [];
|
|
17
|
+
console.log(`Project: ${projectRow.name}`);
|
|
18
|
+
console.log(`Current state: #${head?.id.slice(0, 8)} (${head?.stateType})`);
|
|
19
|
+
console.log(`Last checkpoint: ${head?.createdAt}`);
|
|
20
|
+
console.log(`Files changed in last state: ${changes.length}`);
|
|
21
|
+
const config = loadConfig(ctx.whydidDir);
|
|
22
|
+
printAiBudgetWarning(ctx, projectRow.id, config);
|
|
23
|
+
ctx.db.close();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext, requireProjectId } from "../context.js";
|
|
3
|
+
import { loadConfig } from "../../shared/config.js";
|
|
4
|
+
import { runTestCommand } from "../../core/test-runner.js";
|
|
5
|
+
export function createTestCommand() {
|
|
6
|
+
return new Command("test").description("Run the project's configured test command").action(async () => {
|
|
7
|
+
const ctx = requireEngineContext(process.cwd());
|
|
8
|
+
const projectId = requireProjectId(ctx);
|
|
9
|
+
const config = loadConfig(ctx.whydidDir);
|
|
10
|
+
if (!config.test) {
|
|
11
|
+
console.log('No test command configured. Add this to .whydid/config.json:\n "test": { "command": "npm test", "mode": "manual" }');
|
|
12
|
+
ctx.db.close();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const head = ctx.db.getHeadState(projectId);
|
|
16
|
+
if (!head) {
|
|
17
|
+
console.log("No current state to attach test results to. Run `whydid checkpoint` first.");
|
|
18
|
+
ctx.db.close();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(`Running: ${config.test.command}`);
|
|
22
|
+
const result = await runTestCommand(ctx, head.id, config.test.command);
|
|
23
|
+
if (result.status === "passed") {
|
|
24
|
+
console.log(`✓ Tests passed (${result.durationMs}ms)`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(`✗ Tests failed (exit ${result.exitCode}, ${result.durationMs}ms)`);
|
|
28
|
+
console.log(`See ${result.outputPath} for output.`);
|
|
29
|
+
}
|
|
30
|
+
ctx.db.close();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=test.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext } from "../context.js";
|
|
3
|
+
export function createTimelineCommand() {
|
|
4
|
+
return new Command("timeline").description("Show the project state timeline").action(() => {
|
|
5
|
+
const ctx = requireEngineContext(process.cwd());
|
|
6
|
+
const projectRow = ctx.db.raw.prepare(`SELECT * FROM projects ORDER BY created_at ASC LIMIT 1`).get();
|
|
7
|
+
if (!projectRow) {
|
|
8
|
+
console.log("whydid is not initialized here. Run `whydid init` first.");
|
|
9
|
+
ctx.db.close();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const states = ctx.db.listStates(projectRow.id);
|
|
13
|
+
for (const s of states) {
|
|
14
|
+
const changes = ctx.db.getFileChanges(s.id);
|
|
15
|
+
const added = changes.reduce((sum, c) => sum + c.linesAdded, 0);
|
|
16
|
+
const removed = changes.reduce((sum, c) => sum + c.linesRemoved, 0);
|
|
17
|
+
console.log(`● ${s.createdAt} ${s.label ?? s.stateType}`);
|
|
18
|
+
console.log(` ${changes.length} files changed, +${added} / -${removed}`);
|
|
19
|
+
}
|
|
20
|
+
ctx.db.close();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=timeline.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext } from "../context.js";
|
|
3
|
+
import { computeFileDiff } from "../../core/diff.js";
|
|
4
|
+
import { getFileHistory } from "../../core/file-history.js";
|
|
5
|
+
import { loadConfig } from "../../shared/config.js";
|
|
6
|
+
import { createLLMProvider } from "../../llm/factory.js";
|
|
7
|
+
import { FileSecretStore } from "../../licensing/secret-store.js";
|
|
8
|
+
export function createWhyCommand() {
|
|
9
|
+
return new Command("why")
|
|
10
|
+
.description("Explain a file's full change history — who changed it, why, and what built on top of what")
|
|
11
|
+
.argument("<file>", "file path to explain")
|
|
12
|
+
.option("--ask [question]", "ask your configured LLM to synthesize a fresh explanation")
|
|
13
|
+
.action(async (file, opts) => {
|
|
14
|
+
const ctx = requireEngineContext(process.cwd());
|
|
15
|
+
const projectRow = ctx.db.raw.prepare(`SELECT * FROM projects ORDER BY created_at ASC LIMIT 1`).get();
|
|
16
|
+
if (!projectRow) {
|
|
17
|
+
console.log("whydid is not initialized here. Run `whydid init` first.");
|
|
18
|
+
ctx.db.close();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const history = getFileHistory(ctx.db, projectRow.id, file);
|
|
22
|
+
if (history.length === 0) {
|
|
23
|
+
console.log(`No recorded state introduces changes to ${file}.`);
|
|
24
|
+
ctx.db.close();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
console.log(`${file}\n`);
|
|
28
|
+
console.log(`History (${history.length} change${history.length === 1 ? "" : "s"}):\n`);
|
|
29
|
+
history.forEach((entry, i) => {
|
|
30
|
+
const when = new Date(entry.createdAt).toLocaleString();
|
|
31
|
+
console.log(`${i + 1}. State #${entry.stateId.slice(0, 8)} — ${entry.stateLabel ?? entry.stateType} (${when})`);
|
|
32
|
+
if (entry.turnPrompt) {
|
|
33
|
+
console.log(` Prompt: "${entry.turnPrompt}"`);
|
|
34
|
+
}
|
|
35
|
+
if (entry.annotationReason) {
|
|
36
|
+
console.log(` Why: ${entry.annotationReason}`);
|
|
37
|
+
}
|
|
38
|
+
else if (!entry.turnPrompt) {
|
|
39
|
+
console.log(` Why: unavailable — inferred from project changes only.`);
|
|
40
|
+
}
|
|
41
|
+
if (entry.buildsOnPriorChange) {
|
|
42
|
+
console.log(` ⚠ Builds on top of the previous entry's change to this file — reverting that one may affect this one.`);
|
|
43
|
+
}
|
|
44
|
+
console.log("");
|
|
45
|
+
});
|
|
46
|
+
const latest = history[history.length - 1];
|
|
47
|
+
const latestAnnotation = ctx.db.getAnnotation(latest.stateId);
|
|
48
|
+
if (opts.ask !== undefined) {
|
|
49
|
+
const question = typeof opts.ask === "string" ? opts.ask : `Why did ${file} change in this state?`;
|
|
50
|
+
const config = loadConfig(ctx.whydidDir);
|
|
51
|
+
const provider = await createLLMProvider(config.ai, new FileSecretStore(ctx.whydidDir));
|
|
52
|
+
if (provider.id === "noop") {
|
|
53
|
+
console.log("Ask AI: no LLM provider configured (set ai.provider in .whydid/config.json and the matching *_API_KEY env var).");
|
|
54
|
+
ctx.db.close();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const change = ctx.db.getFileChanges(latest.stateId).find((c) => c.filePath === file);
|
|
58
|
+
const diffText = change ? computeFileDiff(ctx.store, change).unified : "";
|
|
59
|
+
const context = `File: ${file}\nFull history:\n${history.map((h, i) => `${i + 1}. ${h.turnPrompt ?? h.annotationSummary ?? h.stateLabel ?? h.stateType}`).join("\n")}\nLatest stored summary: ${latestAnnotation?.summary ?? "(none)"}\nLatest diff:\n${diffText}`;
|
|
60
|
+
try {
|
|
61
|
+
const answer = await provider.answerQuestion(question, context);
|
|
62
|
+
console.log(`Ask AI (${question}):\n${answer}\n`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.log(`Ask AI failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
ctx.db.close();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=why.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { WhydidDatabase } from "../db/database.js";
|
|
4
|
+
import { ObjectStore } from "../objects/store.js";
|
|
5
|
+
import { IgnoreRules } from "../watcher/ignore-rules.js";
|
|
6
|
+
import { loadConfig } from "../shared/config.js";
|
|
7
|
+
export function whydidDirFor(projectRoot) {
|
|
8
|
+
return join(projectRoot, ".whydid");
|
|
9
|
+
}
|
|
10
|
+
export function buildEngineContext(projectRoot) {
|
|
11
|
+
const whydidDir = whydidDirFor(projectRoot);
|
|
12
|
+
mkdirSync(whydidDir, { recursive: true });
|
|
13
|
+
mkdirSync(join(whydidDir, "objects"), { recursive: true });
|
|
14
|
+
mkdirSync(join(whydidDir, "logs"), { recursive: true });
|
|
15
|
+
const config = loadConfig(whydidDir);
|
|
16
|
+
return {
|
|
17
|
+
projectRoot,
|
|
18
|
+
whydidDir,
|
|
19
|
+
db: new WhydidDatabase(join(whydidDir, "db.sqlite")),
|
|
20
|
+
store: new ObjectStore(join(whydidDir, "objects")),
|
|
21
|
+
ignoreRules: new IgnoreRules(projectRoot, config.ignoredPaths),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function defaultProjectName(projectRoot) {
|
|
25
|
+
return basename(projectRoot);
|
|
26
|
+
}
|
|
27
|
+
export function whydidDirExists(projectRoot) {
|
|
28
|
+
return existsSync(whydidDirFor(projectRoot));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Like buildEngineContext, but for commands that only make sense on an
|
|
32
|
+
* already-initialized project: fails fast with a friendly message instead
|
|
33
|
+
* of silently creating a fresh, empty .whydid/ as a side effect of a
|
|
34
|
+
* read-only command like `status` or `timeline`.
|
|
35
|
+
*/
|
|
36
|
+
export function requireEngineContext(projectRoot) {
|
|
37
|
+
if (!whydidDirExists(projectRoot)) {
|
|
38
|
+
throw new Error("whydid is not initialized here. Run `whydid init` first.");
|
|
39
|
+
}
|
|
40
|
+
return buildEngineContext(projectRoot);
|
|
41
|
+
}
|
|
42
|
+
export function requireProjectId(ctx) {
|
|
43
|
+
const row = ctx.db.raw.prepare(`SELECT id FROM projects ORDER BY created_at ASC LIMIT 1`).get();
|
|
44
|
+
if (!row)
|
|
45
|
+
throw new Error("whydid is not initialized here. Run `whydid init` first.");
|
|
46
|
+
return row.id;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { checkSqliteSupport, printSqliteSupportError } from "./preflight.js";
|
|
6
|
+
// dist/cli/index.js -> package root is two levels up (dist/cli -> dist -> root).
|
|
7
|
+
// npm always includes package.json in a published tarball regardless of the
|
|
8
|
+
// "files" allowlist, so this path is reliable in both a source checkout and
|
|
9
|
+
// a real installed package.
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PACKAGE_ROOT = join(__dirname, "..", "..");
|
|
12
|
+
const PACKAGE_VERSION = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8")).version;
|
|
13
|
+
// Loading every command module (and their transitive deps — LLM providers,
|
|
14
|
+
// the state engine, chokidar, etc.) on every single invocation made simple
|
|
15
|
+
// commands like `whydid status` noticeably slower to start than they need
|
|
16
|
+
// to be, and was the real cause behind `whydid daemon start`'s background
|
|
17
|
+
// process occasionally missing its own startup deadline. Only import the
|
|
18
|
+
// module the invoked command actually needs; fall back to loading
|
|
19
|
+
// everything only for --help / no-args / an unrecognized command, since
|
|
20
|
+
// those need the full listing anyway and aren't a hot path.
|
|
21
|
+
const loaders = {
|
|
22
|
+
init: async () => (await import("./commands/init.js")).createInitCommand(),
|
|
23
|
+
"agent-event": async () => (await import("./commands/agent-event.js")).createAgentEventCommand(),
|
|
24
|
+
status: async () => (await import("./commands/status.js")).createStatusCommand(),
|
|
25
|
+
checkpoint: async () => (await import("./commands/checkpoint.js")).createCheckpointCommand(),
|
|
26
|
+
timeline: async () => (await import("./commands/timeline.js")).createTimelineCommand(),
|
|
27
|
+
why: async () => (await import("./commands/why.js")).createWhyCommand(),
|
|
28
|
+
restore: async () => (await import("./commands/restore-fork.js")).createRestoreCommand(),
|
|
29
|
+
fork: async () => (await import("./commands/restore-fork.js")).createForkCommand(),
|
|
30
|
+
milestone: async () => (await import("./commands/milestone.js")).createMilestoneCommand(),
|
|
31
|
+
key: async () => (await import("./commands/key.js")).createKeyCommand(),
|
|
32
|
+
daemon: async () => (await import("./commands/daemon.js")).createDaemonCommand(),
|
|
33
|
+
test: async () => (await import("./commands/test.js")).createTestCommand(),
|
|
34
|
+
recap: async () => (await import("./commands/recap.js")).createRecapCommand(),
|
|
35
|
+
context: async () => (await import("./commands/context.js")).createContextCommand(),
|
|
36
|
+
export: async () => (await import("./commands/export.js")).createExportCommand(),
|
|
37
|
+
prompt: async () => (await import("./commands/prompt.js")).createPromptCommand(),
|
|
38
|
+
login: async () => (await import("./commands/license.js")).createLoginCommand(),
|
|
39
|
+
logout: async () => (await import("./commands/license.js")).createLogoutCommand(),
|
|
40
|
+
license: async () => (await import("./commands/license.js")).createLicenseCommand(),
|
|
41
|
+
};
|
|
42
|
+
async function main() {
|
|
43
|
+
const support = await checkSqliteSupport();
|
|
44
|
+
if (!support.ok) {
|
|
45
|
+
printSqliteSupportError(support);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const { Command } = await import("commander");
|
|
49
|
+
const program = new Command("whydid").description("A local time machine for AI-assisted coding").version(PACKAGE_VERSION, "-V, --version");
|
|
50
|
+
const rawArg = process.argv[2];
|
|
51
|
+
const commandArg = rawArg === "doctor" ? "recap" : rawArg; // doctor is an alias registered on the recap Command itself
|
|
52
|
+
const { commandNeedsLicense, printLicenseRequiredMessage } = await import("./license-gate.js");
|
|
53
|
+
if (commandNeedsLicense(commandArg)) {
|
|
54
|
+
const { createDefaultLicenseManager } = await import("../licensing/config.js");
|
|
55
|
+
const manager = await createDefaultLicenseManager();
|
|
56
|
+
await manager.refreshIfNeeded().catch(() => undefined); // best-effort, offline-tolerant
|
|
57
|
+
if (!manager.hasFeature("core")) {
|
|
58
|
+
console.error(printLicenseRequiredMessage());
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const loader = commandArg ? loaders[commandArg] : undefined;
|
|
63
|
+
if (loader) {
|
|
64
|
+
program.addCommand(await loader());
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
for (const load of Object.values(loaders)) {
|
|
68
|
+
program.addCommand(await load());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await program.parseAsync(process.argv);
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
console.error(err instanceof Error ? err.message : err);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PRICING_URL } from "../licensing/config.js";
|
|
2
|
+
/** Never gated — a customer must be able to authenticate/check status/see this message without already having a valid license. */
|
|
3
|
+
const EXEMPT_COMMANDS = new Set(["login", "logout", "license"]);
|
|
4
|
+
export function commandNeedsLicense(commandArg) {
|
|
5
|
+
if (!commandArg)
|
|
6
|
+
return false; // no args -> shows help
|
|
7
|
+
if (commandArg === "--help" || commandArg === "-h")
|
|
8
|
+
return false;
|
|
9
|
+
if (commandArg === "--version" || commandArg === "-V")
|
|
10
|
+
return false;
|
|
11
|
+
return !EXEMPT_COMMANDS.has(commandArg);
|
|
12
|
+
}
|
|
13
|
+
export function printLicenseRequiredMessage() {
|
|
14
|
+
return [
|
|
15
|
+
"This product requires a license.",
|
|
16
|
+
"",
|
|
17
|
+
"Get a lifetime license:",
|
|
18
|
+
PRICING_URL,
|
|
19
|
+
"",
|
|
20
|
+
"Already purchased?",
|
|
21
|
+
"Run:",
|
|
22
|
+
"",
|
|
23
|
+
" whydid login <licenseKey>",
|
|
24
|
+
].join("\n");
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=license-gate.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
3
|
+
export async function checkSqliteSupport() {
|
|
4
|
+
const nodeVersion = process.versions.node;
|
|
5
|
+
const execPath = process.execPath;
|
|
6
|
+
try {
|
|
7
|
+
nodeRequire("node:sqlite");
|
|
8
|
+
return { ok: true, nodeVersion, execPath };
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { ok: false, nodeVersion, execPath };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function printSqliteSupportError(result) {
|
|
15
|
+
console.error(`whydid requires a Node.js build with the built-in "node:sqlite" module (Node 22.5+ with`);
|
|
16
|
+
console.error(`--experimental-sqlite, or Node 23.4+ / 24+ unflagged) — the recommended minimum is Node 24.`);
|
|
17
|
+
console.error("");
|
|
18
|
+
console.error(`Detected Node ${result.nodeVersion} at:`);
|
|
19
|
+
console.error(` ${result.execPath}`);
|
|
20
|
+
console.error("");
|
|
21
|
+
console.error("If you have a newer Node install too, check for it and either call it directly or fix your PATH order:");
|
|
22
|
+
console.error(" which -a node");
|
|
23
|
+
console.error(" node --version");
|
|
24
|
+
console.error("");
|
|
25
|
+
console.error("With nvm: nvm install 24 && nvm use 24");
|
|
26
|
+
console.error("With Homebrew (macOS): brew install node@24 && brew link --overwrite node@24");
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const START = "<!-- whydid:context:start -->";
|
|
4
|
+
const END = "<!-- whydid:context:end -->";
|
|
5
|
+
const guidance = [
|
|
6
|
+
START,
|
|
7
|
+
"## whydid project context",
|
|
8
|
+
"",
|
|
9
|
+
"Before starting a task, read `WHYDID_SUMMARY.md` when it exists. Before changing files in a directory, read that directory's `CONTEXT.md` when it exists. These files record recent decisions, reasons, and the prompts behind prior changes; use them alongside the code and current task.",
|
|
10
|
+
"",
|
|
11
|
+
"After meaningful work, leave generated whydid context files intact so the next coding session can continue from the project's recorded decisions.",
|
|
12
|
+
END,
|
|
13
|
+
].join("\n");
|
|
14
|
+
const instructionFiles = [
|
|
15
|
+
"AGENTS.md",
|
|
16
|
+
"CLAUDE.md",
|
|
17
|
+
".cursor/rules/whydid-context.mdc",
|
|
18
|
+
];
|
|
19
|
+
function installInstructionFile(projectRoot, relativePath) {
|
|
20
|
+
const path = join(projectRoot, relativePath);
|
|
21
|
+
const isCursorRule = relativePath.endsWith(".mdc");
|
|
22
|
+
const defaultContent = isCursorRule
|
|
23
|
+
? `---\ndescription: Read whydid project history before coding\nalwaysApply: true\n---\n\n${guidance}\n`
|
|
24
|
+
: `${guidance}\n`;
|
|
25
|
+
if (!existsSync(path)) {
|
|
26
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
27
|
+
writeFileSync(path, defaultContent, "utf-8");
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
const current = readFileSync(path, "utf-8");
|
|
31
|
+
const start = current.indexOf(START);
|
|
32
|
+
const end = current.indexOf(END);
|
|
33
|
+
if (start !== -1 && end > start) {
|
|
34
|
+
const updated = `${current.slice(0, start)}${guidance}${current.slice(end + END.length)}`;
|
|
35
|
+
if (updated !== current)
|
|
36
|
+
writeFileSync(path, updated, "utf-8");
|
|
37
|
+
return updated !== current;
|
|
38
|
+
}
|
|
39
|
+
const separator = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
40
|
+
writeFileSync(path, `${current}${separator}\n${guidance}\n`, "utf-8");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
export function installAgentContextInstructions(projectRoot) {
|
|
44
|
+
return instructionFiles.filter((relativePath) => installInstructionFile(projectRoot, relativePath));
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=agent-context.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
function installJsonHooks(projectRoot, relativePath, events) {
|
|
4
|
+
const path = join(projectRoot, relativePath);
|
|
5
|
+
let config = {};
|
|
6
|
+
if (existsSync(path)) {
|
|
7
|
+
try {
|
|
8
|
+
config = JSON.parse(readFileSync(path, "utf-8"));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (config.hooks != null && (typeof config.hooks !== "object" || Array.isArray(config.hooks)))
|
|
17
|
+
return null;
|
|
18
|
+
if (!config.hooks)
|
|
19
|
+
config.hooks = {};
|
|
20
|
+
for (const [eventName, definition] of Object.entries(events)) {
|
|
21
|
+
if (config.hooks[eventName] != null && !Array.isArray(config.hooks[eventName]))
|
|
22
|
+
return null;
|
|
23
|
+
const entries = Array.isArray(config.hooks[eventName]) ? config.hooks[eventName] : [];
|
|
24
|
+
if (relativePath.startsWith(".codex/")) {
|
|
25
|
+
const command = definition.command;
|
|
26
|
+
const existing = entries.find((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => hook?.command === command));
|
|
27
|
+
if (!existing)
|
|
28
|
+
entries.push({ hooks: [{ type: "command", command }] });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const command = definition.command;
|
|
32
|
+
if (!entries.some((entry) => entry?.command === command))
|
|
33
|
+
entries.push({ command });
|
|
34
|
+
}
|
|
35
|
+
config.hooks[eventName] = entries;
|
|
36
|
+
}
|
|
37
|
+
if (relativePath.startsWith(".codex/"))
|
|
38
|
+
config.description ??= "whydid local session capture";
|
|
39
|
+
else
|
|
40
|
+
config.version ??= 1;
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
43
|
+
return relativePath;
|
|
44
|
+
}
|
|
45
|
+
export function installAgentHookConfigs(projectRoot) {
|
|
46
|
+
const codexEvents = {
|
|
47
|
+
SessionStart: { command: "whydid agent-event codex" },
|
|
48
|
+
UserPromptSubmit: { command: "whydid agent-event codex" },
|
|
49
|
+
Stop: { command: "whydid agent-event codex" },
|
|
50
|
+
SessionEnd: { command: "whydid agent-event codex" },
|
|
51
|
+
};
|
|
52
|
+
const cursorEvents = {
|
|
53
|
+
beforeSubmitPrompt: { command: "whydid agent-event cursor" },
|
|
54
|
+
afterAgentResponse: { command: "whydid agent-event cursor" },
|
|
55
|
+
stop: { command: "whydid agent-event cursor" },
|
|
56
|
+
};
|
|
57
|
+
const installed = [];
|
|
58
|
+
const skipped = [];
|
|
59
|
+
for (const [path, events] of [[".codex/hooks.json", codexEvents], [".cursor/hooks.json", cursorEvents]]) {
|
|
60
|
+
const result = installJsonHooks(projectRoot, path, events);
|
|
61
|
+
if (result)
|
|
62
|
+
installed.push(result);
|
|
63
|
+
else
|
|
64
|
+
skipped.push(path);
|
|
65
|
+
}
|
|
66
|
+
return { installed, skipped };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=agent-hooks.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { estimateCostUsd, computeBudgetStatus } from "../llm/pricing.js";
|
|
2
|
+
import { loadCommunityPricingCache } from "../llm/community-pricing.js";
|
|
3
|
+
export function currentUtcMonthStartIso() {
|
|
4
|
+
const now = new Date();
|
|
5
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Single source of truth for "where does this project stand against its
|
|
9
|
+
* monthly AI budget right now" — shared by the dashboard's GET /api/ai-budget
|
|
10
|
+
* and the CLI's checkpoint/status warnings, so the two can never disagree.
|
|
11
|
+
*/
|
|
12
|
+
export function computeCurrentAiBudget(ctx, projectId, config) {
|
|
13
|
+
const monthStart = currentUtcMonthStartIso();
|
|
14
|
+
const lookup = { overrides: config.pricingOverrides, community: loadCommunityPricingCache(ctx.whydidDir)?.models };
|
|
15
|
+
const byModel = ctx.db.getTokenUsageByModelSince(projectId, monthStart).map((m) => ({
|
|
16
|
+
...m,
|
|
17
|
+
estimatedCostUsd: estimateCostUsd(m.model, m.inputTokens, m.outputTokens, lookup),
|
|
18
|
+
}));
|
|
19
|
+
const pricedModels = byModel.filter((m) => m.estimatedCostUsd !== null);
|
|
20
|
+
const hasUnpricedUsage = byModel.some((m) => m.estimatedCostUsd === null);
|
|
21
|
+
// Zero calls this month is known spend ($0), not "unknown" — only usage
|
|
22
|
+
// that exists but is entirely on unpriced models is genuinely unknown.
|
|
23
|
+
const spentUsd = byModel.length === 0 ? 0 : pricedModels.length > 0 ? pricedModels.reduce((sum, m) => sum + (m.estimatedCostUsd ?? 0), 0) : null;
|
|
24
|
+
const budget = computeBudgetStatus(config.aiBudget.monthlyLimitUsd, config.aiBudget.alertThresholdPercent, spentUsd);
|
|
25
|
+
return { ...budget, hasUnpricedUsage, monthStart };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=ai-budget.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const RECENT_LIMIT = 20;
|
|
2
|
+
const SEARCH_LIMIT = 15;
|
|
3
|
+
/**
|
|
4
|
+
* Grounding context for a chat question: the most recent checkpoints (so
|
|
5
|
+
* "what did I do today/recently" works without the question needing to
|
|
6
|
+
* name anything) plus FTS5 keyword matches against the question's own text
|
|
7
|
+
* (so "why did we choose Stripe" reaches back further than the recent
|
|
8
|
+
* window). Deduped and hydrated into real content; states without a
|
|
9
|
+
* generated annotation are skipped — there's nothing useful to ground on.
|
|
10
|
+
*/
|
|
11
|
+
export function retrieveContextForQuestion(ctx, projectId, question) {
|
|
12
|
+
const recentStateIds = ctx.db.listStates(projectId, { limit: RECENT_LIMIT }).map((s) => s.id);
|
|
13
|
+
const searchStateIds = ctx.db.searchAnnotations(projectId, question, SEARCH_LIMIT);
|
|
14
|
+
const stateIds = [...new Set([...recentStateIds, ...searchStateIds])];
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const stateId of stateIds) {
|
|
17
|
+
const state = ctx.db.getState(stateId);
|
|
18
|
+
const annotation = ctx.db.getAnnotation(stateId);
|
|
19
|
+
if (!state || !annotation || annotation.status !== "generated" || !annotation.summary || !annotation.reason)
|
|
20
|
+
continue;
|
|
21
|
+
const changes = ctx.db.getFileChanges(stateId);
|
|
22
|
+
const prompt = state.turnId ? ctx.db.getTurn(state.turnId)?.prompt ?? null : null;
|
|
23
|
+
entries.push({
|
|
24
|
+
stateId,
|
|
25
|
+
createdAt: state.createdAt,
|
|
26
|
+
summary: annotation.summary,
|
|
27
|
+
reason: annotation.reason,
|
|
28
|
+
decisions: annotation.decisions,
|
|
29
|
+
riskLevel: annotation.riskLevel,
|
|
30
|
+
filesTouched: changes.map((c) => c.filePath),
|
|
31
|
+
prompt,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=chat-retrieval.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const MAX_ENTRIES_PER_DIR = 8;
|
|
4
|
+
const MAX_SUMMARY_ENTRIES = 8;
|
|
5
|
+
const SUMMARY_FILE = "WHYDID_SUMMARY.md";
|
|
6
|
+
/** The immediate parent directory of a project-relative file path, or "" for a root-level file. */
|
|
7
|
+
export function directoryFor(filePath) {
|
|
8
|
+
const dir = dirname(filePath);
|
|
9
|
+
return dir === "." ? "" : dir;
|
|
10
|
+
}
|
|
11
|
+
function contextFileNameFor(dir) {
|
|
12
|
+
return dir === "" ? "CONTEXT.md" : `${dir}/CONTEXT.md`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* True for a file whydid itself writes (CONTEXT.md in any directory, the
|
|
16
|
+
* root WHYDID_SUMMARY.md) — never something a checkpoint should be created
|
|
17
|
+
* *for*. Regenerating these is a side effect of a real checkpoint, and that
|
|
18
|
+
* regeneration is itself a file write the watcher would otherwise see as a
|
|
19
|
+
* new change: without this check, a checkpoint's own doc update becomes the
|
|
20
|
+
* next checkpoint's trigger, forever, each one annotated and regenerating
|
|
21
|
+
* the docs again — a real incident (2026-09-24) that produced 11 checkpoints
|
|
22
|
+
* in nine minutes with no actual source change behind any of them.
|
|
23
|
+
*/
|
|
24
|
+
export function isAutoGeneratedDocFile(filePath) {
|
|
25
|
+
return filePath === SUMMARY_FILE || filePath === "CONTEXT.md" || filePath.endsWith("/CONTEXT.md");
|
|
26
|
+
}
|
|
27
|
+
function loadEntry(ctx, stateId) {
|
|
28
|
+
const state = ctx.db.getState(stateId);
|
|
29
|
+
const annotation = ctx.db.getAnnotation(stateId);
|
|
30
|
+
if (!state || !annotation || annotation.status !== "generated")
|
|
31
|
+
return null;
|
|
32
|
+
const changes = ctx.db.getFileChanges(stateId);
|
|
33
|
+
const prompt = state.turnId ? ctx.db.getTurn(state.turnId)?.prompt ?? null : null;
|
|
34
|
+
return {
|
|
35
|
+
createdAt: state.createdAt,
|
|
36
|
+
summary: annotation.summary ?? "Change",
|
|
37
|
+
reason: annotation.reason ?? "",
|
|
38
|
+
decisions: annotation.decisions,
|
|
39
|
+
riskLevel: annotation.riskLevel,
|
|
40
|
+
filesTouched: changes.map((c) => c.filePath),
|
|
41
|
+
prompt,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function renderMarkdown(title, intro, entries) {
|
|
45
|
+
const lines = [
|
|
46
|
+
`# ${title}`,
|
|
47
|
+
"",
|
|
48
|
+
`_${intro} Auto-generated by whydid from real AI coding sessions — do not edit by hand, it will be overwritten on the next annotated checkpoint here. Safe, and encouraged, to commit._`,
|
|
49
|
+
"",
|
|
50
|
+
];
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
lines.push(`## ${e.summary}`);
|
|
53
|
+
lines.push(`_${new Date(e.createdAt).toLocaleString()}${e.riskLevel ? ` · ${e.riskLevel} risk` : ""}_`);
|
|
54
|
+
lines.push("");
|
|
55
|
+
if (e.prompt) {
|
|
56
|
+
lines.push(`**Prompt:** ${e.prompt}`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
}
|
|
59
|
+
if (e.reason) {
|
|
60
|
+
lines.push(e.reason);
|
|
61
|
+
lines.push("");
|
|
62
|
+
}
|
|
63
|
+
if (e.decisions.length > 0) {
|
|
64
|
+
lines.push("**Decisions:**");
|
|
65
|
+
for (const d of e.decisions)
|
|
66
|
+
lines.push(`- ${d}`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
}
|
|
69
|
+
lines.push(`**Files:** ${e.filesTouched.join(", ")}`);
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push("---");
|
|
72
|
+
lines.push("");
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
function writeContextFile(ctx, projectId, dir, entries) {
|
|
77
|
+
const relPath = contextFileNameFor(dir);
|
|
78
|
+
const abs = join(ctx.projectRoot, relPath);
|
|
79
|
+
const title = dir === "" ? "Context — project root" : `Context — ${dir}`;
|
|
80
|
+
const intro = dir === "" ? "Recent AI-assisted decisions behind the files directly in this directory." : `Recent AI-assisted decisions behind the files in \`${dir}/\`.`;
|
|
81
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
82
|
+
writeFileSync(abs, renderMarkdown(title, intro, entries), "utf-8");
|
|
83
|
+
ctx.db.upsertContextFile({ projectId, dirPath: dir, filePath: relPath, checkpointCount: entries.length, updatedAt: new Date().toISOString() });
|
|
84
|
+
return relPath;
|
|
85
|
+
}
|
|
86
|
+
function writeSummaryFile(ctx, projectId, entries) {
|
|
87
|
+
const abs = join(ctx.projectRoot, SUMMARY_FILE);
|
|
88
|
+
writeFileSync(abs, renderMarkdown("Project summary", "The most significant AI-assisted decisions across this project.", entries), "utf-8");
|
|
89
|
+
ctx.db.upsertContextFile({ projectId, dirPath: "", filePath: SUMMARY_FILE, checkpointCount: entries.length, updatedAt: new Date().toISOString() });
|
|
90
|
+
return SUMMARY_FILE;
|
|
91
|
+
}
|
|
92
|
+
function regenerateDirectory(ctx, projectId, dir) {
|
|
93
|
+
const stateIds = ctx.db.findAnnotatedStateIdsForDirectory(projectId, dir, MAX_ENTRIES_PER_DIR);
|
|
94
|
+
const entries = stateIds.map((id) => loadEntry(ctx, id)).filter((e) => e !== null);
|
|
95
|
+
if (entries.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
return writeContextFile(ctx, projectId, dir, entries);
|
|
98
|
+
}
|
|
99
|
+
function regenerateSummary(ctx, projectId) {
|
|
100
|
+
const stateIds = ctx.db.findAnnotatedStateIdsForDirectory(projectId, null, MAX_SUMMARY_ENTRIES);
|
|
101
|
+
const entries = stateIds.map((id) => loadEntry(ctx, id)).filter((e) => e !== null);
|
|
102
|
+
if (entries.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
return writeSummaryFile(ctx, projectId, entries);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Regenerates the CONTEXT.md for every directory touched by a just-annotated
|
|
108
|
+
* checkpoint, plus the project-wide summary. Call this right after an
|
|
109
|
+
* annotation succeeds (status "generated") — never on a failed/unavailable
|
|
110
|
+
* one, since there'd be nothing new to say.
|
|
111
|
+
*/
|
|
112
|
+
export async function updateContextTree(ctx, projectId, changedFilePaths) {
|
|
113
|
+
const dirs = new Set(changedFilePaths.map(directoryFor));
|
|
114
|
+
const updatedFiles = [...dirs].map((dir) => regenerateDirectory(ctx, projectId, dir));
|
|
115
|
+
updatedFiles.push(regenerateSummary(ctx, projectId));
|
|
116
|
+
return { updatedFiles: updatedFiles.filter((f) => f !== null) };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Rebuilds the entire context tree from a project's full annotated history —
|
|
120
|
+
* for backfilling an existing project the first time this feature runs, or a
|
|
121
|
+
* manual resync (`whydid context`).
|
|
122
|
+
*/
|
|
123
|
+
export async function regenerateAllContextFiles(ctx, projectId) {
|
|
124
|
+
const allStateIds = ctx.db.findAnnotatedStateIdsForDirectory(projectId, null, Number.MAX_SAFE_INTEGER);
|
|
125
|
+
const dirs = new Set();
|
|
126
|
+
for (const stateId of allStateIds) {
|
|
127
|
+
for (const change of ctx.db.getFileChanges(stateId)) {
|
|
128
|
+
dirs.add(directoryFor(change.filePath));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const updatedFiles = [...dirs].map((dir) => regenerateDirectory(ctx, projectId, dir));
|
|
132
|
+
updatedFiles.push(regenerateSummary(ctx, projectId));
|
|
133
|
+
return { updatedFiles: updatedFiles.filter((f) => f !== null) };
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=context-tree.js.map
|