archgraph-argo 0.10.51 → 0.11.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 CHANGED
@@ -43,11 +43,30 @@ argo-deploy
43
43
 
44
44
  Done — the ARGO toolchain, skills, and rules are deployed, and the `argo` MCP server is registered automatically in **GitHub Copilot**, **Cursor**, **OpenCode**, **DeepSeek Harness** (dsh), and **OpenClaw**.
45
45
 
46
- > Semantic (Graph RAG) queries also need **Neo4j** and a **vector engine** configured in
47
- > `~/.argo/.env`; everything else works out of the box.
46
+ ### Prerequisites and configuration
47
+
48
+ Everything works out of the box except **semantic (Graph RAG) queries**, which need:
49
+
50
+ - **Neo4j graph database** — stores the structural projection of your architecture graph. During
51
+ `argo-deploy` you configure `ARGO_NEO4J_DATABASE_URL`, `ARGO_NEO4J_DATABASE_USERNAME`, and
52
+ `ARGO_NEO4J_DATABASE_PASSWORD` in `~/.argo/.env`.
53
+ - **Embedding / vector engine** — powers semantic Graph RAG retrieval. Configure
54
+ `ARGO_EMBEDDING_BASE_URL`, `ARGO_EMBEDDING_MODEL`, `ARGO_EMBEDDING_PROVIDER`,
55
+ `ARGO_EMBEDDING_MODEL_VERSION`, `ARGO_EMBEDDING_DIMENSIONS`, plus the API key `QWEN_KEY`.
56
+
57
+ Where do the values come from? The Neo4j credentials come from the Neo4j instance you own or
58
+ provision (URI, username, password). The embedding configuration and `QWEN_KEY` come from your
59
+ embedding provider's dashboard — for example Alibaba DashScope. `argo-deploy` walks you through the
60
+ prompt (existing non-empty values in `~/.argo/.env` are kept); you can also edit the file afterwards
61
+ and re-run.
48
62
 
49
63
  ## How to use
50
64
 
65
+ **Step 0 — initialize the workspace.** In a fresh project, ask your coding agent to run `argo init`
66
+ (the `initializeWorkspace` MCP call). It creates a starter `design/KG/SystemArchitecture.json` when
67
+ missing, performs the first JSON → Neo4j sync, initializes the semantic (Graph RAG) lifecycle, and
68
+ verifies the architecture. From then on, the intent graph is the source of truth for the project.
69
+
51
70
  After installing, open your project and start a coding agent. It will:
52
71
 
53
72
  1. locate the architecture element behind the task before changing anything,
@@ -56,6 +75,15 @@ After installing, open your project and start a coding agent. It will:
56
75
 
57
76
  The intent architecture graph — modelled in **ArchiMate 3.2** — is the single source of truth.
58
77
 
78
+ ## Community
79
+
80
+ ArchGraph runs on open co-building. Join the community hub to share, browse and reuse **architecture
81
+ subgraphs** across projects, and follow the governance & contribution guides:
82
+
83
+ - **Community site** — https://argo.derekworkspacev5.com/archgraph/ (subgraph library, docs, blog)
84
+ - **graph-wiki repository** — https://github.com/derekhu0002/graph-wiki (graph-asset home: contribute
85
+ a subgraph from your project, or pull one back to reuse)
86
+
59
87
  ## License
60
88
 
61
89
  [Apache License 2.0](LICENSE)
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // graph-mcp stdio bridge for hosts that dial remote MCP servers over SSE GET
5
+ // and fail on Streamable HTTP-only endpoints (CURSOR remote url entries GET
6
+ // /mcp -> 404). This bridge translates stdio JSON-RPC lines into POST
7
+ // Streamable HTTP requests against GRAPH_MCP_URL so the remote graph-mcp
8
+ // server loads as an ordinary stdio MCP server.
9
+ //
10
+ // Deployed by install-argo.ps1 to ~/.cursor/mcp-bridges/graph-mcp-stdio.js
11
+ // and registered as:
12
+ // "graph-mcp": { type: stdio, command: node,
13
+ // args: [<bridge path>],
14
+ // env: { GRAPH_MCP_URL: "https://.../mcp" } }
15
+
16
+ const readline = require('node:readline');
17
+
18
+ const endpoint = process.env.GRAPH_MCP_URL;
19
+ if (!endpoint) {
20
+ console.error('GRAPH_MCP_URL is required');
21
+ process.exit(1);
22
+ }
23
+
24
+ function emit(message) {
25
+ process.stdout.write(`${JSON.stringify(message)}\n`);
26
+ }
27
+
28
+ function parseSse(body) {
29
+ const messages = [];
30
+ // SSE frames are separated by a blank line (\r\n\r\n or \n\n).
31
+ for (const block of body.split(/\r?\n\r?\n/)) {
32
+ const data = block
33
+ .split(/\r?\n/)
34
+ .filter((line) => line.startsWith('data:'))
35
+ .map((line) => line.slice(5).trimStart())
36
+ .join('\n');
37
+ if (data) {
38
+ messages.push(JSON.parse(data));
39
+ }
40
+ }
41
+ return messages;
42
+ }
43
+
44
+ async function forward(message) {
45
+ const response = await fetch(endpoint, {
46
+ method: 'POST',
47
+ headers: {
48
+ Accept: 'application/json, text/event-stream',
49
+ 'Content-Type': 'application/json',
50
+ },
51
+ body: JSON.stringify(message),
52
+ });
53
+
54
+ if (!response.ok) {
55
+ throw new Error(`Remote MCP returned HTTP ${response.status}`);
56
+ }
57
+
58
+ // 202 Accepted means the server will stream the result over a separate SSE
59
+ // connection; there is nothing to return on this POST. Notifications
60
+ // (id === undefined) carry no response either.
61
+ if (response.status === 202 || message.id === undefined) return;
62
+
63
+ const body = await response.text();
64
+ const contentType = response.headers.get('content-type') || '';
65
+ if (contentType.includes('text/event-stream')) {
66
+ for (const item of parseSse(body)) emit(item);
67
+ return;
68
+ }
69
+ if (body.trim()) emit(JSON.parse(body));
70
+ }
71
+
72
+ const input = readline.createInterface({
73
+ input: process.stdin,
74
+ crlfDelay: Infinity,
75
+ });
76
+
77
+ input.on('line', (line) => {
78
+ if (!line.trim()) return;
79
+ let message;
80
+ try {
81
+ message = JSON.parse(line);
82
+ } catch (error) {
83
+ console.error(`Invalid JSON-RPC input: ${error.message}`);
84
+ return;
85
+ }
86
+
87
+ forward(message).catch((error) => {
88
+ if (message.id !== undefined) {
89
+ emit({
90
+ jsonrpc: '2.0',
91
+ id: message.id,
92
+ error: { code: -32000, message: error.message },
93
+ });
94
+ } else {
95
+ console.error(error.message);
96
+ }
97
+ });
98
+ });
package/install-argo.ps1 CHANGED
@@ -5,6 +5,7 @@ param(
5
5
  [string]$PromptsRoot = "$env:APPDATA\Code\User\prompts",
6
6
  [string]$CursorSkillsRoot = "$env:USERPROFILE\.cursor\skills",
7
7
  [string]$CursorMcpPath = "$env:USERPROFILE\.cursor\mcp.json",
8
+ [string]$CursorMcpBridgesRoot = "$env:USERPROFILE\.cursor\mcp-bridges",
8
9
  [string]$OpenCodeSkillsRoot = "$env:USERPROFILE\.config\opencode\skills",
9
10
  [string]$OpenCodeAgentsPath = "$env:USERPROFILE\.config\opencode\AGENTS.md",
10
11
  [string]$OpenCodeConfigPath = "$env:USERPROFILE\.config\opencode\opencode.json",
@@ -25,7 +26,10 @@ param(
25
26
  [string]$OpenClawRepoRoot = '',
26
27
  [switch]$SkipOpenClaw,
27
28
  [string]$McpPath,
28
- [string]$GraphMcpUrl = 'https://argo.derekworkspacev5.com/mcp'
29
+ [string]$GraphMcpUrl = 'https://argo.derekworkspacev5.com/mcp',
30
+ [switch]$SkipEaWeb,
31
+ [int]$EaWebPort = 8787,
32
+ [string]$EaWebRoot = ''
29
33
  )
30
34
 
31
35
  $ErrorActionPreference = 'Stop'
@@ -736,63 +740,68 @@ Write-Host '==> Deploying Argo toolchain'
736
740
 
737
741
  $schemaSrc = Join-Path $argoDir 'schema'
738
742
  $schemaDest = Join-Path $ArgoRoot 'schema'
739
- Write-Host "[1/22] argo\schema -> $schemaDest"
743
+ Write-Host "[1/23] argo\schema -> $schemaDest"
740
744
  Copy-Tree -Source $schemaSrc -Destination $schemaDest
741
745
 
742
746
  $scriptsSrc = Join-Path $argoDir 'scripts'
743
747
  $scriptsDest = Join-Path $ArgoRoot 'scripts'
744
- Write-Host "[2/22] argo\scripts -> $scriptsDest"
748
+ Write-Host "[2/23] argo\scripts -> $scriptsDest"
745
749
  Copy-Tree -Source $scriptsSrc -Destination $scriptsDest
746
750
 
747
751
  $defaultsSrc = Join-Path $argoDir 'defaults'
748
752
  $defaultsDest = Join-Path $ArgoRoot 'defaults'
749
- Write-Host "[3/22] argo\defaults -> $defaultsDest"
753
+ Write-Host "[3/23] argo\defaults -> $defaultsDest"
750
754
  Copy-Tree -Source $defaultsSrc -Destination $defaultsDest
751
755
 
752
756
  $skillSrc = Join-Path (Join-Path $argoDir 'skills') 'argo-init'
753
757
  $skillDest = Join-Path $SkillsRoot 'argo-init'
754
- Write-Host "[4/22] argo\skills\argo-init -> $skillDest"
758
+ Write-Host "[4/23] argo\skills\argo-init -> $skillDest"
755
759
  Copy-Tree -Source $skillSrc -Destination $skillDest
756
760
 
757
761
  $ruleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
758
762
  $ruleDest = Join-Path $PromptsRoot 'archgraph.instructions.md'
759
- Write-Host "[5/22] argo\rules\archgraph.instructions.md -> $ruleDest"
763
+ Write-Host "[5/23] argo\rules\archgraph.instructions.md -> $ruleDest"
760
764
  New-Item -ItemType Directory -Force -Path $PromptsRoot | Out-Null
761
765
  Copy-Item -Force -Path $ruleSrc -Destination $ruleDest
762
766
 
763
767
  $depsSrc = Join-Path $argoDir 'package.json'
764
768
  $depsDest = Join-Path $ArgoRoot 'package.json'
765
- Write-Host "[6/22] argo\package.json -> $depsDest"
769
+ Write-Host "[6/23] argo\package.json -> $depsDest"
766
770
  Copy-Item -Force -Path $depsSrc -Destination $depsDest
767
771
 
768
772
  $cursorSkillDest = Join-Path $CursorSkillsRoot 'argo-init'
769
- Write-Host "[7/22] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
773
+ Write-Host "[7/23] argo\skills\argo-init -> $cursorSkillDest (Cursor)"
770
774
  Copy-Tree -Source $skillSrc -Destination $cursorSkillDest
771
775
 
776
+ $mcpBridgeSrc = Join-Path $argoDir 'mcp-bridges'
777
+ $mcpBridgeDest = Join-Path $CursorMcpBridgesRoot ''
778
+ Write-Host " argo\mcp-bridges -> $mcpBridgeDest (Cursor graph-mcp stdio bridge)"
779
+ Copy-Tree -Source $mcpBridgeSrc -Destination $mcpBridgeDest
780
+
772
781
  $openCodeSkillDest = Join-Path $OpenCodeSkillsRoot 'argo-init'
773
- Write-Host "[8/22] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
782
+ Write-Host "[8/23] argo\skills\argo-init -> $openCodeSkillDest (OpenCode)"
774
783
  Copy-Tree -Source $skillSrc -Destination $openCodeSkillDest
775
784
 
776
- Write-Host "[9/22] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
785
+ Write-Host "[9/23] argo\rules\archgraph.instructions.md -> $OpenCodeAgentsPath (OpenCode global AGENTS.md)"
777
786
  Add-AgentsRule -AgentsPath $OpenCodeAgentsPath -RulePath $ruleSrc
778
787
 
779
788
  $agentsSrc = Join-Path $argoDir 'agents'
780
- Write-Host "[10/22] argo\agents -> $CopilotAgentsRoot (Copilot user-level)"
789
+ Write-Host "[10/23] argo\agents -> $CopilotAgentsRoot (Copilot user-level)"
781
790
  Copy-Agents -Source $agentsSrc -Destination $CopilotAgentsRoot
782
791
 
783
- Write-Host "[11/22] argo\agents -> $CursorAgentsRoot (Cursor user-level, converted to .md)"
792
+ Write-Host "[11/23] argo\agents -> $CursorAgentsRoot (Cursor user-level, converted to .md)"
784
793
  Copy-Agents -Source $agentsSrc -Destination $CursorAgentsRoot -Target cursor
785
794
 
786
- Write-Host "[12/22] argo\agents -> $OpenCodeAgentsRoot (OpenCode user-level, converted to .md)"
795
+ Write-Host "[12/23] argo\agents -> $OpenCodeAgentsRoot (OpenCode user-level, converted to .md)"
787
796
  Copy-Agents -Source $agentsSrc -Destination $OpenCodeAgentsRoot -Target opencode
788
797
 
789
798
  $pluginsSrc = Join-Path $argoDir 'plugins'
790
- Write-Host "[13/22] argo\plugins -> $PluginsRoot (Argo opencode plugins)"
799
+ Write-Host "[13/23] argo\plugins -> $PluginsRoot (Argo opencode plugins)"
791
800
  Copy-Tree -Source $pluginsSrc -Destination $PluginsRoot
792
801
 
793
802
  $cursorRuleSrc = Join-Path (Join-Path $argoDir 'rules') 'archgraph.instructions.md'
794
803
  $cursorRuleDest = Join-Path $CursorRulesRoot 'archgraph.mdc'
795
- Write-Host "[14/22] argo\rules\archgraph.instructions.md -> $cursorRuleDest (Cursor global rule, alwaysApply)"
804
+ Write-Host "[14/23] argo\rules\archgraph.instructions.md -> $cursorRuleDest (Cursor global rule, alwaysApply)"
796
805
  New-Item -ItemType Directory -Force -Path $CursorRulesRoot | Out-Null
797
806
  Convert-RuleFile -SourceFile $cursorRuleSrc -DestinationFile $cursorRuleDest
798
807
 
@@ -804,16 +813,16 @@ if ($SkipDsh) {
804
813
  $patchPath = Join-Path $DshHome 'cordis.patch.yml'
805
814
  $argoServer = (Join-Path $ArgoRoot 'scripts\argo-mcp-server.js').Replace('\', '/')
806
815
 
807
- Write-Host "[15/22] argo\rules\archgraph.instructions.md -> $DshHome\AGENTS.md (DeepSeek Harness user-global rule, frontmatter stripped)"
816
+ Write-Host "[15/23] argo\rules\archgraph.instructions.md -> $DshHome\AGENTS.md (DeepSeek Harness user-global rule, frontmatter stripped)"
808
817
  Write-DshAgentRule -DshHome $DshHome -RuleText $ruleSrcContent
809
818
 
810
- Write-Host "[16/22] argo\skills\argo-init -> $dshSkillDest (DeepSeek Harness skill)"
819
+ Write-Host "[16/23] argo\skills\argo-init -> $dshSkillDest (DeepSeek Harness skill)"
811
820
  Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $dshSkillDest
812
821
 
813
- Write-Host "[17/22] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
822
+ Write-Host "[17/23] argo\rules\<WakeupGuideline> -> $DshHome\plugins\dsh-argo-wakeup\index.js (DeepSeek Harness wakeup plugin)"
814
823
  $wakeupDshPath = New-DshWakeupPlugin -DshHome $DshHome -RuleText $ruleSrcContent
815
824
 
816
- Write-Host "[18/22] argo-workspace + argo-wakeup rows -> $patchPath (DeepSeek Harness MCP bridge + wakeup plugin)"
825
+ Write-Host "[18/23] argo-workspace + argo-wakeup rows -> $patchPath (DeepSeek Harness MCP bridge + wakeup plugin)"
817
826
  # The generated dsh-argo-workspace bridge connects directly to the argo
818
827
  # server (no dsh-mcp-client row), registers every tool as mcp__argo__* and
819
828
  # injects the current session's workspace (SessionHeader.cwd) as the
@@ -844,7 +853,7 @@ if ($SkipDsh) {
844
853
  Write-DshManagedBlock -Path $patchPath -Block $block -MarkerStart '# BEGIN ArchGraph ARGO deployment' -MarkerEnd '# END ArchGraph ARGO deployment'
845
854
  }
846
855
 
847
- Write-Host "[19/22] argo\agents -> $DshHome\.agent-presets\<id> (DeepSeek Harness agent presets)"
856
+ Write-Host "[19/23] argo\agents -> $DshHome\.agent-presets\<id> (DeepSeek Harness agent presets)"
848
857
  New-DshAgentPresets -DshHome $DshHome -AgentsSrc (Join-Path $argoDir 'agents')
849
858
 
850
859
  Write-Host " note: graph-mcp remote ($GraphMcpUrl) is NOT registered for DeepSeek Harness -"
@@ -865,10 +874,10 @@ if ($SkipOpenClaw) {
865
874
  $openClawAgentsDest = Join-Path $OpenClawWorkspace 'AGENTS.md'
866
875
 
867
876
  Write-Host '==> Deploying OpenClaw integration'
868
- Write-Host "[20/22] argo\rules\archgraph.instructions.md -> $openClawAgentsDest (OpenClaw workspace AGENTS.md, frontmatter stripped)"
877
+ Write-Host "[20/23] argo\rules\archgraph.instructions.md -> $openClawAgentsDest (OpenClaw workspace AGENTS.md, frontmatter stripped)"
869
878
  Write-OpenClawAgentRule -OpenClawWorkspace $OpenClawWorkspace -RuleText $ruleSrcContent
870
879
 
871
- Write-Host "[21/22] argo\skills\argo-init -> $openClawSkillDest (OpenClaw managed skill, all agents)"
880
+ Write-Host "[21/23] argo\skills\argo-init -> $openClawSkillDest (OpenClaw managed skill, all agents)"
872
881
  Copy-Tree -Source (Join-Path $argoDir 'skills\argo-init') -Destination $openClawSkillDest
873
882
 
874
883
  Write-Host ' OpenClaw injects AGENTS.md into Project Context on every session, so the wakeup'
@@ -973,9 +982,10 @@ if ($SkipMcp) {
973
982
  # graph-mcp is a Streamable HTTP remote MCP endpoint. Its config shape is
974
983
  # HOST-SPECIFIC (each host's mcp.json schema is different):
975
984
  # - OpenCode (opencode.json mcp.<name>): {type: remote, url, enabled}
976
- # - Cursor (mcp.json mcpServers.<name>): {url}official remote
977
- # entries carry no `type` (type only means "stdio" for local servers);
978
- # Cursor dials url servers over Streamable HTTP.
985
+ # - Cursor (mcp.json mcpServers.<name>): stdio bridge Cursor dials
986
+ # remote url servers over SSE GET /mcp, which the Streamable HTTP
987
+ # endpoint answers with 404, so graph-mcp is exposed as a local stdio
988
+ # bridge (graph-mcp-stdio.js) that forwards to the remote over POST.
979
989
  # - VS Code (mcp.json servers.<name>): {type: http, url}
980
990
  # - OpenClaw (openclaw.json mcp.servers.<name>): {url, transport:
981
991
  # streamable-http} — keyed by transport, not by a type alias.
@@ -986,8 +996,14 @@ if ($SkipMcp) {
986
996
  url = $GraphMcpUrl
987
997
  enabled = $true
988
998
  }
999
+ $graphMcpBridgePath = (Join-Path $CursorMcpBridgesRoot 'graph-mcp-stdio.js').Replace('\', '/')
989
1000
  $graphMcpCursor = [ordered]@{
990
- url = $GraphMcpUrl
1001
+ type = 'stdio'
1002
+ command = 'node'
1003
+ args = @($graphMcpBridgePath)
1004
+ env = [ordered]@{
1005
+ GRAPH_MCP_URL = $GraphMcpUrl
1006
+ }
991
1007
  }
992
1008
  $graphMcpVSCode = [ordered]@{
993
1009
  type = 'http'
@@ -1015,6 +1031,7 @@ if ($SkipMcp) {
1015
1031
  args = @($argoServer)
1016
1032
  }) -ExtraServers ([ordered]@{ 'graph-mcp' = $graphMcpCursor })
1017
1033
  Write-Host "argo MCP config written -> $CursorMcpPath"
1034
+ Write-Host " graph-mcp registered as stdio bridge -> $graphMcpBridgePath"
1018
1035
 
1019
1036
  Write-Host '==> Registering argo MCP server in OpenCode'
1020
1037
  Write-McpConfig -Path $OpenCodeConfigPath -ServersKey 'mcp' -ServerConfig ([ordered]@{
@@ -1029,7 +1046,7 @@ if ($SkipMcp) {
1029
1046
  # location; -OpenClawRepoRoot overrides it (e.g. for tests or when the
1030
1047
  # installer runs from a non-workspace npm-global package dir).
1031
1048
  $openClawRepoRoot = if ($OpenClawRepoRoot) { $OpenClawRepoRoot } else { $repoRoot }
1032
- Write-Host "[22/22] argo MCP server -> $(Join-Path $OpenClawHome 'openclaw.json') (OpenClaw mcp.servers.argo, env.ARGO_REPO_ROOT pinned)"
1049
+ Write-Host "[22/23] argo MCP server -> $(Join-Path $OpenClawHome 'openclaw.json') (OpenClaw mcp.servers.argo, env.ARGO_REPO_ROOT pinned)"
1033
1050
  Write-OpenClawMcpConfig -OpenClawHome $OpenClawHome -RepoRoot $openClawRepoRoot -ArgoServer $argoServer -GraphMcpServer $openClawGraphMcpServer
1034
1051
  }
1035
1052
  }
@@ -1041,5 +1058,68 @@ if (Test-Path $wakeupPluginPath) {
1041
1058
  Write-Host "argo-wakeup plugin registered -> $OpenCodeConfigPath"
1042
1059
  }
1043
1060
 
1061
+ # EA local web service (WP2786): deploy the two service scripts next to the
1062
+ # harness scripts (REPO_ROOT = $ArgoRoot keeps the relative layout intact) plus
1063
+ # the web/ statics, then start the service in the background. Re-running the
1064
+ # installer is idempotent: an occupied port skips the startup.
1065
+ $eaServiceSrc = Join-Path $repoRoot 'scripts\ea-web-service.js'
1066
+ $eaLayoutStoreSrc = Join-Path $repoRoot 'scripts\ea-layout-store.js'
1067
+ $eaWebSrc = Join-Path $repoRoot 'web'
1068
+ Write-Host "[23/23] scripts\ea-web-service.js + scripts\ea-layout-store.js + web -> $ArgoRoot (EA local web service)"
1069
+ if ((Test-Path $eaServiceSrc) -and (Test-Path $eaLayoutStoreSrc) -and (Test-Path $eaWebSrc)) {
1070
+ Copy-Item -Force -Path $eaServiceSrc -Destination (Join-Path $scriptsDest 'ea-web-service.js')
1071
+ Copy-Item -Force -Path $eaLayoutStoreSrc -Destination (Join-Path $scriptsDest 'ea-layout-store.js')
1072
+ Copy-Tree -Source $eaWebSrc -Destination (Join-Path $ArgoRoot 'web')
1073
+ Write-Host " service scripts + web statics deployed -> $scriptsDest / $(Join-Path $ArgoRoot 'web')"
1074
+ if ($SkipEaWeb) {
1075
+ Write-Host ' EA web service startup skipped (-SkipEaWeb)'
1076
+ }
1077
+ else {
1078
+ $eaWebRoot = if ($EaWebRoot) { $EaWebRoot } else { $PWD.Path }
1079
+ $eaPortInUse = $false
1080
+ try {
1081
+ $eaProbe = New-Object System.Net.Sockets.TcpClient
1082
+ $eaProbe.Connect('127.0.0.1', $EaWebPort)
1083
+ $eaPortInUse = $true
1084
+ $eaProbe.Close()
1085
+ }
1086
+ catch {
1087
+ $eaPortInUse = $false
1088
+ }
1089
+ if ($eaPortInUse) {
1090
+ Write-Host " EA web service already listening on 127.0.0.1:$EaWebPort; startup skipped (idempotent re-deploy)"
1091
+ }
1092
+ else {
1093
+ $nodeCmd = Get-Command node -ErrorAction SilentlyContinue
1094
+ if (-not $nodeCmd) {
1095
+ throw 'EA web service startup failed: node not found on PATH'
1096
+ }
1097
+ $eaServiceDest = Join-Path $scriptsDest 'ea-web-service.js'
1098
+ $eaLogOut = Join-Path $ArgoRoot 'ea-web-service.log'
1099
+ $eaLogErr = Join-Path $ArgoRoot 'ea-web-service.err.log'
1100
+ # Start the service through WMI (Win32_Process.Create) instead of
1101
+ # Start-Process: the process is then created by the WMI service and
1102
+ # inherits NONE of this installer's stdio pipe handles. Inherited
1103
+ # pipes would keep spawnSync-based launchers (argo-deploy bin, tests)
1104
+ # blocked until the service exits. cmd.exe is used only to redirect
1105
+ # the service stdout/stderr into log files.
1106
+ $q = [char]34
1107
+ $eaCmdLine = "cmd.exe /S /c $q$q$($nodeCmd.Source)$q $q$eaServiceDest$q --root $q$eaWebRoot$q --port $EaWebPort > $q$eaLogOut$q 2> $q$eaLogErr$q$q"
1108
+ $eaCreate = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
1109
+ CommandLine = $eaCmdLine
1110
+ CurrentDirectory = $ArgoRoot
1111
+ }
1112
+ if ($eaCreate.ReturnValue -ne 0) {
1113
+ throw "EA web service startup failed: Win32_Process.Create ReturnValue $($eaCreate.ReturnValue)"
1114
+ }
1115
+ Write-Host " EA web service started in background (pid $($eaCreate.ProcessId)): http://127.0.0.1:$EaWebPort (root: $eaWebRoot)"
1116
+ Write-Host " logs: $eaLogOut / $eaLogErr"
1117
+ }
1118
+ }
1119
+ }
1120
+ else {
1121
+ Write-Host ' EA web service sources not present in this package; step skipped'
1122
+ }
1123
+
1044
1124
  Write-Host ''
1045
1125
  Write-Host 'Argo deployment complete.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.10.51",
3
+ "version": "0.11.0",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -12,9 +12,13 @@
12
12
  "argo/defaults",
13
13
  "argo/agents",
14
14
  "argo/plugins",
15
+ "argo/mcp-bridges",
15
16
  "argo/skills/argo-init",
16
17
  "argo/rules",
17
18
  "argo/package.json",
19
+ "scripts/ea-web-service.js",
20
+ "scripts/ea-layout-store.js",
21
+ "web",
18
22
  "vendor",
19
23
  "install-argo.ps1",
20
24
  "bin",
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 布局侧车(Layout Sidecar,WP2785 M1-S2,2026-09-03 修订:默认按项目隔离,AT-2785-L5)
5
+ * — 图谱画布坐标独立持久化。
6
+ *
7
+ * 零第三方依赖(仅 Node 内置 path/fs/crypto)。与 design/KG/SystemArchitecture.json
8
+ * 完全物理隔离:坐标不进图谱 JSON、不进 schema;本模块永远不读写图谱文件本身。
9
+ *
10
+ * 存储根(按优先级):
11
+ * 1. 显式覆盖:createLayoutStore({ layoutRoot }) 选项或 EA_LAYOUT_ROOT 环境变量
12
+ * → <layoutRoot>/<projectId>/<view_id>.json(测试 / 自定义集中存储根)。
13
+ * 2. 默认(无任何覆盖):按项目隔离
14
+ * → <projectRoot>/design/KG/ea-layouts/<view_id>.json(仍是独立文件,
15
+ * 只落在 ea-layouts/ 子目录,绝不触碰 SystemArchitecture.json 本身)。
16
+ *
17
+ * 文件内容:{ version, graphKey, view_id, signature, updatedAt, elements: { <elementId>: {x,y} } }
18
+ *
19
+ * 失效键 = 视图成员身份签名(成员身份集合的 sha256,只含身份 id,不含任何内容字段):
20
+ * signature = sha256(sorted(included_elements) + sorted(included_relationships))。
21
+ * 图谱内容级修改(元素 name/description/attributes、关系描述等)不改变成员集合
22
+ * → signature 不变 → 坐标完全不动;仅成员结构变化触发新成员补位/移除成员清理。
23
+ *
24
+ * 合并语义(读取时):以当前图谱视图成员为准——仍有坐标的现存成员原样保留;
25
+ * 新成员(无坐标)按确定性算法补位(沿用 buildViewGraph 的圆形布局公式)并写回侧车;
26
+ * 已不在成员中的坐标清理。
27
+ */
28
+
29
+ const path = require('node:path');
30
+ const fs = require('node:fs');
31
+ const crypto = require('node:crypto');
32
+
33
+ const LAYOUT_VERSION = 1;
34
+ // 默认按项目隔离时,侧车相对项目根的存放目录。
35
+ const PROJECT_LAYOUT_DIR = path.join('design', 'KG', 'ea-layouts');
36
+
37
+ /**
38
+ * 解析显式存储根覆盖:选项 > EA_LAYOUT_ROOT 环境变量。
39
+ * 返回 null 表示无覆盖 → 使用默认的按项目隔离存储。
40
+ */
41
+ function resolveLayoutRoot(explicitRoot) {
42
+ if (explicitRoot) {
43
+ return path.resolve(explicitRoot);
44
+ }
45
+ if (process.env.EA_LAYOUT_ROOT) {
46
+ return path.resolve(process.env.EA_LAYOUT_ROOT);
47
+ }
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * 视图成员身份签名:仅由 included_elements / included_relationships 的身份 id 集合决定,
53
+ * 与元素/关系的任何内容字段无关。
54
+ */
55
+ function computeViewSignature(view) {
56
+ const elements = Array.from(new Set((view && view.included_elements) || [])).sort();
57
+ const relationships = Array.from(new Set((view && view.included_relationships) || [])).sort();
58
+ const payload = `${elements.join('\u0001')}\u0000${relationships.join('\u0001')}`;
59
+ return crypto.createHash('sha256').update(payload).digest('hex');
60
+ }
61
+
62
+ // 文件名安全化(服务端路由已限制 [A-Za-z0-9._-],此处为独立模块的防御性兜底)。
63
+ function safeFileName(segment) {
64
+ return String(segment).replace(/[^A-Za-z0-9._-]/g, '_');
65
+ }
66
+
67
+ /**
68
+ * 确定性补位:沿用 buildViewGraph 的圆形布局公式(成员索引 → 圆周位置)。
69
+ */
70
+ function defaultPosition(index, count) {
71
+ const angle = (2 * Math.PI * index) / Math.max(count, 1);
72
+ return {
73
+ x: Math.round(80 + 160 * Math.cos(angle)),
74
+ y: Math.round(80 + 160 * Math.sin(angle)),
75
+ };
76
+ }
77
+
78
+ function isValidPosition(pos) {
79
+ return !!pos
80
+ && typeof pos === 'object'
81
+ && !Array.isArray(pos)
82
+ && Number.isFinite(Number(pos.x))
83
+ && Number.isFinite(Number(pos.y));
84
+ }
85
+
86
+ function createLayoutStore(options = {}) {
87
+ // null → 默认按项目隔离存储;否则为显式集中存储根。
88
+ const explicitRoot = resolveLayoutRoot(options.layoutRoot);
89
+
90
+ /**
91
+ * 目标解析:target = { project?: {id, root, graphPath}, projectId?, graphKey? }。
92
+ * 显式根模式用 projectId 分桶;默认模式用 project.root 定位项目内 ea-layouts/ 目录。
93
+ */
94
+ function resolveTarget(target, viewId) {
95
+ const t = target || {};
96
+ const project = t.project || {};
97
+ const projectId = t.projectId || project.id || null;
98
+ const graphKey = t.graphKey || project.graphPath || null;
99
+ if (explicitRoot) {
100
+ if (!projectId) {
101
+ throw new Error('layout store: 显式存储根模式下必须提供 projectId(或 project.id)');
102
+ }
103
+ return {
104
+ projectId,
105
+ graphKey,
106
+ filePath: path.join(explicitRoot, safeFileName(projectId), `${safeFileName(viewId)}.json`),
107
+ };
108
+ }
109
+ const projectRoot = project.root;
110
+ if (!projectRoot) {
111
+ throw new Error('layout store: 默认按项目隔离存储必须提供 project.root');
112
+ }
113
+ return {
114
+ projectId,
115
+ graphKey,
116
+ filePath: path.join(path.resolve(projectRoot), PROJECT_LAYOUT_DIR, `${safeFileName(viewId)}.json`),
117
+ };
118
+ }
119
+
120
+ function filePathFor(target, viewId) {
121
+ return resolveTarget(target, viewId).filePath;
122
+ }
123
+
124
+ function readRecord(target, viewId) {
125
+ let text;
126
+ try {
127
+ text = fs.readFileSync(filePathFor(target, viewId), 'utf8');
128
+ } catch {
129
+ return null;
130
+ }
131
+ try {
132
+ const record = JSON.parse(text);
133
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
134
+ return null;
135
+ }
136
+ if (!record.elements || typeof record.elements !== 'object' || Array.isArray(record.elements)) {
137
+ record.elements = {};
138
+ }
139
+ return record;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function writeRecord(target, viewId, record) {
146
+ const filePath = filePathFor(target, viewId);
147
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
148
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
149
+ fs.writeFileSync(tempPath, JSON.stringify(record, null, 2), 'utf8');
150
+ fs.renameSync(tempPath, filePath);
151
+ return filePath;
152
+ }
153
+
154
+ /**
155
+ * 读取合并后的布局(以当前图谱视图成员为准)。
156
+ * 有补位/清理/签名变化时写回侧车;纯内容级修改(签名不变)不触发写入。
157
+ * 入参:{ project?: {id, root, graphPath}, projectId?, graphKey?, view }。
158
+ * 返回 { signature, elements: { <elementId>: {x,y} } }。
159
+ */
160
+ function mergeLayout({ project, projectId, graphKey, view }) {
161
+ if (!view || typeof view.view_id !== 'string') {
162
+ throw new Error('mergeLayout: view with view_id is required');
163
+ }
164
+ const target = { project, projectId, graphKey };
165
+ const viewId = view.view_id;
166
+ const signature = computeViewSignature(view);
167
+ const record = readRecord(target, viewId);
168
+ const saved = record ? record.elements : {};
169
+ const members = Array.isArray(view.included_elements) ? view.included_elements : [];
170
+ const elements = {};
171
+ let dirty = !record || record.signature !== signature;
172
+
173
+ members.forEach((id, index) => {
174
+ const pos = saved[id];
175
+ if (isValidPosition(pos)) {
176
+ elements[id] = { x: Number(pos.x), y: Number(pos.y) };
177
+ } else {
178
+ elements[id] = defaultPosition(index, members.length);
179
+ dirty = true;
180
+ }
181
+ });
182
+ for (const id of Object.keys(saved)) {
183
+ if (!members.includes(id)) {
184
+ dirty = true; // 已不在成员中的坐标:清理
185
+ }
186
+ }
187
+ if (dirty) {
188
+ writeRecord(target, viewId, {
189
+ version: LAYOUT_VERSION,
190
+ graphKey: resolveTarget(target, viewId).graphKey,
191
+ view_id: viewId,
192
+ signature,
193
+ updatedAt: new Date().toISOString(),
194
+ elements,
195
+ });
196
+ }
197
+ return { signature, elements };
198
+ }
199
+
200
+ /**
201
+ * 全量写入布局(按当前文档计算签名后原子写入侧车文件)。
202
+ * 入参:{ project?: {id, root, graphPath}, projectId?, graphKey?, view, elements };
203
+ * elements: { <elementId>: {x,y} };非法坐标抛错(由服务端映射为 400)。
204
+ */
205
+ function putLayout({ project, projectId, graphKey, view, elements }) {
206
+ if (!view || typeof view.view_id !== 'string') {
207
+ throw new Error('putLayout: view with view_id is required');
208
+ }
209
+ if (!elements || typeof elements !== 'object' || Array.isArray(elements)) {
210
+ throw new Error('body.elements 必须是 { <elementId>: {x,y} } 对象');
211
+ }
212
+ const normalized = {};
213
+ for (const [id, pos] of Object.entries(elements)) {
214
+ if (!isValidPosition(pos)) {
215
+ throw new Error(`elements['${id}'] 的坐标必须是有限的 {x,y} 数字`);
216
+ }
217
+ normalized[id] = { x: Number(pos.x), y: Number(pos.y) };
218
+ }
219
+ const target = { project, projectId, graphKey };
220
+ const signature = computeViewSignature(view);
221
+ writeRecord(target, view.view_id, {
222
+ version: LAYOUT_VERSION,
223
+ graphKey: resolveTarget(target, view.view_id).graphKey,
224
+ view_id: view.view_id,
225
+ signature,
226
+ updatedAt: new Date().toISOString(),
227
+ elements: normalized,
228
+ });
229
+ return { signature, view_id: view.view_id };
230
+ }
231
+
232
+ return { root: explicitRoot, filePathFor, readRecord, writeRecord, mergeLayout, putLayout };
233
+ }
234
+
235
+ module.exports = {
236
+ LAYOUT_VERSION,
237
+ PROJECT_LAYOUT_DIR,
238
+ resolveLayoutRoot,
239
+ computeViewSignature,
240
+ defaultPosition,
241
+ createLayoutStore,
242
+ };