@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.
package/dist/mcp/index.js CHANGED
@@ -10014,6 +10014,295 @@ var init_builtin_templates = __esm(() => {
10014
10014
  ];
10015
10015
  });
10016
10016
 
10017
+ // src/lib/environment-snapshots.ts
10018
+ var exports_environment_snapshots = {};
10019
+ __export(exports_environment_snapshots, {
10020
+ writeEnvironmentSnapshot: () => writeEnvironmentSnapshot,
10021
+ recordEnvironmentSnapshot: () => recordEnvironmentSnapshot,
10022
+ readEnvironmentSnapshot: () => readEnvironmentSnapshot,
10023
+ compareEnvironmentSnapshots: () => compareEnvironmentSnapshots,
10024
+ compareEnvironmentSnapshotFiles: () => compareEnvironmentSnapshotFiles,
10025
+ captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
10026
+ });
10027
+ import { createHash as createHash5 } from "crypto";
10028
+ import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync6 } from "fs";
10029
+ import { hostname, platform, arch } from "os";
10030
+ import { dirname as dirname7, join as join7, resolve as resolve9 } from "path";
10031
+ import { tmpdir as tmpdir2 } from "os";
10032
+ function sha2563(value) {
10033
+ return createHash5("sha256").update(value).digest("hex");
10034
+ }
10035
+ function fileRecord(root, relativePath) {
10036
+ const path = join7(root, relativePath);
10037
+ if (!existsSync8(path))
10038
+ return null;
10039
+ const stat = statSync6(path);
10040
+ if (!stat.isFile())
10041
+ return null;
10042
+ const content = readFileSync6(path);
10043
+ return { path: relativePath, sha256: sha2563(content), size_bytes: content.length };
10044
+ }
10045
+ function manifestRecord(root, relativePath) {
10046
+ const base = fileRecord(root, relativePath);
10047
+ if (!base)
10048
+ return null;
10049
+ const parsed = readJsonFile(join7(root, relativePath));
10050
+ if (!parsed)
10051
+ return { ...base, redacted: {} };
10052
+ const redacted = redactValue({
10053
+ name: parsed["name"] ?? null,
10054
+ version: parsed["version"] ?? null,
10055
+ packageManager: parsed["packageManager"] ?? null,
10056
+ scripts: parsed["scripts"] ?? {},
10057
+ dependencies: parsed["dependencies"] ?? {},
10058
+ devDependencies: parsed["devDependencies"] ?? {},
10059
+ peerDependencies: parsed["peerDependencies"] ?? {},
10060
+ optionalDependencies: parsed["optionalDependencies"] ?? {}
10061
+ });
10062
+ return { ...base, redacted };
10063
+ }
10064
+ function runLocalCommand(root, args) {
10065
+ const result = Bun.spawnSync({
10066
+ cmd: args,
10067
+ cwd: root,
10068
+ stdout: "pipe",
10069
+ stderr: "pipe",
10070
+ env: { PATH: process.env["PATH"] || "" }
10071
+ });
10072
+ return {
10073
+ exitCode: result.exitCode,
10074
+ stdout: redactEvidenceText(result.stdout.toString("utf8").trim()),
10075
+ stderr: redactEvidenceText(result.stderr.toString("utf8").trim())
10076
+ };
10077
+ }
10078
+ function summarizeGitStatus(lines) {
10079
+ const summary = { added: 0, modified: 0, deleted: 0, renamed: 0, untracked: 0 };
10080
+ for (const line of lines) {
10081
+ if (line.startsWith("??"))
10082
+ summary.untracked += 1;
10083
+ else if (line.includes("R"))
10084
+ summary.renamed += 1;
10085
+ else if (line.includes("D"))
10086
+ summary.deleted += 1;
10087
+ else if (line.includes("A"))
10088
+ summary.added += 1;
10089
+ else if (line.includes("M"))
10090
+ summary.modified += 1;
10091
+ }
10092
+ return summary;
10093
+ }
10094
+ function captureGit(root, warnings) {
10095
+ const inside = runLocalCommand(root, ["git", "rev-parse", "--is-inside-work-tree"]);
10096
+ if (inside.exitCode !== 0 || inside.stdout !== "true") {
10097
+ return { present: false, branch: null, commit: null, is_dirty: false, status_porcelain: [], status_summary: summarizeGitStatus([]) };
10098
+ }
10099
+ const branch = runLocalCommand(root, ["git", "branch", "--show-current"]);
10100
+ const commit = runLocalCommand(root, ["git", "rev-parse", "HEAD"]);
10101
+ const status = runLocalCommand(root, ["git", "status", "--porcelain=v1"]);
10102
+ if (commit.exitCode !== 0)
10103
+ warnings.push(`git commit unavailable: ${commit.stderr || commit.stdout || "unknown error"}`);
10104
+ if (status.exitCode !== 0)
10105
+ warnings.push(`git status unavailable: ${status.stderr || status.stdout || "unknown error"}`);
10106
+ const lines = status.stdout ? status.stdout.split(/\r?\n/).filter(Boolean) : [];
10107
+ return {
10108
+ present: true,
10109
+ branch: branch.stdout || null,
10110
+ commit: commit.stdout || null,
10111
+ is_dirty: lines.length > 0,
10112
+ status_porcelain: lines,
10113
+ status_summary: summarizeGitStatus(lines)
10114
+ };
10115
+ }
10116
+ function packageManager(env, lockfiles) {
10117
+ const userAgent = (env["npm_config_user_agent"] || "").toLowerCase();
10118
+ if (userAgent.includes("bun"))
10119
+ return "bun";
10120
+ if (lockfiles.some((file) => file.path.startsWith("bun.lock")))
10121
+ return "bun";
10122
+ if (userAgent.includes("npm") || lockfiles.some((file) => file.path.includes("package-lock")))
10123
+ return "npm";
10124
+ return "unknown";
10125
+ }
10126
+ function isSecretEnvKey(key) {
10127
+ return /api[_-]?key|token|secret|password|credential|private|session|cookie/i.test(key);
10128
+ }
10129
+ function commandEnv(env, includeValues) {
10130
+ const keys = Object.keys(env).sort();
10131
+ 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_"));
10132
+ const redactedKeys = interesting.filter(isSecretEnvKey);
10133
+ const values = includeValues ? Object.fromEntries(interesting.map((key) => [key, isSecretEnvKey(key) ? "[REDACTED]" : redactEvidenceText(String(env[key] ?? ""))])) : null;
10134
+ return {
10135
+ command: null,
10136
+ env_keys: interesting,
10137
+ env: values,
10138
+ redacted_keys: redactedKeys
10139
+ };
10140
+ }
10141
+ function defaultSnapshotDir() {
10142
+ const dbPath = getDatabasePath();
10143
+ if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
10144
+ return join7(tmpdir2(), "hasna-todos", "environment-snapshots");
10145
+ return join7(dirname7(resolve9(dbPath)), "environment-snapshots");
10146
+ }
10147
+ function snapshotWithId(snapshot) {
10148
+ const digest = sha2563(JSON.stringify(snapshot)).slice(0, 24);
10149
+ return { id: `env_${digest}`, ...snapshot };
10150
+ }
10151
+ function captureEnvironmentSnapshot(input = {}) {
10152
+ const root = resolve9(input.root || process.cwd());
10153
+ const env = input.env || process.env;
10154
+ const warnings = [];
10155
+ const manifests = MANIFEST_FILES.map((file) => manifestRecord(root, file)).filter((file) => Boolean(file));
10156
+ const lockfiles = LOCKFILES.map((file) => fileRecord(root, file)).filter((file) => Boolean(file));
10157
+ const configHashes = CONFIG_FILES.map((file) => fileRecord(root, file)).filter((file) => Boolean(file));
10158
+ const commandMetadata = commandEnv(env, Boolean(input.include_env_values));
10159
+ commandMetadata.command = input.command ? redactEvidenceText(input.command) : null;
10160
+ if (manifests.length === 0)
10161
+ warnings.push("no package manifest found");
10162
+ if (lockfiles.length === 0)
10163
+ warnings.push("no package lockfile found");
10164
+ return snapshotWithId({
10165
+ schema_version: 1,
10166
+ captured_at: input.now ? new Date(input.now).toISOString() : new Date().toISOString(),
10167
+ root,
10168
+ machine: { hostname: hostname(), platform: platform(), arch: arch() },
10169
+ target: {
10170
+ task_id: input.task_id ?? null,
10171
+ run_id: input.run_id ?? null,
10172
+ agent_id: input.agent_id ?? null
10173
+ },
10174
+ runtime: {
10175
+ bun: Bun.version || null,
10176
+ node: process.version,
10177
+ executable: process.execPath
10178
+ },
10179
+ package_manager: {
10180
+ manager: packageManager(env, lockfiles),
10181
+ user_agent: env["npm_config_user_agent"] ? redactEvidenceText(env["npm_config_user_agent"]) : null,
10182
+ manifests,
10183
+ lockfiles
10184
+ },
10185
+ git: captureGit(root, warnings),
10186
+ config_hashes: configHashes,
10187
+ command_env: commandMetadata,
10188
+ warnings
10189
+ });
10190
+ }
10191
+ function writeEnvironmentSnapshot(snapshot, outputPath) {
10192
+ const path = outputPath ? resolve9(outputPath) : join7(defaultSnapshotDir(), `${snapshot.id}.json`);
10193
+ ensureDir2(dirname7(path));
10194
+ writeJsonFile(path, snapshot);
10195
+ return path;
10196
+ }
10197
+ function readEnvironmentSnapshot(path) {
10198
+ const snapshot = readJsonFile(resolve9(path));
10199
+ if (!snapshot || snapshot.schema_version !== 1 || typeof snapshot.id !== "string") {
10200
+ throw new Error(`Invalid environment snapshot: ${path}`);
10201
+ }
10202
+ return snapshot;
10203
+ }
10204
+ function recordEnvironmentSnapshot(input = {}, db) {
10205
+ let taskId = input.task_id;
10206
+ let runId = input.run_id;
10207
+ const needsDatabase = Boolean(taskId || runId);
10208
+ const d = needsDatabase ? db || getDatabase() : null;
10209
+ if (runId) {
10210
+ runId = resolveTaskRunId(runId, d);
10211
+ const run = getTaskRun(runId, d);
10212
+ if (!run)
10213
+ throw new Error(`Run not found: ${input.run_id}`);
10214
+ taskId = taskId || run.task_id;
10215
+ }
10216
+ if (taskId && d) {
10217
+ taskId = resolvePartialId(d, "tasks", taskId) || taskId;
10218
+ if (!getTask(taskId, d))
10219
+ throw new Error(`Task not found: ${taskId}`);
10220
+ }
10221
+ const snapshot = captureEnvironmentSnapshot({ ...input, task_id: taskId, run_id: runId });
10222
+ const outputPath = writeEnvironmentSnapshot(snapshot, input.output_path);
10223
+ let taskVerificationId = null;
10224
+ let runArtifactId = null;
10225
+ if (runId) {
10226
+ const artifact = addTaskRunArtifact({
10227
+ run_id: runId,
10228
+ path: outputPath,
10229
+ artifact_type: "environment_snapshot",
10230
+ description: "Reproducible local environment snapshot",
10231
+ metadata: { environment_snapshot_id: snapshot.id, schema_version: snapshot.schema_version },
10232
+ store_content: input.store_content ?? true,
10233
+ agent_id: input.agent_id
10234
+ }, d);
10235
+ runArtifactId = artifact.id;
10236
+ } else if (taskId) {
10237
+ const verification = addTaskVerification({
10238
+ task_id: taskId,
10239
+ command: input.command || "capture environment snapshot",
10240
+ status: "unknown",
10241
+ output_summary: `environment snapshot ${snapshot.id}`,
10242
+ artifact_path: outputPath,
10243
+ agent_id: input.agent_id,
10244
+ run_at: snapshot.captured_at
10245
+ }, d);
10246
+ taskVerificationId = verification.id;
10247
+ }
10248
+ return { snapshot, output_path: outputPath, task_verification_id: taskVerificationId, run_artifact_id: runArtifactId };
10249
+ }
10250
+ function keyed(files) {
10251
+ return new Map(files.map((file) => [file.path, file]));
10252
+ }
10253
+ function diffFiles(left, right) {
10254
+ const leftMap = keyed(left);
10255
+ const rightMap = keyed(right);
10256
+ const paths = [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort((a, b) => a.localeCompare(b));
10257
+ 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);
10258
+ }
10259
+ function compareEnvironmentSnapshots(left, right) {
10260
+ const warnings = [];
10261
+ if (left.schema_version !== right.schema_version)
10262
+ warnings.push("snapshot schema versions differ");
10263
+ return {
10264
+ schema_version: 1,
10265
+ left_id: left.id,
10266
+ right_id: right.id,
10267
+ same_root: left.root === right.root,
10268
+ same_machine: left.machine.hostname === right.machine.hostname && left.machine.platform === right.machine.platform && left.machine.arch === right.machine.arch,
10269
+ same_runtime: left.runtime.bun === right.runtime.bun && left.runtime.node === right.runtime.node,
10270
+ same_git_commit: left.git.commit === right.git.commit,
10271
+ dirty_state_changed: left.git.is_dirty !== right.git.is_dirty,
10272
+ changed_config_hashes: diffFiles(left.config_hashes, right.config_hashes),
10273
+ changed_lockfiles: diffFiles(left.package_manager.lockfiles, right.package_manager.lockfiles),
10274
+ changed_manifests: diffFiles(left.package_manager.manifests, right.package_manager.manifests),
10275
+ warnings
10276
+ };
10277
+ }
10278
+ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
10279
+ return compareEnvironmentSnapshots(readEnvironmentSnapshot(leftPath), readEnvironmentSnapshot(rightPath));
10280
+ }
10281
+ var MANIFEST_FILES, LOCKFILES, CONFIG_FILES;
10282
+ var init_environment_snapshots = __esm(() => {
10283
+ init_task_runs();
10284
+ init_task_commits();
10285
+ init_database();
10286
+ init_tasks();
10287
+ init_sync_utils();
10288
+ MANIFEST_FILES = ["package.json", "dashboard/package.json", "sdk/package.json"];
10289
+ LOCKFILES = ["bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json"];
10290
+ CONFIG_FILES = [
10291
+ "AGENTS.md",
10292
+ "CLAUDE.md",
10293
+ "README.md",
10294
+ "SECURITY.md",
10295
+ "bunfig.toml",
10296
+ "tsconfig.json",
10297
+ "components.json",
10298
+ "next.config.js",
10299
+ "next.config.mjs",
10300
+ "next.config.ts",
10301
+ "vite.config.ts",
10302
+ "dashboard/vite.config.ts"
10303
+ ];
10304
+ });
10305
+
10017
10306
  // src/mcp/index.ts
10018
10307
  init_agents();
10019
10308
  init_database();
@@ -14385,6 +14674,8 @@ var MCP_TOOL_GROUPS = {
14385
14674
  "check_file_lock",
14386
14675
  "create_comment",
14387
14676
  "create_handoff",
14677
+ "capture_environment_snapshot",
14678
+ "compare_environment_snapshots",
14388
14679
  "create_inbox_item",
14389
14680
  "delete_comment",
14390
14681
  "detect_file_relationships",
@@ -21853,19 +22144,55 @@ ${lines.join(`
21853
22144
  }
21854
22145
  }
21855
22146
 
22147
+ // src/mcp/tools/environment-snapshots.ts
22148
+ function registerEnvironmentSnapshotTools(server, { shouldRegisterTool, formatError }) {
22149
+ if (shouldRegisterTool("capture_environment_snapshot")) {
22150
+ 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.", {
22151
+ root: exports_external.string().optional(),
22152
+ task_id: exports_external.string().optional(),
22153
+ run_id: exports_external.string().optional(),
22154
+ agent_id: exports_external.string().optional(),
22155
+ command: exports_external.string().optional(),
22156
+ output_path: exports_external.string().optional(),
22157
+ include_env_values: exports_external.boolean().optional()
22158
+ }, async (params) => {
22159
+ try {
22160
+ const { recordEnvironmentSnapshot: recordEnvironmentSnapshot2 } = await Promise.resolve().then(() => (init_environment_snapshots(), exports_environment_snapshots));
22161
+ const result = recordEnvironmentSnapshot2(params);
22162
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
22163
+ } catch (e) {
22164
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
22165
+ }
22166
+ });
22167
+ }
22168
+ if (shouldRegisterTool("compare_environment_snapshots")) {
22169
+ server.tool("compare_environment_snapshots", "Compare two local environment snapshot JSON files and report runtime, git, manifest, lockfile, and config hash drift.", {
22170
+ left_path: exports_external.string(),
22171
+ right_path: exports_external.string()
22172
+ }, async ({ left_path, right_path }) => {
22173
+ try {
22174
+ const { compareEnvironmentSnapshotFiles: compareEnvironmentSnapshotFiles2 } = await Promise.resolve().then(() => (init_environment_snapshots(), exports_environment_snapshots));
22175
+ return { content: [{ type: "text", text: JSON.stringify(compareEnvironmentSnapshotFiles2(left_path, right_path), null, 2) }] };
22176
+ } catch (e) {
22177
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
22178
+ }
22179
+ });
22180
+ }
22181
+ }
22182
+
21856
22183
  // src/lib/package-version.ts
21857
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
21858
- import { dirname as dirname7, join as join7 } from "path";
22184
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
22185
+ import { dirname as dirname8, join as join8 } from "path";
21859
22186
  import { fileURLToPath } from "url";
21860
22187
  function getPackageVersion(fromUrl = import.meta.url) {
21861
22188
  try {
21862
- let dir = dirname7(fileURLToPath(fromUrl));
22189
+ let dir = dirname8(fileURLToPath(fromUrl));
21863
22190
  for (let i = 0;i < 5; i++) {
21864
- const pkgPath = join7(dir, "package.json");
21865
- if (existsSync8(pkgPath)) {
21866
- return JSON.parse(readFileSync6(pkgPath, "utf-8")).version || "0.0.0";
22191
+ const pkgPath = join8(dir, "package.json");
22192
+ if (existsSync9(pkgPath)) {
22193
+ return JSON.parse(readFileSync7(pkgPath, "utf-8")).version || "0.0.0";
21867
22194
  }
21868
- const parent = dirname7(dir);
22195
+ const parent = dirname8(dir);
21869
22196
  if (parent === dir)
21870
22197
  break;
21871
22198
  dir = parent;
@@ -22046,6 +22373,7 @@ registerTaskRelTools(server, toolContext);
22046
22373
  registerCodeTools(server, toolContext);
22047
22374
  registerAgentTools(server, { ...toolContext, agentFocusMap });
22048
22375
  registerTemplateTools(server, toolContext);
22376
+ registerEnvironmentSnapshotTools(server, toolContext);
22049
22377
  registerMachineTools(server, { shouldRegisterTool, formatError });
22050
22378
  registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError });
22051
22379
  async function main() {
@@ -1 +1 @@
1
- {"version":3,"file":"token-utils.d.ts","sourceRoot":"","sources":["../../src/mcp/token-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAE3C,eAAO,MAAM,cAAc,aAoBzB,CAAC;AAEH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAqP7D,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAMhE,CAAC;AAgBF,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,YAAY,qBAA+B,EAC3C,UAAU,qBAAmC,GAC5C,OAAO,CAqCT;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,SAAM,GAAG,MAAM,GAAG,IAAI,CAK5F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,SAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgB1F;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqCtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAexD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAElD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAMD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,UAA4B,GAAG,OAAO,CA0BvH;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAY5D"}
1
+ {"version":3,"file":"token-utils.d.ts","sourceRoot":"","sources":["../../src/mcp/token-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAE3C,eAAO,MAAM,cAAc,aAoBzB,CAAC;AAEH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAuP7D,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAMhE,CAAC;AAgBF,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,EACZ,YAAY,qBAA+B,EAC3C,UAAU,qBAAmC,GAC5C,OAAO,CAqCT;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,SAAM,GAAG,MAAM,GAAG,IAAI,CAK5F;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,SAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgB1F;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqCtF;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAexD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAElD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEtD;AAMD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,UAA4B,GAAG,OAAO,CA0BvH;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAY5D"}
@@ -0,0 +1,8 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ type Helpers = {
3
+ shouldRegisterTool: (name: string) => boolean;
4
+ formatError: (e: unknown) => string;
5
+ };
6
+ export declare function registerEnvironmentSnapshotTools(server: McpServer, { shouldRegisterTool, formatError }: Helpers): void;
7
+ export {};
8
+ //# sourceMappingURL=environment-snapshots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment-snapshots.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/environment-snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGzE,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;CACrC,CAAC;AAEF,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,WAAW,EAAE,EAAE,OAAO,GAAG,IAAI,CA4CtH"}
package/dist/mcp.js CHANGED
@@ -189,6 +189,8 @@ var MCP_TOOL_GROUPS = {
189
189
  "check_file_lock",
190
190
  "create_comment",
191
191
  "create_handoff",
192
+ "capture_environment_snapshot",
193
+ "compare_environment_snapshots",
192
194
  "create_inbox_item",
193
195
  "delete_comment",
194
196
  "detect_file_relationships",
package/dist/registry.js CHANGED
@@ -2433,6 +2433,50 @@ var TODOS_JSON_CONTRACTS = [
2433
2433
  },
2434
2434
  optional: {}
2435
2435
  }),
2436
+ contract({
2437
+ id: "environment_snapshot",
2438
+ name: "Environment Snapshot",
2439
+ description: "Local reproducibility snapshot for task and run verification context.",
2440
+ surfaces: ["cli", "mcp", "sdk"],
2441
+ stability: "stable",
2442
+ required: {
2443
+ schema_version: field("integer", "Snapshot schema version."),
2444
+ id: field("string", "Content-derived snapshot identifier."),
2445
+ captured_at: isoDateField,
2446
+ root: field("string", "Canonical local project root inspected."),
2447
+ machine: field("object", "Local hostname, platform, and architecture metadata."),
2448
+ target: field("object", "Optional task, run, and agent attachment IDs."),
2449
+ runtime: field("object", "Bun, Node, and executable metadata."),
2450
+ package_manager: field("object", "Detected package manager, lockfile hashes, and redacted manifests."),
2451
+ git: field("object", "Local git branch, commit, dirty state, and porcelain status."),
2452
+ config_hashes: field("array", "SHA-256 hashes of relevant local config files."),
2453
+ command_env: field("object", "Redacted command and environment metadata."),
2454
+ warnings: field("array", "Warnings about missing or unavailable local data.")
2455
+ },
2456
+ optional: {}
2457
+ }),
2458
+ contract({
2459
+ id: "environment_snapshot_comparison",
2460
+ name: "Environment Snapshot Comparison",
2461
+ description: "Drift summary returned when comparing two local environment snapshots.",
2462
+ surfaces: ["cli", "mcp", "sdk"],
2463
+ stability: "stable",
2464
+ required: {
2465
+ schema_version: field("integer", "Comparison schema version."),
2466
+ left_id: field("string", "Left snapshot ID."),
2467
+ right_id: field("string", "Right snapshot ID."),
2468
+ same_root: field("boolean", "Whether both snapshots were captured for the same root."),
2469
+ same_machine: field("boolean", "Whether hostname, platform, and architecture match."),
2470
+ same_runtime: field("boolean", "Whether Bun and Node versions match."),
2471
+ same_git_commit: field("boolean", "Whether git commit IDs match."),
2472
+ dirty_state_changed: field("boolean", "Whether the git dirty flag changed."),
2473
+ changed_config_hashes: field("array", "Changed config hash records."),
2474
+ changed_lockfiles: field("array", "Changed lockfile hash records."),
2475
+ changed_manifests: field("array", "Changed manifest hash records."),
2476
+ warnings: field("array", "Comparison warnings.")
2477
+ },
2478
+ optional: {}
2479
+ }),
2436
2480
  contract({
2437
2481
  id: "local_event_hook",
2438
2482
  name: "Local Event Hook",
@@ -7288,6 +7332,8 @@ var MCP_TOOL_GROUPS = {
7288
7332
  "check_file_lock",
7289
7333
  "create_comment",
7290
7334
  "create_handoff",
7335
+ "capture_environment_snapshot",
7336
+ "compare_environment_snapshots",
7291
7337
  "create_inbox_item",
7292
7338
  "delete_comment",
7293
7339
  "detect_file_relationships",
@@ -8262,6 +8308,18 @@ var TODOS_CLI_MCP_PARITY = [
8262
8308
  mcpTool: "build_agent_context_pack"
8263
8309
  }
8264
8310
  },
8311
+ {
8312
+ domain: "environment-snapshots",
8313
+ cliCommands: ["todos env-snapshot"],
8314
+ mcpTools: ["capture_environment_snapshot", "compare_environment_snapshots"],
8315
+ jsonContracts: ["environment_snapshot", "environment_snapshot_comparison", "structured_error", "api_error"],
8316
+ errorContracts: ["structured_error", "api_error"],
8317
+ status: "matched",
8318
+ example: {
8319
+ cli: "todos env-snapshot capture --task 1234abcd --json",
8320
+ mcpTool: "capture_environment_snapshot"
8321
+ }
8322
+ },
8265
8323
  {
8266
8324
  domain: "imports",
8267
8325
  cliCommands: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.11.43",
3
+ "packageVersion": "0.11.44",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "3489eb9408277001cd3be9bb5c2996bb301e431d",
6
- "generatedAt": "2026-05-21T17:17:52.886Z"
5
+ "gitCommit": "f6bc0dc0b71478ba56d4d044eb632f261ec623b4",
6
+ "generatedAt": "2026-05-21T17:49:00.069Z"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/todos",
3
- "version": "0.11.43",
3
+ "version": "0.11.44",
4
4
  "description": "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",