chron-mcp 0.1.7 → 0.1.9
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 +17 -0
- package/dist/db/index.d.ts +3 -1
- package/dist/db/index.js +11 -0
- package/dist/db/schema.d.ts +118 -10
- package/dist/db/schema.js +2 -0
- package/dist/index.js +12 -0
- package/dist/server.js +4 -0
- package/dist/tools/messages.js +25 -1
- package/dist/tools/sessions.js +5 -1
- package/dist/tools/verify.d.ts +9 -0
- package/dist/tools/verify.js +49 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +9 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -150,6 +150,23 @@ Chron ships with `skills/chron.skill.md` — a plain-text instruction file that
|
|
|
150
150
|
| `log_exchange` | Log a user/assistant pair atomically (for batch imports) |
|
|
151
151
|
| `list_sessions` | List all sessions ordered by most recently active |
|
|
152
152
|
| `get_session_history` | Retrieve the full timestamped log for a session |
|
|
153
|
+
| `verify_session` | Verify the tamper-evident hash chain — detects any post-log edits |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Tamper-evident hash chaining
|
|
158
|
+
|
|
159
|
+
Every message is linked to the previous one via a SHA-256 chain:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
content_hash = SHA256(session_id | role | content | created_at | prev_hash)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`verify_session` walks the chain and returns:
|
|
166
|
+
- `valid: true` — no messages were modified after logging
|
|
167
|
+
- `valid: false, first_break: <id>` — exact row ID of the first tampered message
|
|
168
|
+
|
|
169
|
+
This turns your local log into a verifiable audit artifact. Any edit to a stored message — content, timestamp, or role — breaks the chain and is detected immediately.
|
|
153
170
|
|
|
154
171
|
---
|
|
155
172
|
|
package/dist/db/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import * as schema from './schema';
|
|
2
|
-
export declare function initDb(dbPath?: string): Promise<import("drizzle-orm/libsql").LibSQLDatabase<typeof schema
|
|
2
|
+
export declare function initDb(dbPath?: string): Promise<import("drizzle-orm/libsql").LibSQLDatabase<typeof schema> & {
|
|
3
|
+
$client: import("@libsql/client").Client;
|
|
4
|
+
}>;
|
|
3
5
|
export type Db = Awaited<ReturnType<typeof initDb>>;
|
package/dist/db/index.js
CHANGED
|
@@ -60,6 +60,8 @@ const CREATE_SQL = [
|
|
|
60
60
|
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
61
61
|
content TEXT NOT NULL,
|
|
62
62
|
created_at TEXT NOT NULL,
|
|
63
|
+
prev_hash TEXT,
|
|
64
|
+
content_hash TEXT,
|
|
63
65
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
64
66
|
)`,
|
|
65
67
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
|
|
@@ -76,5 +78,14 @@ async function initDb(dbPath) {
|
|
|
76
78
|
for (const sql of CREATE_SQL) {
|
|
77
79
|
await client.execute(sql);
|
|
78
80
|
}
|
|
81
|
+
// Migrate existing messages tables that predate hash chaining columns
|
|
82
|
+
const tableInfo = await client.execute('PRAGMA table_info(messages)');
|
|
83
|
+
const columns = tableInfo.rows.map((r) => r[1]);
|
|
84
|
+
if (!columns.includes('prev_hash')) {
|
|
85
|
+
await client.execute('ALTER TABLE messages ADD COLUMN prev_hash TEXT');
|
|
86
|
+
}
|
|
87
|
+
if (!columns.includes('content_hash')) {
|
|
88
|
+
await client.execute('ALTER TABLE messages ADD COLUMN content_hash TEXT');
|
|
89
|
+
}
|
|
79
90
|
return (0, libsql_1.drizzle)(client, { schema });
|
|
80
91
|
}
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -11,9 +11,16 @@ export declare const sessions: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
11
11
|
driverParam: string;
|
|
12
12
|
notNull: true;
|
|
13
13
|
hasDefault: false;
|
|
14
|
+
isPrimaryKey: true;
|
|
15
|
+
isAutoincrement: false;
|
|
16
|
+
hasRuntimeDefault: false;
|
|
14
17
|
enumValues: [string, ...string[]];
|
|
15
18
|
baseColumn: never;
|
|
16
|
-
|
|
19
|
+
identity: undefined;
|
|
20
|
+
generated: undefined;
|
|
21
|
+
}, {}, {
|
|
22
|
+
length: number | undefined;
|
|
23
|
+
}>;
|
|
17
24
|
title: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
18
25
|
name: "title";
|
|
19
26
|
tableName: "sessions";
|
|
@@ -23,9 +30,16 @@ export declare const sessions: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
23
30
|
driverParam: string;
|
|
24
31
|
notNull: true;
|
|
25
32
|
hasDefault: false;
|
|
33
|
+
isPrimaryKey: false;
|
|
34
|
+
isAutoincrement: false;
|
|
35
|
+
hasRuntimeDefault: false;
|
|
26
36
|
enumValues: [string, ...string[]];
|
|
27
37
|
baseColumn: never;
|
|
28
|
-
|
|
38
|
+
identity: undefined;
|
|
39
|
+
generated: undefined;
|
|
40
|
+
}, {}, {
|
|
41
|
+
length: number | undefined;
|
|
42
|
+
}>;
|
|
29
43
|
ai_tool: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
30
44
|
name: "ai_tool";
|
|
31
45
|
tableName: "sessions";
|
|
@@ -35,9 +49,16 @@ export declare const sessions: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
35
49
|
driverParam: string;
|
|
36
50
|
notNull: false;
|
|
37
51
|
hasDefault: false;
|
|
52
|
+
isPrimaryKey: false;
|
|
53
|
+
isAutoincrement: false;
|
|
54
|
+
hasRuntimeDefault: false;
|
|
38
55
|
enumValues: [string, ...string[]];
|
|
39
56
|
baseColumn: never;
|
|
40
|
-
|
|
57
|
+
identity: undefined;
|
|
58
|
+
generated: undefined;
|
|
59
|
+
}, {}, {
|
|
60
|
+
length: number | undefined;
|
|
61
|
+
}>;
|
|
41
62
|
created_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
42
63
|
name: "created_at";
|
|
43
64
|
tableName: "sessions";
|
|
@@ -47,9 +68,16 @@ export declare const sessions: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
47
68
|
driverParam: string;
|
|
48
69
|
notNull: true;
|
|
49
70
|
hasDefault: false;
|
|
71
|
+
isPrimaryKey: false;
|
|
72
|
+
isAutoincrement: false;
|
|
73
|
+
hasRuntimeDefault: false;
|
|
50
74
|
enumValues: [string, ...string[]];
|
|
51
75
|
baseColumn: never;
|
|
52
|
-
|
|
76
|
+
identity: undefined;
|
|
77
|
+
generated: undefined;
|
|
78
|
+
}, {}, {
|
|
79
|
+
length: number | undefined;
|
|
80
|
+
}>;
|
|
53
81
|
updated_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
54
82
|
name: "updated_at";
|
|
55
83
|
tableName: "sessions";
|
|
@@ -59,9 +87,16 @@ export declare const sessions: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
59
87
|
driverParam: string;
|
|
60
88
|
notNull: true;
|
|
61
89
|
hasDefault: false;
|
|
90
|
+
isPrimaryKey: false;
|
|
91
|
+
isAutoincrement: false;
|
|
92
|
+
hasRuntimeDefault: false;
|
|
62
93
|
enumValues: [string, ...string[]];
|
|
63
94
|
baseColumn: never;
|
|
64
|
-
|
|
95
|
+
identity: undefined;
|
|
96
|
+
generated: undefined;
|
|
97
|
+
}, {}, {
|
|
98
|
+
length: number | undefined;
|
|
99
|
+
}>;
|
|
65
100
|
};
|
|
66
101
|
dialect: "sqlite";
|
|
67
102
|
}>;
|
|
@@ -78,9 +113,16 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
78
113
|
driverParam: string;
|
|
79
114
|
notNull: true;
|
|
80
115
|
hasDefault: false;
|
|
116
|
+
isPrimaryKey: true;
|
|
117
|
+
isAutoincrement: false;
|
|
118
|
+
hasRuntimeDefault: false;
|
|
81
119
|
enumValues: [string, ...string[]];
|
|
82
120
|
baseColumn: never;
|
|
83
|
-
|
|
121
|
+
identity: undefined;
|
|
122
|
+
generated: undefined;
|
|
123
|
+
}, {}, {
|
|
124
|
+
length: number | undefined;
|
|
125
|
+
}>;
|
|
84
126
|
session_id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
85
127
|
name: "session_id";
|
|
86
128
|
tableName: "messages";
|
|
@@ -90,9 +132,16 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
90
132
|
driverParam: string;
|
|
91
133
|
notNull: true;
|
|
92
134
|
hasDefault: false;
|
|
135
|
+
isPrimaryKey: false;
|
|
136
|
+
isAutoincrement: false;
|
|
137
|
+
hasRuntimeDefault: false;
|
|
93
138
|
enumValues: [string, ...string[]];
|
|
94
139
|
baseColumn: never;
|
|
95
|
-
|
|
140
|
+
identity: undefined;
|
|
141
|
+
generated: undefined;
|
|
142
|
+
}, {}, {
|
|
143
|
+
length: number | undefined;
|
|
144
|
+
}>;
|
|
96
145
|
role: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
97
146
|
name: "role";
|
|
98
147
|
tableName: "messages";
|
|
@@ -102,9 +151,16 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
102
151
|
driverParam: string;
|
|
103
152
|
notNull: true;
|
|
104
153
|
hasDefault: false;
|
|
154
|
+
isPrimaryKey: false;
|
|
155
|
+
isAutoincrement: false;
|
|
156
|
+
hasRuntimeDefault: false;
|
|
105
157
|
enumValues: ["user", "assistant"];
|
|
106
158
|
baseColumn: never;
|
|
107
|
-
|
|
159
|
+
identity: undefined;
|
|
160
|
+
generated: undefined;
|
|
161
|
+
}, {}, {
|
|
162
|
+
length: number | undefined;
|
|
163
|
+
}>;
|
|
108
164
|
content: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
109
165
|
name: "content";
|
|
110
166
|
tableName: "messages";
|
|
@@ -114,9 +170,16 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
114
170
|
driverParam: string;
|
|
115
171
|
notNull: true;
|
|
116
172
|
hasDefault: false;
|
|
173
|
+
isPrimaryKey: false;
|
|
174
|
+
isAutoincrement: false;
|
|
175
|
+
hasRuntimeDefault: false;
|
|
117
176
|
enumValues: [string, ...string[]];
|
|
118
177
|
baseColumn: never;
|
|
119
|
-
|
|
178
|
+
identity: undefined;
|
|
179
|
+
generated: undefined;
|
|
180
|
+
}, {}, {
|
|
181
|
+
length: number | undefined;
|
|
182
|
+
}>;
|
|
120
183
|
created_at: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
121
184
|
name: "created_at";
|
|
122
185
|
tableName: "messages";
|
|
@@ -126,9 +189,54 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
|
|
|
126
189
|
driverParam: string;
|
|
127
190
|
notNull: true;
|
|
128
191
|
hasDefault: false;
|
|
192
|
+
isPrimaryKey: false;
|
|
193
|
+
isAutoincrement: false;
|
|
194
|
+
hasRuntimeDefault: false;
|
|
129
195
|
enumValues: [string, ...string[]];
|
|
130
196
|
baseColumn: never;
|
|
131
|
-
|
|
197
|
+
identity: undefined;
|
|
198
|
+
generated: undefined;
|
|
199
|
+
}, {}, {
|
|
200
|
+
length: number | undefined;
|
|
201
|
+
}>;
|
|
202
|
+
prev_hash: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
203
|
+
name: "prev_hash";
|
|
204
|
+
tableName: "messages";
|
|
205
|
+
dataType: "string";
|
|
206
|
+
columnType: "SQLiteText";
|
|
207
|
+
data: string;
|
|
208
|
+
driverParam: string;
|
|
209
|
+
notNull: false;
|
|
210
|
+
hasDefault: false;
|
|
211
|
+
isPrimaryKey: false;
|
|
212
|
+
isAutoincrement: false;
|
|
213
|
+
hasRuntimeDefault: false;
|
|
214
|
+
enumValues: [string, ...string[]];
|
|
215
|
+
baseColumn: never;
|
|
216
|
+
identity: undefined;
|
|
217
|
+
generated: undefined;
|
|
218
|
+
}, {}, {
|
|
219
|
+
length: number | undefined;
|
|
220
|
+
}>;
|
|
221
|
+
content_hash: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
222
|
+
name: "content_hash";
|
|
223
|
+
tableName: "messages";
|
|
224
|
+
dataType: "string";
|
|
225
|
+
columnType: "SQLiteText";
|
|
226
|
+
data: string;
|
|
227
|
+
driverParam: string;
|
|
228
|
+
notNull: false;
|
|
229
|
+
hasDefault: false;
|
|
230
|
+
isPrimaryKey: false;
|
|
231
|
+
isAutoincrement: false;
|
|
232
|
+
hasRuntimeDefault: false;
|
|
233
|
+
enumValues: [string, ...string[]];
|
|
234
|
+
baseColumn: never;
|
|
235
|
+
identity: undefined;
|
|
236
|
+
generated: undefined;
|
|
237
|
+
}, {}, {
|
|
238
|
+
length: number | undefined;
|
|
239
|
+
}>;
|
|
132
240
|
};
|
|
133
241
|
dialect: "sqlite";
|
|
134
242
|
}>;
|
package/dist/db/schema.js
CHANGED
|
@@ -15,4 +15,6 @@ exports.messages = (0, sqlite_core_1.sqliteTable)('messages', {
|
|
|
15
15
|
role: (0, sqlite_core_1.text)('role', { enum: ['user', 'assistant'] }).notNull(),
|
|
16
16
|
content: (0, sqlite_core_1.text)('content').notNull(),
|
|
17
17
|
created_at: (0, sqlite_core_1.text)('created_at').notNull(),
|
|
18
|
+
prev_hash: (0, sqlite_core_1.text)('prev_hash'),
|
|
19
|
+
content_hash: (0, sqlite_core_1.text)('content_hash'),
|
|
18
20
|
});
|
package/dist/index.js
CHANGED
|
@@ -35,9 +35,20 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
})();
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
38
|
+
const os_1 = require("os");
|
|
39
|
+
const path_1 = require("path");
|
|
38
40
|
const index_1 = require("./db/index");
|
|
39
41
|
const server_1 = require("./server");
|
|
42
|
+
const VERSION = '0.1.9';
|
|
43
|
+
// --version: quick install check, safe to run anywhere
|
|
44
|
+
if (process.argv[2] === '--version' || process.argv[2] === '-v') {
|
|
45
|
+
process.stdout.write(`chron-mcp ${VERSION}\n`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
40
48
|
async function main() {
|
|
49
|
+
const dbPath = process.env.CHRON_DB_PATH ?? (0, path_1.join)((0, os_1.homedir)(), '.chron', 'chron.db');
|
|
50
|
+
process.stderr.write(`chron-mcp ${VERSION} starting\n`);
|
|
51
|
+
process.stderr.write(`database: ${dbPath}\n`);
|
|
41
52
|
const db = await (0, index_1.initDb)();
|
|
42
53
|
const server = (0, server_1.createServer)(db);
|
|
43
54
|
if (process.env.CHRON_TRANSPORT === 'http') {
|
|
@@ -47,6 +58,7 @@ async function main() {
|
|
|
47
58
|
else {
|
|
48
59
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
49
60
|
await server.connect(transport);
|
|
61
|
+
process.stderr.write('ready\n');
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
main().catch((err) => {
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const sessions_1 = require("./tools/sessions");
|
|
7
7
|
const messages_1 = require("./tools/messages");
|
|
8
|
+
const verify_1 = require("./tools/verify");
|
|
8
9
|
const roleEnum = zod_1.z.enum(['user', 'assistant']);
|
|
9
10
|
function createServer(db) {
|
|
10
11
|
const server = new mcp_js_1.McpServer({
|
|
@@ -34,5 +35,8 @@ function createServer(db) {
|
|
|
34
35
|
session_id: zod_1.z.string(),
|
|
35
36
|
limit: zod_1.z.number().int().positive().optional().describe('Return only the most recent N messages'),
|
|
36
37
|
}, (0, sessions_1.getSessionHistory)(db));
|
|
38
|
+
server.tool('verify_session', 'Verify the tamper-evident hash chain for a session. Returns valid=true if no rows were edited after logging, or the first broken link if tampering is detected.', {
|
|
39
|
+
session_id: zod_1.z.string().describe('Session ID to verify'),
|
|
40
|
+
}, (0, verify_1.verifySession)(db));
|
|
37
41
|
return server;
|
|
38
42
|
}
|
package/dist/tools/messages.js
CHANGED
|
@@ -6,26 +6,36 @@ const drizzle_orm_1 = require("drizzle-orm");
|
|
|
6
6
|
const uuid_1 = require("uuid");
|
|
7
7
|
const schema_1 = require("../db/schema");
|
|
8
8
|
const time_1 = require("../utils/time");
|
|
9
|
+
const hash_1 = require("../utils/hash");
|
|
9
10
|
function logMessage(db) {
|
|
10
11
|
return async (args) => {
|
|
11
12
|
const existing = await db.select({ id: schema_1.sessions.id }).from(schema_1.sessions).where((0, drizzle_orm_1.eq)(schema_1.sessions.id, args.session_id)).limit(1);
|
|
12
13
|
if (existing.length === 0) {
|
|
13
14
|
return { content: [{ type: 'text', text: `Session not found: ${args.session_id}` }], isError: true };
|
|
14
15
|
}
|
|
16
|
+
const last = await db.select({ content_hash: schema_1.messages.content_hash })
|
|
17
|
+
.from(schema_1.messages)
|
|
18
|
+
.where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, args.session_id))
|
|
19
|
+
.orderBy((0, drizzle_orm_1.desc)(schema_1.messages.created_at), (0, drizzle_orm_1.desc)((0, drizzle_orm_1.sql) `rowid`))
|
|
20
|
+
.limit(1);
|
|
15
21
|
const now = (0, time_1.localISOString)();
|
|
16
22
|
const id = (0, uuid_1.v4)();
|
|
23
|
+
const prevHash = last[0]?.content_hash ?? null;
|
|
24
|
+
const contentHash = (0, hash_1.computeContentHash)(args.session_id, args.role, args.content, now, prevHash);
|
|
17
25
|
await db.insert(schema_1.messages).values({
|
|
18
26
|
id,
|
|
19
27
|
session_id: args.session_id,
|
|
20
28
|
role: args.role,
|
|
21
29
|
content: args.content,
|
|
22
30
|
created_at: now,
|
|
31
|
+
prev_hash: prevHash,
|
|
32
|
+
content_hash: contentHash,
|
|
23
33
|
});
|
|
24
34
|
await db.update(schema_1.sessions)
|
|
25
35
|
.set({ updated_at: now })
|
|
26
36
|
.where((0, drizzle_orm_1.eq)(schema_1.sessions.id, args.session_id));
|
|
27
37
|
return {
|
|
28
|
-
content: [{ type: 'text', text: JSON.stringify({ message_id: id, created_at: now }) }],
|
|
38
|
+
content: [{ type: 'text', text: JSON.stringify({ message_id: id, created_at: now, content_hash: contentHash }) }],
|
|
29
39
|
};
|
|
30
40
|
};
|
|
31
41
|
}
|
|
@@ -35,16 +45,26 @@ function logExchange(db) {
|
|
|
35
45
|
if (existing.length === 0) {
|
|
36
46
|
return { content: [{ type: 'text', text: `Session not found: ${args.session_id}` }], isError: true };
|
|
37
47
|
}
|
|
48
|
+
const last = await db.select({ content_hash: schema_1.messages.content_hash })
|
|
49
|
+
.from(schema_1.messages)
|
|
50
|
+
.where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, args.session_id))
|
|
51
|
+
.orderBy((0, drizzle_orm_1.desc)(schema_1.messages.created_at), (0, drizzle_orm_1.desc)((0, drizzle_orm_1.sql) `rowid`))
|
|
52
|
+
.limit(1);
|
|
38
53
|
const userNow = (0, time_1.localISOString)();
|
|
39
54
|
const userId = (0, uuid_1.v4)();
|
|
40
55
|
const assistantNow = (0, time_1.localISOString)();
|
|
41
56
|
const assistantId = (0, uuid_1.v4)();
|
|
57
|
+
const prevHash = last[0]?.content_hash ?? null;
|
|
58
|
+
const userHash = (0, hash_1.computeContentHash)(args.session_id, 'user', args.user_content, userNow, prevHash);
|
|
59
|
+
const assistantHash = (0, hash_1.computeContentHash)(args.session_id, 'assistant', args.assistant_content, assistantNow, userHash);
|
|
42
60
|
await db.insert(schema_1.messages).values({
|
|
43
61
|
id: userId,
|
|
44
62
|
session_id: args.session_id,
|
|
45
63
|
role: 'user',
|
|
46
64
|
content: args.user_content,
|
|
47
65
|
created_at: userNow,
|
|
66
|
+
prev_hash: prevHash,
|
|
67
|
+
content_hash: userHash,
|
|
48
68
|
});
|
|
49
69
|
await db.insert(schema_1.messages).values({
|
|
50
70
|
id: assistantId,
|
|
@@ -52,6 +72,8 @@ function logExchange(db) {
|
|
|
52
72
|
role: 'assistant',
|
|
53
73
|
content: args.assistant_content,
|
|
54
74
|
created_at: assistantNow,
|
|
75
|
+
prev_hash: userHash,
|
|
76
|
+
content_hash: assistantHash,
|
|
55
77
|
});
|
|
56
78
|
await db.update(schema_1.sessions)
|
|
57
79
|
.set({ updated_at: assistantNow })
|
|
@@ -62,8 +84,10 @@ function logExchange(db) {
|
|
|
62
84
|
text: JSON.stringify({
|
|
63
85
|
user_message_id: userId,
|
|
64
86
|
user_created_at: userNow,
|
|
87
|
+
user_content_hash: userHash,
|
|
65
88
|
assistant_message_id: assistantId,
|
|
66
89
|
assistant_created_at: assistantNow,
|
|
90
|
+
assistant_content_hash: assistantHash,
|
|
67
91
|
}),
|
|
68
92
|
}],
|
|
69
93
|
};
|
package/dist/tools/sessions.js
CHANGED
|
@@ -27,7 +27,11 @@ function startSession(db) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
|
30
|
-
|
|
30
|
+
const isUnique = e?.message?.includes('UNIQUE constraint failed') ||
|
|
31
|
+
e?.code === 'SQLITE_CONSTRAINT_UNIQUE' ||
|
|
32
|
+
e?.cause?.message?.includes('UNIQUE constraint failed') ||
|
|
33
|
+
e?.cause?.extendedCode === 'SQLITE_CONSTRAINT_UNIQUE';
|
|
34
|
+
if (!isUnique)
|
|
31
35
|
throw e;
|
|
32
36
|
const existing = await db.select().from(schema_1.sessions).where((0, drizzle_orm_1.eq)(schema_1.sessions.title, args.title)).limit(1);
|
|
33
37
|
const session = existing[0];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifySession = verifySession;
|
|
4
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
5
|
+
const schema_1 = require("../db/schema");
|
|
6
|
+
const hash_1 = require("../utils/hash");
|
|
7
|
+
function verifySession(db) {
|
|
8
|
+
return async (args) => {
|
|
9
|
+
const rows = await db.select().from(schema_1.messages)
|
|
10
|
+
.where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, args.session_id))
|
|
11
|
+
.orderBy((0, drizzle_orm_1.asc)(schema_1.messages.created_at), (0, drizzle_orm_1.asc)((0, drizzle_orm_1.sql) `rowid`));
|
|
12
|
+
const chained = rows.filter(r => r.content_hash !== null);
|
|
13
|
+
if (chained.length === 0) {
|
|
14
|
+
return {
|
|
15
|
+
content: [{
|
|
16
|
+
type: 'text',
|
|
17
|
+
text: JSON.stringify({ valid: true, messages: rows.length, chained: 0, note: 'No chained messages — pre-dates hash chaining' }),
|
|
18
|
+
}],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
for (let i = 0; i < chained.length; i++) {
|
|
22
|
+
const row = chained[i];
|
|
23
|
+
const expectedPrev = i === 0 ? null : chained[i - 1].content_hash;
|
|
24
|
+
if (row.prev_hash !== expectedPrev) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: JSON.stringify({ valid: false, first_break: row.id, reason: 'prev_hash mismatch' }),
|
|
29
|
+
}],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const expected = (0, hash_1.computeContentHash)(row.session_id, row.role, row.content, row.created_at, row.prev_hash);
|
|
33
|
+
if (row.content_hash !== expected) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: JSON.stringify({ valid: false, first_break: row.id, reason: 'content_hash mismatch — row was tampered' }),
|
|
38
|
+
}],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: JSON.stringify({ valid: true, messages: rows.length, chained: chained.length }),
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function computeContentHash(sessionId: string, role: string, content: string, createdAt: string, prevHash: string | null): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeContentHash = computeContentHash;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
function computeContentHash(sessionId, role, content, createdAt, prevHash) {
|
|
6
|
+
return (0, crypto_1.createHash)('sha256')
|
|
7
|
+
.update(`${sessionId}|${role}|${content}|${createdAt}|${prevHash ?? ''}`)
|
|
8
|
+
.digest('hex');
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chron-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"mcpName": "io.github.SirinivasK/chron",
|
|
5
5
|
"description": "Audit-grade timestamped logs for every AI conversation",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/
|
|
8
|
+
"url": "https://github.com/sirinivask/chron.git"
|
|
9
9
|
},
|
|
10
10
|
"main": "dist/index.js",
|
|
11
11
|
"vitest": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@libsql/client": "^0.17.3",
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
50
|
-
"drizzle-orm": "^0.
|
|
50
|
+
"drizzle-orm": "^0.45.2",
|
|
51
51
|
"express": "^4.18.2",
|
|
52
52
|
"uuid": "^11.1.1",
|
|
53
53
|
"zod": "^3.22.4"
|