claude-code-memory-explorer 0.2.6 → 0.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/README.md +1 -0
- package/package.json +1 -1
- package/public/app.js +70 -11
- package/public/style.css +47 -4
- package/server.js +45 -64
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ Claude Code stores memory across multiple locations:
|
|
|
62
62
|
| CLAUDE.local.md | `./CLAUDE.local.md` | Always |
|
|
63
63
|
| Rules | `.claude/rules/*.md` | Conditional (path-scoped) |
|
|
64
64
|
| Auto memory | `~/.claude/projects/<encoded>/memory/` | Startup (MEMORY.md) or on-demand |
|
|
65
|
+
| Agent memory | `~/.claude/agent-memory/<agent>/`, `./.claude/agent-memory/<agent>/`, `./.claude/agent-memory-local/<agent>/` | Startup (MEMORY.md) or on-demand |
|
|
65
66
|
|
|
66
67
|
The dashboard:
|
|
67
68
|
1. **Scans** all memory locations — `~/.claude/`, ancestor directories, project rules, auto memory
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -243,14 +243,14 @@ function bindModalKeys(inputId, modalId, submitFn) {
|
|
|
243
243
|
|
|
244
244
|
// #region RENDER_TREE
|
|
245
245
|
|
|
246
|
-
const SCOPE_ORDER = ['policy', 'user', '
|
|
246
|
+
const SCOPE_ORDER = ['policy', 'user', 'project', 'rule', 'memory', 'agent-memory'];
|
|
247
247
|
const SCOPE_LABELS = {
|
|
248
248
|
policy: 'Managed Policy',
|
|
249
|
-
'user-memory': 'User Memory',
|
|
250
249
|
user: 'User',
|
|
251
250
|
project: 'Project',
|
|
252
251
|
rule: 'Rules',
|
|
253
252
|
memory: 'Auto Memory',
|
|
253
|
+
'agent-memory': 'Agent Memory',
|
|
254
254
|
};
|
|
255
255
|
const LOAD_ICONS = {
|
|
256
256
|
always: '\u25CF',
|
|
@@ -317,7 +317,8 @@ function renderTree() {
|
|
|
317
317
|
const meta = `${item.lines}L`;
|
|
318
318
|
const isConditional = item.load === 'conditional' || item.load === 'ondemand';
|
|
319
319
|
const pad = indent ? ` style="padding-left:${12 + indent * 16}px"` : '';
|
|
320
|
-
|
|
320
|
+
const muted = item.scope === 'agent-memory' ? ' tree-agent-item' : '';
|
|
321
|
+
let h = `<div class="tree-item${sel}${indent ? ' tree-child' : ''}${isConditional ? ' tree-conditional' : ''}${muted}" data-id="${esc(item.id)}" title="${esc(item.path)}" onclick="selectFile('${escJs(item.id)}')"${pad}>`;
|
|
321
322
|
h += `<span class="load-icon" title="${loadTitle}" style="color:var(--scope-${item.scope})">${loadIcon}</span>`;
|
|
322
323
|
h += `<span class="file-name">${esc(item.name)}</span>`;
|
|
323
324
|
h += `<span class="file-meta">${meta}</span>`;
|
|
@@ -334,12 +335,67 @@ function renderTree() {
|
|
|
334
335
|
const items = groups[scope];
|
|
335
336
|
if (!items) continue;
|
|
336
337
|
const label = SCOPE_LABELS[scope] || scope;
|
|
337
|
-
|
|
338
|
-
|
|
338
|
+
const collapsible = scope === 'agent-memory';
|
|
339
|
+
const collapsed = collapsible && isGroupCollapsed(scope);
|
|
340
|
+
const chevron = collapsible ? `<span class="group-chevron">${collapsed ? '\u25B8' : '\u25BE'}</span>` : '';
|
|
341
|
+
const headerClass = collapsible ? 'tree-group-header tree-group-clickable' : 'tree-group-header';
|
|
342
|
+
const onclick = collapsible ? ` onclick="_toggleGroup('${escJs(scope)}')"` : '';
|
|
343
|
+
html += `<div class="${headerClass}" data-scope="${esc(scope)}"${onclick}>${chevron}<span class="scope-dot" style="color:var(--scope-${scope})">\u25CF</span> ${esc(label)} <span style="opacity:0.5">${items.length}</span></div>`;
|
|
344
|
+
if (collapsed) continue;
|
|
345
|
+
if (scope === 'agent-memory') {
|
|
346
|
+
const sorted = [...items].sort((a, b) => {
|
|
347
|
+
const so = ['user', 'project', 'local'];
|
|
348
|
+
return (
|
|
349
|
+
so.indexOf(a.agentScope) - so.indexOf(b.agentScope) ||
|
|
350
|
+
a.agentName.localeCompare(b.agentName) ||
|
|
351
|
+
(a.name === 'MEMORY.md' ? -1 : 1)
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
let lastKey = null;
|
|
355
|
+
for (const item of sorted) {
|
|
356
|
+
const key = `${item.agentScope}/${item.agentName}`;
|
|
357
|
+
if (key !== lastKey) {
|
|
358
|
+
html += `<div class="tree-subgroup-header" title="${esc(item.agentScope)} scope"><span class="agent-name">${esc(item.agentName)}</span><span class="agent-scope-tag">${esc(item.agentScope)}</span></div>`;
|
|
359
|
+
lastKey = key;
|
|
360
|
+
}
|
|
361
|
+
html += renderItem(item, 1);
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
for (const item of items) html += renderItem(item, 0);
|
|
365
|
+
}
|
|
339
366
|
}
|
|
340
367
|
container.innerHTML = html;
|
|
341
368
|
}
|
|
342
369
|
|
|
370
|
+
const COLLAPSED_DEFAULTS = ['agent-memory'];
|
|
371
|
+
function getCollapsedGroups() {
|
|
372
|
+
try {
|
|
373
|
+
const stored = localStorage.getItem('collapsedGroups');
|
|
374
|
+
if (stored !== null) return new Set(JSON.parse(stored));
|
|
375
|
+
} catch {}
|
|
376
|
+
return new Set(COLLAPSED_DEFAULTS);
|
|
377
|
+
}
|
|
378
|
+
function isGroupCollapsed(scope) {
|
|
379
|
+
return getCollapsedGroups().has(scope);
|
|
380
|
+
}
|
|
381
|
+
function setGroupCollapsed(scope, collapsed) {
|
|
382
|
+
const set = getCollapsedGroups();
|
|
383
|
+
if (collapsed) set.add(scope);
|
|
384
|
+
else set.delete(scope);
|
|
385
|
+
localStorage.setItem('collapsedGroups', JSON.stringify([...set]));
|
|
386
|
+
}
|
|
387
|
+
function _toggleGroup(scope) {
|
|
388
|
+
setGroupCollapsed(scope, !isGroupCollapsed(scope));
|
|
389
|
+
renderTree();
|
|
390
|
+
}
|
|
391
|
+
function getItemScope(item) {
|
|
392
|
+
return item?.parentId ? stackData.find((s) => s.id === item.parentId)?.scope : item?.scope;
|
|
393
|
+
}
|
|
394
|
+
function expandGroupForItem(item) {
|
|
395
|
+
const scope = getItemScope(item);
|
|
396
|
+
if (scope && isGroupCollapsed(scope)) setGroupCollapsed(scope, false);
|
|
397
|
+
}
|
|
398
|
+
|
|
343
399
|
function pushFileState(id) {
|
|
344
400
|
const url = id ? `#${encodeURIComponent(id)}` : location.pathname;
|
|
345
401
|
history.pushState({ fileId: id }, '', url);
|
|
@@ -368,7 +424,9 @@ function navigateTree(direction) {
|
|
|
368
424
|
if (idx < 0) idx = navOrder.length - 1;
|
|
369
425
|
if (idx >= navOrder.length) idx = 0;
|
|
370
426
|
}
|
|
371
|
-
|
|
427
|
+
const target = navOrder[idx];
|
|
428
|
+
selectedFileId = target.id;
|
|
429
|
+
expandGroupForItem(target);
|
|
372
430
|
pushFileState(selectedFileId);
|
|
373
431
|
renderTree();
|
|
374
432
|
renderPreview();
|
|
@@ -381,8 +439,7 @@ function navigateGroup(direction) {
|
|
|
381
439
|
if (!activeScopes.length) return;
|
|
382
440
|
|
|
383
441
|
const current = stackData.find((s) => s.id === selectedFileId);
|
|
384
|
-
|
|
385
|
-
let scopeIdx = activeScopes.indexOf(currentScope);
|
|
442
|
+
let scopeIdx = activeScopes.indexOf(getItemScope(current));
|
|
386
443
|
if (scopeIdx === -1) {
|
|
387
444
|
scopeIdx = direction > 0 ? 0 : activeScopes.length - 1;
|
|
388
445
|
} else {
|
|
@@ -390,7 +447,9 @@ function navigateGroup(direction) {
|
|
|
390
447
|
if (scopeIdx < 0) scopeIdx = activeScopes.length - 1;
|
|
391
448
|
if (scopeIdx >= activeScopes.length) scopeIdx = 0;
|
|
392
449
|
}
|
|
393
|
-
|
|
450
|
+
const target = groups[activeScopes[scopeIdx]][0];
|
|
451
|
+
selectedFileId = target.id;
|
|
452
|
+
expandGroupForItem(target);
|
|
394
453
|
pushFileState(selectedFileId);
|
|
395
454
|
renderTree();
|
|
396
455
|
renderPreview();
|
|
@@ -402,7 +461,7 @@ function navigateGroup(direction) {
|
|
|
402
461
|
// #region MEMORY_INDEX
|
|
403
462
|
|
|
404
463
|
function isMemoryIndex(source) {
|
|
405
|
-
return (source.scope === 'memory' || source.scope === '
|
|
464
|
+
return (source.scope === 'memory' || source.scope === 'agent-memory') && source.name === 'MEMORY.md';
|
|
406
465
|
}
|
|
407
466
|
|
|
408
467
|
function parseMemoryIndex(content) {
|
|
@@ -518,7 +577,7 @@ async function renderPreview() {
|
|
|
518
577
|
return;
|
|
519
578
|
}
|
|
520
579
|
|
|
521
|
-
if ((source.scope === 'memory' || source.scope === '
|
|
580
|
+
if ((source.scope === 'memory' || source.scope === 'agent-memory') && source.load === 'startup' && source.maxLines) {
|
|
522
581
|
const lines = content.split('\n');
|
|
523
582
|
const cutoff = source.maxLines;
|
|
524
583
|
if (lines.length > cutoff) {
|
package/public/style.css
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
--scope-local: #8a9bb5;
|
|
25
25
|
--scope-rule: #a89bc4;
|
|
26
26
|
--scope-memory: #c4a06a;
|
|
27
|
-
--scope-
|
|
27
|
+
--scope-agent-memory: #8aa8a3;
|
|
28
28
|
--scope-policy: #b58a8a;
|
|
29
29
|
--font-mono: 'IBM Plex Mono', monospace;
|
|
30
30
|
--font-serif: 'Playfair Display', serif;
|
|
@@ -261,6 +261,49 @@ body {
|
|
|
261
261
|
align-items: center;
|
|
262
262
|
gap: 6px;
|
|
263
263
|
}
|
|
264
|
+
.tree-subgroup-header {
|
|
265
|
+
padding: 6px 12px 2px 20px;
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
font-family: var(--font-mono);
|
|
268
|
+
color: var(--text-muted);
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 8px;
|
|
272
|
+
opacity: 0.85;
|
|
273
|
+
}
|
|
274
|
+
.tree-subgroup-header .agent-name {
|
|
275
|
+
color: var(--text-tertiary);
|
|
276
|
+
}
|
|
277
|
+
.tree-subgroup-header .agent-scope-tag {
|
|
278
|
+
font-size: 9px;
|
|
279
|
+
font-weight: 600;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
letter-spacing: 0.4px;
|
|
282
|
+
padding: 1px 5px;
|
|
283
|
+
border-radius: 3px;
|
|
284
|
+
background: rgba(138, 168, 163, 0.12);
|
|
285
|
+
color: var(--scope-agent-memory);
|
|
286
|
+
opacity: 0.85;
|
|
287
|
+
}
|
|
288
|
+
.tree-group-clickable {
|
|
289
|
+
cursor: pointer;
|
|
290
|
+
user-select: none;
|
|
291
|
+
}
|
|
292
|
+
.tree-group-clickable:hover {
|
|
293
|
+
color: var(--text-secondary);
|
|
294
|
+
}
|
|
295
|
+
.group-chevron {
|
|
296
|
+
display: inline-block;
|
|
297
|
+
width: 10px;
|
|
298
|
+
font-size: 9px;
|
|
299
|
+
color: var(--text-muted);
|
|
300
|
+
}
|
|
301
|
+
.tree-agent-item .file-name {
|
|
302
|
+
opacity: 0.7;
|
|
303
|
+
}
|
|
304
|
+
body.light .tree-agent-item .file-name {
|
|
305
|
+
opacity: 0.8;
|
|
306
|
+
}
|
|
264
307
|
.tree-item {
|
|
265
308
|
padding: 5px 12px 5px 24px;
|
|
266
309
|
cursor: pointer;
|
|
@@ -491,9 +534,9 @@ body.light .tree-conditional .file-name {
|
|
|
491
534
|
background: rgba(196, 149, 106, 0.18);
|
|
492
535
|
color: var(--scope-memory);
|
|
493
536
|
}
|
|
494
|
-
.scope-
|
|
495
|
-
background: rgba(
|
|
496
|
-
color: var(--scope-
|
|
537
|
+
.scope-agent-memory {
|
|
538
|
+
background: rgba(138, 168, 163, 0.18);
|
|
539
|
+
color: var(--scope-agent-memory);
|
|
497
540
|
}
|
|
498
541
|
.scope-policy {
|
|
499
542
|
background: rgba(181, 138, 138, 0.18);
|
package/server.js
CHANGED
|
@@ -52,6 +52,10 @@ function clearCache() {
|
|
|
52
52
|
|
|
53
53
|
// #region FILESYSTEM_SCANNING
|
|
54
54
|
|
|
55
|
+
const CLAUDE_MD_VARIANTS = ['CLAUDE.md', '.claude/CLAUDE.md', 'CLAUDE.local.md', '.claude/CLAUDE.local.md'];
|
|
56
|
+
|
|
57
|
+
const slug = s => s.replace(/[^a-zA-Z0-9]/g, '-');
|
|
58
|
+
|
|
55
59
|
const MANAGED_POLICY_PATHS = process.platform === 'win32'
|
|
56
60
|
? [path.join(process.env.ProgramFiles || 'C:\\Program Files', 'ClaudeCode', 'CLAUDE.md')]
|
|
57
61
|
: process.platform === 'darwin'
|
|
@@ -219,16 +223,16 @@ function discoverMemorySources(projectPath) {
|
|
|
219
223
|
const ancestors = getAncestorDirs(projectPath);
|
|
220
224
|
const seenPaths = new Set(sources.map(s => s.path));
|
|
221
225
|
for (const dir of ancestors) {
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
|
|
227
|
+
for (const rel of CLAUDE_MD_VARIANTS) {
|
|
228
|
+
const filePath = path.join(dir, rel);
|
|
224
229
|
if (seenPaths.has(filePath)) continue;
|
|
225
230
|
const info = fileInfo(filePath);
|
|
226
231
|
if (!info) continue;
|
|
227
232
|
seenPaths.add(filePath);
|
|
228
|
-
const isProjectRoot = path.resolve(dir) === path.resolve(projectPath);
|
|
229
233
|
sources.push({
|
|
230
|
-
id: `project-claude-md-${
|
|
231
|
-
name:
|
|
234
|
+
id: `project-claude-md-${slug(rel)}-${slug(dir)}`,
|
|
235
|
+
name: rel,
|
|
232
236
|
scope: 'project',
|
|
233
237
|
load: 'always',
|
|
234
238
|
...info,
|
|
@@ -236,23 +240,6 @@ function discoverMemorySources(projectPath) {
|
|
|
236
240
|
isProjectRoot,
|
|
237
241
|
...spreadImports(info.path, info.content),
|
|
238
242
|
});
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const localPath = path.join(dir, 'CLAUDE.local.md');
|
|
243
|
-
if (seenPaths.has(localPath)) continue;
|
|
244
|
-
const localInfo = fileInfo(localPath);
|
|
245
|
-
if (localInfo) {
|
|
246
|
-
seenPaths.add(localPath);
|
|
247
|
-
sources.push({
|
|
248
|
-
id: `local-claude-md-${dir.replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
249
|
-
name: 'CLAUDE.local.md',
|
|
250
|
-
scope: 'project',
|
|
251
|
-
load: 'always',
|
|
252
|
-
...localInfo,
|
|
253
|
-
dir,
|
|
254
|
-
...spreadImports(localInfo.path, localInfo.content),
|
|
255
|
-
});
|
|
256
243
|
}
|
|
257
244
|
}
|
|
258
245
|
|
|
@@ -265,7 +252,7 @@ function discoverMemorySources(projectPath) {
|
|
|
265
252
|
for (const entry of entries) {
|
|
266
253
|
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
267
254
|
const subdir = path.join(dir, entry.name);
|
|
268
|
-
for (const name of
|
|
255
|
+
for (const name of CLAUDE_MD_VARIANTS) {
|
|
269
256
|
const filePath = path.join(subdir, name);
|
|
270
257
|
if (seenPaths.has(filePath)) continue;
|
|
271
258
|
const info = fileInfo(filePath);
|
|
@@ -273,8 +260,8 @@ function discoverMemorySources(projectPath) {
|
|
|
273
260
|
seenPaths.add(filePath);
|
|
274
261
|
const rel = path.relative(projectPath, subdir).replace(/\\/g, '/');
|
|
275
262
|
sources.push({
|
|
276
|
-
id: `project-claude-md-${
|
|
277
|
-
name: `${rel}/${name
|
|
263
|
+
id: `project-claude-md-${slug(name)}-${slug(subdir)}`,
|
|
264
|
+
name: `${rel}/${name}`,
|
|
278
265
|
scope: 'project',
|
|
279
266
|
load: 'tree',
|
|
280
267
|
...info,
|
|
@@ -306,67 +293,61 @@ function discoverMemorySources(projectPath) {
|
|
|
306
293
|
}
|
|
307
294
|
}
|
|
308
295
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (memoryDir && fs.existsSync(memoryDir)) {
|
|
312
|
-
const memoryMd = path.join(memoryDir, 'MEMORY.md');
|
|
296
|
+
function pushMemoryDir(dir, scope, idPrefix, extraFields = {}) {
|
|
297
|
+
const memoryMd = path.join(dir, 'MEMORY.md');
|
|
313
298
|
const memInfo = fileInfo(memoryMd);
|
|
314
299
|
if (memInfo) {
|
|
315
300
|
sources.push({
|
|
316
|
-
id:
|
|
301
|
+
id: `${idPrefix}-index`,
|
|
317
302
|
name: 'MEMORY.md',
|
|
318
|
-
scope
|
|
303
|
+
scope,
|
|
319
304
|
load: 'startup',
|
|
305
|
+
...extraFields,
|
|
320
306
|
...memInfo,
|
|
321
307
|
maxLines: 200,
|
|
322
308
|
maxBytes: 25 * 1024,
|
|
323
309
|
...spreadImports(memInfo.path, memInfo.content),
|
|
324
310
|
});
|
|
325
311
|
}
|
|
326
|
-
for (const file of findMdFiles(
|
|
312
|
+
for (const file of findMdFiles(dir)) {
|
|
327
313
|
if (path.basename(file) === 'MEMORY.md') continue;
|
|
328
314
|
const info = fileInfo(file);
|
|
329
315
|
if (!info) continue;
|
|
330
316
|
sources.push({
|
|
331
|
-
id:
|
|
317
|
+
id: `${idPrefix}-${path.basename(file, '.md')}`,
|
|
332
318
|
name: path.basename(file),
|
|
333
|
-
scope
|
|
319
|
+
scope,
|
|
334
320
|
load: 'ondemand',
|
|
321
|
+
...extraFields,
|
|
335
322
|
...info,
|
|
336
323
|
...spreadImports(info.path, info.content),
|
|
337
324
|
});
|
|
338
325
|
}
|
|
339
326
|
}
|
|
340
327
|
|
|
341
|
-
//
|
|
342
|
-
const
|
|
343
|
-
if (fs.existsSync(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
name: path.basename(file),
|
|
365
|
-
scope: 'user-memory',
|
|
366
|
-
load: 'ondemand',
|
|
367
|
-
...info,
|
|
368
|
-
...spreadImports(info.path, info.content),
|
|
369
|
-
});
|
|
328
|
+
// 6. Auto memory
|
|
329
|
+
const memoryDir = findMemoryDir(projectPath);
|
|
330
|
+
if (memoryDir && fs.existsSync(memoryDir)) {
|
|
331
|
+
pushMemoryDir(memoryDir, 'memory', 'memory');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 7. Subagent persistent memory (https://code.claude.com/docs/en/sub-agents#enable-persistent-memory)
|
|
335
|
+
// user: ~/.claude/agent-memory/<agent>/
|
|
336
|
+
// project: <project>/.claude/agent-memory/<agent>/
|
|
337
|
+
// local: <project>/.claude/agent-memory-local/<agent>/
|
|
338
|
+
const agentMemoryRoots = [
|
|
339
|
+
{ root: path.join(os.homedir(), '.claude', 'agent-memory'), agentScope: 'user' },
|
|
340
|
+
{ root: path.join(projectPath, '.claude', 'agent-memory'), agentScope: 'project' },
|
|
341
|
+
{ root: path.join(projectPath, '.claude', 'agent-memory-local'), agentScope: 'local' },
|
|
342
|
+
];
|
|
343
|
+
for (const { root, agentScope } of agentMemoryRoots) {
|
|
344
|
+
let agentDirs;
|
|
345
|
+
try { agentDirs = fs.readdirSync(root, { withFileTypes: true }); } catch { continue; }
|
|
346
|
+
for (const entry of agentDirs) {
|
|
347
|
+
if (!entry.isDirectory()) continue;
|
|
348
|
+
const agentName = entry.name;
|
|
349
|
+
const idPrefix = `agent-memory-${agentScope}-${agentName.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
350
|
+
pushMemoryDir(path.join(root, agentName), 'agent-memory', idPrefix, { agentScope, agentName });
|
|
370
351
|
}
|
|
371
352
|
}
|
|
372
353
|
|
|
@@ -384,7 +365,7 @@ function discoverMemorySources(projectPath) {
|
|
|
384
365
|
for (const { imp, parent, hard } of batch) {
|
|
385
366
|
if (seen.has(imp)) {
|
|
386
367
|
const existing = sourceByPath[imp];
|
|
387
|
-
if (existing && !existing.parentId && existing.scope === 'memory') {
|
|
368
|
+
if (existing && !existing.parentId && (existing.scope === 'memory' || existing.scope === 'agent-memory')) {
|
|
388
369
|
existing.parentId = parent.id;
|
|
389
370
|
if (hard) existing.load = 'import';
|
|
390
371
|
}
|