@hasna/todos 0.11.43 → 0.11.44

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.
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerEnvironmentSnapshotCommands(program: Command): void;
3
+ //# sourceMappingURL=environment-snapshots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-snapshots.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/environment-snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWzC,wBAAgB,mCAAmC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+E1E"}
package/dist/cli/index.js CHANGED
@@ -26613,6 +26613,8 @@ var init_token_utils = __esm(() => {
26613
26613
  "check_file_lock",
26614
26614
  "create_comment",
26615
26615
  "create_handoff",
26616
+ "capture_environment_snapshot",
26617
+ "compare_environment_snapshots",
26616
26618
  "create_inbox_item",
26617
26619
  "delete_comment",
26618
26620
  "detect_file_relationships",
@@ -33135,6 +33137,334 @@ var init_templates2 = __esm(() => {
33135
33137
  init_tasks();
33136
33138
  });
33137
33139
 
33140
+ // src/lib/environment-snapshots.ts
33141
+ var exports_environment_snapshots = {};
33142
+ __export(exports_environment_snapshots, {
33143
+ writeEnvironmentSnapshot: () => writeEnvironmentSnapshot,
33144
+ recordEnvironmentSnapshot: () => recordEnvironmentSnapshot,
33145
+ readEnvironmentSnapshot: () => readEnvironmentSnapshot,
33146
+ compareEnvironmentSnapshots: () => compareEnvironmentSnapshots,
33147
+ compareEnvironmentSnapshotFiles: () => compareEnvironmentSnapshotFiles,
33148
+ captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
33149
+ });
33150
+ import { createHash as createHash6 } from "crypto";
33151
+ import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
33152
+ import { hostname, platform, arch } from "os";
33153
+ import { dirname as dirname10, join as join14, resolve as resolve12 } from "path";
33154
+ import { tmpdir as tmpdir2 } from "os";
33155
+ function sha2563(value) {
33156
+ return createHash6("sha256").update(value).digest("hex");
33157
+ }
33158
+ function fileRecord(root, relativePath) {
33159
+ const path = join14(root, relativePath);
33160
+ if (!existsSync13(path))
33161
+ return null;
33162
+ const stat = statSync6(path);
33163
+ if (!stat.isFile())
33164
+ return null;
33165
+ const content = readFileSync9(path);
33166
+ return { path: relativePath, sha256: sha2563(content), size_bytes: content.length };
33167
+ }
33168
+ function manifestRecord(root, relativePath) {
33169
+ const base = fileRecord(root, relativePath);
33170
+ if (!base)
33171
+ return null;
33172
+ const parsed = readJsonFile(join14(root, relativePath));
33173
+ if (!parsed)
33174
+ return { ...base, redacted: {} };
33175
+ const redacted = redactValue({
33176
+ name: parsed["name"] ?? null,
33177
+ version: parsed["version"] ?? null,
33178
+ packageManager: parsed["packageManager"] ?? null,
33179
+ scripts: parsed["scripts"] ?? {},
33180
+ dependencies: parsed["dependencies"] ?? {},
33181
+ devDependencies: parsed["devDependencies"] ?? {},
33182
+ peerDependencies: parsed["peerDependencies"] ?? {},
33183
+ optionalDependencies: parsed["optionalDependencies"] ?? {}
33184
+ });
33185
+ return { ...base, redacted };
33186
+ }
33187
+ function runLocalCommand(root, args) {
33188
+ const result = Bun.spawnSync({
33189
+ cmd: args,
33190
+ cwd: root,
33191
+ stdout: "pipe",
33192
+ stderr: "pipe",
33193
+ env: { PATH: process.env["PATH"] || "" }
33194
+ });
33195
+ return {
33196
+ exitCode: result.exitCode,
33197
+ stdout: redactEvidenceText(result.stdout.toString("utf8").trim()),
33198
+ stderr: redactEvidenceText(result.stderr.toString("utf8").trim())
33199
+ };
33200
+ }
33201
+ function summarizeGitStatus(lines) {
33202
+ const summary = { added: 0, modified: 0, deleted: 0, renamed: 0, untracked: 0 };
33203
+ for (const line of lines) {
33204
+ if (line.startsWith("??"))
33205
+ summary.untracked += 1;
33206
+ else if (line.includes("R"))
33207
+ summary.renamed += 1;
33208
+ else if (line.includes("D"))
33209
+ summary.deleted += 1;
33210
+ else if (line.includes("A"))
33211
+ summary.added += 1;
33212
+ else if (line.includes("M"))
33213
+ summary.modified += 1;
33214
+ }
33215
+ return summary;
33216
+ }
33217
+ function captureGit(root, warnings) {
33218
+ const inside = runLocalCommand(root, ["git", "rev-parse", "--is-inside-work-tree"]);
33219
+ if (inside.exitCode !== 0 || inside.stdout !== "true") {
33220
+ return { present: false, branch: null, commit: null, is_dirty: false, status_porcelain: [], status_summary: summarizeGitStatus([]) };
33221
+ }
33222
+ const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
33223
+ const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
33224
+ const status = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
33225
+ if (commit.exitCode !== 0)
33226
+ warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
33227
+ if (status.exitCode !== 0)
33228
+ warnings.push(`git status unavailable: ${status.stderr || status.stdout || "unknown error"}`);
33229
+ const lines = status.stdout ? status.stdout.split(/\r?\n/).filter(Boolean) : [];
33230
+ return {
33231
+ present: true,
33232
+ branch: branch.stdout || null,
33233
+ commit: commit.stdout || null,
33234
+ is_dirty: lines.length > 0,
33235
+ status_porcelain: lines,
33236
+ status_summary: summarizeGitStatus(lines)
33237
+ };
33238
+ }
33239
+ function packageManager(env, lockfiles) {
33240
+ const userAgent = (env["npm_config_user_agent"] || "").toLowerCase();
33241
+ if (userAgent.includes("bun"))
33242
+ return "bun";
33243
+ if (lockfiles.some((file) => file.path.startsWith("bun.lock")))
33244
+ return "bun";
33245
+ if (userAgent.includes("npm") || lockfiles.some((file) => file.path.includes("package-lock")))
33246
+ return "npm";
33247
+ return "unknown";
33248
+ }
33249
+ function isSecretEnvKey(key) {
33250
+ return /api[_-]?key|token|secret|password|credential|private|session|cookie/i.test(key);
33251
+ }
33252
+ function commandEnv(env, includeValues) {
33253
+ const keys = Object.keys(env).sort();
33254
+ const interesting = keys.filter((key) => isSecretEnvKey(key) || ["CI", "NODE_ENV", "BUN_ENV", "SHELL", "TERM", "PATH", "PWD", "USER", "npm_config_user_agent"].includes(key) || key.startsWith("TODOS_") || key.startsWith("BUN_"));
33255
+ const redactedKeys = interesting.filter(isSecretEnvKey);
33256
+ const values = includeValues ? Object.fromEntries(interesting.map((key) => [key, isSecretEnvKey(key) ? "[REDACTED]" : redactEvidenceText(String(env[key] ?? ""))])) : null;
33257
+ return {
33258
+ command: null,
33259
+ env_keys: interesting,
33260
+ env: values,
33261
+ redacted_keys: redactedKeys
33262
+ };
33263
+ }
33264
+ function defaultSnapshotDir() {
33265
+ const dbPath = getDatabasePath();
33266
+ if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
33267
+ return join14(tmpdir2(), "hasna-todos", "environment-snapshots");
33268
+ return join14(dirname10(resolve12(dbPath)), "environment-snapshots");
33269
+ }
33270
+ function snapshotWithId(snapshot) {
33271
+ const digest = sha2563(JSON.stringify(snapshot)).slice(0, 24);
33272
+ return { id: `env_${digest}`, ...snapshot };
33273
+ }
33274
+ function captureEnvironmentSnapshot(input = {}) {
33275
+ const root = resolve12(input.root || process.cwd());
33276
+ const env = input.env || process.env;
33277
+ const warnings = [];
33278
+ const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
33279
+ const lockfiles = LOCKFILES.map((file) => fileRecord(root, file)).filter((file) => Boolean(file));
33280
+ const configHashes = CONFIG_FILES.map((file) => fileRecord(root, file)).filter((file) => Boolean(file));
33281
+ const commandMetadata = commandEnv(env, Boolean(input.include_env_values));
33282
+ commandMetadata.command = input.command ? redactEvidenceText(input.command) : null;
33283
+ if (manifests.length === 0)
33284
+ warnings.push("no package manifest found");
33285
+ if (lockfiles.length === 0)
33286
+ warnings.push("no package lockfile found");
33287
+ return snapshotWithId({
33288
+ schema_version: 1,
33289
+ captured_at: input.now ? new Date(input.now).toISOString() : new Date().toISOString(),
33290
+ root,
33291
+ machine: { hostname: hostname(), platform: platform(), arch: arch() },
33292
+ target: {
33293
+ task_id: input.task_id ?? null,
33294
+ run_id: input.run_id ?? null,
33295
+ agent_id: input.agent_id ?? null
33296
+ },
33297
+ runtime: {
33298
+ bun: Bun.version || null,
33299
+ node: process.version,
33300
+ executable: process.execPath
33301
+ },
33302
+ package_manager: {
33303
+ manager: packageManager(env, lockfiles),
33304
+ user_agent: env["npm_config_user_agent"] ? redactEvidenceText(env["npm_config_user_agent"]) : null,
33305
+ manifests,
33306
+ lockfiles
33307
+ },
33308
+ git: captureGit(root, warnings),
33309
+ config_hashes: configHashes,
33310
+ command_env: commandMetadata,
33311
+ warnings
33312
+ });
33313
+ }
33314
+ function writeEnvironmentSnapshot(snapshot, outputPath) {
33315
+ const path = outputPath ? resolve12(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
33316
+ ensureDir2(dirname10(path));
33317
+ writeJsonFile(path, snapshot);
33318
+ return path;
33319
+ }
33320
+ function readEnvironmentSnapshot(path) {
33321
+ const snapshot = readJsonFile(resolve12(path));
33322
+ if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
33323
+ throw new Error(`Invalid environment snapshot: ${path}`);
33324
+ }
33325
+ return snapshot;
33326
+ }
33327
+ function recordEnvironmentSnapshot(input = {}, db) {
33328
+ let taskId = input.task_id;
33329
+ let runId = input.run_id;
33330
+ const needsDatabase = Boolean(taskId || runId);
33331
+ const d = needsDatabase ? db || getDatabase() : null;
33332
+ if (runId) {
33333
+ runId = resolveTaskRunId(runId, d);
33334
+ const run = getTaskRun(runId, d);
33335
+ if (!run)
33336
+ throw new Error(`Run not found: ${input.run_id}`);
33337
+ taskId = taskId || run.task_id;
33338
+ }
33339
+ if (taskId && d) {
33340
+ taskId = resolvePartialId(d, "tasks", taskId) || taskId;
33341
+ if (!getTask(taskId, d))
33342
+ throw new Error(`Task not found: ${taskId}`);
33343
+ }
33344
+ const snapshot = captureEnvironmentSnapshot({ ...input, task_id: taskId, run_id: runId });
33345
+ const outputPath = writeEnvironmentSnapshot(snapshot, input.output_path);
33346
+ let taskVerificationId = null;
33347
+ let runArtifactId = null;
33348
+ if (runId) {
33349
+ const artifact = addTaskRunArtifact({
33350
+ run_id: runId,
33351
+ path: outputPath,
33352
+ artifact_type: "environment_snapshot",
33353
+ description: "Reproducible local environment snapshot",
33354
+ metadata: { environment_snapshot_id: snapshot.id, schema_version: snapshot.schema_version },
33355
+ store_content: input.store_content ?? true,
33356
+ agent_id: input.agent_id
33357
+ }, d);
33358
+ runArtifactId = artifact.id;
33359
+ } else if (taskId) {
33360
+ const verification = addTaskVerification({
33361
+ task_id: taskId,
33362
+ command: input.command || "capture environment snapshot",
33363
+ status: "unknown",
33364
+ output_summary: `environment snapshot ${snapshot.id}`,
33365
+ artifact_path: outputPath,
33366
+ agent_id: input.agent_id,
33367
+ run_at: snapshot.captured_at
33368
+ }, d);
33369
+ taskVerificationId = verification.id;
33370
+ }
33371
+ return { snapshot, output_path: outputPath, task_verification_id: taskVerificationId, run_artifact_id: runArtifactId };
33372
+ }
33373
+ function keyed(files) {
33374
+ return new Map(files.map((file) => [file.path, file]));
33375
+ }
33376
+ function diffFiles(left, right) {
33377
+ const leftMap = keyed(left);
33378
+ const rightMap = keyed(right);
33379
+ const paths = [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort((a, b) => a.localeCompare(b));
33380
+ return paths.map((path) => ({ path, left_sha256: leftMap.get(path)?.sha256 ?? null, right_sha256: rightMap.get(path)?.sha256 ?? null })).filter((entry) => entry.left_sha256 !== entry.right_sha256);
33381
+ }
33382
+ function compareEnvironmentSnapshots(left, right) {
33383
+ const warnings = [];
33384
+ if (left.schema_version !== right.schema_version)
33385
+ warnings.push("snapshot schema versions differ");
33386
+ return {
33387
+ schema_version: 1,
33388
+ left_id: left.id,
33389
+ right_id: right.id,
33390
+ same_root: left.root === right.root,
33391
+ same_machine: left.machine.hostname === right.machine.hostname && left.machine.platform === right.machine.platform && left.machine.arch === right.machine.arch,
33392
+ same_runtime: left.runtime.bun === right.runtime.bun && left.runtime.node === right.runtime.node,
33393
+ same_git_commit: left.git.commit === right.git.commit,
33394
+ dirty_state_changed: left.git.is_dirty !== right.git.is_dirty,
33395
+ changed_config_hashes: diffFiles(left.config_hashes, right.config_hashes),
33396
+ changed_lockfiles: diffFiles(left.package_manager.lockfiles, right.package_manager.lockfiles),
33397
+ changed_manifests: diffFiles(left.package_manager.manifests, right.package_manager.manifests),
33398
+ warnings
33399
+ };
33400
+ }
33401
+ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
33402
+ return compareEnvironmentSnapshots(readEnvironmentSnapshot(leftPath), readEnvironmentSnapshot(rightPath));
33403
+ }
33404
+ var MANIFEST_FILES, LOCKFILES, CONFIG_FILES;
33405
+ var init_environment_snapshots = __esm(() => {
33406
+ init_task_runs();
33407
+ init_task_commits();
33408
+ init_database();
33409
+ init_tasks();
33410
+ init_sync_utils();
33411
+ MANIFEST_FILES = ["package.json", "dashboard/package.json", "sdk/package.json"];
33412
+ LOCKFILES = ["bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json"];
33413
+ CONFIG_FILES = [
33414
+ "AGENTS.md",
33415
+ "CLAUDE.md",
33416
+ "README.md",
33417
+ "SECURITY.md",
33418
+ "bunfig.toml",
33419
+ "tsconfig.json",
33420
+ "components.json",
33421
+ "next.config.js",
33422
+ "next.config.mjs",
33423
+ "next.config.ts",
33424
+ "vite.config.ts",
33425
+ "dashboard/vite.config.ts"
33426
+ ];
33427
+ });
33428
+
33429
+ // src/mcp/tools/environment-snapshots.ts
33430
+ function registerEnvironmentSnapshotTools(server, { shouldRegisterTool, formatError }) {
33431
+ if (shouldRegisterTool("capture_environment_snapshot")) {
33432
+ server.tool("capture_environment_snapshot", "Capture a local reproducible environment snapshot with Bun/node versions, package-manager state, git status, config hashes, command metadata, and redacted manifests. Optionally attach it to a local task or run.", {
33433
+ root: exports_external.string().optional(),
33434
+ task_id: exports_external.string().optional(),
33435
+ run_id: exports_external.string().optional(),
33436
+ agent_id: exports_external.string().optional(),
33437
+ command: exports_external.string().optional(),
33438
+ output_path: exports_external.string().optional(),
33439
+ include_env_values: exports_external.boolean().optional()
33440
+ }, async (params) => {
33441
+ try {
33442
+ const { recordEnvironmentSnapshot: recordEnvironmentSnapshot2 } = await Promise.resolve().then(() => (init_environment_snapshots(), exports_environment_snapshots));
33443
+ const result = recordEnvironmentSnapshot2(params);
33444
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
33445
+ } catch (e) {
33446
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33447
+ }
33448
+ });
33449
+ }
33450
+ if (shouldRegisterTool("compare_environment_snapshots")) {
33451
+ server.tool("compare_environment_snapshots", "Compare two local environment snapshot JSON files and report runtime, git, manifest, lockfile, and config hash drift.", {
33452
+ left_path: exports_external.string(),
33453
+ right_path: exports_external.string()
33454
+ }, async ({ left_path, right_path }) => {
33455
+ try {
33456
+ const { compareEnvironmentSnapshotFiles: compareEnvironmentSnapshotFiles2 } = await Promise.resolve().then(() => (init_environment_snapshots(), exports_environment_snapshots));
33457
+ return { content: [{ type: "text", text: JSON.stringify(compareEnvironmentSnapshotFiles2(left_path, right_path), null, 2) }] };
33458
+ } catch (e) {
33459
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
33460
+ }
33461
+ });
33462
+ }
33463
+ }
33464
+ var init_environment_snapshots2 = __esm(() => {
33465
+ init_zod();
33466
+ });
33467
+
33138
33468
  // src/mcp/index.ts
33139
33469
  var exports_mcp = {};
33140
33470
  __export(exports_mcp, {
@@ -33304,6 +33634,7 @@ var init_mcp = __esm(() => {
33304
33634
  init_machines2();
33305
33635
  init_agents2();
33306
33636
  init_templates2();
33637
+ init_environment_snapshots2();
33307
33638
  init_package_version();
33308
33639
  init_token_utils();
33309
33640
  if (hasVersionFlag()) {
@@ -33335,6 +33666,7 @@ var init_mcp = __esm(() => {
33335
33666
  registerCodeTools(server, toolContext);
33336
33667
  registerAgentTools(server, { ...toolContext, agentFocusMap });
33337
33668
  registerTemplateTools(server, toolContext);
33669
+ registerEnvironmentSnapshotTools(server, toolContext);
33338
33670
  registerMachineTools(server, { shouldRegisterTool, formatError });
33339
33671
  registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError });
33340
33672
  main().catch(async (err) => {
@@ -33354,43 +33686,43 @@ __export(exports_mcp_hooks_commands, {
33354
33686
  });
33355
33687
  import chalk8 from "chalk";
33356
33688
  import { execSync as execSync3 } from "child_process";
33357
- import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, chmodSync as chmodSync2 } from "fs";
33358
- import { dirname as dirname10, join as join14 } from "path";
33689
+ import { existsSync as existsSync14, readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8, chmodSync as chmodSync2 } from "fs";
33690
+ import { dirname as dirname11, join as join15 } from "path";
33359
33691
  function getMcpBinaryPath() {
33360
33692
  try {
33361
33693
  const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
33362
33694
  if (p)
33363
33695
  return p;
33364
33696
  } catch {}
33365
- const bunBin = join14(HOME2, ".bun", "bin", "todos-mcp");
33366
- if (existsSync13(bunBin))
33697
+ const bunBin = join15(HOME2, ".bun", "bin", "todos-mcp");
33698
+ if (existsSync14(bunBin))
33367
33699
  return bunBin;
33368
33700
  return "todos-mcp";
33369
33701
  }
33370
33702
  function readJsonFile2(path) {
33371
- if (!existsSync13(path))
33703
+ if (!existsSync14(path))
33372
33704
  return {};
33373
33705
  try {
33374
- return JSON.parse(readFileSync9(path, "utf-8"));
33706
+ return JSON.parse(readFileSync10(path, "utf-8"));
33375
33707
  } catch {
33376
33708
  return {};
33377
33709
  }
33378
33710
  }
33379
33711
  function writeJsonFile2(path, data) {
33380
- const dir = dirname10(path);
33381
- if (!existsSync13(dir))
33712
+ const dir = dirname11(path);
33713
+ if (!existsSync14(dir))
33382
33714
  mkdirSync8(dir, { recursive: true });
33383
33715
  writeFileSync6(path, JSON.stringify(data, null, 2) + `
33384
33716
  `);
33385
33717
  }
33386
33718
  function readTomlFile(path) {
33387
- if (!existsSync13(path))
33719
+ if (!existsSync14(path))
33388
33720
  return "";
33389
- return readFileSync9(path, "utf-8");
33721
+ return readFileSync10(path, "utf-8");
33390
33722
  }
33391
33723
  function writeTomlFile(path, content) {
33392
- const dir = dirname10(path);
33393
- if (!existsSync13(dir))
33724
+ const dir = dirname11(path);
33725
+ if (!existsSync14(dir))
33394
33726
  mkdirSync8(dir, { recursive: true });
33395
33727
  writeFileSync6(path, content);
33396
33728
  }
@@ -33456,7 +33788,7 @@ function unregisterClaude(_global) {
33456
33788
  }
33457
33789
  }
33458
33790
  function registerCodex(binPath) {
33459
- const configPath = join14(HOME2, ".codex", "config.toml");
33791
+ const configPath = join15(HOME2, ".codex", "config.toml");
33460
33792
  let content = readTomlFile(configPath);
33461
33793
  content = removeTomlBlock(content, "mcp_servers.todos");
33462
33794
  const block = `
@@ -33470,7 +33802,7 @@ args = []
33470
33802
  console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
33471
33803
  }
33472
33804
  function unregisterCodex() {
33473
- const configPath = join14(HOME2, ".codex", "config.toml");
33805
+ const configPath = join15(HOME2, ".codex", "config.toml");
33474
33806
  let content = readTomlFile(configPath);
33475
33807
  if (!content.includes("[mcp_servers.todos]")) {
33476
33808
  console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
@@ -33482,7 +33814,7 @@ function unregisterCodex() {
33482
33814
  console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
33483
33815
  }
33484
33816
  function registerGemini(binPath) {
33485
- const configPath = join14(HOME2, ".gemini", "settings.json");
33817
+ const configPath = join15(HOME2, ".gemini", "settings.json");
33486
33818
  const config = readJsonFile2(configPath);
33487
33819
  if (!config["mcpServers"]) {
33488
33820
  config["mcpServers"] = {};
@@ -33496,7 +33828,7 @@ function registerGemini(binPath) {
33496
33828
  console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
33497
33829
  }
33498
33830
  function unregisterGemini() {
33499
- const configPath = join14(HOME2, ".gemini", "settings.json");
33831
+ const configPath = join15(HOME2, ".gemini", "settings.json");
33500
33832
  const config = readJsonFile2(configPath);
33501
33833
  const servers = config["mcpServers"];
33502
33834
  if (!servers || !("todos" in servers)) {
@@ -33553,8 +33885,8 @@ function registerMcpHooksCommands(program2) {
33553
33885
  if (p)
33554
33886
  todosBin = p;
33555
33887
  } catch {}
33556
- const hooksDir = join14(process.cwd(), ".claude", "hooks");
33557
- if (!existsSync13(hooksDir))
33888
+ const hooksDir = join15(process.cwd(), ".claude", "hooks");
33889
+ if (!existsSync14(hooksDir))
33558
33890
  mkdirSync8(hooksDir, { recursive: true });
33559
33891
  const hookScript = `#!/usr/bin/env bash
33560
33892
  # Auto-generated by: todos hooks install
@@ -33579,11 +33911,11 @@ esac
33579
33911
 
33580
33912
  exit 0
33581
33913
  `;
33582
- const hookPath = join14(hooksDir, "todos-sync.sh");
33914
+ const hookPath = join15(hooksDir, "todos-sync.sh");
33583
33915
  writeFileSync6(hookPath, hookScript);
33584
33916
  execSync3(`chmod +x "${hookPath}"`);
33585
33917
  console.log(chalk8.green(`Hook script created: ${hookPath}`));
33586
- const settingsPath = join14(process.cwd(), ".claude", "settings.json");
33918
+ const settingsPath = join15(process.cwd(), ".claude", "settings.json");
33587
33919
  const settings = readJsonFile2(settingsPath);
33588
33920
  if (!settings["hooks"]) {
33589
33921
  settings["hooks"] = {};
@@ -34326,8 +34658,8 @@ Artifacts:`));
34326
34658
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
34327
34659
  const hookPath = `${gitDir}/hooks/post-commit`;
34328
34660
  const marker = "# todos-auto-link";
34329
- if (existsSync13(hookPath)) {
34330
- const existing = readFileSync9(hookPath, "utf-8");
34661
+ if (existsSync14(hookPath)) {
34662
+ const existing = readFileSync10(hookPath, "utf-8");
34331
34663
  if (existing.includes(marker)) {
34332
34664
  console.log(chalk8.yellow("Hook already installed."));
34333
34665
  return;
@@ -34354,11 +34686,11 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
34354
34686
  const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
34355
34687
  const hookPath = `${gitDir}/hooks/post-commit`;
34356
34688
  const marker = "# todos-auto-link";
34357
- if (!existsSync13(hookPath)) {
34689
+ if (!existsSync14(hookPath)) {
34358
34690
  console.log(chalk8.dim("No post-commit hook found."));
34359
34691
  return;
34360
34692
  }
34361
- const content = readFileSync9(hookPath, "utf-8");
34693
+ const content = readFileSync10(hookPath, "utf-8");
34362
34694
  if (!content.includes(marker)) {
34363
34695
  console.log(chalk8.dim("Hook not managed by todos."));
34364
34696
  return;
@@ -34535,8 +34867,8 @@ __export(exports_machines2, {
34535
34867
  import chalk10 from "chalk";
34536
34868
  import { execSync as execSync4 } from "child_process";
34537
34869
  import { writeFileSync as writeFileSync7 } from "fs";
34538
- import { tmpdir as tmpdir2 } from "os";
34539
- import { join as join15 } from "path";
34870
+ import { tmpdir as tmpdir3 } from "os";
34871
+ import { join as join16 } from "path";
34540
34872
  function getOrCreateLocalMachineName() {
34541
34873
  return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
34542
34874
  }
@@ -34730,7 +35062,7 @@ Warning: No primary machine set.`));
34730
35062
  if (opts.push) {
34731
35063
  try {
34732
35064
  const localTasks = listTasks3();
34733
- const tmpFile = join15(tmpdir2(), `todos-export-${uuid()}.json`);
35065
+ const tmpFile = join16(tmpdir3(), `todos-export-${uuid()}.json`);
34734
35066
  writeFileSync7(tmpFile, JSON.stringify(localTasks, null, 2));
34735
35067
  execSync4(`scp ${tmpFile} ${ssh}:/tmp/todos-import.json`, { timeout: 15000 });
34736
35068
  const importCmd = `ssh ${ssh} 'node -e "const fs=require(\\'fs\\');const tasks=JSON.parse(fs.readFileSync(\\'/tmp/todos-import.json\\',\\'utf-8\\'));console.log(JSON.stringify(tasks.length))"'`;
@@ -34880,6 +35212,84 @@ var init_api_key_commands = __esm(() => {
34880
35212
  init_helpers();
34881
35213
  });
34882
35214
 
35215
+ // src/cli/commands/environment-snapshots.ts
35216
+ var exports_environment_snapshots2 = {};
35217
+ __export(exports_environment_snapshots2, {
35218
+ registerEnvironmentSnapshotCommands: () => registerEnvironmentSnapshotCommands
35219
+ });
35220
+ import chalk12 from "chalk";
35221
+ function printJson(value) {
35222
+ console.log(JSON.stringify(value, null, 2));
35223
+ }
35224
+ function registerEnvironmentSnapshotCommands(program2) {
35225
+ const envCmd = program2.command("env-snapshot").alias("environment-snapshot").description("Capture and compare local reproducible environment snapshots");
35226
+ envCmd.command("capture").description("Capture runtime, package-manager, git, config hash, and redacted environment metadata").option("--root <path>", "Project root to inspect").option("--task <id>", "Attach snapshot evidence to a task").option("--run <id>", "Attach snapshot artifact to a task run").option("--agent <name>", "Agent name for attached evidence").option("--command <command>", "Command or verification step this snapshot explains").option("--output <path>", "Write snapshot JSON to a specific path").option("--include-env-values", "Include nonsecret environment values; secret-like keys are still redacted").action((opts) => {
35227
+ const globalOpts = program2.opts();
35228
+ try {
35229
+ const result = recordEnvironmentSnapshot({
35230
+ root: opts.root,
35231
+ task_id: opts.task,
35232
+ run_id: opts.run,
35233
+ agent_id: opts.agent || globalOpts.agent,
35234
+ command: opts.command,
35235
+ output_path: opts.output,
35236
+ include_env_values: Boolean(opts.includeEnvValues)
35237
+ });
35238
+ if (globalOpts.json) {
35239
+ printJson(result);
35240
+ return;
35241
+ }
35242
+ console.log(chalk12.green("Captured") + ` ${result.snapshot.id}`);
35243
+ console.log(`path: ${result.output_path}`);
35244
+ console.log(`root: ${result.snapshot.root}`);
35245
+ console.log(`git: ${result.snapshot.git.commit || "none"}${result.snapshot.git.is_dirty ? " dirty" : ""}`);
35246
+ console.log(`runtime: bun ${result.snapshot.runtime.bun || "unknown"} / node ${result.snapshot.runtime.node}`);
35247
+ if (result.run_artifact_id)
35248
+ console.log(`run artifact: ${result.run_artifact_id}`);
35249
+ if (result.task_verification_id)
35250
+ console.log(`task verification: ${result.task_verification_id}`);
35251
+ for (const warning of result.snapshot.warnings)
35252
+ console.log(chalk12.yellow(`warning: ${warning}`));
35253
+ } catch (error) {
35254
+ const message = error instanceof Error ? error.message : String(error);
35255
+ if (globalOpts.json)
35256
+ printJson({ error: message });
35257
+ else
35258
+ console.error(chalk12.red(`Error: ${message}`));
35259
+ process.exit(1);
35260
+ }
35261
+ });
35262
+ envCmd.command("compare").description("Compare two environment snapshot JSON files").argument("<left>", "Left snapshot JSON path").argument("<right>", "Right snapshot JSON path").action((left, right) => {
35263
+ const globalOpts = program2.opts();
35264
+ try {
35265
+ const comparison = compareEnvironmentSnapshotFiles(left, right);
35266
+ if (globalOpts.json) {
35267
+ printJson(comparison);
35268
+ return;
35269
+ }
35270
+ console.log(`left: ${comparison.left_id}`);
35271
+ console.log(`right: ${comparison.right_id}`);
35272
+ console.log(`same root: ${comparison.same_root}`);
35273
+ console.log(`same machine: ${comparison.same_machine}`);
35274
+ console.log(`same runtime: ${comparison.same_runtime}`);
35275
+ console.log(`same git commit: ${comparison.same_git_commit}`);
35276
+ console.log(`dirty state changed: ${comparison.dirty_state_changed}`);
35277
+ const changed = comparison.changed_config_hashes.length + comparison.changed_lockfiles.length + comparison.changed_manifests.length;
35278
+ console.log(`changed files: ${changed}`);
35279
+ } catch (error) {
35280
+ const message = error instanceof Error ? error.message : String(error);
35281
+ if (globalOpts.json)
35282
+ printJson({ error: message });
35283
+ else
35284
+ console.error(chalk12.red(`Error: ${message}`));
35285
+ process.exit(1);
35286
+ }
35287
+ });
35288
+ }
35289
+ var init_environment_snapshots3 = __esm(() => {
35290
+ init_environment_snapshots();
35291
+ });
35292
+
34883
35293
  // src/cli/index.tsx
34884
35294
  init_esm();
34885
35295
  init_package_version();
@@ -34895,7 +35305,8 @@ var [
34895
35305
  { registerMcpHooksCommands: registerMcpHooksCommands2 },
34896
35306
  { registerDispatchCommands: registerDispatchCommands2 },
34897
35307
  { registerMachineCommands: registerMachineCommands2 },
34898
- { registerApiKeyCommands: registerApiKeyCommands2 }
35308
+ { registerApiKeyCommands: registerApiKeyCommands2 },
35309
+ { registerEnvironmentSnapshotCommands: registerEnvironmentSnapshotCommands2 }
34899
35310
  ] = await Promise.all([
34900
35311
  Promise.resolve().then(() => (init_task_commands(), exports_task_commands)),
34901
35312
  Promise.resolve().then(() => (init_plan_template_commands(), exports_plan_template_commands)),
@@ -34906,7 +35317,8 @@ var [
34906
35317
  Promise.resolve().then(() => (init_mcp_hooks_commands(), exports_mcp_hooks_commands)),
34907
35318
  Promise.resolve().then(() => (init_dispatch3(), exports_dispatch2)),
34908
35319
  Promise.resolve().then(() => (init_machines3(), exports_machines2)),
34909
- Promise.resolve().then(() => (init_api_key_commands(), exports_api_key_commands))
35320
+ Promise.resolve().then(() => (init_api_key_commands(), exports_api_key_commands)),
35321
+ Promise.resolve().then(() => (init_environment_snapshots3(), exports_environment_snapshots2))
34910
35322
  ]);
34911
35323
  registerTaskCommands2(program2);
34912
35324
  registerPlanTemplateCommands2(program2);
@@ -34918,4 +35330,5 @@ registerMcpHooksCommands2(program2);
34918
35330
  registerDispatchCommands2(program2);
34919
35331
  registerMachineCommands2(program2);
34920
35332
  registerApiKeyCommands2(program2);
35333
+ registerEnvironmentSnapshotCommands2(program2);
34921
35334
  program2.parse();
@@ -1,4 +1,4 @@
1
- export type TodosCliMcpParityDomain = "tasks" | "local-fields" | "dedupe" | "verification-providers" | "projects" | "plans" | "templates" | "workspace-trust" | "runner-sandbox" | "policy-packs" | "approval-gates" | "local-event-hooks" | "encryption" | "agent-runs" | "handoffs" | "runs" | "comments" | "search" | "context-packs" | "imports" | "exports";
1
+ export type TodosCliMcpParityDomain = "tasks" | "local-fields" | "dedupe" | "verification-providers" | "projects" | "plans" | "templates" | "workspace-trust" | "runner-sandbox" | "policy-packs" | "approval-gates" | "local-event-hooks" | "encryption" | "agent-runs" | "handoffs" | "runs" | "comments" | "search" | "context-packs" | "environment-snapshots" | "imports" | "exports";
2
2
  export type TodosCliMcpParityStatus = "matched" | "intentional-gap";
3
3
  export interface CreateCliMcpParityManifestOptions {
4
4
  version?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"cli-mcp-parity.d.ts","sourceRoot":"","sources":["../src/cli-mcp-parity.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,uBAAuB,GAC/B,OAAO,GACP,cAAc,GACd,QAAQ,GACR,wBAAwB,GACxB,UAAU,GACV,OAAO,GACP,WAAW,GACX,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,gBAAgB,GAChB,mBAAmB,GACnB,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,MAAM,GACN,UAAU,GACV,QAAQ,GACR,eAAe,GACf,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,iBAAiB,CAAC;AAEpE,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,aAAa,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,uBAAuB,CAAC;IAChC,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,8BAA8B,CAAC;IACxC,SAAS,EAAE,IAAI,CAAC;IAChB,iBAAiB,EAAE,IAAI,CAAC;IACxB,MAAM,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAUD,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,EAsjBxD,CAAC;AAYF,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,iCAAsC,GAC9C,yBAAyB,CAW3B;AAED,eAAO,MAAM,6BAA6B,2BAExC,CAAC"}
1
+ {"version":3,"file":"cli-mcp-parity.d.ts","sourceRoot":"","sources":["../src/cli-mcp-parity.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,uBAAuB,GAC/B,OAAO,GACP,cAAc,GACd,QAAQ,GACR,wBAAwB,GACxB,UAAU,GACV,OAAO,GACP,WAAW,GACX,iBAAiB,GACjB,gBAAgB,GAChB,cAAc,GACd,gBAAgB,GAChB,mBAAmB,GACnB,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,MAAM,GACN,UAAU,GACV,QAAQ,GACR,eAAe,GACf,uBAAuB,GACvB,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,MAAM,uBAAuB,GAAG,SAAS,GAAG,iBAAiB,CAAC;AAEpE,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,aAAa,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,EAAE,uBAAuB,CAAC;IAChC,eAAe,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,CAAC,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,8BAA8B,CAAC;IACxC,SAAS,EAAE,IAAI,CAAC;IAChB,iBAAiB,EAAE,IAAI,CAAC;IACxB,MAAM,EAAE,sBAAsB,EAAE,CAAC;CAClC;AAUD,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,EAkkBxD,CAAC;AAYF,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,iCAAsC,GAC9C,yBAAyB,CAW3B;AAED,eAAO,MAAM,6BAA6B,2BAExC,CAAC"}