@otskit/mcp 0.1.4 → 0.1.6

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 CHANGED
@@ -1,36 +1,98 @@
1
1
  <p align="center">
2
- <img src="docs/header.png" alt="OTSkit.ts MCP" width="480" />
2
+ <img src="https://raw.githubusercontent.com/OTSkit/OTSkit-MCP/master/docs/header.png" alt="OTSkit.ts MCP" width="480" />
3
3
  </p>
4
4
 
5
5
  # @otskit/mcp
6
6
 
7
- OpenTimestamps MCP server — stamp, upgrade, and verify timestamps via AI agents (Claude).
7
+ OpenTimestamps MCP server — stamp, upgrade, and verify Bitcoin timestamps via AI agents.
8
8
 
9
- ## Installation
9
+ Exposes a set of tools to any MCP-compatible agent so it can timestamp documents, monitor confirmation status, and verify proofs against the Bitcoin blockchain — all from a conversation.
10
+
11
+ > **Note on confirmation times:** After stamping, a proof is `pending` until Bitcoin confirms it. This typically takes **10–60 minutes** but can take **several hours** during network congestion. Use `ots-mcp watch` or `upgrade_timestamp` to monitor. A pending status is not an error.
12
+
13
+ ## Install
10
14
 
11
15
  ```bash
12
- pnpm install
13
- pnpm build
16
+ npm install -g @otskit/mcp
14
17
  ```
15
18
 
16
- ## Usage
19
+ ## Agent setup
17
20
 
18
21
  ```bash
19
- ots-mcp
22
+ ots-mcp setup claude # Claude Desktop
23
+ ots-mcp setup codex # Codex CLI
20
24
  ```
21
25
 
22
- ## Dependencies
26
+ Each command writes the MCP entry into the agent's config file, makes a `.bak` backup if the file already exists, and skips if `ots-mcp` is already configured. Restart the agent afterwards to apply the changes.
23
27
 
24
- - [`@otskit/core`](../otskit-core) — OpenTimestamps core logic
25
- - [`@otskit/client`](../otskit-client) — OTS client
26
- - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) MCP SDK
27
- - `better-sqlite3` — local database
28
- - `archiver` ZIP support
28
+ ## CLI commands
29
+
30
+ | Command | Description |
31
+ |---|---|
32
+ | `ots-mcp serve` | Start the MCP server (stdio transport) |
33
+ | `ots-mcp stamp <sha256>` | Stamp a SHA-256 hash against Bitcoin calendars |
34
+ | `ots-mcp upgrade <id>` | Check if a pending stamp has been confirmed |
35
+ | `ots-mcp verify <id>` | Verify a stamp against Bitcoin |
36
+ | `ots-mcp list [status]` | List stamps (`pending` / `confirmed` / `failed`) |
37
+ | `ots-mcp watch [minutes]` | Poll pending stamps in real-time (default: 5 min) |
38
+ | `ots-mcp check-pending` | Run one upgrade pass over all pending stamps |
39
+ | `ots-mcp scheduler install\|remove\|status` | Manage OS-level scheduler for auto-upgrades |
40
+ | `ots-mcp backup [dest]` | Backup the SQLite database |
41
+ | `ots-mcp setup <claude\|codex>` | Configure MCP for an agent |
42
+
43
+ ## MCP tools exposed to agents
44
+
45
+ | Tool | Description |
46
+ |---|---|
47
+ | `create_timestamp` | Stamp a SHA-256 hash against 4 public OTS calendars |
48
+ | `upgrade_timestamp` | Check if a pending stamp has been confirmed in Bitcoin |
49
+ | `verify_timestamp` | Verify a stamp — proves hash existed before a given Bitcoin block |
50
+ | `inspect_timestamp` | Inspect a stored proof file without network calls |
51
+ | `list_pending` | List stamps with status, retry count, and filters |
52
+ | `watch` | Open a terminal window monitoring pending stamps in real-time |
53
+
54
+ ## Data directory
55
+
56
+ All data is stored in `~/.ots-mcp/`:
57
+
58
+ ```
59
+ ~/.ots-mcp/
60
+ ots-mcp.db # SQLite database (stamps, proof files)
61
+ config.json # Optional config overrides
62
+ ots-mcp.log # Log file
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ Create `~/.ots-mcp/config.json` to override defaults:
68
+
69
+ ```json
70
+ {
71
+ "stamp_enabled": true,
72
+ "scheduler_interval_minutes": 30,
73
+ "retry_max_attempts": 20,
74
+ "calendar_timeout_ms": 10000,
75
+ "esplora_url": "https://blockstream.info/api",
76
+ "calendars": [
77
+ "https://alice.btc.calendar.opentimestamps.org",
78
+ "https://bob.btc.calendar.opentimestamps.org",
79
+ "https://finney.calendar.eternitywall.com",
80
+ "https://btc.calendar.catallaxy.com"
81
+ ]
82
+ }
83
+ ```
29
84
 
30
85
  ## Development
31
86
 
32
87
  ```bash
33
- pnpm dev # watch mode
34
- pnpm test # run tests
35
- pnpm build # production build
88
+ npm run build # production build
89
+ npm run dev # watch mode
90
+ npm test # run tests
36
91
  ```
92
+
93
+ ## Dependencies
94
+
95
+ - [`@otskit/core`](https://github.com/AlexAlves87/otskit-core) — OpenTimestamps core logic
96
+ - [`@otskit/client`](https://github.com/AlexAlves87/otskit-client) — OTS calendar client
97
+ - [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) — MCP SDK
98
+ - `better-sqlite3` — local database
@@ -0,0 +1,123 @@
1
+ // src/config.ts
2
+ import { readFileSync, existsSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ function getDataDir() {
6
+ return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
7
+ }
8
+ var DEFAULTS = {
9
+ stamp_enabled: true,
10
+ preserve_enabled: true,
11
+ preserve_whitelist: [],
12
+ preserve_max_bytes: 104857600,
13
+ preserve_max_files: 1e4,
14
+ scheduler_interval_minutes: 30,
15
+ calendar_timeout_ms: 1e4,
16
+ calendar_max_response_bytes: 1048576,
17
+ retry_max_attempts: 20,
18
+ log_file: join(getDataDir(), "ots-mcp.log"),
19
+ calendars: [
20
+ "https://alice.btc.calendar.opentimestamps.org",
21
+ "https://bob.btc.calendar.opentimestamps.org",
22
+ "https://finney.calendar.eternitywall.com",
23
+ "https://btc.calendar.catallaxy.com"
24
+ ],
25
+ esplora_url: "https://blockstream.info/api"
26
+ };
27
+ function loadConfig() {
28
+ const dir = getDataDir();
29
+ mkdirSync(dir, { recursive: true });
30
+ const configPath = join(dir, "config.json");
31
+ if (!existsSync(configPath)) return { ...DEFAULTS };
32
+ const raw = JSON.parse(readFileSync(configPath, "utf8"));
33
+ return { ...DEFAULTS, ...raw };
34
+ }
35
+
36
+ // src/db/index.ts
37
+ import DatabaseConstructor from "better-sqlite3";
38
+ import { join as join2 } from "path";
39
+ import { mkdirSync as mkdirSync2, statSync } from "fs";
40
+
41
+ // src/db/schema.ts
42
+ function initDb(db) {
43
+ db.pragma("journal_mode = WAL");
44
+ db.pragma("busy_timeout = 5000");
45
+ db.pragma("foreign_keys = ON");
46
+ runMigrations(db);
47
+ }
48
+ function runMigrations(db) {
49
+ const version = db.pragma("user_version", { simple: true });
50
+ if (version < 1) migrateTo1(db);
51
+ }
52
+ function migrateTo1(db) {
53
+ db.transaction(() => {
54
+ db.exec(`
55
+ CREATE TABLE IF NOT EXISTS stamps (
56
+ id TEXT PRIMARY KEY,
57
+ hash TEXT NOT NULL,
58
+ status TEXT NOT NULL,
59
+ created_at TEXT NOT NULL,
60
+ confirmed_at TEXT,
61
+ bitcoin_block INTEGER,
62
+ bitcoin_time TEXT,
63
+ proof_path TEXT,
64
+ archive_path TEXT,
65
+ last_attempt_at TEXT,
66
+ attempt_count INTEGER NOT NULL DEFAULT 0,
67
+ last_error TEXT,
68
+ next_retry_at TEXT,
69
+ metadata TEXT
70
+ );
71
+ CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
72
+ CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
73
+
74
+ CREATE TABLE IF NOT EXISTS operations_log (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ stamp_id TEXT NOT NULL REFERENCES stamps(id),
77
+ action TEXT NOT NULL,
78
+ result TEXT NOT NULL,
79
+ error_msg TEXT,
80
+ calendar_uri TEXT,
81
+ response_time_ms INTEGER,
82
+ created_at TEXT NOT NULL
83
+ );
84
+ CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
85
+ CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
86
+ `);
87
+ db.pragma("user_version = 1");
88
+ })();
89
+ }
90
+
91
+ // src/db/index.ts
92
+ var _db = null;
93
+ function getDb() {
94
+ if (_db) return _db;
95
+ const dir = getDataDir();
96
+ mkdirSync2(dir, { recursive: true });
97
+ _db = new DatabaseConstructor(join2(dir, "db.sqlite"));
98
+ initDb(_db);
99
+ reconcileOrphans(_db);
100
+ return _db;
101
+ }
102
+ function backupDb(destPath) {
103
+ getDb().backup(destPath);
104
+ }
105
+ function reconcileOrphans(db) {
106
+ const pending = db.prepare(
107
+ `SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
108
+ ).all();
109
+ for (const row of pending) {
110
+ try {
111
+ statSync(row.proof_path);
112
+ } catch {
113
+ db.prepare(`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`).run("proof file missing on disk", row.id);
114
+ }
115
+ }
116
+ }
117
+
118
+ export {
119
+ getDataDir,
120
+ loadConfig,
121
+ getDb,
122
+ backupDb
123
+ };
@@ -1,128 +1,15 @@
1
+ import {
2
+ getDataDir
3
+ } from "./chunk-4ZDPWS7C.js";
1
4
  import {
2
5
  writeAtomic
3
6
  } from "./chunk-YFSUDT24.js";
4
7
 
5
- // src/config.ts
6
- import { readFileSync, existsSync, mkdirSync } from "fs";
8
+ // src/tools/create-timestamp.ts
9
+ import { mkdirSync } from "fs";
7
10
  import { join } from "path";
8
- import { homedir } from "os";
9
- function getDataDir() {
10
- return process.env.OTS_MCP_DATA_DIR ?? join(homedir(), ".ots-mcp");
11
- }
12
- var DEFAULTS = {
13
- stamp_enabled: true,
14
- preserve_enabled: true,
15
- preserve_whitelist: [],
16
- preserve_max_bytes: 104857600,
17
- preserve_max_files: 1e4,
18
- scheduler_interval_minutes: 30,
19
- calendar_timeout_ms: 1e4,
20
- calendar_max_response_bytes: 1048576,
21
- retry_max_attempts: 20,
22
- log_file: join(getDataDir(), "ots-mcp.log"),
23
- calendars: [
24
- "https://alice.btc.calendar.opentimestamps.org",
25
- "https://bob.btc.calendar.opentimestamps.org",
26
- "https://finney.calendar.eternitywall.com",
27
- "https://btc.calendar.catallaxy.com"
28
- ],
29
- esplora_url: "https://blockstream.info/api"
30
- };
31
- function loadConfig() {
32
- const dir = getDataDir();
33
- mkdirSync(dir, { recursive: true });
34
- const configPath = join(dir, "config.json");
35
- if (!existsSync(configPath)) return { ...DEFAULTS };
36
- const raw = JSON.parse(readFileSync(configPath, "utf8"));
37
- return { ...DEFAULTS, ...raw };
38
- }
39
-
40
- // src/db/index.ts
41
- import DatabaseConstructor from "better-sqlite3";
42
- import { join as join2 } from "path";
43
- import { mkdirSync as mkdirSync2, statSync } from "fs";
44
-
45
- // src/db/schema.ts
46
- function initDb(db) {
47
- db.pragma("journal_mode = WAL");
48
- db.pragma("busy_timeout = 5000");
49
- db.pragma("foreign_keys = ON");
50
- runMigrations(db);
51
- }
52
- function runMigrations(db) {
53
- const version = db.pragma("user_version", { simple: true });
54
- if (version < 1) migrateTo1(db);
55
- }
56
- function migrateTo1(db) {
57
- db.transaction(() => {
58
- db.exec(`
59
- CREATE TABLE IF NOT EXISTS stamps (
60
- id TEXT PRIMARY KEY,
61
- hash TEXT NOT NULL,
62
- status TEXT NOT NULL,
63
- created_at TEXT NOT NULL,
64
- confirmed_at TEXT,
65
- bitcoin_block INTEGER,
66
- bitcoin_time TEXT,
67
- proof_path TEXT,
68
- archive_path TEXT,
69
- last_attempt_at TEXT,
70
- attempt_count INTEGER NOT NULL DEFAULT 0,
71
- last_error TEXT,
72
- next_retry_at TEXT,
73
- metadata TEXT
74
- );
75
- CREATE INDEX IF NOT EXISTS idx_stamps_hash ON stamps(hash);
76
- CREATE INDEX IF NOT EXISTS idx_stamps_status ON stamps(status);
77
-
78
- CREATE TABLE IF NOT EXISTS operations_log (
79
- id INTEGER PRIMARY KEY AUTOINCREMENT,
80
- stamp_id TEXT NOT NULL REFERENCES stamps(id),
81
- action TEXT NOT NULL,
82
- result TEXT NOT NULL,
83
- error_msg TEXT,
84
- calendar_uri TEXT,
85
- response_time_ms INTEGER,
86
- created_at TEXT NOT NULL
87
- );
88
- CREATE INDEX IF NOT EXISTS idx_oplog_stamp_id ON operations_log(stamp_id);
89
- CREATE INDEX IF NOT EXISTS idx_oplog_created ON operations_log(created_at);
90
- `);
91
- db.pragma("user_version = 1");
92
- })();
93
- }
94
-
95
- // src/db/index.ts
96
- var _db = null;
97
- function getDb() {
98
- if (_db) return _db;
99
- const dir = getDataDir();
100
- mkdirSync2(dir, { recursive: true });
101
- _db = new DatabaseConstructor(join2(dir, "db.sqlite"));
102
- initDb(_db);
103
- reconcileOrphans(_db);
104
- return _db;
105
- }
106
- function backupDb(destPath) {
107
- getDb().backup(destPath);
108
- }
109
- function reconcileOrphans(db) {
110
- const pending = db.prepare(
111
- `SELECT id, proof_path FROM stamps WHERE status = 'pending' AND proof_path IS NOT NULL`
112
- ).all();
113
- for (const row of pending) {
114
- try {
115
- statSync(row.proof_path);
116
- } catch {
117
- db.prepare(`UPDATE stamps SET status = 'failed', last_error = ? WHERE id = ?`).run("proof file missing on disk", row.id);
118
- }
119
- }
120
- }
121
-
122
- // src/tools/upgrade-timestamp.ts
123
- import { readFileSync as readFileSync2 } from "fs";
124
- import { OpenTimestampsClient, UpgradeError } from "@otskit/client";
125
- import { DetachedTimestampFile } from "@otskit/core";
11
+ import { randomUUID } from "crypto";
12
+ import { OpenTimestampsClient } from "@otskit/client";
126
13
 
127
14
  // src/db/stamps.ts
128
15
  function insertStamp(db, params) {
@@ -167,6 +54,10 @@ function listStamps(db, params) {
167
54
  conds.push("created_at < ?");
168
55
  vals.push(cutoff);
169
56
  }
57
+ if (params.due_now) {
58
+ conds.push("(next_retry_at IS NULL OR next_retry_at <= ?)");
59
+ vals.push((/* @__PURE__ */ new Date()).toISOString());
60
+ }
170
61
  const where = conds.length ? `WHERE ${conds.join(" AND ")}` : "";
171
62
  const total = db.prepare(`SELECT COUNT(*) as n FROM stamps ${where}`).get(...vals).n;
172
63
  const items = db.prepare(
@@ -191,17 +82,66 @@ function logOperation(db, params) {
191
82
  );
192
83
  }
193
84
 
85
+ // src/tools/create-timestamp.ts
86
+ var HEX64 = /^[0-9a-f]{64}$/i;
87
+ async function createTimestamp(input, db, config) {
88
+ if (!HEX64.test(input.hash)) {
89
+ return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
90
+ }
91
+ const normalizedHash = input.hash.toLowerCase();
92
+ const client = new OpenTimestampsClient({
93
+ calendars: config.calendars,
94
+ resilience: { timeout: config.calendar_timeout_ms }
95
+ });
96
+ const t0 = Date.now();
97
+ let proofBuffer;
98
+ try {
99
+ proofBuffer = await client.stamp(normalizedHash);
100
+ } catch (e) {
101
+ return { error: "calendar_error", details: String(e) };
102
+ }
103
+ const responseTimeMs = Date.now() - t0;
104
+ const id = randomUUID();
105
+ const proofDir = join(getDataDir(), "proofs");
106
+ mkdirSync(proofDir, { recursive: true });
107
+ const proofPath = join(proofDir, `${id}.ots`);
108
+ try {
109
+ writeAtomic(proofPath, proofBuffer);
110
+ } catch (e) {
111
+ return { error: "storage_error", details: String(e) };
112
+ }
113
+ const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
114
+ logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
115
+ return {
116
+ id: record.id,
117
+ hash: record.hash,
118
+ status: "pending",
119
+ calendars: config.calendars,
120
+ created_at: record.created_at,
121
+ proof_path: proofPath
122
+ };
123
+ }
124
+
194
125
  // src/tools/upgrade-timestamp.ts
126
+ import { readFileSync } from "fs";
127
+ import { OpenTimestampsClient as OpenTimestampsClient2, UpgradeError } from "@otskit/client";
128
+ import { DetachedTimestampFile, StreamDeserializationContext } from "@otskit/core";
129
+ function collectAttestations(ts) {
130
+ const atts = [...ts.attestations];
131
+ for (const branch of ts.branches) {
132
+ atts.push(...collectAttestations(branch.stamp));
133
+ }
134
+ return atts;
135
+ }
195
136
  function checkBitcoinConfirmation(bytes) {
196
137
  try {
197
- const proof = DetachedTimestampFile.deserialize(new Uint8Array(bytes));
198
- const attestations = proof.timestamp.getAttestations();
199
- const bitcoin = attestations.filter((a) => a.kind === "bitcoin");
200
- if (bitcoin.length > 0) {
201
- const block = Math.min(...bitcoin.map((a) => a.height));
202
- return { confirmed: true, block };
203
- }
204
- return { confirmed: false };
138
+ const ctx = new StreamDeserializationContext(new Uint8Array(bytes));
139
+ const dtf = DetachedTimestampFile.deserialize(ctx);
140
+ const attestations = collectAttestations(dtf.timestamp);
141
+ const bitcoinAtts = attestations.filter((a) => a.kind === "bitcoin");
142
+ if (bitcoinAtts.length === 0) return { confirmed: false };
143
+ const block = Math.min(...bitcoinAtts.map((a) => a.height));
144
+ return { confirmed: true, block };
205
145
  } catch {
206
146
  return { confirmed: false };
207
147
  }
@@ -215,8 +155,8 @@ async function upgradeTimestamp(input, db, config) {
215
155
  const record = getStamp(db, input.id);
216
156
  if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
217
157
  if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
218
- const proofBefore = readFileSync2(record.proof_path);
219
- const client = new OpenTimestampsClient({
158
+ const proofBefore = readFileSync(record.proof_path);
159
+ const client = new OpenTimestampsClient2({
220
160
  calendars: config.calendars,
221
161
  resilience: { timeout: config.calendar_timeout_ms }
222
162
  });
@@ -256,14 +196,66 @@ async function upgradeTimestamp(input, db, config) {
256
196
  return { id: input.id, status: "pending", attempt_count: newAttemptCount, last_attempt_at: now, next_retry_at: next };
257
197
  }
258
198
 
199
+ // src/tools/verify-timestamp.ts
200
+ import { readFileSync as readFileSync2 } from "fs";
201
+ import { OpenTimestampsClient as OpenTimestampsClient3 } from "@otskit/client";
202
+ async function verifyTimestamp(input, db, config) {
203
+ const record = getStamp(db, input.id);
204
+ if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
205
+ if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
206
+ let proofBytes;
207
+ try {
208
+ proofBytes = readFileSync2(record.proof_path);
209
+ } catch (e) {
210
+ return { error: "storage_error", details: String(e) };
211
+ }
212
+ const client = new OpenTimestampsClient3({
213
+ calendars: config.calendars,
214
+ resilience: { timeout: config.calendar_timeout_ms }
215
+ });
216
+ let result;
217
+ try {
218
+ result = await client.verify(proofBytes, record.hash);
219
+ } catch (e) {
220
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
221
+ return { status: "network_error", hash: record.hash, details: String(e) };
222
+ }
223
+ if (!result.valid) {
224
+ if (result.error?.includes("No Bitcoin attestation")) {
225
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
226
+ return { status: "pending", hash: record.hash, calendars: config.calendars };
227
+ }
228
+ if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
229
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
230
+ return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
231
+ }
232
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
233
+ return { status: "unknown", hash: record.hash };
234
+ }
235
+ logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
236
+ return {
237
+ status: "confirmed",
238
+ hash: record.hash,
239
+ bitcoin_block: result.blockHeight,
240
+ bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
241
+ };
242
+ }
243
+
244
+ // src/tools/list-pending.ts
245
+ function listPending(input, db, _config) {
246
+ return listStamps(db, {
247
+ status: input.status ?? "pending",
248
+ limit: Math.min(input.limit ?? 50, 200),
249
+ offset: input.offset ?? 0,
250
+ older_than_hours: input.older_than_hours,
251
+ due_now: input.due_now
252
+ });
253
+ }
254
+
259
255
  export {
260
- getDataDir,
261
- loadConfig,
262
- getDb,
263
- backupDb,
264
- insertStamp,
265
256
  getStamp,
266
- listStamps,
267
- logOperation,
268
- upgradeTimestamp
257
+ createTimestamp,
258
+ upgradeTimestamp,
259
+ verifyTimestamp,
260
+ listPending
269
261
  };
@@ -0,0 +1,54 @@
1
+ // src/setup/claude.ts
2
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ function getConfigPath() {
6
+ if (process.platform === "win32") {
7
+ return join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
8
+ } else if (process.platform === "darwin") {
9
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
10
+ } else {
11
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
12
+ }
13
+ }
14
+ function setupClaude() {
15
+ const configPath = getConfigPath();
16
+ const configDir = join(configPath, "..");
17
+ if (!existsSync(configDir)) {
18
+ mkdirSync(configDir, { recursive: true });
19
+ }
20
+ let config = {};
21
+ if (existsSync(configPath)) {
22
+ try {
23
+ config = JSON.parse(readFileSync(configPath, "utf8"));
24
+ } catch {
25
+ process.stderr.write(` Aviso: no se pudo parsear la config existente, se crear\xE1 nueva.
26
+ `);
27
+ }
28
+ const existing = config.mcpServers ?? {};
29
+ if ("otskit" in existing) {
30
+ process.stdout.write(` ots-mcp ya est\xE1 configurado en ${configPath}
31
+ `);
32
+ process.stdout.write(` No se hizo ning\xFAn cambio.
33
+ `);
34
+ return;
35
+ }
36
+ const backupPath = configPath + ".bak";
37
+ copyFileSync(configPath, backupPath);
38
+ process.stdout.write(` Backup guardado en ${backupPath}
39
+ `);
40
+ }
41
+ const mcpServers = config.mcpServers ?? {};
42
+ mcpServers["otskit"] = { command: "npx", args: ["-y", "@otskit/mcp", "serve"] };
43
+ config.mcpServers = mcpServers;
44
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
45
+ process.stdout.write(`OTSkit MCP configurado para Claude Desktop
46
+ `);
47
+ process.stdout.write(` Config: ${configPath}
48
+ `);
49
+ process.stdout.write(` Reinicia Claude Desktop para aplicar los cambios.
50
+ `);
51
+ }
52
+ export {
53
+ setupClaude
54
+ };
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  createTimestamp,
3
3
  listPending,
4
+ upgradeTimestamp,
4
5
  verifyTimestamp
5
- } from "./chunk-45GR42GV.js";
6
+ } from "./chunk-VHQD7HZD.js";
6
7
  import {
7
8
  backupDb,
8
9
  getDb,
9
- loadConfig,
10
- upgradeTimestamp
11
- } from "./chunk-ASPLCXMC.js";
10
+ loadConfig
11
+ } from "./chunk-4ZDPWS7C.js";
12
12
  import "./chunk-YFSUDT24.js";
13
13
 
14
14
  // src/cli.ts
@@ -68,7 +68,7 @@ async function runCli(command, args) {
68
68
  break;
69
69
  }
70
70
  case "check-pending": {
71
- const { items } = listPending({ status: "pending", limit: 200 }, db, config);
71
+ const { items } = listPending({ status: "pending", limit: 200, due_now: true }, db, config);
72
72
  process.stderr.write(`Processing ${items.length} pending stamps...
73
73
  `);
74
74
  for (const record of items) {
@@ -77,7 +77,7 @@ async function runCli(command, args) {
77
77
  process.stderr.write(`${record.id.slice(0, 8)}: ${statusStr}
78
78
  `);
79
79
  }
80
- break;
80
+ process.exit(0);
81
81
  }
82
82
  case "backup": {
83
83
  const dest = args[0] ?? `ots-mcp-backup-${Date.now()}.sqlite`;
@@ -87,7 +87,7 @@ async function runCli(command, args) {
87
87
  break;
88
88
  }
89
89
  case "scheduler": {
90
- const { runScheduler } = await import("./scheduler-VVSCSTMC.js");
90
+ const { runScheduler } = await import("./scheduler-YF6WMSR4.js");
91
91
  await runScheduler(args);
92
92
  break;
93
93
  }
@@ -0,0 +1,41 @@
1
+ // src/setup/codex.ts
2
+ import { readFileSync, writeFileSync, copyFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ var BLOCK = `
6
+ [mcp_servers.ots-mcp]
7
+ command = "npx"
8
+ args = ["-y", "@otskit/mcp", "serve"]
9
+ `;
10
+ function setupCodex() {
11
+ const configPath = join(homedir(), ".codex", "config.toml");
12
+ const configDir = join(configPath, "..");
13
+ if (!existsSync(configDir)) {
14
+ mkdirSync(configDir, { recursive: true });
15
+ }
16
+ let content = "";
17
+ if (existsSync(configPath)) {
18
+ content = readFileSync(configPath, "utf8");
19
+ if (content.includes("[mcp_servers.ots-mcp]")) {
20
+ process.stdout.write(` ots-mcp ya est\xE1 configurado en ${configPath}
21
+ `);
22
+ process.stdout.write(` No se hizo ning\xFAn cambio.
23
+ `);
24
+ return;
25
+ }
26
+ const backupPath = configPath + ".bak";
27
+ copyFileSync(configPath, backupPath);
28
+ process.stdout.write(` Backup guardado en ${backupPath}
29
+ `);
30
+ }
31
+ writeFileSync(configPath, content.trimEnd() + "\n" + BLOCK, "utf8");
32
+ process.stdout.write(`OTSkit MCP configurado para Codex
33
+ `);
34
+ process.stdout.write(` Config: ${configPath}
35
+ `);
36
+ process.stdout.write(` Reinicia Codex para aplicar los cambios.
37
+ `);
38
+ }
39
+ export {
40
+ setupCodex
41
+ };
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ if (!command || command === "--help" || command === "help") {
6
6
  process.stderr.write(`Usage: ots-mcp <command>
7
7
  Commands:
8
8
  serve Start MCP server (stdio transport)
9
- install-claude Install MCP in Claude Desktop config (auto-setup)
9
+ setup <target> Configure MCP for an agent (claude | codex)
10
10
  watch [interval] Watch pending stamps in real-time (default: 5 min)
11
11
  stamp <hash> Stamp a SHA-256 hash
12
12
  upgrade <id> Upgrade a pending stamp
@@ -20,18 +20,38 @@ Commands:
20
20
  }
21
21
  switch (command) {
22
22
  case "serve": {
23
- const { runServer } = await import("./server-2YEI77IT.js");
23
+ const { runServer } = await import("./server-D5YXBC55.js");
24
24
  await runServer();
25
25
  break;
26
26
  }
27
+ case "setup": {
28
+ const target = args[0];
29
+ if (!target || target !== "claude" && target !== "codex") {
30
+ process.stderr.write(`Usage: ots-mcp setup <claude|codex>
31
+ `);
32
+ process.exit(1);
33
+ }
34
+ if (target === "claude") {
35
+ const { setupClaude } = await import("./claude-QTEB3Z5V.js");
36
+ setupClaude();
37
+ } else {
38
+ const { setupCodex } = await import("./codex-P7EDO5GY.js");
39
+ setupCodex();
40
+ }
41
+ break;
42
+ }
27
43
  case "install-claude": {
28
44
  const { installClaude } = await import("./install-claude-UKSS65BW.js");
29
45
  installClaude();
30
46
  break;
31
47
  }
32
48
  case "watch": {
33
- const { watchPending } = await import("./watch-JWDMAZWS.js");
34
- const interval = args[0] ? parseInt(args[0]) : 5;
49
+ const { watchPending } = await import("./watch-7NPUWR6I.js");
50
+ const parsed = args[0] ? parseInt(args[0], 10) : NaN;
51
+ const interval = isNaN(parsed) || parsed < 1 ? 5 : parsed;
52
+ if (args[0] && (isNaN(parsed) || parsed < 1))
53
+ process.stderr.write(`Argumento inv\xE1lido "${args[0]}", usando intervalo por defecto: 5 min
54
+ `);
35
55
  await watchPending(interval);
36
56
  break;
37
57
  }
@@ -42,7 +62,7 @@ switch (command) {
42
62
  case "check-pending":
43
63
  case "backup":
44
64
  case "scheduler": {
45
- const { runCli } = await import("./cli-67APZKT6.js");
65
+ const { runCli } = await import("./cli-TF5PYS3T.js");
46
66
  await runCli(command, args);
47
67
  break;
48
68
  }
@@ -1,15 +1,14 @@
1
1
  import {
2
2
  createTimestamp,
3
+ getStamp,
3
4
  listPending,
5
+ upgradeTimestamp,
4
6
  verifyTimestamp
5
- } from "./chunk-45GR42GV.js";
7
+ } from "./chunk-VHQD7HZD.js";
6
8
  import {
7
- getDataDir,
8
9
  getDb,
9
- getStamp,
10
- loadConfig,
11
- upgradeTimestamp
12
- } from "./chunk-ASPLCXMC.js";
10
+ loadConfig
11
+ } from "./chunk-4ZDPWS7C.js";
13
12
  import "./chunk-YFSUDT24.js";
14
13
 
15
14
  // src/server.ts
@@ -32,15 +31,15 @@ function inspectTimestamp(input, db, _config) {
32
31
  } catch {
33
32
  return { error: "proof_missing", details: `Cannot read proof: ${record.proof_path}` };
34
33
  }
35
- let attestationCount = 0;
36
- let hasBitcoin = false;
34
+ let calendarAttestations = 0;
35
+ let bitcoinAttestations = 0;
37
36
  let bitcoinBlock = null;
38
37
  try {
39
38
  const proof = DetachedTimestampFile.deserialize(new Uint8Array(proofBytes));
40
39
  const attestations = proof.timestamp.getAttestations();
41
- attestationCount = attestations.length;
42
- hasBitcoin = attestations.some((a) => a.kind === "bitcoin");
43
- if (hasBitcoin) {
40
+ bitcoinAttestations = attestations.filter((a) => a.kind === "bitcoin").length;
41
+ calendarAttestations = attestations.filter((a) => a.kind !== "bitcoin").length;
42
+ if (bitcoinAttestations > 0) {
44
43
  const blocks = attestations.filter((a) => a.kind === "bitcoin").map((a) => a.height);
45
44
  bitcoinBlock = blocks.length > 0 ? Math.min(...blocks) : null;
46
45
  }
@@ -53,100 +52,23 @@ function inspectTimestamp(input, db, _config) {
53
52
  created_at: record.created_at,
54
53
  proof_path: record.proof_path,
55
54
  proof_size_bytes: proofSize,
56
- attestation_count: attestationCount,
57
- has_bitcoin_confirmation: hasBitcoin,
55
+ calendar_attestations: calendarAttestations,
56
+ bitcoin_attestations: bitcoinAttestations,
57
+ bitcoin_confirmed: bitcoinAttestations > 0,
58
58
  bitcoin_block: bitcoinBlock
59
59
  };
60
60
  }
61
61
 
62
- // src/tools/preserve.ts
63
- import { createReadStream, createWriteStream, realpathSync, statSync as statSync2, mkdirSync } from "fs";
64
- import { join, resolve, sep } from "path";
65
- import { createHash, randomUUID } from "crypto";
66
- import archiver from "archiver";
67
- function isPathInWhitelist(resolvedPath, whitelist) {
68
- const parts = resolvedPath.split(sep);
69
- return whitelist.some((entry) => {
70
- const entryParts = resolve(entry).split(sep);
71
- return entryParts.length <= parts.length && entryParts.every((part, i) => parts[i] === part);
72
- });
73
- }
74
- async function preserve(input, db, config) {
75
- if (!config.preserve_enabled || config.preserve_whitelist.length === 0) {
76
- return { error: "whitelist_not_configured", details: "Set preserve_whitelist in ~/.ots-mcp/config.json" };
77
- }
78
- let resolvedInput;
79
- try {
80
- resolvedInput = realpathSync(resolve(input.dir_path));
81
- } catch (e) {
82
- return { error: "path_not_in_whitelist", details: `Cannot resolve path: ${e}` };
83
- }
84
- const resolvedWhitelist = config.preserve_whitelist.map((p) => {
85
- try {
86
- return realpathSync(resolve(p));
87
- } catch {
88
- return resolve(p);
89
- }
90
- });
91
- if (!isPathInWhitelist(resolvedInput, resolvedWhitelist)) {
92
- return { error: "path_not_in_whitelist", details: `${resolvedInput} is not in the configured whitelist` };
93
- }
94
- let st;
95
- try {
96
- st = statSync2(resolvedInput);
97
- } catch (e) {
98
- return { error: "path_not_in_whitelist", details: String(e) };
99
- }
100
- if (!st.isDirectory()) {
101
- return { error: "path_not_in_whitelist", details: "Path must be a directory" };
102
- }
103
- const archiveDir = join(getDataDir(), "archives");
104
- mkdirSync(archiveDir, { recursive: true });
105
- const label = input.label ? `-${input.label.replace(/[^a-z0-9-]/gi, "_")}` : "";
106
- const archivePath = join(archiveDir, `${randomUUID()}${label}.zip`);
107
- const warnings = [];
108
- let filesCount = 0;
109
- await new Promise((res, rej) => {
110
- const output = createWriteStream(archivePath);
111
- const arc = archiver("zip", { zlib: { level: 6 } });
112
- arc.on("warning", (e) => {
113
- if (e.code === "ENOENT") warnings.push(e.message);
114
- else rej(e);
115
- });
116
- arc.on("error", rej);
117
- arc.on("entry", () => {
118
- filesCount++;
119
- });
120
- output.on("close", res);
121
- arc.pipe(output);
122
- arc.directory(resolvedInput, false);
123
- arc.finalize();
124
- });
125
- const archiveSize = statSync2(archivePath).size;
126
- if (archiveSize > config.preserve_max_bytes) {
127
- return { error: "resource_limit_exceeded", details: `Archive ${archiveSize} bytes exceeds limit ${config.preserve_max_bytes}` };
128
- }
129
- const hash = await new Promise((res, rej) => {
130
- const h = createHash("sha256");
131
- createReadStream(archivePath).on("data", (d) => h.update(d)).on("end", () => res(h.digest("hex"))).on("error", rej);
62
+ // src/tools/watch-window.ts
63
+ import { exec } from "child_process";
64
+ function openWatchWindow(intervalMinutes = 5) {
65
+ const minutes = Math.max(1, Math.floor(intervalMinutes));
66
+ const cmd = `start powershell.exe -NoExit -Command "ots-mcp watch ${minutes}"`;
67
+ let errorMsg;
68
+ exec(cmd, { shell: "cmd" }, (err) => {
69
+ if (err) errorMsg = err.message;
132
70
  });
133
- const stampResult = await createTimestamp({ hash }, db, config);
134
- if ("error" in stampResult) return { error: "stamp_error", details: stampResult.details };
135
- db.prepare("UPDATE stamps SET archive_path = ?, metadata = ? WHERE id = ?").run(
136
- archivePath,
137
- JSON.stringify({ source_path: resolvedInput, files_count: filesCount, total_bytes: archiveSize }),
138
- stampResult.id
139
- );
140
- return {
141
- id: stampResult.id,
142
- hash,
143
- archive_path: archivePath,
144
- proof_path: stampResult.proof_path,
145
- status: "pending",
146
- files_count: filesCount,
147
- archive_size_bytes: archiveSize,
148
- warnings
149
- };
71
+ return { opened: true, interval_minutes: minutes, ...errorMsg ? { error: errorMsg } : {} };
150
72
  }
151
73
 
152
74
  // src/tool-definitions.ts
@@ -198,7 +120,7 @@ var TOOL_DEFINITIONS = [
198
120
  },
199
121
  {
200
122
  name: "inspect_timestamp",
201
- description: "Shows the contents of a stored proof file without making any network calls. Useful for debugging: returns size, parsed attestations, and confirmation status from the proof itself.",
123
+ description: "Inspects a stored proof file without network calls. Returns calendar_attestations (promises from OTS servers, NOT Bitcoin confirmation) and bitcoin_attestations (actual Bitcoin blocks). A stamp is only truly confirmed when bitcoin_attestations > 0 and bitcoin_confirmed is true.",
202
124
  inputSchema: {
203
125
  type: "object",
204
126
  properties: { id: { type: "string", description: "UUID from the stamp record" } },
@@ -231,21 +153,19 @@ var TOOL_DEFINITIONS = [
231
153
  }
232
154
  },
233
155
  {
234
- name: "preserve",
235
- description: "FILESYSTEM-SENSITIVE: Compresses a directory to ZIP, stamps its SHA-256 hash, stores archive in whitelist directory. Requires preserve_whitelist config.",
156
+ name: "watch",
157
+ description: "Opens a new terminal window that monitors pending stamps in real-time, polling at the given interval. The window stays open so the user can watch progress.",
236
158
  inputSchema: {
237
159
  type: "object",
238
160
  properties: {
239
- dir_path: { type: "string", description: "Absolute path to directory (must be in preserve_whitelist)" },
240
- label: { type: "string", description: "Optional label for the archive filename" }
241
- },
242
- required: ["dir_path"]
161
+ interval_minutes: { type: "number", description: "Polling interval in minutes (default: 5, minimum: 1)" }
162
+ }
243
163
  },
244
164
  annotations: {
245
- readOnlyHint: false,
165
+ readOnlyHint: true,
246
166
  destructiveHint: false,
247
167
  idempotentHint: false,
248
- openWorldHint: true
168
+ openWorldHint: false
249
169
  }
250
170
  }
251
171
  ];
@@ -281,8 +201,8 @@ async function runServer() {
281
201
  case "list_pending":
282
202
  result = listPending(args, db, config);
283
203
  break;
284
- case "preserve":
285
- result = await preserve(args, db, config);
204
+ case "watch":
205
+ result = openWatchWindow(args?.interval_minutes);
286
206
  break;
287
207
  default:
288
208
  return { content: [{ type: "text", text: JSON.stringify({ error: "unknown_tool", tool: name }) }], isError: true };
@@ -0,0 +1,41 @@
1
+ import {
2
+ getDb,
3
+ loadConfig
4
+ } from "./chunk-4ZDPWS7C.js";
5
+
6
+ // src/tools/watch.ts
7
+ async function watchPending(intervalMinutes = 5) {
8
+ const config = loadConfig();
9
+ const db = getDb(config);
10
+ process.stdout.write(`Watching pending stamps every ${intervalMinutes} min. Ctrl+C to stop.
11
+
12
+ `);
13
+ function tick() {
14
+ const rows = db.prepare(
15
+ `SELECT id, hash, status, attempt_count, bitcoin_block, confirmed_at FROM stamps WHERE status != 'confirmed' ORDER BY created_at DESC`
16
+ ).all();
17
+ const confirmed = db.prepare(`SELECT COUNT(*) as n FROM stamps WHERE status = 'confirmed'`).get();
18
+ process.stdout.write(`${now()} \u2014 ${rows.length} pendientes, ${confirmed.n} confirmados
19
+ `);
20
+ for (const row of rows) {
21
+ process.stdout.write(` ${row.id.slice(0, 8)} ${row.status} (${row.attempt_count} intentos)
22
+ `);
23
+ }
24
+ if (rows.length === 0) {
25
+ process.stdout.write(` (ning\xFAn sello pendiente)
26
+ `);
27
+ }
28
+ process.stdout.write("\n");
29
+ }
30
+ function now() {
31
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
32
+ }
33
+ function loop() {
34
+ tick();
35
+ setTimeout(loop, Math.max(6e4, intervalMinutes * 60 * 1e3));
36
+ }
37
+ loop();
38
+ }
39
+ export {
40
+ watchPending
41
+ };
package/package.json CHANGED
@@ -1,32 +1,46 @@
1
1
  {
2
2
  "name": "@otskit/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "OpenTimestamps MCP server — stamp, upgrade, verify via AI agents",
5
5
  "type": "module",
6
- "engines": { "node": ">=18" },
7
- "bin": { "ots-mcp": "dist/index.js" },
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "bin": {
10
+ "ots-mcp": "dist/index.js"
11
+ },
8
12
  "exports": "./dist/index.js",
9
- "files": ["dist"],
13
+ "files": [
14
+ "dist"
15
+ ],
10
16
  "scripts": {
11
17
  "build": "tsup src/index.ts --format esm --clean",
12
- "dev": "tsup src/index.ts --format esm --watch",
13
- "test": "vitest run",
18
+ "dev": "tsup src/index.ts --format esm --watch",
19
+ "test": "vitest run",
14
20
  "test:watch": "vitest"
15
21
  },
16
22
  "dependencies": {
17
- "@otskit/core": "^0.1.0",
18
- "@otskit/client": "^0.1.1",
19
23
  "@modelcontextprotocol/sdk": "^1.29.0",
20
- "better-sqlite3": "^12.10.0",
21
- "archiver": "^7.0.0"
24
+ "@otskit/client": "^0.1.1",
25
+ "@otskit/core": "^0.1.0",
26
+ "better-sqlite3": "^12.10.0"
22
27
  },
23
28
  "pnpm": {
24
- "onlyBuiltDependencies": ["better-sqlite3", "esbuild"]
29
+ "onlyBuiltDependencies": [
30
+ "better-sqlite3",
31
+ "esbuild"
32
+ ]
25
33
  },
26
34
  "devDependencies": {
35
+ "@semantic-release/changelog": "^6",
36
+ "@semantic-release/commit-analyzer": "^13.0.0",
37
+ "@semantic-release/git": "^10",
38
+ "@semantic-release/github": "^12.0.8",
39
+ "@semantic-release/npm": "^13.1.5",
40
+ "@semantic-release/release-notes-generator": "^14.0.0",
27
41
  "@types/better-sqlite3": "^7.6.13",
28
- "@types/archiver": "^6.0.3",
29
42
  "@types/node": "^22.0.0",
43
+ "semantic-release": "^25.0.3",
30
44
  "tsup": "^8.3.5",
31
45
  "typescript": "^5.6.3",
32
46
  "vitest": "^2.1.4"
@@ -1,115 +0,0 @@
1
- import {
2
- getDataDir,
3
- getStamp,
4
- insertStamp,
5
- listStamps,
6
- logOperation
7
- } from "./chunk-ASPLCXMC.js";
8
- import {
9
- writeAtomic
10
- } from "./chunk-YFSUDT24.js";
11
-
12
- // src/tools/create-timestamp.ts
13
- import { mkdirSync } from "fs";
14
- import { join } from "path";
15
- import { randomUUID } from "crypto";
16
- import { OpenTimestampsClient } from "@otskit/client";
17
- var HEX64 = /^[0-9a-f]{64}$/i;
18
- async function createTimestamp(input, db, config) {
19
- if (!HEX64.test(input.hash)) {
20
- return { error: "invalid_hash", details: "hash must be 64 hex characters (SHA-256)" };
21
- }
22
- const normalizedHash = input.hash.toLowerCase();
23
- const client = new OpenTimestampsClient({
24
- calendars: config.calendars,
25
- resilience: { timeout: config.calendar_timeout_ms }
26
- });
27
- const t0 = Date.now();
28
- let proofBuffer;
29
- try {
30
- proofBuffer = await client.stamp(normalizedHash);
31
- } catch (e) {
32
- return { error: "calendar_error", details: String(e) };
33
- }
34
- const responseTimeMs = Date.now() - t0;
35
- const id = randomUUID();
36
- const proofDir = join(getDataDir(), "proofs");
37
- mkdirSync(proofDir, { recursive: true });
38
- const proofPath = join(proofDir, `${id}.ots`);
39
- try {
40
- writeAtomic(proofPath, proofBuffer);
41
- } catch (e) {
42
- return { error: "storage_error", details: String(e) };
43
- }
44
- const record = insertStamp(db, { id, hash: normalizedHash, proof_path: proofPath });
45
- logOperation(db, { stamp_id: id, action: "stamp", result: "success", response_time_ms: responseTimeMs });
46
- return {
47
- id: record.id,
48
- hash: record.hash,
49
- status: "pending",
50
- calendars: config.calendars,
51
- created_at: record.created_at,
52
- proof_path: proofPath
53
- };
54
- }
55
-
56
- // src/tools/verify-timestamp.ts
57
- import { readFileSync } from "fs";
58
- import { OpenTimestampsClient as OpenTimestampsClient2 } from "@otskit/client";
59
- async function verifyTimestamp(input, db, config) {
60
- const record = getStamp(db, input.id);
61
- if (!record) return { error: "not_found", details: `No stamp with id ${input.id}` };
62
- if (!record.proof_path) return { error: "storage_error", details: "No proof_path on record" };
63
- let proofBytes;
64
- try {
65
- proofBytes = readFileSync(record.proof_path);
66
- } catch (e) {
67
- return { error: "storage_error", details: String(e) };
68
- }
69
- const client = new OpenTimestampsClient2({
70
- calendars: config.calendars,
71
- resilience: { timeout: config.calendar_timeout_ms }
72
- });
73
- let result;
74
- try {
75
- result = await client.verify(proofBytes, record.hash);
76
- } catch (e) {
77
- logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: String(e) });
78
- return { status: "network_error", hash: record.hash, details: String(e) };
79
- }
80
- if (!result.valid) {
81
- if (result.error?.includes("No Bitcoin attestation")) {
82
- logOperation(db, { stamp_id: input.id, action: "verify", result: "pending" });
83
- return { status: "pending", hash: record.hash, calendars: config.calendars };
84
- }
85
- if (result.error?.toLowerCase().includes("invalid") || result.error?.toLowerCase().includes("corrupt")) {
86
- logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
87
- return { status: "invalid", hash: record.hash, reason: result.error ?? "unknown" };
88
- }
89
- logOperation(db, { stamp_id: input.id, action: "verify", result: "failed", error_msg: result.error });
90
- return { status: "unknown", hash: record.hash };
91
- }
92
- logOperation(db, { stamp_id: input.id, action: "verify", result: "success" });
93
- return {
94
- status: "confirmed",
95
- hash: record.hash,
96
- bitcoin_block: result.blockHeight,
97
- bitcoin_time: new Date(result.timestamp * 1e3).toISOString()
98
- };
99
- }
100
-
101
- // src/tools/list-pending.ts
102
- function listPending(input, db, _config) {
103
- return listStamps(db, {
104
- status: input.status ?? "pending",
105
- limit: Math.min(input.limit ?? 50, 200),
106
- offset: input.offset ?? 0,
107
- older_than_hours: input.older_than_hours
108
- });
109
- }
110
-
111
- export {
112
- createTimestamp,
113
- verifyTimestamp,
114
- listPending
115
- };
@@ -1,47 +0,0 @@
1
- import {
2
- getDb,
3
- loadConfig,
4
- upgradeTimestamp
5
- } from "./chunk-ASPLCXMC.js";
6
- import "./chunk-YFSUDT24.js";
7
-
8
- // src/tools/watch.ts
9
- async function watchPending(intervalMinutes = 5) {
10
- const config = loadConfig();
11
- const db = getDb(config);
12
- process.stdout.write(`Watching pending stamps every ${intervalMinutes} min. Ctrl+C to stop.
13
-
14
- `);
15
- async function tick() {
16
- const rows = db.prepare(`SELECT id, hash, status FROM stamps WHERE status = 'pending'`).all();
17
- if (rows.length === 0) {
18
- process.stdout.write(`${now()} All stamps confirmed.
19
- `);
20
- process.exit(0);
21
- }
22
- process.stdout.write(`${now()} Checking ${rows.length} pending stamps...
23
- `);
24
- for (const row of rows) {
25
- const result = await upgradeTimestamp({ id: row.id }, db, config);
26
- if ("error" in result) {
27
- process.stdout.write(` ${row.id.slice(0, 8)}... ERROR: ${result.error}
28
- `);
29
- } else if (result.status === "confirmed") {
30
- process.stdout.write(` ${row.id.slice(0, 8)}... CONFIRMED block #${result.bitcoin_block}
31
- `);
32
- } else {
33
- process.stdout.write(` ${row.id.slice(0, 8)}... pending (attempt ${result.attempt_count})
34
- `);
35
- }
36
- }
37
- process.stdout.write("\n");
38
- }
39
- function now() {
40
- return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
41
- }
42
- await tick();
43
- setInterval(tick, intervalMinutes * 60 * 1e3);
44
- }
45
- export {
46
- watchPending
47
- };