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,139 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { openSync } from "node:fs";
|
|
7
|
+
import { requireEngineContext, requireProjectId } from "../context.js";
|
|
8
|
+
import { createDaemonApp } from "../../daemon/server.js";
|
|
9
|
+
import { readPidFile, writePidFile, removePidFile, isProcessAlive } from "../../daemon/pidfile.js";
|
|
10
|
+
import { listenWithPortReuse } from "../../daemon/listen.js";
|
|
11
|
+
import { ProjectWatcher } from "../../watcher/watcher.js";
|
|
12
|
+
import { loadConfig } from "../../shared/config.js";
|
|
13
|
+
import { createLLMProvider } from "../../llm/factory.js";
|
|
14
|
+
import { FileSecretStore } from "../../licensing/secret-store.js";
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
// Waits specifically for *this* start's pidfile, not just "a pidfile" — a
|
|
19
|
+
// stale file from a crashed previous run is still sitting there when the
|
|
20
|
+
// new child spawns, so polling for mere existence would report success
|
|
21
|
+
// immediately with the wrong (dead) pid/port.
|
|
22
|
+
async function waitForPidFile(whydidDir, timeoutMs, expectedPid) {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
while (Date.now() - start < timeoutMs) {
|
|
25
|
+
const pidFile = readPidFile(whydidDir);
|
|
26
|
+
if (pidFile && pidFile.pid === expectedPid)
|
|
27
|
+
return pidFile;
|
|
28
|
+
await sleep(100);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
async function runForeground(projectRoot) {
|
|
34
|
+
// A long-running local daemon should degrade, not die, on an unexpected
|
|
35
|
+
// async error — an uncaught rejection anywhere outside the Express
|
|
36
|
+
// request cycle (a background poll, a fire-and-forget call missing a
|
|
37
|
+
// .catch) otherwise takes the whole process down by default, silently
|
|
38
|
+
// orphaning every open dashboard tab.
|
|
39
|
+
process.on("uncaughtException", (err) => {
|
|
40
|
+
console.error("[whydid daemon] uncaught exception (daemon is still running):", err);
|
|
41
|
+
});
|
|
42
|
+
process.on("unhandledRejection", (reason) => {
|
|
43
|
+
console.error("[whydid daemon] unhandled rejection (daemon is still running):", reason);
|
|
44
|
+
});
|
|
45
|
+
const ctx = requireEngineContext(projectRoot);
|
|
46
|
+
const projectId = requireProjectId(ctx);
|
|
47
|
+
const config = loadConfig(ctx.whydidDir);
|
|
48
|
+
const llmProvider = await createLLMProvider(config.ai, new FileSecretStore(ctx.whydidDir));
|
|
49
|
+
ctx.db.pruneOldSocialPosts(projectId, config.buildInPublic.retentionDays);
|
|
50
|
+
const watcher = new ProjectWatcher(ctx, projectId, config.checkpoint, llmProvider, undefined, undefined, config.privacy, config.contextTree.enabled, config.buildInPublic.enabled, config.buildInPublic.retentionDays);
|
|
51
|
+
watcher.start();
|
|
52
|
+
// Reuse the previous run's port and token when they're available (e.g.
|
|
53
|
+
// after a crash, or a plain restart) so a dashboard tab left open from
|
|
54
|
+
// before can often just be reloaded instead of needing a brand-new URL.
|
|
55
|
+
const previous = readPidFile(ctx.whydidDir);
|
|
56
|
+
const token = previous?.token ?? randomBytes(16).toString("hex");
|
|
57
|
+
const app = createDaemonApp(ctx, projectId, token, watcher);
|
|
58
|
+
const server = await listenWithPortReuse(app, previous?.port);
|
|
59
|
+
const address = server.address();
|
|
60
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
61
|
+
writePidFile(ctx.whydidDir, { pid: process.pid, port, token });
|
|
62
|
+
console.log(`whydid dashboard: http://127.0.0.1:${port}/?token=${token}`);
|
|
63
|
+
const shutdown = () => {
|
|
64
|
+
removePidFile(ctx.whydidDir);
|
|
65
|
+
void watcher.stop();
|
|
66
|
+
server.close(() => process.exit(0));
|
|
67
|
+
};
|
|
68
|
+
process.on("SIGINT", shutdown);
|
|
69
|
+
process.on("SIGTERM", shutdown);
|
|
70
|
+
}
|
|
71
|
+
export function createDaemonCommand() {
|
|
72
|
+
const daemon = new Command("daemon").description("Manage the whydid local daemon");
|
|
73
|
+
daemon
|
|
74
|
+
.command("start")
|
|
75
|
+
.description("Start the local daemon and print the dashboard URL")
|
|
76
|
+
.option("--foreground", "run in the foreground instead of detaching")
|
|
77
|
+
.action(async (opts) => {
|
|
78
|
+
const projectRoot = process.cwd();
|
|
79
|
+
const ctx = requireEngineContext(projectRoot);
|
|
80
|
+
const existing = readPidFile(ctx.whydidDir);
|
|
81
|
+
ctx.db.close();
|
|
82
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
83
|
+
console.log(`Daemon already running (pid ${existing.pid}) at http://127.0.0.1:${existing.port}/?token=${existing.token}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (existing) {
|
|
87
|
+
console.log(`Found a stale daemon record (pid ${existing.pid} is no longer running) — starting fresh.`);
|
|
88
|
+
}
|
|
89
|
+
if (opts.foreground) {
|
|
90
|
+
await runForeground(projectRoot);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const logFd = openSync(join(ctx.whydidDir, "logs", "daemon.log"), "a");
|
|
94
|
+
const child = spawn(process.execPath, [join(__dirname, "..", "..", "..", "dist", "cli", "index.js"), "daemon", "start", "--foreground"], { cwd: projectRoot, detached: true, stdio: ["ignore", logFd, logFd] });
|
|
95
|
+
child.unref();
|
|
96
|
+
const started = await waitForPidFile(ctx.whydidDir, 8000, child.pid);
|
|
97
|
+
if (started) {
|
|
98
|
+
console.log(`whydid dashboard: http://127.0.0.1:${started.port}/?token=${started.token}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log("Daemon is taking longer than expected to start.");
|
|
102
|
+
console.log(`Check .whydid/logs/daemon.log or run \`whydid daemon status\`.`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
daemon.command("stop").description("Stop the running daemon").action(() => {
|
|
106
|
+
const ctx = requireEngineContext(process.cwd());
|
|
107
|
+
const existing = readPidFile(ctx.whydidDir);
|
|
108
|
+
ctx.db.close();
|
|
109
|
+
if (!existing) {
|
|
110
|
+
console.log("Daemon is not running.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!isProcessAlive(existing.pid)) {
|
|
114
|
+
console.log("Daemon process not found; clearing stale pidfile.");
|
|
115
|
+
removePidFile(ctx.whydidDir);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
process.kill(existing.pid, "SIGTERM");
|
|
120
|
+
console.log("Daemon stopped.");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
console.log("Daemon process not found; clearing stale pidfile.");
|
|
124
|
+
}
|
|
125
|
+
removePidFile(ctx.whydidDir);
|
|
126
|
+
});
|
|
127
|
+
daemon.command("status").description("Show whether the daemon is running and its URL").action(() => {
|
|
128
|
+
const ctx = requireEngineContext(process.cwd());
|
|
129
|
+
const existing = readPidFile(ctx.whydidDir);
|
|
130
|
+
ctx.db.close();
|
|
131
|
+
if (!existing || !isProcessAlive(existing.pid)) {
|
|
132
|
+
console.log("Daemon is not running.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(`Daemon running (pid ${existing.pid}): http://127.0.0.1:${existing.port}/?token=${existing.token}`);
|
|
136
|
+
});
|
|
137
|
+
return daemon;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=daemon.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { requireEngineContext, requireProjectId } from "../context.js";
|
|
5
|
+
import { buildExportData, exportAsJson, exportAsMarkdown, exportAsHtml } from "../../core/export.js";
|
|
6
|
+
const EXTENSIONS = { json: "json", markdown: "md", html: "html" };
|
|
7
|
+
export function createExportCommand() {
|
|
8
|
+
return new Command("export")
|
|
9
|
+
.description("Export the project's whydid history as JSON, Markdown, or HTML")
|
|
10
|
+
.option("--format <format>", "json | markdown | html", "json")
|
|
11
|
+
.option("--out <path>", "output file path (default: whydid-export.<ext> in the current directory)")
|
|
12
|
+
.action((opts) => {
|
|
13
|
+
const ctx = requireEngineContext(process.cwd());
|
|
14
|
+
const projectId = requireProjectId(ctx);
|
|
15
|
+
const project = ctx.db.getProject(projectId);
|
|
16
|
+
const data = buildExportData(ctx.db, project);
|
|
17
|
+
let content;
|
|
18
|
+
if (opts.format === "markdown")
|
|
19
|
+
content = exportAsMarkdown(data);
|
|
20
|
+
else if (opts.format === "html")
|
|
21
|
+
content = exportAsHtml(data);
|
|
22
|
+
else if (opts.format === "json")
|
|
23
|
+
content = exportAsJson(data);
|
|
24
|
+
else {
|
|
25
|
+
console.log(`Unknown format: ${opts.format}. Use json, markdown, or html.`);
|
|
26
|
+
ctx.db.close();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const ext = EXTENSIONS[opts.format];
|
|
30
|
+
const outPath = opts.out ?? join(process.cwd(), `whydid-export.${ext}`);
|
|
31
|
+
writeFileSync(outPath, content, "utf-8");
|
|
32
|
+
console.log(`✓ Exported ${data.states.length} states to ${outPath}`);
|
|
33
|
+
ctx.db.close();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { buildEngineContext, defaultProjectName, whydidDirExists } from "../context.js";
|
|
5
|
+
import { initProject, createCheckpoint } from "../../core/engine.js";
|
|
6
|
+
import { detectGit } from "../../git/git.js";
|
|
7
|
+
import { DEFAULT_CONFIG, saveConfig } from "../../shared/config.js";
|
|
8
|
+
import { installAgentContextInstructions } from "../../core/agent-context.js";
|
|
9
|
+
import { installAgentHookConfigs } from "../../core/agent-hooks.js";
|
|
10
|
+
function ensureGitignored(projectRoot) {
|
|
11
|
+
const path = join(projectRoot, ".gitignore");
|
|
12
|
+
const entry = ".whydid/";
|
|
13
|
+
if (!existsSync(path)) {
|
|
14
|
+
writeFileSync(path, `${entry}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const content = readFileSync(path, "utf-8");
|
|
18
|
+
if (content.split("\n").some((line) => line.trim() === entry || line.trim() === ".whydid"))
|
|
19
|
+
return;
|
|
20
|
+
appendFileSync(path, `${content.endsWith("\n") || content === "" ? "" : "\n"}${entry}\n`);
|
|
21
|
+
}
|
|
22
|
+
export async function runInit(projectRoot) {
|
|
23
|
+
const alreadyInitialized = whydidDirExists(projectRoot);
|
|
24
|
+
const ctx = buildEngineContext(projectRoot);
|
|
25
|
+
saveConfig(ctx.whydidDir, { ...DEFAULT_CONFIG, projectName: defaultProjectName(projectRoot) });
|
|
26
|
+
ensureGitignored(projectRoot);
|
|
27
|
+
const agentInstructionFiles = installAgentContextInstructions(projectRoot);
|
|
28
|
+
const agentHooks = installAgentHookConfigs(projectRoot);
|
|
29
|
+
if (alreadyInitialized) {
|
|
30
|
+
const row = ctx.db.raw.prepare(`SELECT id FROM projects ORDER BY created_at ASC LIMIT 1`).get();
|
|
31
|
+
if (row) {
|
|
32
|
+
const result = await createCheckpoint(ctx, { projectId: row.id });
|
|
33
|
+
ctx.db.close();
|
|
34
|
+
return { projectId: row.id, stateId: result.stateId, agentInstructionFiles, agentHookFiles: agentHooks.installed, skippedAgentHookFiles: agentHooks.skipped };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const result = await initProject(ctx, defaultProjectName(projectRoot));
|
|
38
|
+
ctx.db.close();
|
|
39
|
+
return { ...result, agentInstructionFiles, agentHookFiles: agentHooks.installed, skippedAgentHookFiles: agentHooks.skipped };
|
|
40
|
+
}
|
|
41
|
+
export function createInitCommand() {
|
|
42
|
+
return new Command("init").description("Initialize whydid for the current project").action(async () => {
|
|
43
|
+
const projectRoot = process.cwd();
|
|
44
|
+
console.log("whydid\n");
|
|
45
|
+
const git = await detectGit(projectRoot);
|
|
46
|
+
const result = await runInit(projectRoot);
|
|
47
|
+
console.log(`✓ Project detected: ${projectRoot.split("/").pop()}`);
|
|
48
|
+
console.log(git.isRepo ? "✓ Git repository detected" : " Git repository not detected (whydid still works)");
|
|
49
|
+
console.log("✓ Local database initialized");
|
|
50
|
+
console.log(`✓ Initial project state captured (${result.stateId.slice(0, 8)})`);
|
|
51
|
+
if (result.agentInstructionFiles.length > 0) {
|
|
52
|
+
console.log(`✓ Shared whydid context instructions installed for ${result.agentInstructionFiles.length} coding-agent entry point${result.agentInstructionFiles.length === 1 ? "" : "s"}`);
|
|
53
|
+
}
|
|
54
|
+
if (result.agentHookFiles.length > 0) {
|
|
55
|
+
console.log(`✓ Local Codex/Cursor session hooks configured (${result.agentHookFiles.join(", ")})`);
|
|
56
|
+
}
|
|
57
|
+
if (result.skippedAgentHookFiles.length > 0) {
|
|
58
|
+
console.log(` Could not update invalid hook config: ${result.skippedAgentHookFiles.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
console.log("\nwhydid is ready.\n\nRun `whydid daemon start` to open the local dashboard.");
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext } from "../context.js";
|
|
3
|
+
import { FileSecretStore, getSecretStatuses } from "../../licensing/secret-store.js";
|
|
4
|
+
export async function runSetKey(projectRoot, provider, value) {
|
|
5
|
+
const ctx = requireEngineContext(projectRoot);
|
|
6
|
+
const store = new FileSecretStore(ctx.whydidDir);
|
|
7
|
+
await store.set(provider, value);
|
|
8
|
+
ctx.db.close();
|
|
9
|
+
}
|
|
10
|
+
export async function runClearKey(projectRoot, provider) {
|
|
11
|
+
const ctx = requireEngineContext(projectRoot);
|
|
12
|
+
const store = new FileSecretStore(ctx.whydidDir);
|
|
13
|
+
await store.delete(provider);
|
|
14
|
+
ctx.db.close();
|
|
15
|
+
}
|
|
16
|
+
export async function runListKeys(projectRoot) {
|
|
17
|
+
const ctx = requireEngineContext(projectRoot);
|
|
18
|
+
const store = new FileSecretStore(ctx.whydidDir);
|
|
19
|
+
const statuses = await getSecretStatuses(store);
|
|
20
|
+
ctx.db.close();
|
|
21
|
+
return statuses;
|
|
22
|
+
}
|
|
23
|
+
export function createKeyCommand() {
|
|
24
|
+
const key = new Command("key").description("Manage local API keys for AI providers (stored per-project in .whydid/secrets.json)");
|
|
25
|
+
key
|
|
26
|
+
.command("set")
|
|
27
|
+
.description("Save an API key locally for this project — takes precedence over a same-provider environment variable")
|
|
28
|
+
.argument("<provider>", 'provider name, e.g. "anthropic" or "openai"')
|
|
29
|
+
.argument("<value>", "the API key")
|
|
30
|
+
.action(async (provider, value) => {
|
|
31
|
+
const name = provider.toLowerCase();
|
|
32
|
+
await runSetKey(process.cwd(), name, value);
|
|
33
|
+
console.log(`✓ Saved a local ${name} key for this project.`);
|
|
34
|
+
console.log(` It's used instead of any ${name.toUpperCase()}_API_KEY environment variable, for this project only.`);
|
|
35
|
+
});
|
|
36
|
+
key
|
|
37
|
+
.command("clear")
|
|
38
|
+
.description("Remove a locally saved API key for this project")
|
|
39
|
+
.argument("<provider>", 'provider name, e.g. "anthropic" or "openai"')
|
|
40
|
+
.action(async (provider) => {
|
|
41
|
+
const name = provider.toLowerCase();
|
|
42
|
+
await runClearKey(process.cwd(), name);
|
|
43
|
+
console.log(`✓ Removed the local ${name} key for this project.`);
|
|
44
|
+
console.log(` If ${name.toUpperCase()}_API_KEY is set in your environment, that will be used instead.`);
|
|
45
|
+
});
|
|
46
|
+
key
|
|
47
|
+
.command("list")
|
|
48
|
+
.description("Show which providers have a key configured, and where it's coming from")
|
|
49
|
+
.action(async () => {
|
|
50
|
+
const statuses = await runListKeys(process.cwd());
|
|
51
|
+
for (const s of statuses) {
|
|
52
|
+
if (!s.configured) {
|
|
53
|
+
console.log(`${s.provider}: not set`);
|
|
54
|
+
}
|
|
55
|
+
else if (s.source === "file") {
|
|
56
|
+
console.log(`${s.provider}: ${s.masked} (saved locally for this project)`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(`${s.provider}: ${s.masked} (from ${s.provider.toUpperCase()}_API_KEY in your environment)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return key;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=key.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createDefaultLicenseManager, PRICING_URL } from "../../licensing/config.js";
|
|
3
|
+
export async function runLogin(manager, licenseKey) {
|
|
4
|
+
await manager.activate(licenseKey);
|
|
5
|
+
}
|
|
6
|
+
export async function runLogout(manager) {
|
|
7
|
+
await manager.deactivate();
|
|
8
|
+
}
|
|
9
|
+
/** Reads only the locally-stored, already-verified license — no network call, matching whydid's local-first design even for its own status check. */
|
|
10
|
+
export async function runLicenseStatus(manager) {
|
|
11
|
+
if (!manager.isActivated())
|
|
12
|
+
return null;
|
|
13
|
+
return manager.getLicense();
|
|
14
|
+
}
|
|
15
|
+
export function createLoginCommand() {
|
|
16
|
+
return new Command("login")
|
|
17
|
+
.description("Activate whydid with your license key")
|
|
18
|
+
.argument("<licenseKey>", "the license key from your purchase email")
|
|
19
|
+
.action(async (licenseKey) => {
|
|
20
|
+
const manager = await createDefaultLicenseManager();
|
|
21
|
+
try {
|
|
22
|
+
await runLogin(manager, licenseKey);
|
|
23
|
+
console.log("✓ Activated. Run `whydid license` any time to check status.");
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
27
|
+
console.error(`Manage your devices: ${PRICING_URL.replace("/pricing", "/account/devices")}`);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function createLogoutCommand() {
|
|
33
|
+
return new Command("logout")
|
|
34
|
+
.description("Deactivate this machine")
|
|
35
|
+
.action(async () => {
|
|
36
|
+
const manager = await createDefaultLicenseManager();
|
|
37
|
+
await runLogout(manager);
|
|
38
|
+
console.log("✓ Deactivated this machine.");
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function createLicenseCommand() {
|
|
42
|
+
return new Command("license")
|
|
43
|
+
.description("Show local license status (no network call)")
|
|
44
|
+
.action(async () => {
|
|
45
|
+
const manager = await createDefaultLicenseManager();
|
|
46
|
+
const status = await runLicenseStatus(manager);
|
|
47
|
+
if (!status) {
|
|
48
|
+
console.log("Not activated. Run `whydid login <licenseKey>`.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.log(`Product: ${status.product}`);
|
|
52
|
+
console.log(`Plan: ${status.plan}`);
|
|
53
|
+
console.log(`Features: ${status.features.join(", ")}`);
|
|
54
|
+
console.log(`Last validated: ${status.lastValidatedAt}`);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=license.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext, requireProjectId } from "../context.js";
|
|
3
|
+
export function runSetMilestone(projectRoot, stateId, label) {
|
|
4
|
+
const ctx = requireEngineContext(projectRoot);
|
|
5
|
+
requireProjectId(ctx);
|
|
6
|
+
const state = ctx.db.getState(stateId);
|
|
7
|
+
if (!state) {
|
|
8
|
+
ctx.db.close();
|
|
9
|
+
throw new Error(`Unknown state: ${stateId}`);
|
|
10
|
+
}
|
|
11
|
+
ctx.db.setStateMilestone(stateId, label);
|
|
12
|
+
ctx.db.close();
|
|
13
|
+
return { stateId, milestoneLabel: label };
|
|
14
|
+
}
|
|
15
|
+
export function runListMilestones(projectRoot) {
|
|
16
|
+
const ctx = requireEngineContext(projectRoot);
|
|
17
|
+
const projectId = requireProjectId(ctx);
|
|
18
|
+
const milestones = ctx.db.listMilestones(projectId);
|
|
19
|
+
ctx.db.close();
|
|
20
|
+
return milestones;
|
|
21
|
+
}
|
|
22
|
+
export function createMilestoneCommand() {
|
|
23
|
+
return new Command("milestone")
|
|
24
|
+
.description("Tag a state as a milestone (e.g. \"v1 launch\"), or list all tagged milestones")
|
|
25
|
+
.argument("[stateId]", "state id to tag — omit to list all milestones")
|
|
26
|
+
.argument("[label]", "milestone label — omit with --clear to remove")
|
|
27
|
+
.option("--clear", "remove the milestone tag from this state")
|
|
28
|
+
.action((stateId, label, opts) => {
|
|
29
|
+
const projectRoot = process.cwd();
|
|
30
|
+
if (!stateId) {
|
|
31
|
+
const milestones = runListMilestones(projectRoot);
|
|
32
|
+
if (milestones.length === 0) {
|
|
33
|
+
console.log("No milestones tagged yet. Run `whydid milestone <stateId> <label>` to tag one.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const m of milestones) {
|
|
37
|
+
console.log(`★ ${m.milestoneLabel} — #${m.id.slice(0, 8)} (${new Date(m.createdAt).toLocaleString()})`);
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (opts.clear) {
|
|
42
|
+
try {
|
|
43
|
+
runSetMilestone(projectRoot, stateId, null);
|
|
44
|
+
console.log(`✓ Removed milestone tag from #${stateId.slice(0, 8)}`);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
console.log(err instanceof Error ? err.message : String(err));
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!label) {
|
|
52
|
+
console.log("A label is required, e.g. `whydid milestone <stateId> \"v1 launch\"`, or pass --clear to remove one.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
runSetMilestone(projectRoot, stateId, label);
|
|
57
|
+
console.log(`★ Tagged #${stateId.slice(0, 8)} as milestone: "${label}"`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.log(err instanceof Error ? err.message : String(err));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=milestone.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
import { generateAgentPrompt } from "../../llm/agent-prompt.js";
|
|
9
|
+
export function createPromptCommand() {
|
|
10
|
+
return new Command("prompt")
|
|
11
|
+
.description("Generate a ready-to-paste prompt for your AI coding agent to revert or build on a past change")
|
|
12
|
+
.argument("<file>", "file path")
|
|
13
|
+
.option("--state <stateId>", "which state's change to target (default: the most recent change to this file)")
|
|
14
|
+
.option("--mode <mode>", "revert | continue", "revert")
|
|
15
|
+
.option("--instruction <text>", "required with --mode continue: what to build on top")
|
|
16
|
+
.action(async (file, opts) => {
|
|
17
|
+
const ctx = requireEngineContext(process.cwd());
|
|
18
|
+
const projectRow = ctx.db.raw.prepare(`SELECT * FROM projects ORDER BY created_at ASC LIMIT 1`).get();
|
|
19
|
+
if (!projectRow) {
|
|
20
|
+
console.log("whydid is not initialized here. Run `whydid init` first.");
|
|
21
|
+
ctx.db.close();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const mode = opts.mode;
|
|
25
|
+
if (mode !== "revert" && mode !== "continue") {
|
|
26
|
+
console.log(`Unknown mode: ${opts.mode}. Use "revert" or "continue".`);
|
|
27
|
+
ctx.db.close();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (mode === "continue" && !opts.instruction) {
|
|
31
|
+
console.log("`--mode continue` requires `--instruction \"what to build on top\"`.");
|
|
32
|
+
ctx.db.close();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const history = getFileHistory(ctx.db, projectRow.id, file);
|
|
36
|
+
if (history.length === 0) {
|
|
37
|
+
console.log(`No recorded state introduces changes to ${file}.`);
|
|
38
|
+
ctx.db.close();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const targetIndex = opts.state ? history.findIndex((h) => h.stateId === opts.state) : history.length - 1;
|
|
42
|
+
if (targetIndex === -1) {
|
|
43
|
+
console.log(`State ${opts.state} did not change ${file}. Run \`whydid why ${file}\` to see the real history.`);
|
|
44
|
+
ctx.db.close();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const targetEntry = history[targetIndex];
|
|
48
|
+
const laterEntries = history.slice(targetIndex + 1);
|
|
49
|
+
const config = loadConfig(ctx.whydidDir);
|
|
50
|
+
const provider = await createLLMProvider(config.ai, new FileSecretStore(ctx.whydidDir));
|
|
51
|
+
if (provider.id === "noop") {
|
|
52
|
+
console.log("Generating an agent prompt requires an LLM provider (set ai.provider in .whydid/config.json and the matching *_API_KEY env var).");
|
|
53
|
+
ctx.db.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const change = ctx.db.getFileChanges(targetEntry.stateId).find((c) => c.filePath === file);
|
|
57
|
+
const targetDiff = change ? computeFileDiff(ctx.store, change).unified : "(diff unavailable)";
|
|
58
|
+
try {
|
|
59
|
+
const prompt = await generateAgentPrompt(provider, {
|
|
60
|
+
filePath: file,
|
|
61
|
+
targetEntry,
|
|
62
|
+
laterEntries,
|
|
63
|
+
targetDiff,
|
|
64
|
+
mode,
|
|
65
|
+
continueInstruction: opts.instruction,
|
|
66
|
+
});
|
|
67
|
+
console.log(prompt);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.log(`Failed to generate a prompt: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
|
+
}
|
|
72
|
+
ctx.db.close();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { requireEngineContext, requireProjectId } from "../context.js";
|
|
3
|
+
import { computeRecap } from "../../core/recap.js";
|
|
4
|
+
function startOfToday() {
|
|
5
|
+
const now = new Date();
|
|
6
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
|
|
7
|
+
}
|
|
8
|
+
export function createRecapCommand() {
|
|
9
|
+
return new Command("recap")
|
|
10
|
+
.alias("doctor")
|
|
11
|
+
.description("Summarize today's AI-assisted changes: churn, risk, unexplained work")
|
|
12
|
+
.action(() => {
|
|
13
|
+
const ctx = requireEngineContext(process.cwd());
|
|
14
|
+
const projectId = requireProjectId(ctx);
|
|
15
|
+
const recap = computeRecap(ctx.db, projectId, startOfToday());
|
|
16
|
+
console.log("PROJECT RECAP\n");
|
|
17
|
+
console.log(`You made ${recap.totalChanges} AI-assisted change${recap.totalChanges === 1 ? "" : "s"} today.\n`);
|
|
18
|
+
if (recap.mainAreas.length > 0) {
|
|
19
|
+
console.log("Main areas:");
|
|
20
|
+
for (const area of recap.mainAreas)
|
|
21
|
+
console.log(`• ${area}`);
|
|
22
|
+
console.log("");
|
|
23
|
+
}
|
|
24
|
+
if (recap.highChurnFiles.length > 0) {
|
|
25
|
+
console.log("High-churn files:");
|
|
26
|
+
for (const f of recap.highChurnFiles)
|
|
27
|
+
console.log(`• ${f.filePath} — ${f.changes} change${f.changes === 1 ? "" : "s"}`);
|
|
28
|
+
console.log("");
|
|
29
|
+
}
|
|
30
|
+
if (recap.riskyAreas.length > 0) {
|
|
31
|
+
console.log("Potentially risky areas:");
|
|
32
|
+
for (const r of recap.riskyAreas)
|
|
33
|
+
console.log(`• ${r}`);
|
|
34
|
+
console.log("");
|
|
35
|
+
}
|
|
36
|
+
if (recap.latestTestRun) {
|
|
37
|
+
console.log("Tests:");
|
|
38
|
+
console.log(recap.latestTestRun.status === "passed" ? `✓ passed (${recap.latestTestRun.durationMs}ms)` : `✗ failed (exit ${recap.latestTestRun.exitCode})`);
|
|
39
|
+
console.log("");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log("Tests:\nNo test results recorded. Configure `test.command` and run `whydid test`.\n");
|
|
43
|
+
}
|
|
44
|
+
console.log(`Unexplained changes:\n${recap.unexplainedChanges}`);
|
|
45
|
+
ctx.db.close();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=recap.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { requireEngineContext, requireProjectId } from "../context.js";
|
|
4
|
+
import { createCheckpoint, restoreState, restoreFiles, forkState } from "../../core/engine.js";
|
|
5
|
+
export async function runCheckpoint(projectRoot) {
|
|
6
|
+
const ctx = requireEngineContext(projectRoot);
|
|
7
|
+
const result = await createCheckpoint(ctx);
|
|
8
|
+
ctx.db.close();
|
|
9
|
+
return { stateId: result.stateId };
|
|
10
|
+
}
|
|
11
|
+
export async function runRestore(projectRoot, targetStateId) {
|
|
12
|
+
const ctx = requireEngineContext(projectRoot);
|
|
13
|
+
const projectId = requireProjectId(ctx);
|
|
14
|
+
const result = await restoreState(ctx, projectId, targetStateId);
|
|
15
|
+
ctx.db.close();
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
export async function runRestoreFiles(projectRoot, targetStateId, filePaths) {
|
|
19
|
+
const ctx = requireEngineContext(projectRoot);
|
|
20
|
+
const projectId = requireProjectId(ctx);
|
|
21
|
+
const result = await restoreFiles(ctx, projectId, targetStateId, filePaths);
|
|
22
|
+
ctx.db.close();
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
export async function runFork(projectRoot, sourceStateId, name) {
|
|
26
|
+
const ctx = requireEngineContext(projectRoot);
|
|
27
|
+
const projectId = requireProjectId(ctx);
|
|
28
|
+
const result = await forkState(ctx, projectId, sourceStateId, name);
|
|
29
|
+
ctx.db.close();
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
async function confirm(message) {
|
|
33
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
35
|
+
rl.close();
|
|
36
|
+
return answer.trim().toLowerCase() === "y";
|
|
37
|
+
}
|
|
38
|
+
export function createRestoreCommand() {
|
|
39
|
+
return new Command("restore")
|
|
40
|
+
.description("Safely restore an earlier state (current state is preserved automatically)")
|
|
41
|
+
.argument("<stateId>", "state id to restore")
|
|
42
|
+
.option("--yes", "skip confirmation prompt")
|
|
43
|
+
.option("--only <paths>", "comma-separated list of file paths to restore, instead of the whole state")
|
|
44
|
+
.action(async (stateId, opts) => {
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
const ctx = requireEngineContext(projectRoot);
|
|
47
|
+
const projectId = requireProjectId(ctx);
|
|
48
|
+
const target = ctx.db.getState(stateId);
|
|
49
|
+
if (!target) {
|
|
50
|
+
console.log(`No state found matching "${stateId}". Run \`whydid timeline\` to see valid state ids.`);
|
|
51
|
+
ctx.db.close();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const fileCount = ctx.db.getStateManifest(stateId).length;
|
|
55
|
+
ctx.db.close();
|
|
56
|
+
const only = opts.only?.split(",").map((p) => p.trim()).filter(Boolean);
|
|
57
|
+
if (only && only.length > 0) {
|
|
58
|
+
console.log(`Restore ${only.length} file${only.length === 1 ? "" : "s"} from State #${stateId.slice(0, 8)} — ${target.label ?? target.stateType}?\n`);
|
|
59
|
+
only.forEach((p) => console.log(` ${p}`));
|
|
60
|
+
console.log("\nEverything else in your current state stays exactly as it is.");
|
|
61
|
+
if (!opts.yes && !(await confirm("\nProceed?"))) {
|
|
62
|
+
console.log("Cancelled.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const result = await runRestoreFiles(projectRoot, stateId, only);
|
|
66
|
+
console.log(`\n✓ Restored ${result.filesRestored.length} file${result.filesRestored.length === 1 ? "" : "s"} from #${stateId.slice(0, 8)}`);
|
|
67
|
+
console.log(`Your previous state was preserved as: #${result.safetyStateId.slice(0, 8)}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log(`Restore State #${stateId.slice(0, 8)} — ${target.label ?? target.stateType}?\n`);
|
|
71
|
+
console.log("Your current state will be preserved automatically.");
|
|
72
|
+
console.log(`${fileCount} file${fileCount === 1 ? "" : "s"} in the target state.\n`);
|
|
73
|
+
if (!opts.yes && !(await confirm("Proceed?"))) {
|
|
74
|
+
console.log("Cancelled.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const result = await runRestore(projectRoot, stateId);
|
|
78
|
+
console.log(`\n✓ Restored to #${stateId.slice(0, 8)}`);
|
|
79
|
+
console.log(`Your previous state was preserved as: #${result.safetyStateId.slice(0, 8)}`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function createForkCommand() {
|
|
83
|
+
return new Command("fork")
|
|
84
|
+
.description("Fork a new branch from a state")
|
|
85
|
+
.argument("<stateId>", "state id to fork from")
|
|
86
|
+
.argument("<name>", "fork name")
|
|
87
|
+
.action(async (stateId, name) => {
|
|
88
|
+
const projectRoot = process.cwd();
|
|
89
|
+
const ctx = requireEngineContext(projectRoot);
|
|
90
|
+
requireProjectId(ctx);
|
|
91
|
+
const target = ctx.db.getState(stateId);
|
|
92
|
+
ctx.db.close();
|
|
93
|
+
if (!target) {
|
|
94
|
+
console.log(`No state found matching "${stateId}". Run \`whydid timeline\` to see valid state ids.`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const result = await runFork(projectRoot, stateId, name);
|
|
98
|
+
console.log(`✓ Forked "${name}" from #${stateId.slice(0, 8)} → new state #${result.forkStateId.slice(0, 8)}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=restore-fork.js.map
|