claude-memory-layer 1.0.6 → 1.0.8

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 (45) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.history/package_20260201142928.json +46 -0
  4. package/.history/package_20260201192048.json +47 -0
  5. package/README.md +26 -26
  6. package/dist/.claude-plugin/plugin.json +3 -3
  7. package/dist/cli/index.js +1109 -25
  8. package/dist/cli/index.js.map +4 -4
  9. package/dist/core/index.js +192 -5
  10. package/dist/core/index.js.map +4 -4
  11. package/dist/hooks/session-end.js +262 -18
  12. package/dist/hooks/session-end.js.map +4 -4
  13. package/dist/hooks/session-start.js +262 -18
  14. package/dist/hooks/session-start.js.map +4 -4
  15. package/dist/hooks/stop.js +262 -18
  16. package/dist/hooks/stop.js.map +4 -4
  17. package/dist/hooks/user-prompt-submit.js +262 -18
  18. package/dist/hooks/user-prompt-submit.js.map +4 -4
  19. package/dist/server/api/index.js +4728 -0
  20. package/dist/server/api/index.js.map +7 -0
  21. package/dist/server/index.js +4790 -0
  22. package/dist/server/index.js.map +7 -0
  23. package/dist/services/memory-service.js +269 -18
  24. package/dist/services/memory-service.js.map +4 -4
  25. package/dist/ui/index.html +1225 -0
  26. package/package.json +4 -2
  27. package/scripts/build.ts +33 -3
  28. package/src/cli/index.ts +311 -6
  29. package/src/core/db-wrapper.ts +8 -1
  30. package/src/core/event-store.ts +52 -3
  31. package/src/core/graduation-worker.ts +171 -0
  32. package/src/core/graduation.ts +15 -2
  33. package/src/core/index.ts +1 -0
  34. package/src/core/retriever.ts +18 -0
  35. package/src/core/types.ts +1 -1
  36. package/src/mcp/index.ts +2 -2
  37. package/src/mcp/tools.ts +1 -1
  38. package/src/server/api/citations.ts +7 -3
  39. package/src/server/api/events.ts +7 -3
  40. package/src/server/api/search.ts +7 -3
  41. package/src/server/api/sessions.ts +7 -3
  42. package/src/server/api/stats.ts +175 -5
  43. package/src/server/index.ts +18 -9
  44. package/src/services/memory-service.ts +107 -19
  45. package/src/ui/index.html +1225 -0
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "claude-memory-layer",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Claude Code plugin that learns from conversations to provide personalized assistance",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "code-memory": "dist/cli/index.js"
7
+ "claude-memory-layer": "dist/cli/index.js"
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
@@ -29,10 +29,12 @@
29
29
  "node": ">=18.0.0"
30
30
  },
31
31
  "dependencies": {
32
+ "@hono/node-server": "^1.13.0",
32
33
  "@lancedb/lancedb": "^0.5.0",
33
34
  "@xenova/transformers": "^2.17.0",
34
35
  "commander": "^12.0.0",
35
36
  "duckdb": "^0.10.0",
37
+ "hono": "^4.0.0",
36
38
  "zod": "^3.22.0"
37
39
  },
38
40
  "devDependencies": {
package/scripts/build.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Build script for code-memory plugin
2
+ * Build script for claude-memory-layer plugin
3
3
  * Uses esbuild for fast bundling
4
4
  */
5
5
 
@@ -23,11 +23,16 @@ const commonOptions: esbuild.BuildOptions = {
23
23
  format: 'esm',
24
24
  sourcemap: true,
25
25
  external: [
26
+ '@hono/node-server',
27
+ '@hono/node-server/serve-static',
26
28
  '@lancedb/lancedb',
27
29
  '@xenova/transformers',
28
30
  'duckdb',
29
31
  'commander',
30
- 'zod'
32
+ 'zod',
33
+ 'hono',
34
+ 'hono/cors',
35
+ 'hono/logger'
31
36
  ],
32
37
  banner: {
33
38
  js: `import { createRequire } from 'module';
@@ -40,7 +45,7 @@ const __dirname = dirname(__filename);`
40
45
  };
41
46
 
42
47
  async function build() {
43
- console.log('šŸ”Ø Building code-memory plugin...\n');
48
+ console.log('šŸ”Ø Building claude-memory-layer plugin...\n');
44
49
 
45
50
  // Build CLI
46
51
  console.log('šŸ“¦ Building CLI...');
@@ -83,16 +88,41 @@ async function build() {
83
88
  outfile: 'dist/services/memory-service.js'
84
89
  });
85
90
 
91
+ // Build server
92
+ console.log('šŸ“¦ Building server...');
93
+ await esbuild.build({
94
+ ...commonOptions,
95
+ entryPoints: ['src/server/index.ts'],
96
+ outfile: 'dist/server/index.js',
97
+ external: [...(commonOptions.external || []), 'hono']
98
+ });
99
+
100
+ // Build server API
101
+ await esbuild.build({
102
+ ...commonOptions,
103
+ entryPoints: ['src/server/api/index.ts'],
104
+ outfile: 'dist/server/api/index.js',
105
+ external: [...(commonOptions.external || []), 'hono']
106
+ });
107
+
86
108
  // Copy plugin manifest
87
109
  console.log('šŸ“‹ Copying plugin files...');
88
110
  fs.cpSync('.claude-plugin', path.join(outdir, '.claude-plugin'), { recursive: true });
89
111
 
112
+ // Copy UI files
113
+ console.log('šŸ“‹ Copying UI files...');
114
+ if (fs.existsSync('src/ui')) {
115
+ fs.cpSync('src/ui', path.join(outdir, 'ui'), { recursive: true });
116
+ }
117
+
90
118
  console.log('\nāœ… Build complete!');
91
119
  console.log(`\nOutput: ${outdir}/`);
92
120
  console.log(' - cli/index.js');
93
121
  console.log(' - hooks/*.js');
94
122
  console.log(' - core/index.js');
95
123
  console.log(' - services/memory-service.js');
124
+ console.log(' - server/index.js');
125
+ console.log(' - ui/index.html');
96
126
  console.log(' - .claude-plugin/');
97
127
  }
98
128
 
package/src/cli/index.ts CHANGED
@@ -5,19 +5,247 @@
5
5
  */
6
6
 
7
7
  import { Command } from 'commander';
8
+ import { exec } from 'child_process';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
8
12
  import {
9
13
  getDefaultMemoryService,
10
14
  getMemoryServiceForProject
11
15
  } from '../services/memory-service.js';
12
16
  import { createSessionHistoryImporter } from '../services/session-history-importer.js';
17
+ import { startServer, stopServer, isServerRunning } from '../server/index.js';
18
+
19
+ // ============================================================
20
+ // Hook Installation Utilities
21
+ // ============================================================
22
+
23
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
24
+
25
+ interface ClaudeSettings {
26
+ hooks?: {
27
+ UserPromptSubmit?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
28
+ PostToolUse?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
29
+ SessionStart?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
30
+ Stop?: Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
31
+ };
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ function getPluginPath(): string {
36
+ // Try to find the dist directory
37
+ const possiblePaths = [
38
+ path.join(__dirname, '..'), // When running from dist/cli
39
+ path.join(__dirname, '../..', 'dist'), // When running from src
40
+ path.join(process.cwd(), 'dist'), // Current working directory
41
+ ];
42
+
43
+ for (const p of possiblePaths) {
44
+ const hooksPath = path.join(p, 'hooks', 'user-prompt-submit.js');
45
+ if (fs.existsSync(hooksPath)) {
46
+ return p;
47
+ }
48
+ }
49
+
50
+ // Fallback to npm global installation path
51
+ return path.join(os.homedir(), '.npm-global', 'lib', 'node_modules', 'claude-memory-layer', 'dist');
52
+ }
53
+
54
+ function loadClaudeSettings(): ClaudeSettings {
55
+ try {
56
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
57
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf-8');
58
+ return JSON.parse(content);
59
+ }
60
+ } catch (error) {
61
+ console.error('Warning: Could not read existing settings:', error);
62
+ }
63
+ return {};
64
+ }
65
+
66
+ function saveClaudeSettings(settings: ClaudeSettings): void {
67
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
68
+ if (!fs.existsSync(dir)) {
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ }
71
+
72
+ // Atomic write
73
+ const tempPath = CLAUDE_SETTINGS_PATH + '.tmp';
74
+ fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2));
75
+ fs.renameSync(tempPath, CLAUDE_SETTINGS_PATH);
76
+ }
77
+
78
+ function getHooksConfig(pluginPath: string): ClaudeSettings['hooks'] {
79
+ return {
80
+ UserPromptSubmit: [
81
+ {
82
+ matcher: '',
83
+ hooks: [
84
+ {
85
+ type: 'command',
86
+ command: `node ${path.join(pluginPath, 'hooks', 'user-prompt-submit.js')}`
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ PostToolUse: [
92
+ {
93
+ matcher: '',
94
+ hooks: [
95
+ {
96
+ type: 'command',
97
+ command: `node ${path.join(pluginPath, 'hooks', 'post-tool-use.js')}`
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ };
103
+ }
13
104
 
14
105
  const program = new Command();
15
106
 
16
107
  program
17
- .name('code-memory')
108
+ .name('claude-memory-layer')
18
109
  .description('Claude Code Memory Plugin CLI')
19
110
  .version('1.0.0');
20
111
 
112
+ // ============================================================
113
+ // Install / Uninstall Commands
114
+ // ============================================================
115
+
116
+ /**
117
+ * Install command - register hooks with Claude Code
118
+ */
119
+ program
120
+ .command('install')
121
+ .description('Install hooks into Claude Code settings')
122
+ .option('--path <path>', 'Custom plugin path (defaults to auto-detect)')
123
+ .action(async (options) => {
124
+ try {
125
+ const pluginPath = options.path || getPluginPath();
126
+
127
+ // Verify hooks exist
128
+ const userPromptHook = path.join(pluginPath, 'hooks', 'user-prompt-submit.js');
129
+ if (!fs.existsSync(userPromptHook)) {
130
+ console.error(`\nāŒ Hook files not found at: ${pluginPath}`);
131
+ console.error(' Make sure you have built the plugin with "npm run build"');
132
+ process.exit(1);
133
+ }
134
+
135
+ // Load existing settings
136
+ const settings = loadClaudeSettings();
137
+
138
+ // Add hooks (merge with existing)
139
+ const newHooks = getHooksConfig(pluginPath);
140
+ settings.hooks = {
141
+ ...settings.hooks,
142
+ ...newHooks
143
+ };
144
+
145
+ // Save settings
146
+ saveClaudeSettings(settings);
147
+
148
+ console.log('\nāœ… Claude Memory Layer installed!\n');
149
+ console.log('Hooks registered:');
150
+ console.log(' - UserPromptSubmit: Memory retrieval on user input');
151
+ console.log(' - PostToolUse: Store tool observations\n');
152
+ console.log('Plugin path:', pluginPath);
153
+ console.log('\nāš ļø Restart Claude Code for changes to take effect.\n');
154
+ console.log('Commands:');
155
+ console.log(' claude-memory-layer dashboard - Open web dashboard');
156
+ console.log(' claude-memory-layer search - Search memories');
157
+ console.log(' claude-memory-layer stats - View statistics');
158
+ console.log(' claude-memory-layer uninstall - Remove hooks\n');
159
+ } catch (error) {
160
+ console.error('Install failed:', error);
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ /**
166
+ * Uninstall command - remove hooks from Claude Code
167
+ */
168
+ program
169
+ .command('uninstall')
170
+ .description('Remove hooks from Claude Code settings')
171
+ .action(async () => {
172
+ try {
173
+ // Load existing settings
174
+ const settings = loadClaudeSettings();
175
+
176
+ if (!settings.hooks) {
177
+ console.log('\nšŸ“‹ No hooks installed.\n');
178
+ return;
179
+ }
180
+
181
+ // Remove our hooks
182
+ delete settings.hooks.UserPromptSubmit;
183
+ delete settings.hooks.PostToolUse;
184
+
185
+ // Clean up empty hooks object
186
+ if (Object.keys(settings.hooks).length === 0) {
187
+ delete settings.hooks;
188
+ }
189
+
190
+ // Save settings
191
+ saveClaudeSettings(settings);
192
+
193
+ console.log('\nāœ… Claude Memory Layer uninstalled!\n');
194
+ console.log('Hooks removed from Claude Code settings.');
195
+ console.log('Your memory data is preserved and can be accessed with:');
196
+ console.log(' claude-memory-layer dashboard\n');
197
+ console.log('āš ļø Restart Claude Code for changes to take effect.\n');
198
+ } catch (error) {
199
+ console.error('Uninstall failed:', error);
200
+ process.exit(1);
201
+ }
202
+ });
203
+
204
+ /**
205
+ * Status command - check installation status
206
+ */
207
+ program
208
+ .command('status')
209
+ .description('Check plugin installation status')
210
+ .action(async () => {
211
+ try {
212
+ const settings = loadClaudeSettings();
213
+ const pluginPath = getPluginPath();
214
+
215
+ console.log('\n🧠 Claude Memory Layer Status\n');
216
+
217
+ // Check hooks
218
+ const hasUserPromptHook = settings.hooks?.UserPromptSubmit?.some(h =>
219
+ h.hooks?.some(hook => hook.command?.includes('user-prompt-submit'))
220
+ );
221
+ const hasPostToolHook = settings.hooks?.PostToolUse?.some(h =>
222
+ h.hooks?.some(hook => hook.command?.includes('post-tool-use'))
223
+ );
224
+
225
+ console.log('Hooks:');
226
+ console.log(` UserPromptSubmit: ${hasUserPromptHook ? 'āœ… Installed' : 'āŒ Not installed'}`);
227
+ console.log(` PostToolUse: ${hasPostToolHook ? 'āœ… Installed' : 'āŒ Not installed'}`);
228
+
229
+ // Check plugin files
230
+ const hooksExist = fs.existsSync(path.join(pluginPath, 'hooks', 'user-prompt-submit.js'));
231
+ console.log(`\nPlugin files: ${hooksExist ? 'āœ… Found' : 'āŒ Not found'}`);
232
+ console.log(` Path: ${pluginPath}`);
233
+
234
+ // Check dashboard
235
+ const dashboardRunning = await isServerRunning(37777);
236
+ console.log(`\nDashboard: ${dashboardRunning ? 'āœ… Running at http://localhost:37777' : 'ā¹ļø Not running'}`);
237
+
238
+ if (!hasUserPromptHook || !hasPostToolHook) {
239
+ console.log('\nšŸ’” Run "claude-memory-layer install" to set up hooks.\n');
240
+ } else {
241
+ console.log('\nāœ… Plugin is fully installed and configured.\n');
242
+ }
243
+ } catch (error) {
244
+ console.error('Status check failed:', error);
245
+ process.exit(1);
246
+ }
247
+ });
248
+
21
249
  /**
22
250
  * Search command
23
251
  */
@@ -349,7 +577,7 @@ program
349
577
  console.log(`... and ${sessions.length - 20} more sessions`);
350
578
  }
351
579
 
352
- console.log('\nUse "code-memory import --session <path>" to import a specific session');
580
+ console.log('\nUse "claude-memory-layer import --session <path>" to import a specific session');
353
581
  } catch (error) {
354
582
  console.error('List failed:', error);
355
583
  process.exit(1);
@@ -389,7 +617,7 @@ endlessCmd
389
617
  console.log(' - Working Set: Recent context kept active');
390
618
  console.log(' - Consolidation: Automatic memory integration');
391
619
  console.log(' - Continuity: Seamless context transitions\n');
392
- console.log('Use "code-memory endless status" to view current state');
620
+ console.log('Use "claude-memory-layer endless status" to view current state');
393
621
 
394
622
  await service.shutdown();
395
623
  } catch (error) {
@@ -462,7 +690,7 @@ endlessCmd
462
690
  }
463
691
  } else {
464
692
  console.log('Endless Mode is disabled.');
465
- console.log('Use "code-memory endless enable" to activate.');
693
+ console.log('Use "claude-memory-layer endless enable" to activate.');
466
694
  }
467
695
 
468
696
  await service.shutdown();
@@ -488,7 +716,7 @@ endlessCmd
488
716
 
489
717
  if (!service.isEndlessModeActive()) {
490
718
  console.log('\nāš ļø Endless Mode is not active');
491
- console.log('Use "code-memory endless enable" first');
719
+ console.log('Use "claude-memory-layer endless enable" first');
492
720
  process.exit(1);
493
721
  }
494
722
 
@@ -527,7 +755,7 @@ endlessCmd
527
755
 
528
756
  if (!service.isEndlessModeActive()) {
529
757
  console.log('\nāš ļø Endless Mode is not active');
530
- console.log('Use "code-memory endless enable" first');
758
+ console.log('Use "claude-memory-layer endless enable" first');
531
759
  process.exit(1);
532
760
  }
533
761
 
@@ -628,4 +856,81 @@ endlessCmd
628
856
  }
629
857
  });
630
858
 
859
+ /**
860
+ * Dashboard command - start web dashboard
861
+ */
862
+ program
863
+ .command('dashboard')
864
+ .description('Open memory dashboard in browser')
865
+ .option('-p, --port <port>', 'Server port', '37777')
866
+ .option('--no-open', 'Do not auto-open browser')
867
+ .action(async (options) => {
868
+ const port = parseInt(options.port, 10);
869
+
870
+ try {
871
+ // Check if server is already running
872
+ const running = await isServerRunning(port);
873
+ if (running) {
874
+ console.log(`\n🧠 Dashboard already running at http://localhost:${port}\n`);
875
+ if (options.open) {
876
+ openBrowser(`http://localhost:${port}`);
877
+ }
878
+ return;
879
+ }
880
+
881
+ // Start the server
882
+ console.log('\n🧠 Starting Code Memory Dashboard...\n');
883
+ startServer(port);
884
+
885
+ // Open browser
886
+ if (options.open) {
887
+ setTimeout(() => {
888
+ openBrowser(`http://localhost:${port}`);
889
+ }, 500);
890
+ }
891
+
892
+ console.log(`\nšŸ“Š Dashboard: http://localhost:${port}`);
893
+ console.log('Press Ctrl+C to stop the server\n');
894
+
895
+ // Handle graceful shutdown
896
+ const shutdown = () => {
897
+ console.log('\n\nšŸ‘‹ Shutting down dashboard...');
898
+ stopServer();
899
+ process.exit(0);
900
+ };
901
+
902
+ process.on('SIGINT', shutdown);
903
+ process.on('SIGTERM', shutdown);
904
+
905
+ // Keep process alive
906
+ await new Promise(() => {});
907
+ } catch (error) {
908
+ console.error('Dashboard failed:', error);
909
+ process.exit(1);
910
+ }
911
+ });
912
+
913
+ /**
914
+ * Open URL in default browser
915
+ */
916
+ function openBrowser(url: string): void {
917
+ const platform = process.platform;
918
+ let command: string;
919
+
920
+ if (platform === 'darwin') {
921
+ command = `open "${url}"`;
922
+ } else if (platform === 'win32') {
923
+ command = `start "" "${url}"`;
924
+ } else {
925
+ command = `xdg-open "${url}"`;
926
+ }
927
+
928
+ exec(command, (error) => {
929
+ if (error) {
930
+ console.log(`\nāš ļø Could not open browser automatically.`);
931
+ console.log(` Please open ${url} manually.\n`);
932
+ }
933
+ });
934
+ }
935
+
631
936
  program.parse();
@@ -37,10 +37,17 @@ export function toDate(value: unknown): Date {
37
37
  return new Date(String(value));
38
38
  }
39
39
 
40
+ export interface DatabaseOptions {
41
+ readOnly?: boolean;
42
+ }
43
+
40
44
  /**
41
45
  * Creates a new DuckDB database with Promise-based API
42
46
  */
43
- export function createDatabase(path: string): Database {
47
+ export function createDatabase(path: string, options?: DatabaseOptions): Database {
48
+ if (options?.readOnly) {
49
+ return new duckdb.Database(path, { access_mode: 'READ_ONLY' });
50
+ }
44
51
  return new duckdb.Database(path);
45
52
  }
46
53
 
@@ -12,14 +12,20 @@ import {
12
12
  OutboxItem
13
13
  } from './types.js';
14
14
  import { makeCanonicalKey, makeDedupeKey } from './canonical-key.js';
15
- import { createDatabase, dbRun, dbAll, dbClose, toDate, type Database } from './db-wrapper.js';
15
+ import { createDatabase, dbRun, dbAll, dbClose, toDate, type Database, type DatabaseOptions } from './db-wrapper.js';
16
+
17
+ export interface EventStoreOptions extends DatabaseOptions {
18
+ // Additional options can be added here
19
+ }
16
20
 
17
21
  export class EventStore {
18
22
  private db: Database;
19
23
  private initialized = false;
24
+ private readonly readOnly: boolean;
20
25
 
21
- constructor(private dbPath: string) {
22
- this.db = createDatabase(dbPath);
26
+ constructor(private dbPath: string, options?: EventStoreOptions) {
27
+ this.readOnly = options?.readOnly ?? false;
28
+ this.db = createDatabase(dbPath, { readOnly: this.readOnly });
23
29
  }
24
30
 
25
31
  /**
@@ -28,6 +34,12 @@ export class EventStore {
28
34
  async initialize(): Promise<void> {
29
35
  if (this.initialized) return;
30
36
 
37
+ // In read-only mode, skip schema creation (tables already exist)
38
+ if (this.readOnly) {
39
+ this.initialized = true;
40
+ return;
41
+ }
42
+
31
43
  // L0 EventStore: Single Source of Truth (immutable, append-only)
32
44
  await dbRun(this.db, `
33
45
  CREATE TABLE IF NOT EXISTS events (
@@ -611,6 +623,43 @@ export class EventStore {
611
623
  return rows;
612
624
  }
613
625
 
626
+ /**
627
+ * Get events by memory level
628
+ */
629
+ async getEventsByLevel(level: string, options?: { limit?: number; offset?: number }): Promise<MemoryEvent[]> {
630
+ await this.initialize();
631
+
632
+ const limit = options?.limit || 50;
633
+ const offset = options?.offset || 0;
634
+
635
+ const rows = await dbAll<Record<string, unknown>>(
636
+ this.db,
637
+ `SELECT e.* FROM events e
638
+ INNER JOIN memory_levels ml ON e.id = ml.event_id
639
+ WHERE ml.level = ?
640
+ ORDER BY e.timestamp DESC
641
+ LIMIT ? OFFSET ?`,
642
+ [level, limit, offset]
643
+ );
644
+
645
+ return rows.map(row => this.rowToEvent(row));
646
+ }
647
+
648
+ /**
649
+ * Get memory level for a specific event
650
+ */
651
+ async getEventLevel(eventId: string): Promise<string | null> {
652
+ await this.initialize();
653
+
654
+ const rows = await dbAll<{ level: string }>(
655
+ this.db,
656
+ `SELECT level FROM memory_levels WHERE event_id = ?`,
657
+ [eventId]
658
+ );
659
+
660
+ return rows.length > 0 ? rows[0].level : null;
661
+ }
662
+
614
663
  // ============================================================
615
664
  // Endless Mode Helper Methods
616
665
  // ============================================================