chron-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
@@ -75,7 +75,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
75
75
  ### Claude Code
76
76
 
77
77
  ```bash
78
- claude mcp add chron npx -y chron-mcp
78
+ claude mcp add chron -- npx -y chron-mcp
79
79
  ```
80
80
 
81
81
  Then add the skill hook to `~/.claude/settings.json`:
package/dist/db/index.js CHANGED
@@ -49,7 +49,7 @@ function getDbPath() {
49
49
  const CREATE_SQL = [
50
50
  `CREATE TABLE IF NOT EXISTS sessions (
51
51
  id TEXT PRIMARY KEY,
52
- title TEXT NOT NULL,
52
+ title TEXT NOT NULL UNIQUE,
53
53
  ai_tool TEXT,
54
54
  created_at TEXT NOT NULL,
55
55
  updated_at TEXT NOT NULL
@@ -60,8 +60,10 @@ 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
- FOREIGN KEY (session_id) REFERENCES sessions(id)
63
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
64
64
  )`,
65
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_title ON sessions(title)`,
66
+ `CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)`,
65
67
  ];
66
68
  async function initDb(dbPath) {
67
69
  const path = dbPath ?? getDbPath();
package/dist/db/schema.js CHANGED
@@ -4,14 +4,14 @@ exports.messages = exports.sessions = void 0;
4
4
  const sqlite_core_1 = require("drizzle-orm/sqlite-core");
5
5
  exports.sessions = (0, sqlite_core_1.sqliteTable)('sessions', {
6
6
  id: (0, sqlite_core_1.text)('id').primaryKey(),
7
- title: (0, sqlite_core_1.text)('title').notNull(),
7
+ title: (0, sqlite_core_1.text)('title').notNull().unique(),
8
8
  ai_tool: (0, sqlite_core_1.text)('ai_tool'),
9
9
  created_at: (0, sqlite_core_1.text)('created_at').notNull(),
10
10
  updated_at: (0, sqlite_core_1.text)('updated_at').notNull(),
11
11
  });
12
12
  exports.messages = (0, sqlite_core_1.sqliteTable)('messages', {
13
13
  id: (0, sqlite_core_1.text)('id').primaryKey(),
14
- session_id: (0, sqlite_core_1.text)('session_id').notNull(),
14
+ session_id: (0, sqlite_core_1.text)('session_id').notNull().references(() => exports.sessions.id),
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(),
@@ -10,29 +10,45 @@ const middleware_1 = require("./middleware");
10
10
  const server_1 = require("../server");
11
11
  async function startHttpServer(server, db) {
12
12
  const app = (0, express_1.default)();
13
- app.use(express_1.default.json());
13
+ app.use(express_1.default.json({ limit: '1mb' }));
14
14
  const apiKey = process.env.CHRON_API_KEY;
15
15
  if (apiKey) {
16
16
  app.use((0, middleware_1.apiKeyMiddleware)(apiKey));
17
17
  }
18
18
  const transports = new Map();
19
19
  app.get('/sse', async (req, res) => {
20
- const instanceServer = (0, server_1.createServer)(db);
21
- const transport = new sse_js_1.SSEServerTransport('/messages', res);
22
- transports.set(transport.sessionId, { transport, server: instanceServer });
23
- transport.onclose = () => transports.delete(transport.sessionId);
24
- await instanceServer.connect(transport);
20
+ try {
21
+ const instanceServer = (0, server_1.createServer)(db);
22
+ const transport = new sse_js_1.SSEServerTransport('/messages', res);
23
+ transports.set(transport.sessionId, { transport, server: instanceServer });
24
+ transport.onclose = () => transports.delete(transport.sessionId);
25
+ await instanceServer.connect(transport);
26
+ }
27
+ catch (err) {
28
+ process.stderr.write(`SSE error: ${err}\n`);
29
+ if (!res.headersSent)
30
+ res.status(500).json({ error: 'Internal server error' });
31
+ }
25
32
  });
26
33
  app.post('/messages', async (req, res) => {
27
- const sessionId = req.query.sessionId;
28
- const entry = transports.get(sessionId);
29
- if (!entry) {
30
- res.status(404).json({ error: 'Session not found' });
31
- return;
34
+ try {
35
+ const sessionId = req.query.sessionId;
36
+ const entry = transports.get(sessionId);
37
+ if (!entry) {
38
+ res.status(404).json({ error: 'Session not found' });
39
+ return;
40
+ }
41
+ await entry.transport.handlePostMessage(req, res);
42
+ }
43
+ catch (err) {
44
+ process.stderr.write(`Messages error: ${err}\n`);
45
+ if (!res.headersSent)
46
+ res.status(500).json({ error: 'Internal server error' });
32
47
  }
33
- await entry.transport.handlePostMessage(req, res);
34
48
  });
35
49
  const port = parseInt(process.env.PORT ?? '3001', 10);
50
+ if (isNaN(port))
51
+ throw new Error(`Invalid PORT value: ${process.env.PORT}`);
36
52
  await new Promise(resolve => app.listen(port, resolve));
37
53
  process.stderr.write(`Chron HTTP server listening on port ${port}\n`);
38
54
  }
package/dist/server.js CHANGED
@@ -9,19 +9,19 @@ const roleEnum = zod_1.z.enum(['user', 'assistant']);
9
9
  function createServer(db) {
10
10
  const server = new mcp_js_1.McpServer({
11
11
  name: 'chron',
12
- version: '0.1.0',
12
+ version: '0.1.6',
13
13
  });
14
14
  // @ts-expect-error — MCP SDK deep generic type instantiation limit hit; runtime types are correct
15
15
  server.tool('start_session', 'Create a new audit session or resume an existing one by title. Call this at the start of every conversation.', {
16
16
  title: zod_1.z.string().describe('Descriptive session title, e.g. "Contract review — 2026-05-08"'),
17
17
  ai_tool: zod_1.z.string().optional().describe('AI tool name: "claude", "cursor", "windsurf", etc.'),
18
18
  }, (0, sessions_1.startSession)(db));
19
- server.tool('log_message', 'Record a single message (user or assistant) with the current UTC datetime. Call before responding for user messages, and before sending for assistant messages.', {
19
+ server.tool('log_message', 'Record a single message (user or assistant) with the current local datetime and timezone offset. Call before responding for user messages, and before sending for assistant messages.', {
20
20
  session_id: zod_1.z.string().describe('Session ID returned by start_session'),
21
21
  role: roleEnum,
22
22
  content: zod_1.z.string().describe('Full message text'),
23
23
  }, (0, messages_1.logMessage)(db));
24
- server.tool('log_exchange', 'Log a user message and assistant response together in one call. Preferred over two log_message calls faster and atomic.', {
24
+ server.tool('log_exchange', 'Record a user+assistant exchange from historical or batch imports only. Do NOT use for live conversations — both timestamps are captured at the same instant with no real gap. For live sessions always call log_message twice: once for the user message, once for the assistant response.', {
25
25
  session_id: zod_1.z.string().describe('Session ID returned by start_session'),
26
26
  user_content: zod_1.z.string().describe('The exact user message'),
27
27
  assistant_content: zod_1.z.string().describe('The exact assistant response'),
@@ -8,6 +8,13 @@ export declare function logMessage(db: Db): (args: {
8
8
  type: "text";
9
9
  text: string;
10
10
  }[];
11
+ isError: boolean;
12
+ } | {
13
+ content: {
14
+ type: "text";
15
+ text: string;
16
+ }[];
17
+ isError?: undefined;
11
18
  }>;
12
19
  export declare function logExchange(db: Db): (args: {
13
20
  session_id: string;
@@ -18,4 +25,11 @@ export declare function logExchange(db: Db): (args: {
18
25
  type: "text";
19
26
  text: string;
20
27
  }[];
28
+ isError: boolean;
29
+ } | {
30
+ content: {
31
+ type: "text";
32
+ text: string;
33
+ }[];
34
+ isError?: undefined;
21
35
  }>;
@@ -8,6 +8,10 @@ const schema_1 = require("../db/schema");
8
8
  const time_1 = require("../utils/time");
9
9
  function logMessage(db) {
10
10
  return async (args) => {
11
+ 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
+ if (existing.length === 0) {
13
+ return { content: [{ type: 'text', text: `Session not found: ${args.session_id}` }], isError: true };
14
+ }
11
15
  const now = (0, time_1.localISOString)();
12
16
  const id = (0, uuid_1.v4)();
13
17
  await db.insert(schema_1.messages).values({
@@ -27,6 +31,10 @@ function logMessage(db) {
27
31
  }
28
32
  function logExchange(db) {
29
33
  return async (args) => {
34
+ 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);
35
+ if (existing.length === 0) {
36
+ return { content: [{ type: 'text', text: `Session not found: ${args.session_id}` }], isError: true };
37
+ }
30
38
  const userNow = (0, time_1.localISOString)();
31
39
  const userId = (0, uuid_1.v4)();
32
40
  const assistantNow = (0, time_1.localISOString)();
@@ -9,34 +9,37 @@ const schema_1 = require("../db/schema");
9
9
  const time_1 = require("../utils/time");
10
10
  function startSession(db) {
11
11
  return async (args) => {
12
- const existing = await db.select().from(schema_1.sessions).where((0, drizzle_orm_1.eq)(schema_1.sessions.title, args.title));
13
- if (existing.length > 0) {
12
+ const id = (0, uuid_1.v4)();
13
+ const now = (0, time_1.localISOString)();
14
+ try {
15
+ await db.insert(schema_1.sessions).values({
16
+ id,
17
+ title: args.title,
18
+ ai_tool: args.ai_tool ?? null,
19
+ created_at: now,
20
+ updated_at: now,
21
+ });
22
+ return {
23
+ content: [{
24
+ type: 'text',
25
+ text: JSON.stringify({ session_id: id, created: true, message_count: 0, ai_tool: args.ai_tool ?? null }),
26
+ }],
27
+ };
28
+ }
29
+ catch (e) {
30
+ if (!e?.message?.includes('UNIQUE constraint failed'))
31
+ throw e;
32
+ const existing = await db.select().from(schema_1.sessions).where((0, drizzle_orm_1.eq)(schema_1.sessions.title, args.title)).limit(1);
14
33
  const session = existing[0];
15
- const now = (0, time_1.localISOString)();
16
- const allMessages = await db.select().from(schema_1.messages).where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, session.id));
34
+ const [countRow] = await db.select({ count: (0, drizzle_orm_1.sql) `count(*)` }).from(schema_1.messages).where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, session.id));
17
35
  await db.update(schema_1.sessions).set({ updated_at: now }).where((0, drizzle_orm_1.eq)(schema_1.sessions.id, session.id));
18
36
  return {
19
37
  content: [{
20
38
  type: 'text',
21
- text: JSON.stringify({ session_id: session.id, created: false, message_count: allMessages.length, ai_tool: session.ai_tool }),
39
+ text: JSON.stringify({ session_id: session.id, created: false, message_count: countRow?.count ?? 0, ai_tool: session.ai_tool }),
22
40
  }],
23
41
  };
24
42
  }
25
- const now = (0, time_1.localISOString)();
26
- const id = (0, uuid_1.v4)();
27
- await db.insert(schema_1.sessions).values({
28
- id,
29
- title: args.title,
30
- ai_tool: args.ai_tool ?? null,
31
- created_at: now,
32
- updated_at: now,
33
- });
34
- return {
35
- content: [{
36
- type: 'text',
37
- text: JSON.stringify({ session_id: id, created: true, message_count: 0, ai_tool: args.ai_tool ?? null }),
38
- }],
39
- };
40
43
  };
41
44
  }
42
45
  function listSessions(db) {
@@ -64,16 +67,21 @@ function listSessions(db) {
64
67
  }
65
68
  function getSessionHistory(db) {
66
69
  return async (args) => {
67
- const all = await db.select().from(schema_1.messages)
70
+ const [countRow] = await db.select({ count: (0, drizzle_orm_1.sql) `count(*)` })
71
+ .from(schema_1.messages)
72
+ .where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, args.session_id));
73
+ const total = countRow?.count ?? 0;
74
+ const baseQuery = db.select().from(schema_1.messages)
68
75
  .where((0, drizzle_orm_1.eq)(schema_1.messages.session_id, args.session_id))
69
- .orderBy((0, drizzle_orm_1.asc)(schema_1.messages.created_at));
70
- const limited = args.limit != null ? all.slice(-args.limit) : all;
76
+ .orderBy((0, drizzle_orm_1.desc)(schema_1.messages.created_at), (0, drizzle_orm_1.desc)((0, drizzle_orm_1.sql) `rowid`));
77
+ const rows = await (args.limit != null ? baseQuery.limit(args.limit) : baseQuery);
78
+ const limited = rows.reverse();
71
79
  return {
72
80
  content: [{
73
81
  type: 'text',
74
82
  text: JSON.stringify({
75
83
  messages: limited.map(m => ({ role: m.role, content: m.content, created_at: m.created_at })),
76
- total: all.length,
84
+ total,
77
85
  }),
78
86
  }],
79
87
  };
@@ -14,5 +14,6 @@ function localISOString() {
14
14
  const hours = String(d.getHours()).padStart(2, '0');
15
15
  const minutes = String(d.getMinutes()).padStart(2, '0');
16
16
  const seconds = String(d.getSeconds()).padStart(2, '0');
17
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${sign}${hh}:${mm}`;
17
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
18
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${sign}${hh}:${mm}`;
18
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chron-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "mcpName": "io.github.SirinivasK/chron",
5
5
  "description": "Audit-grade timestamped logs for every AI conversation",
6
6
  "repository": {
@@ -13,7 +13,7 @@ Chron requires the MCP server to be running. If not yet configured:
13
13
 
14
14
  ```bash
15
15
  # Claude Code
16
- claude mcp add chron npx -y chron-mcp
16
+ claude mcp add chron -- npx -y chron-mcp
17
17
  ```
18
18
 
19
19
  For other tools, add to your MCP config:
@@ -1,5 +1,7 @@
1
1
  # Chron — AI Audit Log Skill
2
2
 
3
+ > **Applies in ALL contexts** — main sessions, dispatched subagents, parallel agents, and worktrees. The `SUBAGENT-STOP` guard in `using-superpowers` does NOT apply here. Log every exchange regardless of context.
4
+
3
5
  Chron creates an audit trail of every AI conversation. Every message is logged with a precise local datetime (with timezone offset) to a local SQLite database the user owns.
4
6
 
5
7
  ## Session Start
@@ -28,7 +30,7 @@ T1 and T2 are real server timestamps — T2 − T1 is the actual generation time
28
30
 
29
31
  **Mid-task check-ins follow the same rule.** If you pause mid-task and ask the user something ("I've done the analysis, what's next?" / "Which approach do you prefer?"), that pause is an exchange — log the assistant message before outputting it, log the user's reply when it arrives. Every turn that crosses the user boundary gets timestamped, regardless of whether the task is "complete."
30
32
 
31
- **Permission grants** — if Claude pauses for a permission prompt and the user allows/denies, log a user message on resume: `"[permission: <tool> <action> — allowed/denied]"`. This is the only visible record of the interaction since permission clicks are not typed messages.
33
+ **Permission grants** — whenever Claude resumes after a permission prompt (allow or deny), log a user message IMMEDIATELY before any other output or tool call: `"[permission: <tool> <action> — allowed/denied]"`. This applies mid-task, mid-worktree, and mid-subagent. Do not skip it even if the task is already in progress. This is the only visible record of the interaction since permission clicks are not typed messages.
32
34
 
33
35
  **Do not use `log_exchange` for live conversations** — it captures both timestamps at the same instant and cannot show a real gap. Use it only for batch imports of historical messages.
34
36