claude-threads 0.12.0

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/dist/changelog.d.ts +20 -0
  5. package/dist/changelog.js +134 -0
  6. package/dist/claude/cli.d.ts +42 -0
  7. package/dist/claude/cli.js +173 -0
  8. package/dist/claude/session.d.ts +256 -0
  9. package/dist/claude/session.js +1964 -0
  10. package/dist/config.d.ts +27 -0
  11. package/dist/config.js +94 -0
  12. package/dist/git/worktree.d.ts +50 -0
  13. package/dist/git/worktree.js +228 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +371 -0
  16. package/dist/logo.d.ts +31 -0
  17. package/dist/logo.js +57 -0
  18. package/dist/mattermost/api.d.ts +85 -0
  19. package/dist/mattermost/api.js +124 -0
  20. package/dist/mattermost/api.test.d.ts +1 -0
  21. package/dist/mattermost/api.test.js +319 -0
  22. package/dist/mattermost/client.d.ts +56 -0
  23. package/dist/mattermost/client.js +321 -0
  24. package/dist/mattermost/emoji.d.ts +43 -0
  25. package/dist/mattermost/emoji.js +65 -0
  26. package/dist/mattermost/emoji.test.d.ts +1 -0
  27. package/dist/mattermost/emoji.test.js +131 -0
  28. package/dist/mattermost/types.d.ts +71 -0
  29. package/dist/mattermost/types.js +1 -0
  30. package/dist/mcp/permission-server.d.ts +2 -0
  31. package/dist/mcp/permission-server.js +201 -0
  32. package/dist/onboarding.d.ts +1 -0
  33. package/dist/onboarding.js +116 -0
  34. package/dist/persistence/session-store.d.ts +65 -0
  35. package/dist/persistence/session-store.js +127 -0
  36. package/dist/update-notifier.d.ts +3 -0
  37. package/dist/update-notifier.js +31 -0
  38. package/dist/utils/logger.d.ts +34 -0
  39. package/dist/utils/logger.js +42 -0
  40. package/dist/utils/logger.test.d.ts +1 -0
  41. package/dist/utils/logger.test.js +121 -0
  42. package/dist/utils/tool-formatter.d.ts +56 -0
  43. package/dist/utils/tool-formatter.js +247 -0
  44. package/dist/utils/tool-formatter.test.d.ts +1 -0
  45. package/dist/utils/tool-formatter.test.js +357 -0
  46. package/package.json +85 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Worktree information for a session
3
+ */
4
+ export interface WorktreeInfo {
5
+ repoRoot: string;
6
+ worktreePath: string;
7
+ branch: string;
8
+ }
9
+ /**
10
+ * Persisted session state for resuming after bot restart
11
+ */
12
+ export interface PersistedSession {
13
+ threadId: string;
14
+ claudeSessionId: string;
15
+ startedBy: string;
16
+ startedAt: string;
17
+ sessionNumber: number;
18
+ workingDir: string;
19
+ sessionAllowedUsers: string[];
20
+ forceInteractivePermissions: boolean;
21
+ sessionStartPostId: string | null;
22
+ tasksPostId: string | null;
23
+ lastActivityAt: string;
24
+ planApproved: boolean;
25
+ worktreeInfo?: WorktreeInfo;
26
+ pendingWorktreePrompt?: boolean;
27
+ worktreePromptDisabled?: boolean;
28
+ queuedPrompt?: string;
29
+ }
30
+ /**
31
+ * SessionStore - Persistence layer for session state
32
+ * Stores session data as JSON file for resume after restart
33
+ */
34
+ export declare class SessionStore {
35
+ private debug;
36
+ constructor();
37
+ /**
38
+ * Load all persisted sessions
39
+ */
40
+ load(): Map<string, PersistedSession>;
41
+ /**
42
+ * Save a session (creates or updates)
43
+ */
44
+ save(threadId: string, session: PersistedSession): void;
45
+ /**
46
+ * Remove a session
47
+ */
48
+ remove(threadId: string): void;
49
+ /**
50
+ * Remove sessions older than maxAgeMs
51
+ */
52
+ cleanStale(maxAgeMs: number): string[];
53
+ /**
54
+ * Clear all sessions
55
+ */
56
+ clear(): void;
57
+ /**
58
+ * Load raw data from file
59
+ */
60
+ private loadRaw;
61
+ /**
62
+ * Write data atomically (write to temp file, then rename)
63
+ */
64
+ private writeAtomic;
65
+ }
@@ -0,0 +1,127 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ const STORE_VERSION = 1;
5
+ const CONFIG_DIR = join(homedir(), '.config', 'claude-threads');
6
+ const SESSIONS_FILE = join(CONFIG_DIR, 'sessions.json');
7
+ /**
8
+ * SessionStore - Persistence layer for session state
9
+ * Stores session data as JSON file for resume after restart
10
+ */
11
+ export class SessionStore {
12
+ debug = process.env.DEBUG === '1' || process.argv.includes('--debug');
13
+ constructor() {
14
+ // Ensure config directory exists
15
+ if (!existsSync(CONFIG_DIR)) {
16
+ mkdirSync(CONFIG_DIR, { recursive: true });
17
+ }
18
+ }
19
+ /**
20
+ * Load all persisted sessions
21
+ */
22
+ load() {
23
+ const sessions = new Map();
24
+ if (!existsSync(SESSIONS_FILE)) {
25
+ if (this.debug)
26
+ console.log(' [persist] No sessions file found');
27
+ return sessions;
28
+ }
29
+ try {
30
+ const data = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
31
+ // Version check for future migrations
32
+ if (data.version !== STORE_VERSION) {
33
+ console.warn(` [persist] Sessions file version mismatch (${data.version} vs ${STORE_VERSION}), starting fresh`);
34
+ return sessions;
35
+ }
36
+ for (const [threadId, session] of Object.entries(data.sessions)) {
37
+ sessions.set(threadId, session);
38
+ }
39
+ if (this.debug) {
40
+ console.log(` [persist] Loaded ${sessions.size} session(s)`);
41
+ }
42
+ }
43
+ catch (err) {
44
+ console.error(' [persist] Failed to load sessions:', err);
45
+ }
46
+ return sessions;
47
+ }
48
+ /**
49
+ * Save a session (creates or updates)
50
+ */
51
+ save(threadId, session) {
52
+ const data = this.loadRaw();
53
+ data.sessions[threadId] = session;
54
+ this.writeAtomic(data);
55
+ if (this.debug) {
56
+ const shortId = threadId.substring(0, 8);
57
+ console.log(` [persist] Saved session ${shortId}...`);
58
+ }
59
+ }
60
+ /**
61
+ * Remove a session
62
+ */
63
+ remove(threadId) {
64
+ const data = this.loadRaw();
65
+ if (data.sessions[threadId]) {
66
+ delete data.sessions[threadId];
67
+ this.writeAtomic(data);
68
+ if (this.debug) {
69
+ const shortId = threadId.substring(0, 8);
70
+ console.log(` [persist] Removed session ${shortId}...`);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * Remove sessions older than maxAgeMs
76
+ */
77
+ cleanStale(maxAgeMs) {
78
+ const data = this.loadRaw();
79
+ const now = Date.now();
80
+ const staleIds = [];
81
+ for (const [threadId, session] of Object.entries(data.sessions)) {
82
+ const lastActivity = new Date(session.lastActivityAt).getTime();
83
+ if (now - lastActivity > maxAgeMs) {
84
+ staleIds.push(threadId);
85
+ delete data.sessions[threadId];
86
+ }
87
+ }
88
+ if (staleIds.length > 0) {
89
+ this.writeAtomic(data);
90
+ if (this.debug) {
91
+ console.log(` [persist] Cleaned ${staleIds.length} stale session(s)`);
92
+ }
93
+ }
94
+ return staleIds;
95
+ }
96
+ /**
97
+ * Clear all sessions
98
+ */
99
+ clear() {
100
+ this.writeAtomic({ version: STORE_VERSION, sessions: {} });
101
+ if (this.debug) {
102
+ console.log(' [persist] Cleared all sessions');
103
+ }
104
+ }
105
+ /**
106
+ * Load raw data from file
107
+ */
108
+ loadRaw() {
109
+ if (!existsSync(SESSIONS_FILE)) {
110
+ return { version: STORE_VERSION, sessions: {} };
111
+ }
112
+ try {
113
+ return JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
114
+ }
115
+ catch {
116
+ return { version: STORE_VERSION, sessions: {} };
117
+ }
118
+ }
119
+ /**
120
+ * Write data atomically (write to temp file, then rename)
121
+ */
122
+ writeAtomic(data) {
123
+ const tempFile = `${SESSIONS_FILE}.tmp`;
124
+ writeFileSync(tempFile, JSON.stringify(data, null, 2), 'utf-8');
125
+ renameSync(tempFile, SESSIONS_FILE);
126
+ }
127
+ }
@@ -0,0 +1,3 @@
1
+ import { type UpdateInfo } from 'update-notifier';
2
+ export declare function checkForUpdates(): void;
3
+ export declare function getUpdateInfo(): UpdateInfo | undefined;
@@ -0,0 +1,31 @@
1
+ import updateNotifier from 'update-notifier';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
6
+ let cachedUpdateInfo;
7
+ export function checkForUpdates() {
8
+ if (process.env.NO_UPDATE_NOTIFIER)
9
+ return;
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
12
+ const notifier = updateNotifier({
13
+ pkg,
14
+ updateCheckInterval: 1000 * 60 * 30, // Check every 30 minutes
15
+ });
16
+ // Cache for Mattermost notifications
17
+ cachedUpdateInfo = notifier.update;
18
+ // Show CLI notification
19
+ notifier.notify({
20
+ message: `Update available: {currentVersion} → {latestVersion}
21
+ Run: npm install -g claude-threads`,
22
+ });
23
+ }
24
+ catch {
25
+ // Silently fail - update checking is not critical
26
+ }
27
+ }
28
+ // Returns update info if available, for posting to Mattermost
29
+ export function getUpdateInfo() {
30
+ return cachedUpdateInfo;
31
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Simple debug logging utility
3
+ *
4
+ * Provides consistent logging across the codebase with:
5
+ * - Configurable prefix for different components
6
+ * - DEBUG environment variable check
7
+ * - stdout vs stderr routing option
8
+ */
9
+ export interface Logger {
10
+ /** Log a debug message (only when DEBUG=1) */
11
+ debug: (msg: string) => void;
12
+ /** Log an info message (always shown) */
13
+ info: (msg: string) => void;
14
+ /** Log an error message (always shown, to stderr) */
15
+ error: (msg: string) => void;
16
+ }
17
+ /**
18
+ * Create a logger with a specific prefix
19
+ *
20
+ * @param prefix - Prefix to add to all messages (e.g., '[MCP]', '[ws]')
21
+ * @param useStderr - If true, use stderr for all output (default: false)
22
+ * @returns Logger object with debug, info, and error methods
23
+ */
24
+ export declare function createLogger(prefix: string, useStderr?: boolean): Logger;
25
+ /**
26
+ * Pre-configured logger for MCP permission server
27
+ * Uses stderr (required for MCP stdio communication)
28
+ */
29
+ export declare const mcpLogger: Logger;
30
+ /**
31
+ * Pre-configured logger for WebSocket client
32
+ * Uses stdout with indentation for visual hierarchy
33
+ */
34
+ export declare const wsLogger: Logger;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Simple debug logging utility
3
+ *
4
+ * Provides consistent logging across the codebase with:
5
+ * - Configurable prefix for different components
6
+ * - DEBUG environment variable check
7
+ * - stdout vs stderr routing option
8
+ */
9
+ /**
10
+ * Create a logger with a specific prefix
11
+ *
12
+ * @param prefix - Prefix to add to all messages (e.g., '[MCP]', '[ws]')
13
+ * @param useStderr - If true, use stderr for all output (default: false)
14
+ * @returns Logger object with debug, info, and error methods
15
+ */
16
+ export function createLogger(prefix, useStderr = false) {
17
+ const isDebug = () => process.env.DEBUG === '1';
18
+ const log = useStderr ? console.error : console.log;
19
+ return {
20
+ debug: (msg) => {
21
+ if (isDebug()) {
22
+ log(`${prefix} ${msg}`);
23
+ }
24
+ },
25
+ info: (msg) => {
26
+ log(`${prefix} ${msg}`);
27
+ },
28
+ error: (msg) => {
29
+ console.error(`${prefix} ${msg}`);
30
+ },
31
+ };
32
+ }
33
+ /**
34
+ * Pre-configured logger for MCP permission server
35
+ * Uses stderr (required for MCP stdio communication)
36
+ */
37
+ export const mcpLogger = createLogger('[MCP]', true);
38
+ /**
39
+ * Pre-configured logger for WebSocket client
40
+ * Uses stdout with indentation for visual hierarchy
41
+ */
42
+ export const wsLogger = createLogger(' [ws]', false);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createLogger, mcpLogger, wsLogger } from './logger.js';
3
+ describe('createLogger', () => {
4
+ let consoleLogSpy;
5
+ let consoleErrorSpy;
6
+ const originalEnv = process.env.DEBUG;
7
+ beforeEach(() => {
8
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
9
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
10
+ delete process.env.DEBUG;
11
+ });
12
+ afterEach(() => {
13
+ consoleLogSpy.mockRestore();
14
+ consoleErrorSpy.mockRestore();
15
+ if (originalEnv !== undefined) {
16
+ process.env.DEBUG = originalEnv;
17
+ }
18
+ else {
19
+ delete process.env.DEBUG;
20
+ }
21
+ });
22
+ describe('debug', () => {
23
+ it('does not log when DEBUG is not set', () => {
24
+ const logger = createLogger('[test]');
25
+ logger.debug('test message');
26
+ expect(consoleLogSpy).not.toHaveBeenCalled();
27
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
28
+ });
29
+ it('logs to stdout when DEBUG=1 and useStderr=false', () => {
30
+ process.env.DEBUG = '1';
31
+ const logger = createLogger('[test]');
32
+ logger.debug('test message');
33
+ expect(consoleLogSpy).toHaveBeenCalledWith('[test] test message');
34
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
35
+ });
36
+ it('logs to stderr when DEBUG=1 and useStderr=true', () => {
37
+ process.env.DEBUG = '1';
38
+ const logger = createLogger('[test]', true);
39
+ logger.debug('test message');
40
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[test] test message');
41
+ expect(consoleLogSpy).not.toHaveBeenCalled();
42
+ });
43
+ it('does not log when DEBUG is set to something other than 1', () => {
44
+ process.env.DEBUG = 'true';
45
+ const logger = createLogger('[test]');
46
+ logger.debug('test message');
47
+ expect(consoleLogSpy).not.toHaveBeenCalled();
48
+ });
49
+ });
50
+ describe('info', () => {
51
+ it('always logs to stdout when useStderr=false', () => {
52
+ const logger = createLogger('[test]');
53
+ logger.info('info message');
54
+ expect(consoleLogSpy).toHaveBeenCalledWith('[test] info message');
55
+ });
56
+ it('logs to stderr when useStderr=true', () => {
57
+ const logger = createLogger('[test]', true);
58
+ logger.info('info message');
59
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[test] info message');
60
+ });
61
+ it('logs even when DEBUG is not set', () => {
62
+ const logger = createLogger('[test]');
63
+ logger.info('info message');
64
+ expect(consoleLogSpy).toHaveBeenCalled();
65
+ });
66
+ });
67
+ describe('error', () => {
68
+ it('always logs to stderr', () => {
69
+ const logger = createLogger('[test]');
70
+ logger.error('error message');
71
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[test] error message');
72
+ });
73
+ it('logs to stderr even when useStderr=false', () => {
74
+ const logger = createLogger('[test]', false);
75
+ logger.error('error message');
76
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[test] error message');
77
+ });
78
+ it('logs even when DEBUG is not set', () => {
79
+ const logger = createLogger('[test]');
80
+ logger.error('error message');
81
+ expect(consoleErrorSpy).toHaveBeenCalled();
82
+ });
83
+ });
84
+ describe('prefix formatting', () => {
85
+ it('includes prefix in debug messages', () => {
86
+ process.env.DEBUG = '1';
87
+ const logger = createLogger('[MyPrefix]');
88
+ logger.debug('my message');
89
+ expect(consoleLogSpy).toHaveBeenCalledWith('[MyPrefix] my message');
90
+ });
91
+ it('includes prefix in info messages', () => {
92
+ const logger = createLogger('[MyPrefix]');
93
+ logger.info('my message');
94
+ expect(consoleLogSpy).toHaveBeenCalledWith('[MyPrefix] my message');
95
+ });
96
+ it('includes prefix in error messages', () => {
97
+ const logger = createLogger('[MyPrefix]');
98
+ logger.error('my message');
99
+ expect(consoleErrorSpy).toHaveBeenCalledWith('[MyPrefix] my message');
100
+ });
101
+ });
102
+ });
103
+ describe('pre-configured loggers', () => {
104
+ // Note: mcpLogger and wsLogger are module-level singletons created at import time.
105
+ // Since they capture console.log/error at creation, we test their behavior by
106
+ // verifying they have the expected interface and configuration.
107
+ describe('mcpLogger', () => {
108
+ it('has debug, info, and error methods', () => {
109
+ expect(typeof mcpLogger.debug).toBe('function');
110
+ expect(typeof mcpLogger.info).toBe('function');
111
+ expect(typeof mcpLogger.error).toBe('function');
112
+ });
113
+ });
114
+ describe('wsLogger', () => {
115
+ it('has debug, info, and error methods', () => {
116
+ expect(typeof wsLogger.debug).toBe('function');
117
+ expect(typeof wsLogger.info).toBe('function');
118
+ expect(typeof wsLogger.error).toBe('function');
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Tool formatting utilities for displaying Claude tool calls in Mattermost
3
+ *
4
+ * This module provides shared formatting logic used by both:
5
+ * - src/claude/session.ts (main bot)
6
+ * - src/mcp/permission-server.ts (MCP permission handler)
7
+ */
8
+ export interface ToolInput {
9
+ [key: string]: unknown;
10
+ }
11
+ export interface FormatOptions {
12
+ /** Include detailed previews (diffs, file content). Default: false */
13
+ detailed?: boolean;
14
+ /** Max command length for Bash. Default: 50 */
15
+ maxCommandLength?: number;
16
+ /** Max path display length. Default: 60 */
17
+ maxPathLength?: number;
18
+ /** Max lines to show in previews. Default: 20 for diff, 6 for content */
19
+ maxPreviewLines?: number;
20
+ }
21
+ /**
22
+ * Shorten a file path for display by replacing home directory with ~
23
+ */
24
+ export declare function shortenPath(path: string, homeDir?: string): string;
25
+ /**
26
+ * Check if a tool name is an MCP tool and extract server/tool parts
27
+ */
28
+ export declare function parseMcpToolName(toolName: string): {
29
+ server: string;
30
+ tool: string;
31
+ } | null;
32
+ /**
33
+ * Format a tool use for display in Mattermost
34
+ *
35
+ * @param toolName - The name of the tool being called
36
+ * @param input - The tool input parameters
37
+ * @param options - Formatting options
38
+ * @returns Formatted string or null if the tool should not be displayed
39
+ */
40
+ export declare function formatToolUse(toolName: string, input: ToolInput, options?: FormatOptions): string | null;
41
+ /**
42
+ * Format tool info for permission prompts (simpler format)
43
+ *
44
+ * @param toolName - The name of the tool
45
+ * @param input - The tool input parameters
46
+ * @returns Formatted string for permission prompts
47
+ */
48
+ export declare function formatToolForPermission(toolName: string, input: ToolInput): string;
49
+ /**
50
+ * Format Claude in Chrome tool calls
51
+ *
52
+ * @param tool - The Chrome tool name (after mcp__claude-in-chrome__)
53
+ * @param input - The tool input parameters
54
+ * @returns Formatted string for display
55
+ */
56
+ export declare function formatChromeToolUse(tool: string, input: ToolInput): string;