@andespindola/brainlink 0.1.0-beta.98 → 1.0.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/AGENTS.md +6 -6
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +186 -38
  4. package/dist/application/add-note.js +13 -44
  5. package/dist/application/analyze-vault.js +1 -1
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +119 -20
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/frontend/client-css.js +212 -42
  10. package/dist/application/frontend/client-html.js +42 -28
  11. package/dist/application/frontend/client-js.js +1294 -3217
  12. package/dist/application/frontend/client-render-worker-js.js +676 -0
  13. package/dist/application/get-graph-contexts.js +33 -0
  14. package/dist/application/get-graph-layout.js +62 -8
  15. package/dist/application/get-graph-stream-chunk.js +326 -0
  16. package/dist/application/get-graph-view.js +246 -0
  17. package/dist/application/graph-view-state.js +66 -0
  18. package/dist/application/import-legacy-sqlite.js +3 -33
  19. package/dist/application/index-vault.js +35 -22
  20. package/dist/application/migrate-context-links.js +79 -0
  21. package/dist/application/search-graph-node-ids.js +63 -3
  22. package/dist/application/server/routes.js +197 -12
  23. package/dist/cli/commands/read-commands.js +39 -3
  24. package/dist/cli/commands/vault-commands.js +182 -0
  25. package/dist/cli/commands/write-commands.js +147 -12
  26. package/dist/cli/main.js +2 -0
  27. package/dist/cli/runtime.js +10 -2
  28. package/dist/domain/context.js +1 -0
  29. package/dist/domain/graph-contexts.js +180 -0
  30. package/dist/domain/graph-layout.js +347 -21
  31. package/dist/domain/markdown.js +53 -9
  32. package/dist/infrastructure/config.js +105 -6
  33. package/dist/infrastructure/context-packs.js +122 -0
  34. package/dist/infrastructure/file-index.js +6 -3
  35. package/dist/infrastructure/index-state.js +2 -0
  36. package/dist/infrastructure/vault-migration-state.js +69 -0
  37. package/dist/infrastructure/volatile-memory.js +100 -0
  38. package/dist/mcp/http-server.js +97 -0
  39. package/dist/mcp/runtime.js +20 -0
  40. package/dist/mcp/server.js +36 -13
  41. package/dist/mcp/tools.js +203 -14
  42. package/docs/AGENT_USAGE.md +50 -5
  43. package/docs/ARCHITECTURE.md +11 -0
  44. package/docs/QUICKSTART.md +3 -1
  45. package/docs/RELEASE.md +4 -3
  46. package/package.json +3 -1
@@ -8,15 +8,19 @@ import { buildContextPackage } from '../../application/build-context.js';
8
8
  import { resolveDuplicateNotes, scanDuplicateNotes } from '../../application/dedupe-notes.js';
9
9
  import { importLegacySqliteDatabase } from '../../application/import-legacy-sqlite.js';
10
10
  import { indexVault, indexVaultWithOptions } from '../../application/index-vault.js';
11
+ import { migrateContextLinks } from '../../application/migrate-context-links.js';
12
+ import { canonicalizeContextLinks } from '../../application/canonical-context-links.js';
11
13
  import { migrateVaultContent, planVaultMigration, previewVaultMigration, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
12
14
  import { createOfflinePackBackup } from '../../application/offline-pack-backup.js';
13
15
  import { startServer } from '../../application/start-server.js';
14
16
  import { startVaultWatcher } from '../../application/watch-vault.js';
15
17
  import { doctorVault, getStats, validateVault } from '../../application/analyze-vault.js';
16
- import { defaultBrainlinkConfig, sanitizeSearchMode } from '../../infrastructure/config.js';
18
+ import { defaultBrainlinkConfig, sanitizeContextStrategy, sanitizeSearchMode } from '../../infrastructure/config.js';
17
19
  import { loadBrainlinkConfig } from '../../infrastructure/config.js';
18
- import { assertVaultAllowed, ensureVault } from '../../infrastructure/file-system-vault.js';
20
+ import { assertVaultAllowed, ensureVault, isBucketVaultPath } from '../../infrastructure/file-system-vault.js';
19
21
  import { getBootstrapPolicy, getBootstrapSessionStatus, touchBootstrapSession } from '../../infrastructure/session-state.js';
22
+ import { addVolatileMemory, clearVolatileMemory } from '../../infrastructure/volatile-memory.js';
23
+ import { startRemoteMcpServer } from '../../mcp/http-server.js';
20
24
  import { installAgentIntegration } from './agent-commands.js';
21
25
  import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
22
26
  const resolveAddContent = (options) => {
@@ -698,6 +702,68 @@ export const registerWriteCommands = (program) => {
698
702
  return `${summary}${indexMessage}${reportMessage}`;
699
703
  });
700
704
  });
705
+ program
706
+ .command('migrate-context-links')
707
+ .option('-v, --vault <vault>', 'vault directory')
708
+ .option('-a, --agent <agent>', 'agent memory namespace')
709
+ .option('-l, --limit <limit>', 'maximum context links to add per note', '5')
710
+ .option('--dry-run', 'preview context-link migration without writing files')
711
+ .option('--no-index', 'skip reindexing after migration')
712
+ .option('--json', 'print machine-readable JSON')
713
+ .description('add concise Context Links sections from existing wiki-link mentions')
714
+ .action(async (options) => {
715
+ const resolved = await resolveOptions(options);
716
+ const result = await migrateContextLinks(resolved.vault, {
717
+ dryRun: options.dryRun === true,
718
+ limit: parsePositiveInteger(options.limit ?? '5', 5),
719
+ agentId: resolved.agent
720
+ });
721
+ const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
722
+ const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
723
+ print(options.json, {
724
+ vault: resolved.vault,
725
+ agent: resolved.agent ?? 'shared',
726
+ ...result,
727
+ ...(index ? { index } : {})
728
+ }, () => {
729
+ const mode = result.dryRun ? 'Previewed' : 'Migrated';
730
+ const indexMessage = index
731
+ ? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
732
+ : '';
733
+ return `${mode} ${result.scanned} notes: changed=${result.changed}, skipped=${result.skipped}, limit=${result.limit}.${indexMessage}`;
734
+ });
735
+ });
736
+ program
737
+ .command('canonicalize-context-links')
738
+ .option('-v, --vault <vault>', 'vault directory')
739
+ .option('-a, --agent <agent>', 'agent memory namespace')
740
+ .option('--dry-run', 'preview canonical context links without writing files')
741
+ .option('--no-create-hubs', 'do not create missing context hub notes')
742
+ .option('--no-index', 'skip reindexing after canonicalization')
743
+ .option('--json', 'print machine-readable JSON')
744
+ .description('ensure notes have canonical Context Links to their inferred context hubs')
745
+ .action(async (options) => {
746
+ const resolved = await resolveOptions(options);
747
+ const result = await canonicalizeContextLinks(resolved.vault, {
748
+ dryRun: options.dryRun === true,
749
+ agentId: resolved.agent,
750
+ createMissingHubs: options.createHubs !== false
751
+ });
752
+ const shouldIndex = options.index !== false && !result.dryRun && result.changed > 0;
753
+ const index = shouldIndex ? await indexVault(resolved.vault, { full: true }) : undefined;
754
+ print(options.json, {
755
+ vault: resolved.vault,
756
+ agent: resolved.agent ?? 'shared',
757
+ ...result,
758
+ ...(index ? { index } : {})
759
+ }, () => {
760
+ const mode = result.dryRun ? 'Previewed' : 'Canonicalized';
761
+ const indexMessage = index
762
+ ? ` Fully reindexed ${index.documentCount} documents, ${index.chunkCount} chunks and ${index.linkCount} context links.`
763
+ : '';
764
+ return `${mode} ${result.scanned} notes: changed=${result.changed}, createdHubs=${result.createdHubs}, skipped=${result.skipped}.${indexMessage}`;
765
+ });
766
+ });
701
767
  program
702
768
  .command('db-import')
703
769
  .option('-v, --vault <vault>', 'vault directory')
@@ -729,6 +795,29 @@ export const registerWriteCommands = (program) => {
729
795
  return `${summary}${indexMessage}${dryRunMessage}`;
730
796
  });
731
797
  });
798
+ program
799
+ .command('volatile')
800
+ .option('-c, --content <content>', 'temporary memory content to add')
801
+ .option('--ttl <minutes>', 'time-to-live in minutes', '240')
802
+ .option('--tag <tag...>', 'volatile memory tag')
803
+ .option('-v, --vault <vault>', 'vault directory')
804
+ .option('-a, --agent <agent>', 'agent memory namespace')
805
+ .option('--clear', 'clear volatile memory for the current agent namespace')
806
+ .option('--json', 'print machine-readable JSON')
807
+ .description('add or clear temporary agent-decided memory')
808
+ .action(async (options) => {
809
+ const resolved = await resolveOptions(options);
810
+ if (options.clear) {
811
+ const cleared = await clearVolatileMemory(resolved.vault, resolved.agent);
812
+ print(options.json, { cleared, agent: resolved.agent ?? 'shared' }, () => `Cleared ${cleared} volatile memories.`);
813
+ return;
814
+ }
815
+ if (!options.content || options.content.trim().length === 0) {
816
+ throw new Error('Use --content to add volatile memory, or --clear to remove it.');
817
+ }
818
+ const entry = await addVolatileMemory(resolved.vault, options.content, resolved.agent ?? 'shared', parsePositiveInteger(options.ttl ?? '240', 240), options.tag ?? []);
819
+ print(options.json, { entry }, () => `Stored volatile memory until ${entry.expiresAt}.`);
820
+ });
732
821
  program
733
822
  .command('add')
734
823
  .argument('<title>', 'note title')
@@ -737,6 +826,7 @@ export const registerWriteCommands = (program) => {
737
826
  .option('-v, --vault <vault>', 'vault directory')
738
827
  .option('-a, --agent <agent>', 'agent memory namespace')
739
828
  .option('--allow-sensitive', 'allow writing content that looks like a secret')
829
+ .option('--no-auto-context-links', 'skip canonical Context Links for this note')
740
830
  .option('--no-auto-index', 'skip reindexing after add')
741
831
  .option('--json', 'print machine-readable JSON')
742
832
  .description('add a markdown note to the vault')
@@ -744,7 +834,8 @@ export const registerWriteCommands = (program) => {
744
834
  const resolved = await resolveOptions(options);
745
835
  const content = resolveAddContent(options);
746
836
  const added = await addNoteWithMetadata(resolved.vault, title, content, resolved.agent, {
747
- allowSensitive: Boolean(options.allowSensitive)
837
+ allowSensitive: Boolean(options.allowSensitive),
838
+ autoContextLinks: options.autoContextLinks !== false && resolved.config.autoCanonicalContextLinks
748
839
  });
749
840
  const shouldAutoIndex = options.autoIndex !== false && resolved.config.autoIndexOnWrite;
750
841
  const index = shouldAutoIndex ? await indexVault(resolved.vault) : undefined;
@@ -768,7 +859,9 @@ export const registerWriteCommands = (program) => {
768
859
  writeConnectivity: {
769
860
  autoLinked: added.autoLinked,
770
861
  linkTarget: added.linkTarget,
771
- guaranteedEdge: true
862
+ context: added.context,
863
+ hubCreated: added.hubCreated,
864
+ guaranteedEdge: added.autoLinked
772
865
  },
773
866
  possibleDuplicates,
774
867
  ...(index ? { index } : {})
@@ -776,7 +869,8 @@ export const registerWriteCommands = (program) => {
776
869
  const duplicateMessage = possibleDuplicates.length > 0
777
870
  ? `\nPotential duplicates: ${possibleDuplicates.length}. Use "blink dedupe --json" or "blink dedupe-resolve".`
778
871
  : '';
779
- return `Created note at ${added.path}${duplicateMessage}`;
872
+ const linkMessage = added.autoLinked ? ` Linked to [[${added.linkTarget}]].` : '';
873
+ return `Created note at ${added.path}.${linkMessage}${duplicateMessage}`;
780
874
  });
781
875
  });
782
876
  program
@@ -836,12 +930,17 @@ export const registerWriteCommands = (program) => {
836
930
  program
837
931
  .command('index')
838
932
  .option('-v, --vault <vault>', 'vault directory')
933
+ .option('--full', 'force a complete reindex from Markdown source without reusing unchanged index entries')
839
934
  .option('--json', 'print machine-readable JSON')
840
935
  .description('index markdown notes, links, tags and chunks')
841
936
  .action(async (options) => {
842
937
  const resolved = await resolveOptions(options);
843
- const result = await indexVault(resolved.vault);
844
- print(options.json, result, () => `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
938
+ const result = await indexVault(resolved.vault, {
939
+ full: options.full === true
940
+ });
941
+ print(options.json, result, () => options.full === true
942
+ ? `Fully reindexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`
943
+ : `Indexed ${result.documentCount} documents, ${result.chunkCount} chunks and ${result.linkCount} links`);
845
944
  });
846
945
  program
847
946
  .command('bench')
@@ -976,26 +1075,28 @@ export const registerWriteCommands = (program) => {
976
1075
  .option('-p, --port <port>', 'server port', '4321')
977
1076
  .option('--no-index', 'skip indexing before starting the server')
978
1077
  .option('--no-open', 'do not open the graph UI automatically')
979
- .option('-w, --watch', 'watch markdown files and reindex on changes')
1078
+ .option('-w, --watch', 'watch markdown files and reindex on changes', true)
1079
+ .option('--no-watch', 'disable markdown file watching')
980
1080
  .option('--json', 'print machine-readable JSON')
981
1081
  .description('start a local web UI for the knowledge graph')
982
1082
  .action(async (options) => {
983
1083
  const resolved = await resolveOptions(options);
1084
+ const shouldWatch = options.watch !== false && !isBucketVaultPath(resolved.vault);
984
1085
  const server = await startServer({
985
1086
  vaultPath: resolved.vault,
986
1087
  host: options.host ?? resolved.config.host,
987
1088
  port: parsePositiveInteger(options.port ?? String(resolved.config.port), resolved.config.port),
988
1089
  shouldIndex: options.index,
989
- shouldWatch: Boolean(options.watch)
1090
+ shouldWatch
990
1091
  });
991
1092
  const openResult = options.open !== false ? openUrlInUi(server.url, process.pid) : { opened: false, mode: 'none' };
992
1093
  print(options.json, {
993
1094
  url: server.url,
994
- watch: Boolean(options.watch),
1095
+ watch: shouldWatch,
995
1096
  readonly: true,
996
1097
  openedUi: openResult.opened,
997
1098
  openMode: openResult.mode
998
- }, () => `Brainlink graph server running at ${server.url}${openResult.opened
1099
+ }, () => `Brainlink graph server running at ${server.url} (${shouldWatch ? 'watching for changes' : 'watch disabled'})${openResult.opened
999
1100
  ? openResult.mode === 'native-gui'
1000
1101
  ? ' (opened in native desktop GUI)'
1001
1102
  : openResult.mode === 'app-window'
@@ -1005,12 +1106,45 @@ export const registerWriteCommands = (program) => {
1005
1106
  ? ' (auto-open disabled)'
1006
1107
  : ''}`);
1007
1108
  });
1109
+ program
1110
+ .command('mcp-server')
1111
+ .option('-v, --vault <vault>', 'vault directory')
1112
+ .option('-a, --agent <agent>', 'agent memory namespace')
1113
+ .option('-h, --host <host>', 'remote MCP server host', '0.0.0.0')
1114
+ .option('-p, --port <port>', 'remote MCP server port', '3333')
1115
+ .option('--path <path>', 'remote MCP endpoint path', '/mcp')
1116
+ .option('--token <token>', 'bearer token required for MCP requests')
1117
+ .option('--no-index', 'skip indexing before starting the MCP server')
1118
+ .option('--json', 'print machine-readable JSON')
1119
+ .description('start a remote MCP server for centralized cluster access')
1120
+ .action(async (options) => {
1121
+ const resolved = await resolveOptions(options);
1122
+ const token = options.token ?? process.env.BRAINLINK_MCP_TOKEN;
1123
+ const server = await startRemoteMcpServer({
1124
+ vaultPath: resolved.vault,
1125
+ agent: resolved.agent,
1126
+ host: options.host ?? '0.0.0.0',
1127
+ port: parsePositiveInteger(options.port ?? '3333', 3333),
1128
+ path: options.path ?? '/mcp',
1129
+ token,
1130
+ shouldIndex: options.index
1131
+ });
1132
+ print(options.json, {
1133
+ url: server.url,
1134
+ healthUrl: server.healthUrl,
1135
+ readyUrl: server.readyUrl,
1136
+ vault: resolved.vault,
1137
+ agent: resolved.agent ?? '*',
1138
+ auth: token === undefined ? 'disabled' : 'bearer'
1139
+ }, () => `Brainlink remote MCP server running at ${server.url} (health: ${server.healthUrl}, readiness: ${server.readyUrl})`);
1140
+ });
1008
1141
  program
1009
1142
  .command('quickstart')
1010
1143
  .option('-v, --vault <vault>', 'vault directory')
1011
1144
  .option('-a, --agent <agent>', 'agent memory namespace')
1012
1145
  .option('--query <query>', 'optional task query to return immediate grounded context')
1013
1146
  .option('--mode <mode>', 'search mode for context (fts|semantic|hybrid)')
1147
+ .option('--strategy <strategy>', 'context strategy for context (rag|cag|auto)')
1014
1148
  .option('--limit <limit>', 'maximum context sections')
1015
1149
  .option('--tokens <tokens>', 'maximum context token budget')
1016
1150
  .option('--no-install-agent', 'skip agent MCP/plugin installation and upgrade automation')
@@ -1025,6 +1159,7 @@ export const registerWriteCommands = (program) => {
1025
1159
  const limit = parsePositiveInteger(options.limit ?? String(resolved.defaults.defaultSearchLimit), resolved.defaults.defaultSearchLimit);
1026
1160
  const tokens = parsePositiveInteger(options.tokens ?? String(resolved.defaults.defaultContextTokens), resolved.defaults.defaultContextTokens);
1027
1161
  const mode = sanitizeSearchMode(options.mode, resolved.defaults.defaultSearchMode);
1162
+ const strategy = sanitizeContextStrategy(options.strategy, resolved.defaults.defaultContextStrategy);
1028
1163
  const index = await indexVault(resolved.vault);
1029
1164
  const stats = await getStats(resolved.vault, resolved.agent);
1030
1165
  const validation = await validateVault(resolved.vault, resolved.agent);
@@ -1033,7 +1168,7 @@ export const registerWriteCommands = (program) => {
1033
1168
  const policy = await getBootstrapPolicy();
1034
1169
  const bootstrapStatus = await getBootstrapSessionStatus(resolved.vault, resolved.agent);
1035
1170
  const context = options.query
1036
- ? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode)
1171
+ ? await buildContextPackage(resolved.vault, options.query, limit, tokens, resolved.agent, mode, strategy, resolved.defaults.defaultContextCacheTtlMs)
1037
1172
  : null;
1038
1173
  const agentIntegration = options.installAgent === false
1039
1174
  ? null
package/dist/cli/main.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { registerAgentCommands } from './commands/agent-commands.js';
7
7
  import { registerConfigCommands } from './commands/config-commands.js';
8
8
  import { registerReadCommands } from './commands/read-commands.js';
9
+ import { registerVaultCommands } from './commands/vault-commands.js';
9
10
  import { registerWriteCommands } from './commands/write-commands.js';
10
11
  const readPackageVersion = () => {
11
12
  const packagePath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
@@ -24,6 +25,7 @@ program
24
25
  registerWriteCommands(program);
25
26
  registerReadCommands(program);
26
27
  registerConfigCommands(program);
28
+ registerVaultCommands(program);
27
29
  registerAgentCommands(program);
28
30
  program.parseAsync().catch((error) => {
29
31
  const message = error instanceof Error ? error.message : String(error);
@@ -1,11 +1,19 @@
1
- import { loadBrainlinkConfig, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
1
+ import { autoMigrateConfiguredVaultIfChanged } from '../application/auto-migrate-configured-vault.js';
2
+ import { loadBrainlinkConfigWithSource, resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
2
3
  import { assertVaultAllowed } from '../infrastructure/file-system-vault.js';
3
4
  export const parsePositiveInteger = (value, fallback) => {
4
5
  const parsed = Number.parseInt(value, 10);
5
6
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
6
7
  };
7
8
  export const resolveOptions = async (options) => {
8
- const config = await loadBrainlinkConfig();
9
+ const { config, vaultSource } = await loadBrainlinkConfigWithSource();
10
+ if (options.vault === undefined) {
11
+ const sourceKey = vaultSource.sourcePath ? `${vaultSource.source}:${vaultSource.sourcePath}` : vaultSource.source;
12
+ await autoMigrateConfiguredVaultIfChanged({
13
+ configKey: sourceKey,
14
+ configuredVault: config.vault
15
+ });
16
+ }
9
17
  const vault = options.vault ?? config.vault;
10
18
  const allowedVault = assertVaultAllowed(vault, config.allowedVaults);
11
19
  const agent = options.agent ?? config.defaultAgent;
@@ -76,6 +76,7 @@ export const formatContextPackage = (query, sections) => {
76
76
  section.tags.length > 0 ? `Tags: ${section.tags.map((tag) => `#${tag}`).join(' ')}` : null,
77
77
  `Score: ${section.score.toFixed(3)}`,
78
78
  `Mode: ${section.searchMode}`,
79
+ section.volatile ? `Volatile: true${section.expiresAt ? `, expires ${section.expiresAt}` : ''}` : null,
79
80
  '',
80
81
  section.content
81
82
  ]
@@ -0,0 +1,180 @@
1
+ const normalize = (value) => value.trim().toLowerCase();
2
+ const includesAny = (value, patterns) => patterns.some((pattern) => pattern.test(value));
3
+ const contextId = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
4
+ const context = (title) => ({
5
+ id: contextId(title),
6
+ title
7
+ });
8
+ const byTitle = (left, right) => left.title.localeCompare(right.title);
9
+ const edgeKey = (source, target) => source < target ? `${source}|${target}` : `${target}|${source}`;
10
+ const nodeSearchText = (node) => normalize([node.title, node.path, ...node.tags].join(' '));
11
+ export const inferExplicitVisualGraphContext = (node) => {
12
+ const text = nodeSearchText(node);
13
+ const path = normalize(node.path);
14
+ if (includesAny(text, [/\bgithub repositories hub\b/]))
15
+ return context('GitHub Repositories');
16
+ if (includesAny(text, [/\bgithub organizations hub\b/]))
17
+ return context('GitHub Organizations');
18
+ if (includesAny(text, [/\bmachine configuration hub\b/]))
19
+ return context('Machine Configuration');
20
+ if (includesAny(text, [/\buser preferences hub\b/]))
21
+ return context('User Preferences');
22
+ if (includesAny(text, [/\bneovim lazyvim hub\b/]))
23
+ return context('Neovim LazyVim');
24
+ if (includesAny(text, [/\bgit workflow hub\b/]))
25
+ return context('Git Workflow');
26
+ if (includesAny(text, [/\bagent memory hub\b/]))
27
+ return context('Agent Memory');
28
+ if (includesAny(text, [/pingu_ai_codding_pair_programming/, /\bpingu\b/]))
29
+ return context('Pingu');
30
+ if (path.startsWith('github-repos/'))
31
+ return context('GitHub Repositories');
32
+ if (path.startsWith('github-org-repos/'))
33
+ return context('GitHub Organizations');
34
+ if (path.startsWith('machine-config/'))
35
+ return context('Machine Configuration');
36
+ if (includesAny(text, [/\bbrainlink\b/]))
37
+ return context('Brainlink');
38
+ if (includesAny(text, [/\banonspace\b/]))
39
+ return context('AnonSpace');
40
+ if (includesAny(text, [/\bsubstructa\b/]))
41
+ return context('Substructa');
42
+ if (includesAny(text, [/\bnebula\b/]))
43
+ return context('Nebula');
44
+ if (includesAny(text, [/\bsnippets?\b/, /\bupgrader\b/, /\bversion-map\b/]))
45
+ return context('Snippets');
46
+ if (includesAny(text, [
47
+ /\bpreference\b/,
48
+ /\bpreferences\b/,
49
+ /\bpreferencia\b/,
50
+ /\bpreferencias\b/,
51
+ /\bpreferência\b/,
52
+ /\bpreferências\b/,
53
+ /\bplaybook\b/,
54
+ /\bdirective\b/,
55
+ /\bdirectives\b/,
56
+ /\bdiretiva\b/,
57
+ /\bdiretivas\b/,
58
+ /\bengineering-style\b/,
59
+ /\bglobal-engineering\b/,
60
+ /\bcoding-identity\b/,
61
+ /\bagents\.md\b/,
62
+ /\bagents-md\b/,
63
+ /\bordem direta\b/,
64
+ /\bordem-direta\b/,
65
+ /\bman-in-the-loop\b/,
66
+ /\bconfig geral\b/,
67
+ /\bconfig-geral\b/,
68
+ /\bsync config_files\b/,
69
+ /\bsync-config-files\b/,
70
+ /\bregra operacional\b/,
71
+ /\bregras operacionais\b/,
72
+ /\boperational rule\b/,
73
+ /\boperational rules\b/,
74
+ /\boperational policy\b/
75
+ ])) {
76
+ return context('User Preferences');
77
+ }
78
+ if (includesAny(text, [/\binkdrop\b/]))
79
+ return context('Inkdrop');
80
+ if (includesAny(text, [/\blazyvim\b/, /\bneovim\b/, /\bnvim\b/, /\bmason\b/, /\bwrapper\b/]))
81
+ return context('Neovim LazyVim');
82
+ if (includesAny(text, [/\bgit-flow\b/, /\borigin-sync\b/, /\bgit-identidade\b/, /\bcommit\b/, /\bpush\b/]))
83
+ return context('Git Workflow');
84
+ if (includesAny(text, [/\bdocker\b/, /\bkubernetes\b/, /\bdeploy\b/, /\bredeploy\b/]))
85
+ return context('Operations');
86
+ if (path.startsWith('agents/'))
87
+ return context('Agent Memory');
88
+ return null;
89
+ };
90
+ export const inferVisualGraphContext = (node) => {
91
+ const explicit = inferExplicitVisualGraphContext(node);
92
+ if (explicit) {
93
+ return explicit;
94
+ }
95
+ const [root] = node.path.split('/').filter(Boolean);
96
+ return context(root ? root.replace(/[-_]+/g, ' ') : 'Root');
97
+ };
98
+ export const groupNodesByVisualContext = (nodes) => {
99
+ const groups = new Map();
100
+ nodes.forEach((node) => {
101
+ const visualContext = inferVisualGraphContext(node);
102
+ const bucket = groups.get(visualContext.title);
103
+ if (bucket) {
104
+ bucket.push(node);
105
+ return;
106
+ }
107
+ groups.set(visualContext.title, [node]);
108
+ });
109
+ return new Map(Array.from(groups.entries(), ([title, groupedNodes]) => [title, [...groupedNodes].sort(byTitle)]));
110
+ };
111
+ const countDegrees = (edges) => edges.reduce((degrees, edge) => {
112
+ degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + edge.weight);
113
+ if (edge.target) {
114
+ degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + edge.weight);
115
+ }
116
+ return degrees;
117
+ }, new Map());
118
+ const selectVisualHub = (contextTitle, nodes, degrees) => {
119
+ const normalizedContext = normalize(contextTitle).replace(/\s+/g, ' ');
120
+ const ranked = [...nodes].sort((left, right) => {
121
+ const leftTitle = normalize(left.title);
122
+ const rightTitle = normalize(right.title);
123
+ const leftHubScore = leftTitle === normalizedContext || leftTitle === `${normalizedContext} hub`
124
+ ? 4
125
+ : leftTitle.includes(normalizedContext) && /\bhub\b/.test(leftTitle)
126
+ ? 3
127
+ : /\b(memory hub|knowledge root|moc|map|hub)\b/.test(leftTitle)
128
+ ? 2
129
+ : 0;
130
+ const rightHubScore = rightTitle === normalizedContext || rightTitle === `${normalizedContext} hub`
131
+ ? 4
132
+ : rightTitle.includes(normalizedContext) && /\bhub\b/.test(rightTitle)
133
+ ? 3
134
+ : /\b(memory hub|knowledge root|moc|map|hub)\b/.test(rightTitle)
135
+ ? 2
136
+ : 0;
137
+ const hubDelta = rightHubScore - leftHubScore;
138
+ if (hubDelta !== 0)
139
+ return hubDelta;
140
+ const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
141
+ return degreeDelta === 0 ? left.title.localeCompare(right.title) : degreeDelta;
142
+ });
143
+ return ranked[0] ?? null;
144
+ };
145
+ export const addVisualContextEdges = (graph) => {
146
+ const existingPairs = new Set(graph.edges
147
+ .filter((edge) => Boolean(edge.target))
148
+ .map((edge) => edgeKey(edge.source, edge.target)));
149
+ const degrees = countDegrees(graph.edges);
150
+ const derivedEdges = [];
151
+ for (const [contextTitle, nodes] of groupNodesByVisualContext(graph.nodes).entries()) {
152
+ if (nodes.length <= 1) {
153
+ continue;
154
+ }
155
+ const hub = selectVisualHub(contextTitle, nodes, degrees);
156
+ if (!hub) {
157
+ continue;
158
+ }
159
+ nodes
160
+ .filter((node) => node.id !== hub.id)
161
+ .forEach((node) => {
162
+ const key = edgeKey(hub.id, node.id);
163
+ if (existingPairs.has(key)) {
164
+ return;
165
+ }
166
+ existingPairs.add(key);
167
+ derivedEdges.push({
168
+ source: hub.id,
169
+ target: node.id,
170
+ targetTitle: node.title,
171
+ weight: 0.5,
172
+ priority: 'low'
173
+ });
174
+ });
175
+ }
176
+ return {
177
+ nodes: graph.nodes,
178
+ edges: [...graph.edges, ...derivedEdges]
179
+ };
180
+ };