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 +30 -2
- package/argo/defaults/EA-model-template.feap +0 -0
- package/argo/mcp-bridges/graph-mcp-stdio.js +98 -0
- package/install-argo.ps1 +107 -27
- package/package.json +5 -1
- package/scripts/ea-layout-store.js +242 -0
- package/scripts/ea-web-service.js +1349 -0
- package/web/app.js +398 -0
- package/web/index.html +86 -0
- package/web/style.css +251 -0
- package/web/vendor/g6.min.js +68 -0
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
|
-
|
|
47
|
-
|
|
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)
|
|
Binary file
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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>):
|
|
977
|
-
#
|
|
978
|
-
#
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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
|
+
};
|