claude-multi-session 1.0.1 → 2.3.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.
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Decision Journal — tracks architectural decisions, choices, and rationale
3
+ * Part of the Session Continuity Engine (Layer 0)
4
+ *
5
+ * This module helps maintain a record of important decisions made during
6
+ * development, including the reasoning behind them. This helps future sessions
7
+ * understand why certain choices were made.
8
+ *
9
+ * Storage: JSONL format (one JSON object per line)
10
+ * Location: ~/.claude-multi-session/continuity/{projectHash}/journal.jsonl
11
+ */
12
+
13
+ const crypto = require('crypto');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+ const { readJsonSafe, appendJsonl } = require('./atomic-io');
18
+
19
+ /**
20
+ * DecisionJournal class
21
+ *
22
+ * Manages a journal of decisions for a specific project.
23
+ * Each decision includes:
24
+ * - What was decided
25
+ * - Why it was decided
26
+ * - Related files
27
+ * - Tags for categorization
28
+ * - Additional context
29
+ */
30
+ class DecisionJournal {
31
+ /**
32
+ * Create a new Decision Journal
33
+ *
34
+ * @param {string} projectPath - Absolute path to the project directory
35
+ *
36
+ * The journal is stored in a directory based on a hash of the project path,
37
+ * so each project gets its own isolated journal.
38
+ */
39
+ constructor(projectPath) {
40
+ // Create a unique hash for this project (16 characters)
41
+ // This ensures each project has its own journal storage
42
+ const projectHash = crypto
43
+ .createHash('sha256')
44
+ .update(projectPath)
45
+ .digest('hex')
46
+ .slice(0, 16);
47
+
48
+ // Set up storage directory: ~/.claude-multi-session/continuity/{hash}
49
+ this.storageDir = path.join(
50
+ os.homedir(),
51
+ '.claude-multi-session',
52
+ 'continuity',
53
+ projectHash
54
+ );
55
+
56
+ // The journal file is a JSONL file (one JSON object per line)
57
+ this.journalPath = path.join(this.storageDir, 'journal.jsonl');
58
+
59
+ // Create the storage directory if it doesn't exist yet
60
+ if (!fs.existsSync(this.storageDir)) {
61
+ fs.mkdirSync(this.storageDir, { recursive: true });
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Add a new decision to the journal
67
+ *
68
+ * @param {Object} params - Decision parameters
69
+ * @param {string} params.decision - The decision that was made (required)
70
+ * @param {string} [params.reason] - Why this decision was made
71
+ * @param {string[]} [params.files] - Related file paths
72
+ * @param {string[]} [params.tags] - Tags for categorization (e.g., ['auth', 'security'])
73
+ * @param {string} [params.context] - Additional context or notes
74
+ * @returns {Object} The created decision entry with ID and timestamp
75
+ *
76
+ * Example:
77
+ * journal.add({
78
+ * decision: 'Use bcrypt for password hashing',
79
+ * reason: 'More mature and widely tested than argon2',
80
+ * files: ['src/auth.js'],
81
+ * tags: ['security', 'auth']
82
+ * })
83
+ */
84
+ add({ decision, reason, files, tags, context }) {
85
+ // Generate a unique ID for this decision
86
+ // Format: d_{timestamp}_{random} (e.g., d_1707840000000_a3f2b1c4)
87
+ const id = 'd_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
88
+
89
+ // Build the decision entry
90
+ const entry = {
91
+ id,
92
+ decision, // The decision itself (required)
93
+ reason: reason || '', // Why it was made (optional)
94
+ files: files || [], // Related files (optional)
95
+ tags: tags || [], // Tags for filtering (optional)
96
+ context: context || '', // Extra context (optional)
97
+ createdAt: new Date().toISOString() // When it was created
98
+ };
99
+
100
+ // Append to the journal file (one line per decision)
101
+ appendJsonl(this.journalPath, entry);
102
+
103
+ // Return the entry so the caller knows what was saved
104
+ return entry;
105
+ }
106
+
107
+ /**
108
+ * List decisions from the journal
109
+ *
110
+ * @param {Object} [options] - Filter and pagination options
111
+ * @param {number} [options.limit=50] - Maximum number of decisions to return
112
+ * @param {string} [options.tag] - Only return decisions with this tag
113
+ * @param {string} [options.since] - Only return decisions after this date (ISO string)
114
+ * @returns {Object[]} Array of decision entries, newest first
115
+ *
116
+ * Example:
117
+ * journal.list({ limit: 10, tag: 'auth' })
118
+ * journal.list({ since: '2024-01-01T00:00:00Z' })
119
+ */
120
+ list({ limit = 50, tag, since } = {}) {
121
+ // If the journal file doesn't exist yet, return empty array
122
+ if (!fs.existsSync(this.journalPath)) {
123
+ return [];
124
+ }
125
+
126
+ // Read the entire journal file
127
+ const content = fs.readFileSync(this.journalPath, 'utf-8');
128
+
129
+ // Parse each line as JSON (JSONL format)
130
+ const entries = content
131
+ .split('\n') // Split into lines
132
+ .filter(line => line.trim()) // Remove empty lines
133
+ .map(line => JSON.parse(line)); // Parse each line as JSON
134
+
135
+ // Start with all entries
136
+ let filtered = entries;
137
+
138
+ // Filter by tag if specified
139
+ if (tag) {
140
+ filtered = filtered.filter(entry => entry.tags.includes(tag));
141
+ }
142
+
143
+ // Filter by date if specified
144
+ if (since) {
145
+ const sinceDate = new Date(since);
146
+ filtered = filtered.filter(entry => new Date(entry.createdAt) >= sinceDate);
147
+ }
148
+
149
+ // Sort by creation date, newest first
150
+ // (Reverse because JSONL is in chronological order)
151
+ filtered.reverse();
152
+
153
+ // Return only the requested number of entries
154
+ return filtered.slice(0, limit);
155
+ }
156
+
157
+ /**
158
+ * Search for decisions by keyword
159
+ *
160
+ * @param {string} keyword - Search term (case-insensitive)
161
+ * @returns {Object[]} Array of matching decision entries, newest first
162
+ *
163
+ * Searches across the decision text, reason, and context fields.
164
+ *
165
+ * Example:
166
+ * journal.search('bcrypt')
167
+ * journal.search('authentication')
168
+ */
169
+ search(keyword) {
170
+ // If the journal file doesn't exist yet, return empty array
171
+ if (!fs.existsSync(this.journalPath)) {
172
+ return [];
173
+ }
174
+
175
+ // Read and parse all entries (same as list())
176
+ const content = fs.readFileSync(this.journalPath, 'utf-8');
177
+ const entries = content
178
+ .split('\n')
179
+ .filter(line => line.trim())
180
+ .map(line => JSON.parse(line));
181
+
182
+ // Convert keyword to lowercase for case-insensitive search
183
+ const searchTerm = keyword.toLowerCase();
184
+
185
+ // Find entries where the keyword appears in decision, reason, or context
186
+ const matches = entries.filter(entry => {
187
+ return (
188
+ entry.decision.toLowerCase().includes(searchTerm) ||
189
+ entry.reason.toLowerCase().includes(searchTerm) ||
190
+ entry.context.toLowerCase().includes(searchTerm)
191
+ );
192
+ });
193
+
194
+ // Return matches, newest first
195
+ return matches.reverse();
196
+ }
197
+
198
+ /**
199
+ * Get a specific decision by its ID
200
+ *
201
+ * @param {string} decisionId - The decision ID to retrieve
202
+ * @returns {Object|null} The decision entry, or null if not found
203
+ *
204
+ * Example:
205
+ * journal.get('d_1707840000000_a3f2b1c4')
206
+ */
207
+ get(decisionId) {
208
+ // If the journal file doesn't exist yet, return null
209
+ if (!fs.existsSync(this.journalPath)) {
210
+ return null;
211
+ }
212
+
213
+ // Read and parse all entries
214
+ const content = fs.readFileSync(this.journalPath, 'utf-8');
215
+ const entries = content
216
+ .split('\n')
217
+ .filter(line => line.trim())
218
+ .map(line => JSON.parse(line));
219
+
220
+ // Find the entry with matching ID
221
+ const entry = entries.find(e => e.id === decisionId);
222
+
223
+ // Return the entry or null if not found
224
+ return entry || null;
225
+ }
226
+ }
227
+
228
+ // Export the class so other modules can use it
229
+ module.exports = DecisionJournal;
package/src/delegate.js CHANGED
@@ -119,7 +119,7 @@ class Delegate {
119
119
  : null;
120
120
 
121
121
  // Build the full prompt with context and instructions
122
- const fullPrompt = this._buildPrompt(task, options.context);
122
+ const fullPrompt = this._buildPrompt(task, options.context, name);
123
123
 
124
124
  // Spawn the session
125
125
  let spawnResult;
@@ -248,22 +248,11 @@ class Delegate {
248
248
 
249
249
  /**
250
250
  * Build the full prompt with context and structured output instructions.
251
+ * Uses the production-quality delegate prompt template from prompts.js.
251
252
  */
252
- _buildPrompt(task, context) {
253
- let prompt = '';
254
-
255
- if (context) {
256
- prompt += `CONTEXT:\n${context}\n\n`;
257
- }
258
-
259
- prompt += `TASK:\n${task}\n\n`;
260
- prompt += `INSTRUCTIONS:\n`;
261
- prompt += `- Complete the task thoroughly\n`;
262
- prompt += `- If you encounter errors, try to fix them\n`;
263
- prompt += `- If you need clarification, say what you need and stop\n`;
264
- prompt += `- At the end, provide a brief summary of what you did\n`;
265
-
266
- return prompt;
253
+ _buildPrompt(task, context, name) {
254
+ const { buildDelegatePrompt } = require('./prompts');
255
+ return buildDelegatePrompt(task, context, name);
267
256
  }
268
257
 
269
258
  /**