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.
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "claude-multi-session",
3
- "version": "1.0.1",
3
+ "version": "2.3.0",
4
4
  "description": "Multi-session orchestrator for Claude Code CLI — spawn, control, pause, resume, and send multiple inputs to Claude Code sessions programmatically",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "cms": "bin/cli.js",
8
8
  "claude-multi-session": "bin/cli.js",
9
9
  "cms-mcp": "bin/mcp.js",
10
- "cms-setup": "bin/setup.js"
10
+ "cms-setup": "bin/setup.js",
11
+ "cms-continuity-hook": "bin/continuity-hook.js"
11
12
  },
12
13
  "scripts": {
13
14
  "test": "node test-stream.js",
14
- "postinstall": "node bin/setup.js --postinstall"
15
+ "postinstall": "node bin/setup.js --postinstall-hint"
15
16
  },
16
17
  "keywords": [
17
18
  "claude",
@@ -38,5 +39,9 @@
38
39
  "repository": {
39
40
  "type": "git",
40
41
  "url": ""
42
+ },
43
+ "dependencies": {
44
+ "@clack/prompts": "^0.9.1",
45
+ "picocolors": "^1.1.1"
41
46
  }
42
47
  }
@@ -0,0 +1,639 @@
1
+ /**
2
+ * artifact-store.js
3
+ * Layer 2: Artifact Store with Versioning, Immutability, and Schema Validation
4
+ *
5
+ * This module provides a versioned artifact storage system that ensures:
6
+ * - Immutability: Once published, artifact versions cannot be changed
7
+ * - Schema validation: Data is validated against JSON schemas before storage
8
+ * - Version tracking: Each artifact can have multiple versions
9
+ * - Metadata: Rich metadata including tags, lineage, checksums
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const os = require('os');
15
+ const crypto = require('crypto');
16
+
17
+ // Import Layer 1 utilities (being built in parallel)
18
+ const { atomicWriteJson, writeImmutable, readJsonSafe } = require('./atomic-io');
19
+ const { acquireLock, releaseLock } = require('./file-lock');
20
+
21
+ /**
22
+ * ArtifactStore manages versioned, immutable artifacts with schema validation
23
+ *
24
+ * Directory structure:
25
+ * team/{teamName}/artifacts/
26
+ * index.json - artifact registry (locked for writes)
27
+ * schemas/ - JSON schema files for well-known types
28
+ * data/{artifactId}/ - one dir per artifact
29
+ * v1.json, v2.json - immutable version files
30
+ */
31
+ class ArtifactStore {
32
+ /**
33
+ * Create a new ArtifactStore instance
34
+ * @param {string} teamName - Name of the team (default: 'default')
35
+ */
36
+ constructor(teamName = 'default') {
37
+ // Set up base directory structure
38
+ const baseDir = path.join(os.homedir(), '.claude-multi-session');
39
+ const teamDir = path.join(baseDir, 'team', teamName);
40
+
41
+ // Define all directory paths
42
+ this.artifactsDir = path.join(teamDir, 'artifacts');
43
+ this.indexPath = path.join(this.artifactsDir, 'index.json');
44
+ this.schemasDir = path.join(this.artifactsDir, 'schemas');
45
+ this.dataDir = path.join(this.artifactsDir, 'data');
46
+ this.locksDir = path.join(teamDir, 'locks');
47
+
48
+ // Create all necessary directories
49
+ this._ensureDirectories();
50
+
51
+ // Create default schemas if they don't exist
52
+ this._createDefaultSchemas();
53
+ }
54
+
55
+ /**
56
+ * Ensure all required directories exist
57
+ * @private
58
+ */
59
+ _ensureDirectories() {
60
+ // Create directories recursively if they don't exist
61
+ [this.artifactsDir, this.schemasDir, this.dataDir, this.locksDir].forEach(dir => {
62
+ if (!fs.existsSync(dir)) {
63
+ fs.mkdirSync(dir, { recursive: true });
64
+ }
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Load a JSON schema for a specific artifact type
70
+ * @param {string} type - The artifact type
71
+ * @returns {Object|null} The schema object, or null if not found
72
+ * @private
73
+ */
74
+ _loadSchema(type) {
75
+ const schemaPath = path.join(this.schemasDir, `${type}.json`);
76
+
77
+ // Try to read the schema file
78
+ try {
79
+ if (!fs.existsSync(schemaPath)) {
80
+ return null; // Custom types have no schema
81
+ }
82
+
83
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
84
+ return JSON.parse(schemaContent);
85
+ } catch (err) {
86
+ // If schema file is corrupted or invalid, return null
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Validate data against a JSON schema
93
+ * Implements basic JSON Schema validation (required fields, types, enums)
94
+ * @param {string} type - The artifact type
95
+ * @param {Object} data - The data to validate
96
+ * @returns {Object} { valid: true } or { valid: false, errors: [...] }
97
+ * @private
98
+ */
99
+ _validateData(type, data) {
100
+ // Load the schema for this type
101
+ const schema = this._loadSchema(type);
102
+
103
+ // If no schema exists, validation passes (custom types)
104
+ if (!schema) {
105
+ return { valid: true };
106
+ }
107
+
108
+ const errors = [];
109
+
110
+ // Check required fields
111
+ if (schema.required && Array.isArray(schema.required)) {
112
+ for (const field of schema.required) {
113
+ if (!(field in data)) {
114
+ errors.push(`field '${field}' is required`);
115
+ }
116
+ }
117
+ }
118
+
119
+ // Check types for each field in schema.properties
120
+ if (schema.properties) {
121
+ for (const [field, fieldSchema] of Object.entries(schema.properties)) {
122
+ // Only validate if field exists in data
123
+ if (field in data) {
124
+ const value = data[field];
125
+ const expectedType = fieldSchema.type;
126
+
127
+ // Type checking
128
+ if (expectedType) {
129
+ let actualType = typeof value;
130
+
131
+ // Special case: arrays
132
+ if (expectedType === 'array') {
133
+ if (!Array.isArray(value)) {
134
+ errors.push(`field '${field}' must be an array`);
135
+ continue;
136
+ }
137
+
138
+ // Check array items if schema defines items
139
+ if (fieldSchema.items) {
140
+ const itemsSchema = fieldSchema.items;
141
+
142
+ // Check each array item
143
+ for (let i = 0; i < value.length; i++) {
144
+ const item = value[i];
145
+
146
+ // Check item type
147
+ if (itemsSchema.type) {
148
+ const itemActualType = typeof item;
149
+ if (itemsSchema.type === 'object' && itemActualType !== 'object') {
150
+ errors.push(`field '${field}[${i}]' must be an object`);
151
+ } else if (itemsSchema.type !== 'object' && itemActualType !== itemsSchema.type) {
152
+ errors.push(`field '${field}[${i}]' must be a ${itemsSchema.type}`);
153
+ }
154
+ }
155
+
156
+ // Check required fields in array items
157
+ if (itemsSchema.required && typeof item === 'object') {
158
+ for (const requiredField of itemsSchema.required) {
159
+ if (!(requiredField in item)) {
160
+ errors.push(`field '${field}[${i}].${requiredField}' is required`);
161
+ }
162
+ }
163
+ }
164
+
165
+ // Check enum values in array items
166
+ if (itemsSchema.properties) {
167
+ for (const [itemField, itemFieldSchema] of Object.entries(itemsSchema.properties)) {
168
+ if (itemField in item && itemFieldSchema.enum) {
169
+ if (!itemFieldSchema.enum.includes(item[itemField])) {
170
+ errors.push(`field '${field}[${i}].${itemField}' must be one of: ${itemFieldSchema.enum.join(', ')}`);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
177
+ } else if (expectedType === 'object') {
178
+ // Check if value is an object (but not null or array)
179
+ if (actualType !== 'object' || value === null || Array.isArray(value)) {
180
+ errors.push(`field '${field}' must be an object`);
181
+ }
182
+ } else if (expectedType === 'number') {
183
+ if (actualType !== 'number') {
184
+ errors.push(`field '${field}' must be a number`);
185
+ }
186
+ } else if (expectedType === 'string') {
187
+ if (actualType !== 'string') {
188
+ errors.push(`field '${field}' must be a string`);
189
+ }
190
+ } else if (expectedType === 'boolean') {
191
+ if (actualType !== 'boolean') {
192
+ errors.push(`field '${field}' must be a boolean`);
193
+ }
194
+ }
195
+ }
196
+
197
+ // Enum validation
198
+ if (fieldSchema.enum && !fieldSchema.enum.includes(value)) {
199
+ errors.push(`field '${field}' must be one of: ${fieldSchema.enum.join(', ')}`);
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ // Return validation result
206
+ if (errors.length > 0) {
207
+ return { valid: false, errors };
208
+ }
209
+
210
+ return { valid: true };
211
+ }
212
+
213
+ /**
214
+ * Create default schemas for well-known artifact types
215
+ * @private
216
+ */
217
+ _createDefaultSchemas() {
218
+ // Define default schemas for well-known types
219
+ const defaultSchemas = {
220
+ 'api-contract': {
221
+ type: 'object',
222
+ required: ['endpoints'],
223
+ properties: {
224
+ endpoints: {
225
+ type: 'array',
226
+ items: {
227
+ type: 'object',
228
+ required: ['method', 'path'],
229
+ properties: {
230
+ method: {
231
+ type: 'string',
232
+ enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
233
+ },
234
+ path: {
235
+ type: 'string'
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ },
242
+ 'schema-change': {
243
+ type: 'object',
244
+ required: ['models'],
245
+ properties: {
246
+ models: {
247
+ type: 'array',
248
+ items: {
249
+ type: 'object',
250
+ required: ['name'],
251
+ properties: {
252
+ name: {
253
+ type: 'string'
254
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ },
260
+ 'test-results': {
261
+ type: 'object',
262
+ required: ['total', 'passed', 'failed'],
263
+ properties: {
264
+ total: {
265
+ type: 'number'
266
+ },
267
+ passed: {
268
+ type: 'number'
269
+ },
270
+ failed: {
271
+ type: 'number'
272
+ }
273
+ }
274
+ },
275
+ 'component-spec': {
276
+ type: 'object',
277
+ required: ['componentName'],
278
+ properties: {
279
+ componentName: {
280
+ type: 'string'
281
+ }
282
+ }
283
+ },
284
+ 'file-manifest': {
285
+ type: 'object',
286
+ required: ['files'],
287
+ properties: {
288
+ files: {
289
+ type: 'array'
290
+ }
291
+ }
292
+ },
293
+ 'config-change': {
294
+ type: 'object',
295
+ required: ['changes'],
296
+ properties: {
297
+ changes: {
298
+ type: 'array'
299
+ }
300
+ }
301
+ }
302
+ };
303
+
304
+ // Write each schema file if it doesn't exist
305
+ for (const [type, schema] of Object.entries(defaultSchemas)) {
306
+ const schemaPath = path.join(this.schemasDir, `${type}.json`);
307
+
308
+ if (!fs.existsSync(schemaPath)) {
309
+ try {
310
+ fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2), 'utf8');
311
+ } catch (err) {
312
+ // Ignore errors - schemas are optional
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Calculate SHA-256 checksum for data
320
+ * @param {Object} data - The data to checksum
321
+ * @returns {string} Checksum prefixed with 'sha256:'
322
+ * @private
323
+ */
324
+ _calculateChecksum(data) {
325
+ const hash = crypto.createHash('sha256')
326
+ .update(JSON.stringify(data))
327
+ .digest('hex');
328
+ return `sha256:${hash}`;
329
+ }
330
+
331
+ /**
332
+ * Publish a new version of an artifact
333
+ * @param {string} artifactId - Unique identifier for the artifact
334
+ * @param {Object} options - Publication options
335
+ * @param {string} options.type - Artifact type (e.g., 'api-contract')
336
+ * @param {string} options.name - Human-readable name
337
+ * @param {string} [options.summary=''] - Brief description of this version
338
+ * @param {Object} options.data - The actual artifact data
339
+ * @param {string[]} [options.tags=[]] - Tags for categorization
340
+ * @param {string} options.publisher - Who/what published this version
341
+ * @param {string[]} [options.derivedFrom=[]] - Artifact IDs this was derived from
342
+ * @returns {Object} { artifactId, version, type }
343
+ */
344
+ publish(artifactId, { type, name, summary = '', data, tags = [], publisher, derivedFrom = [] }) {
345
+ // Step 1: Validate data against schema
346
+ const validation = this._validateData(type, data);
347
+ if (!validation.valid) {
348
+ throw new Error(`Schema validation failed: ${validation.errors.join(', ')}`);
349
+ }
350
+
351
+ // Step 2: Acquire lock on the artifacts index
352
+ acquireLock(this.locksDir, 'artifacts-index');
353
+
354
+ try {
355
+ // Step 3: Read the current index
356
+ const index = readJsonSafe(this.indexPath, {});
357
+
358
+ // Step 4: Determine the version number
359
+ let version = 1;
360
+ let createdAt = new Date().toISOString();
361
+
362
+ if (index[artifactId]) {
363
+ // Artifact exists - increment version
364
+ version = index[artifactId].latestVersion + 1;
365
+ createdAt = index[artifactId].createdAt; // Preserve original creation time
366
+ }
367
+
368
+ // Step 5: Create version file data
369
+ const versionData = {
370
+ artifactId,
371
+ version,
372
+ type,
373
+ publisher,
374
+ publishedAt: new Date().toISOString(),
375
+ summary,
376
+ name,
377
+ data,
378
+ tags,
379
+ lineage: {
380
+ producedBy: null,
381
+ derivedFrom
382
+ },
383
+ checksum: this._calculateChecksum(data)
384
+ };
385
+
386
+ // Step 6: Write the version file (immutable)
387
+ const artifactDir = path.join(this.dataDir, artifactId);
388
+
389
+ // Ensure artifact directory exists
390
+ if (!fs.existsSync(artifactDir)) {
391
+ fs.mkdirSync(artifactDir, { recursive: true });
392
+ }
393
+
394
+ const versionPath = path.join(artifactDir, `v${version}.json`);
395
+
396
+ try {
397
+ // Attempt to write the immutable version file
398
+ writeImmutable(versionPath, versionData);
399
+ } catch (err) {
400
+ // Handle race condition - file already exists
401
+ if (err.code === 'EEXIST') {
402
+ // Retry with incremented version
403
+ version += 1;
404
+ const retryPath = path.join(artifactDir, `v${version}.json`);
405
+ writeImmutable(retryPath, { ...versionData, version });
406
+ } else {
407
+ throw err;
408
+ }
409
+ }
410
+
411
+ // Step 7: Update the index entry
412
+ index[artifactId] = {
413
+ artifactId,
414
+ type,
415
+ name,
416
+ publisher,
417
+ createdAt,
418
+ updatedAt: new Date().toISOString(),
419
+ latestVersion: version,
420
+ tags
421
+ };
422
+
423
+ // Step 8: Save the index atomically
424
+ atomicWriteJson(this.indexPath, index);
425
+
426
+ // Step 9: Release the lock
427
+ releaseLock(this.locksDir, 'artifacts-index');
428
+
429
+ // Step 10: Return publication result
430
+ return { artifactId, version, type };
431
+
432
+ } catch (err) {
433
+ // Ensure lock is released even on error
434
+ releaseLock(this.locksDir, 'artifacts-index');
435
+ throw err;
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Get a specific version of an artifact
441
+ * @param {string} artifactId - The artifact identifier
442
+ * @param {number|null} [version=null] - Version number (null = latest)
443
+ * @returns {Object|null} The version data, or null if not found
444
+ */
445
+ get(artifactId, version = null) {
446
+ // If version is null, get the latest version from index
447
+ if (version === null) {
448
+ const index = readJsonSafe(this.indexPath, {});
449
+
450
+ if (!index[artifactId]) {
451
+ return null; // Artifact doesn't exist
452
+ }
453
+
454
+ version = index[artifactId].latestVersion;
455
+ }
456
+
457
+ // Read the version file
458
+ const versionPath = path.join(this.dataDir, artifactId, `v${version}.json`);
459
+
460
+ if (!fs.existsSync(versionPath)) {
461
+ return null; // Version doesn't exist
462
+ }
463
+
464
+ try {
465
+ const content = fs.readFileSync(versionPath, 'utf8');
466
+ return JSON.parse(content);
467
+ } catch (err) {
468
+ return null; // Corrupted or invalid file
469
+ }
470
+ }
471
+
472
+ /**
473
+ * List artifacts with optional filtering
474
+ * @param {Object} [filters={}] - Filter criteria
475
+ * @param {string} [filters.type] - Filter by artifact type
476
+ * @param {string} [filters.publisher] - Filter by publisher
477
+ * @param {string} [filters.tag] - Filter by tag
478
+ * @returns {Array} Array of index entries
479
+ */
480
+ list({ type, publisher, tag } = {}) {
481
+ // Read the index
482
+ const index = readJsonSafe(this.indexPath, {});
483
+
484
+ // Convert index object to array
485
+ let results = Object.values(index);
486
+
487
+ // Apply filters
488
+ if (type) {
489
+ results = results.filter(item => item.type === type);
490
+ }
491
+
492
+ if (publisher) {
493
+ results = results.filter(item => item.publisher === publisher);
494
+ }
495
+
496
+ if (tag) {
497
+ results = results.filter(item => item.tags && item.tags.includes(tag));
498
+ }
499
+
500
+ return results;
501
+ }
502
+
503
+ /**
504
+ * Get version history for an artifact
505
+ * @param {string} artifactId - The artifact identifier
506
+ * @returns {Array} Array of version summaries
507
+ */
508
+ history(artifactId) {
509
+ // Read the index to get latest version
510
+ const index = readJsonSafe(this.indexPath, {});
511
+
512
+ if (!index[artifactId]) {
513
+ return []; // Artifact doesn't exist
514
+ }
515
+
516
+ const latestVersion = index[artifactId].latestVersion;
517
+ const artifactDir = path.join(this.dataDir, artifactId);
518
+ const history = [];
519
+
520
+ // Read all version files from v1 to vN
521
+ for (let v = 1; v <= latestVersion; v++) {
522
+ const versionPath = path.join(artifactDir, `v${v}.json`);
523
+
524
+ if (fs.existsSync(versionPath)) {
525
+ try {
526
+ const content = fs.readFileSync(versionPath, 'utf8');
527
+ const versionData = JSON.parse(content);
528
+
529
+ // Add summary to history
530
+ history.push({
531
+ version: versionData.version,
532
+ publishedAt: versionData.publishedAt,
533
+ publisher: versionData.publisher,
534
+ summary: versionData.summary,
535
+ checksum: versionData.checksum
536
+ });
537
+ } catch (err) {
538
+ // Skip corrupted version files
539
+ }
540
+ }
541
+ }
542
+
543
+ return history;
544
+ }
545
+
546
+ /**
547
+ * Repair the index by scanning all version files
548
+ * Useful for recovering from index corruption
549
+ * @returns {Object} { repaired: count }
550
+ */
551
+ repair() {
552
+ // Acquire lock on the artifacts index
553
+ acquireLock(this.locksDir, 'artifacts-index');
554
+
555
+ try {
556
+ const newIndex = {};
557
+ let repairedCount = 0;
558
+
559
+ // Scan the data directory for all artifact directories
560
+ if (!fs.existsSync(this.dataDir)) {
561
+ releaseLock(this.locksDir, 'artifacts-index');
562
+ return { repaired: 0 };
563
+ }
564
+
565
+ const artifactDirs = fs.readdirSync(this.dataDir);
566
+
567
+ // Process each artifact directory
568
+ for (const artifactId of artifactDirs) {
569
+ const artifactDir = path.join(this.dataDir, artifactId);
570
+
571
+ // Skip if not a directory
572
+ if (!fs.statSync(artifactDir).isDirectory()) {
573
+ continue;
574
+ }
575
+
576
+ // Find all version files
577
+ const files = fs.readdirSync(artifactDir);
578
+ const versionFiles = files.filter(f => f.match(/^v\d+\.json$/));
579
+
580
+ if (versionFiles.length === 0) {
581
+ continue; // No version files
582
+ }
583
+
584
+ // Sort version files numerically
585
+ versionFiles.sort((a, b) => {
586
+ const versionA = parseInt(a.match(/\d+/)[0]);
587
+ const versionB = parseInt(b.match(/\d+/)[0]);
588
+ return versionA - versionB;
589
+ });
590
+
591
+ // Read the latest version file
592
+ const latestVersionFile = versionFiles[versionFiles.length - 1];
593
+ const latestVersionPath = path.join(artifactDir, latestVersionFile);
594
+
595
+ try {
596
+ const content = fs.readFileSync(latestVersionPath, 'utf8');
597
+ const versionData = JSON.parse(content);
598
+
599
+ // Read the first version file for createdAt
600
+ const firstVersionPath = path.join(artifactDir, versionFiles[0]);
601
+ const firstContent = fs.readFileSync(firstVersionPath, 'utf8');
602
+ const firstVersionData = JSON.parse(firstContent);
603
+
604
+ // Rebuild index entry
605
+ newIndex[artifactId] = {
606
+ artifactId,
607
+ type: versionData.type,
608
+ name: versionData.name,
609
+ publisher: versionData.publisher,
610
+ createdAt: firstVersionData.publishedAt,
611
+ updatedAt: versionData.publishedAt,
612
+ latestVersion: versionData.version,
613
+ tags: versionData.tags || []
614
+ };
615
+
616
+ repairedCount++;
617
+ } catch (err) {
618
+ // Skip corrupted artifacts
619
+ }
620
+ }
621
+
622
+ // Write the rebuilt index atomically
623
+ atomicWriteJson(this.indexPath, newIndex);
624
+
625
+ // Release lock
626
+ releaseLock(this.locksDir, 'artifacts-index');
627
+
628
+ return { repaired: repairedCount };
629
+
630
+ } catch (err) {
631
+ // Ensure lock is released even on error
632
+ releaseLock(this.locksDir, 'artifacts-index');
633
+ throw err;
634
+ }
635
+ }
636
+ }
637
+
638
+ // Export the ArtifactStore class
639
+ module.exports = ArtifactStore;