insightfactory-cli 1.0.3.dev17__py3-none-any.whl → 1.0.3.dev20__py3-none-any.whl
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.
- if_cli/assets/__init__.py +1 -0
- if_cli/assets/insightfactoryai-logo.svg +55 -0
- if_cli/callback_page.py +215 -0
- if_cli/cli.py +6 -0
- if_cli/commands/mcp.py +164 -0
- if_cli/commands/profiles.py +3 -1
- if_cli/config.py +86 -0
- if_cli/main.py +10 -0
- if_cli/oauth.py +17 -9
- if_cli/router/__init__.py +0 -0
- if_cli/router/catalog.py +132 -0
- if_cli/router/policy.py +86 -0
- if_cli/router/server.py +285 -0
- if_cli/router/upstream.py +107 -0
- {insightfactory_cli-1.0.3.dev17.dist-info → insightfactory_cli-1.0.3.dev20.dist-info}/METADATA +89 -2
- insightfactory_cli-1.0.3.dev20.dist-info/RECORD +33 -0
- insightfactory_cli-1.0.3.dev17.dist-info/RECORD +0 -24
- {insightfactory_cli-1.0.3.dev17.dist-info → insightfactory_cli-1.0.3.dev20.dist-info}/WHEEL +0 -0
- {insightfactory_cli-1.0.3.dev17.dist-info → insightfactory_cli-1.0.3.dev20.dist-info}/entry_points.txt +0 -0
- {insightfactory_cli-1.0.3.dev17.dist-info → insightfactory_cli-1.0.3.dev20.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Static files shipped with the CLI."""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<svg class="logo" role="img" aria-label="insightfactory.ai" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 286.67 62.53">
|
|
2
|
+
<defs>
|
|
3
|
+
<style>
|
|
4
|
+
.iflogo-1 {
|
|
5
|
+
fill: #fab314;
|
|
6
|
+
}
|
|
7
|
+
.iflogo-2 {
|
|
8
|
+
fill: #f8e86c;
|
|
9
|
+
}
|
|
10
|
+
.iflogo-3 {
|
|
11
|
+
fill: #fad121;
|
|
12
|
+
}
|
|
13
|
+
.iflogo-4 {
|
|
14
|
+
fill: #1f252e;
|
|
15
|
+
}
|
|
16
|
+
</style>
|
|
17
|
+
</defs>
|
|
18
|
+
<g id="ARTWORK">
|
|
19
|
+
<g>
|
|
20
|
+
<g>
|
|
21
|
+
<polygon class="iflogo-4" points="32.49 37.52 32.5 37.53 32.49 37.52 32.49 37.52"/>
|
|
22
|
+
<polygon class="iflogo-4" points="21.66 18.76 0 31.27 21.66 43.78 21.66 18.76"/>
|
|
23
|
+
<g>
|
|
24
|
+
<polygon class="iflogo-4" points="43.32 31.27 43.32 43.78 54.15 37.52 43.32 31.27"/>
|
|
25
|
+
<polygon class="iflogo-4" points="43.32 43.78 43.32 56.28 54.15 62.53 64.98 56.28 43.32 43.78"/>
|
|
26
|
+
</g>
|
|
27
|
+
<polygon class="iflogo-2" points="21.66 6.26 32.49 12.51 43.32 6.26 32.48 0 21.66 6.26"/>
|
|
28
|
+
<polygon class="iflogo-3" points="32.49 12.51 32.49 12.51 21.66 6.26 21.66 18.76 21.66 18.76 21.66 43.78 32.49 50.03 32.49 37.52 32.49 12.51"/>
|
|
29
|
+
<g>
|
|
30
|
+
<polygon class="iflogo-1" points="32.49 12.51 32.5 25.02 43.32 18.76 43.32 6.26 32.49 12.51"/>
|
|
31
|
+
<polygon class="iflogo-1" points="43.32 56.28 43.32 43.78 43.32 31.27 32.49 37.52 32.49 50.03 43.32 56.28"/>
|
|
32
|
+
</g>
|
|
33
|
+
</g>
|
|
34
|
+
<g>
|
|
35
|
+
<path class="iflogo-4" d="m75.28,23.74c-.81,0-1.47-.27-1.99-.8-.52-.53-.77-1.17-.77-1.91s.26-1.41.77-1.93,1.18-.78,1.99-.78c.76,0,1.41.26,1.94.78.53.52.79,1.17.79,1.93s-.26,1.38-.79,1.91c-.53.53-1.17.8-1.94.8Zm-2.19,2.23h4.31v17.39h-4.31v-17.39Z"/>
|
|
36
|
+
<path class="iflogo-4" d="m79.46,25.97h3.91l.3,1.77c.54-.63,1.2-1.12,1.97-1.48.78-.36,1.61-.54,2.51-.54,1.33,0,2.46.29,3.39.87.93.58,1.65,1.42,2.16,2.52.51,1.1.76,2.46.76,4.09v10.16h-4.35v-9.57c0-1.34-.26-2.35-.78-3.03s-1.29-1.01-2.33-1.01-1.86.34-2.41,1.03c-.55.68-.83,1.7-.83,3.04v9.53h-4.31v-17.39Z"/>
|
|
37
|
+
<path class="iflogo-4" d="m100.08,37.69c.13.58.36,1.02.69,1.32.33.3.7.52,1.11.64.42.13.84.19,1.26.19.79,0,1.4-.16,1.85-.47.45-.31.67-.71.67-1.2,0-.42-.16-.75-.47-.99-.31-.24-.71-.43-1.2-.57-.48-.14-.99-.27-1.53-.38-.61-.16-1.25-.34-1.94-.54-.69-.2-1.33-.48-1.94-.85-.61-.37-1.11-.85-1.5-1.44-.39-.59-.59-1.34-.59-2.24,0-1.09.28-2.03.84-2.83.56-.8,1.35-1.43,2.38-1.9s2.21-.7,3.56-.7c1.59,0,2.94.35,4.04,1.04,1.1.7,1.9,1.69,2.39,2.99l-3.78,1.11c-.13-.32-.34-.59-.61-.8s-.59-.36-.96-.45c-.37-.09-.75-.14-1.13-.14-.65,0-1.19.13-1.62.4-.43.27-.64.63-.64,1.1,0,.28.08.5.24.68s.38.32.66.43c.28.12.61.22.98.31.37.09.78.2,1.23.31.7.19,1.4.4,2.11.64.71.24,1.36.55,1.96.92.59.37,1.07.87,1.43,1.5.36.63.56,1.42.61,2.37,0,1.02-.28,1.94-.84,2.77s-1.37,1.48-2.43,1.96c-1.06.49-2.34.73-3.84.73-1.75,0-3.24-.38-4.47-1.15s-2.03-1.96-2.41-3.58l3.88-1.18Z"/>
|
|
38
|
+
<path class="iflogo-4" d="m114.61,23.74c-.81,0-1.47-.27-1.99-.8-.52-.53-.77-1.17-.77-1.91s.26-1.41.77-1.93,1.18-.78,1.99-.78c.76,0,1.41.26,1.94.78.53.52.79,1.17.79,1.93s-.26,1.38-.79,1.91c-.53.53-1.17.8-1.94.8Zm-2.19,2.23h4.31v17.39h-4.31v-17.39Z"/>
|
|
39
|
+
<path class="iflogo-4" d="m131.9,25.97h3.91v14.82c0,1.97-.47,3.67-1.42,5.1-.94,1.43-2.31,2.52-4.09,3.29-1.79.76-3.93,1.15-6.42,1.15l-.67-3.76c2.72-.05,4.78-.54,6.18-1.5s2.11-2.32,2.11-4.1v-.07l.57-.21c-.54.81-1.28,1.5-2.22,2.07s-2.07.85-3.37.85c-1.48,0-2.79-.37-3.91-1.11-1.12-.74-2.01-1.78-2.65-3.11-.64-1.33-.96-2.91-.96-4.71s.32-3.39.96-4.73c.64-1.34,1.52-2.38,2.65-3.11,1.12-.73,2.43-1.1,3.91-1.1.76,0,1.48.11,2.14.33.66.22,1.25.51,1.75.87.5.36.93.76,1.26,1.2l.27-2.16Zm-8.32,8.7c0,1.46.37,2.64,1.09,3.53.73.89,1.7,1.34,2.92,1.34.7,0,1.34-.19,1.92-.57.58-.38,1.06-.94,1.43-1.69.37-.74.56-1.61.56-2.61s-.19-1.92-.56-2.64c-.37-.72-.85-1.27-1.45-1.65-.59-.38-1.23-.57-1.9-.57-1.21,0-2.19.45-2.92,1.34-.73.89-1.09,2.07-1.09,3.53Z"/>
|
|
40
|
+
<path class="iflogo-4" d="m138.5,18.32h4.31v9.36c.52-.58,1.16-1.05,1.94-1.41.78-.36,1.65-.54,2.61-.54,1.28,0,2.39.29,3.34.87.94.58,1.68,1.42,2.21,2.52.53,1.1.79,2.46.79,4.09v10.16h-4.55v-9.57c0-1.34-.26-2.35-.78-3.03s-1.29-1.01-2.33-1.01-1.86.37-2.41,1.1c-.55.73-.83,1.77-.83,3.11v9.39h-4.31v-25.04Z"/>
|
|
41
|
+
<path class="iflogo-4" d="m157.01,25.97v-4.87h4.35v4.87h3.74v3.93h-3.74v7.06c0,.9.19,1.54.57,1.91.38.37.86.57,1.43.59.57.02,1.15-.01,1.74-.1v3.83c-1.03.23-2.04.3-3.02.21-.98-.09-1.85-.37-2.61-.82-.76-.45-1.36-1.14-1.8-2.07-.44-.93-.66-2.11-.66-3.55v-7.06h-2.39v-3.93h2.39Z"/>
|
|
42
|
+
<path class="iflogo-4" d="m170.42,23.92c0-1.27.2-2.35.61-3.23s1.05-1.52,1.94-1.93c.89-.41,2.07-.55,3.56-.43v2.33c-1.35-.12-2.28.11-2.8.68s-.78,1.43-.78,2.59v2.05h3.71v2.43h-3.71v14.96h-2.53v-14.96h-2.36v-2.43h2.36v-2.05Z"/>
|
|
43
|
+
<path class="iflogo-4" d="m188.25,43.36l-.2-3.13c-.47,1.07-1.17,1.9-2.09,2.49-.92.59-2.09.89-3.5.89-1.21,0-2.25-.2-3.1-.59-.85-.39-1.51-.96-1.97-1.69-.46-.73-.69-1.59-.69-2.59,0-1.46.53-2.66,1.58-3.6,1.06-.94,2.58-1.51,4.58-1.72l5.19-.59v-1.39c0-.97-.35-1.76-1.04-2.35s-1.64-.89-2.83-.89-2.14.28-2.92.83c-.78.56-1.31,1.36-1.6,2.4l-2.29-.83c.45-1.53,1.28-2.72,2.48-3.58,1.2-.86,2.67-1.29,4.4-1.29,2,0,3.56.53,4.67,1.58,1.11,1.06,1.67,2.49,1.67,4.3v11.76h-2.33Zm-.2-8.31l-5.22.63c-1.12.14-1.98.46-2.56.96-.58.5-.88,1.15-.88,1.96s.29,1.46.86,1.95c.57.49,1.4.73,2.48.73,1.24,0,2.25-.25,3.03-.77.79-.51,1.37-1.2,1.74-2.07.37-.87.56-1.81.56-2.83v-.56Z"/>
|
|
44
|
+
<path class="iflogo-4" d="m201.02,43.61c-1.64,0-3.08-.37-4.31-1.11-1.24-.74-2.2-1.78-2.88-3.11-.69-1.33-1.03-2.88-1.03-4.64s.34-3.36,1.03-4.71,1.65-2.41,2.88-3.17c1.24-.75,2.67-1.13,4.31-1.13,1.73,0,3.25.41,4.55,1.23,1.3.82,2.25,1.97,2.83,3.43l-2.33.83c-.34-.93-.95-1.66-1.84-2.21-.89-.54-1.92-.82-3.08-.82s-2.14.27-2.98.8-1.49,1.29-1.96,2.26c-.46.97-.69,2.12-.69,3.44,0,1.95.51,3.51,1.52,4.68s2.36,1.76,4.04,1.76c1.15,0,2.17-.27,3.07-.8.9-.53,1.56-1.25,1.99-2.16l2.29.83c-.61,1.44-1.56,2.56-2.86,3.38-1.3.81-2.82,1.22-4.55,1.22Z"/>
|
|
45
|
+
<path class="iflogo-4" d="m212.07,25.97v-4.87h2.53v4.87h4.48v2.43h-4.48v9.36c0,1.07.22,1.84.66,2.33.44.49,1,.78,1.68.87.69.09,1.4.09,2.14,0v2.26c-.88.21-1.74.27-2.58.17-.84-.09-1.6-.35-2.26-.78-.66-.43-1.19-1.04-1.58-1.83-.39-.79-.59-1.8-.59-3.03v-9.36h-2.53v-2.43h2.53Z"/>
|
|
46
|
+
<path class="iflogo-4" d="m228.49,43.61c-1.6,0-3.02-.36-4.28-1.08-1.26-.72-2.25-1.74-2.97-3.08-.72-1.33-1.08-2.92-1.08-4.75s.37-3.46,1.1-4.8c.73-1.34,1.73-2.38,3-3.1,1.27-.72,2.7-1.08,4.3-1.08s3.03.36,4.3,1.08c1.27.72,2.27,1.75,3,3.08.73,1.33,1.1,2.92,1.1,4.75s-.37,3.46-1.11,4.8-1.75,2.38-3.03,3.1-2.72,1.08-4.31,1.08Zm-.03-2.47c.99,0,1.92-.23,2.8-.68.88-.45,1.59-1.16,2.14-2.12.55-.96.83-2.19.83-3.67s-.27-2.74-.81-3.69c-.54-.95-1.24-1.65-2.11-2.1s-1.79-.68-2.78-.68-1.91.23-2.76.68-1.54,1.16-2.07,2.12c-.53.96-.79,2.19-.79,3.67s.26,2.74.78,3.69c.52.95,1.2,1.65,2.04,2.1.84.45,1.76.68,2.75.68Z"/>
|
|
47
|
+
<path class="iflogo-4" d="m238.93,25.97h2.33l.13,2.54c.45-.63.99-1.16,1.63-1.6.64-.44,1.35-.75,2.12-.92.77-.17,1.6-.18,2.48-.02v2.61c-.94-.18-1.79-.18-2.54,0-.75.19-1.4.53-1.94,1.04-.54.51-.96,1.15-1.25,1.93-.29.78-.44,1.68-.44,2.7v9.11h-2.53v-17.39Z"/>
|
|
48
|
+
<path class="iflogo-4" d="m262.19,25.97h2.63l-9.64,24.35h-2.43l2.87-7.2-6.81-17.15h2.76l5.39,14.05,5.22-14.05Z"/>
|
|
49
|
+
<path class="iflogo-4" d="m263.09,43.52c-.46,0-.84-.16-1.15-.5-.31-.33-.46-.73-.46-1.21s.15-.88.46-1.21c.31-.33.69-.5,1.15-.5s.82.17,1.14.5c.32.33.48.73.48,1.21s-.16.88-.48,1.21c-.32.33-.7.5-1.14.5Z"/>
|
|
50
|
+
<path class="iflogo-4" d="m278.25,43.36l-.2-3.13c-.47,1.07-1.17,1.9-2.09,2.49-.92.59-2.09.89-3.5.89-1.21,0-2.25-.2-3.1-.59-.85-.39-1.51-.96-1.97-1.69-.46-.73-.69-1.59-.69-2.59,0-1.46.53-2.66,1.58-3.6,1.06-.94,2.58-1.51,4.58-1.72l5.19-.59v-1.39c0-.97-.35-1.76-1.04-2.35s-1.64-.89-2.83-.89-2.14.28-2.92.83c-.78.56-1.31,1.36-1.6,2.4l-2.29-.83c.45-1.53,1.28-2.72,2.48-3.58,1.2-.86,2.67-1.29,4.4-1.29,2,0,3.56.53,4.67,1.58,1.11,1.06,1.67,2.49,1.67,4.3v11.76h-2.33Zm-.2-8.31l-5.22.63c-1.12.14-1.98.46-2.56.96-.58.5-.88,1.15-.88,1.96s.29,1.46.86,1.95c.57.49,1.4.73,2.48.73,1.24,0,2.25-.25,3.03-.77.79-.51,1.37-1.2,1.74-2.07.37-.87.56-1.81.56-2.83v-.56Z"/>
|
|
51
|
+
<path class="iflogo-4" d="m284.92,22.49c-.52,0-.94-.17-1.28-.52-.34-.35-.51-.78-.51-1.29s.17-.97.51-1.32.76-.52,1.28-.52c.47,0,.88.17,1.23.52.35.35.52.79.52,1.32s-.17.94-.52,1.29c-.35.35-.76.52-1.23.52Zm-1.28,3.48h2.49v17.39h-2.49v-17.39Z"/>
|
|
52
|
+
</g>
|
|
53
|
+
</g>
|
|
54
|
+
</g>
|
|
55
|
+
</svg>
|
if_cli/callback_page.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Branded HTML for the loopback OAuth callback tab.
|
|
2
|
+
|
|
3
|
+
This tab is the frame straight after the Auth0 Universal Login widget, so it
|
|
4
|
+
copies that theme rather than the Interfaces app. Every value here mirrors
|
|
5
|
+
``auth0_branding_theme.insight_factory`` in
|
|
6
|
+
``terraform-Insightfactory-infrastructure/Auth0/modules/auth0/branding.tf``, and
|
|
7
|
+
the comments name the Terraform attribute each one came from. Change them together.
|
|
8
|
+
|
|
9
|
+
The page is self-contained: the logo is inline SVG and the CSS is inline, so it
|
|
10
|
+
renders from the loopback server with no network access beyond the webfont.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from functools import lru_cache
|
|
16
|
+
from importlib.resources import files
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache(maxsize=1)
|
|
20
|
+
def logo_svg() -> str:
|
|
21
|
+
"""The wordmark Auth0 serves from the CDN, shipped with the package.
|
|
22
|
+
|
|
23
|
+
Auth0 loads it from cdn.insightfactory.ai. We inline the same file so the
|
|
24
|
+
callback page does not depend on the CDN being reachable.
|
|
25
|
+
"""
|
|
26
|
+
return files("if_cli.assets").joinpath("insightfactoryai-logo.svg").read_text(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
STYLES = """\
|
|
30
|
+
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap");
|
|
31
|
+
|
|
32
|
+
:root {
|
|
33
|
+
--page-background: #20252e; /* page_background.background_color */
|
|
34
|
+
--widget-background: #ffffff; /* colors.widget_background */
|
|
35
|
+
--header: #20252e; /* colors.header */
|
|
36
|
+
--body-text: #20252e; /* colors.body_text */
|
|
37
|
+
--icons: #65676e; /* colors.icons */
|
|
38
|
+
--input-border: #c9cace; /* colors.input_border */
|
|
39
|
+
--success: #13a688; /* colors.success */
|
|
40
|
+
--error: #d03c38; /* colors.error */
|
|
41
|
+
--success-soft: rgba(19, 166, 136, 0.12);
|
|
42
|
+
--error-soft: rgba(208, 60, 56, 0.12);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
* {
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
body {
|
|
50
|
+
margin: 0;
|
|
51
|
+
min-height: 100vh;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center; /* page_layout: center */
|
|
54
|
+
justify-content: center;
|
|
55
|
+
padding: 24px;
|
|
56
|
+
background: var(--page-background);
|
|
57
|
+
color: var(--body-text);
|
|
58
|
+
font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
59
|
+
font-size: 16px; /* fonts.reference_text_size */
|
|
60
|
+
font-weight: 300;
|
|
61
|
+
line-height: 1.5;
|
|
62
|
+
-webkit-font-smoothing: antialiased;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
main {
|
|
66
|
+
width: 100%;
|
|
67
|
+
max-width: 400px;
|
|
68
|
+
padding: 40px 32px 32px;
|
|
69
|
+
border-radius: 8px; /* borders.widget_corner_radius */
|
|
70
|
+
background: var(--widget-background);
|
|
71
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28); /* borders.show_widget_shadow */
|
|
72
|
+
text-align: center; /* widget.header_text_alignment */
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.logo {
|
|
76
|
+
height: 68px; /* widget.logo_height */
|
|
77
|
+
width: auto;
|
|
78
|
+
max-width: 100%;
|
|
79
|
+
display: block;
|
|
80
|
+
margin: 0 auto 24px; /* widget.logo_position: center */
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.pill {
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 7px;
|
|
87
|
+
margin-bottom: 16px;
|
|
88
|
+
padding: 4px 12px 4px 10px;
|
|
89
|
+
border-radius: 999px;
|
|
90
|
+
background: var(--pill-soft);
|
|
91
|
+
color: var(--pill);
|
|
92
|
+
font-size: 14px;
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.dot {
|
|
97
|
+
width: 7px;
|
|
98
|
+
height: 7px;
|
|
99
|
+
border-radius: 50%;
|
|
100
|
+
background: currentColor;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
h1 {
|
|
104
|
+
margin: 0 0 10px;
|
|
105
|
+
color: var(--header); /* colors.header */
|
|
106
|
+
font-size: 24px; /* fonts.title: 150% of 16px */
|
|
107
|
+
font-weight: 300; /* fonts.title.bold = false */
|
|
108
|
+
line-height: 1.25;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
p {
|
|
112
|
+
margin: 0;
|
|
113
|
+
color: var(--icons);
|
|
114
|
+
font-size: 14px; /* fonts.subtitle: 87.5% of 16px */
|
|
115
|
+
font-weight: 400;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
code {
|
|
119
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
120
|
+
font-size: 13px;
|
|
121
|
+
color: var(--body-text);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pre {
|
|
125
|
+
margin: 20px 0 0;
|
|
126
|
+
padding: 12px 14px;
|
|
127
|
+
border: 1px solid var(--input-border); /* colors.input_border */
|
|
128
|
+
border-radius: 3px; /* borders.input_border_radius */
|
|
129
|
+
color: var(--body-text);
|
|
130
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
131
|
+
font-size: 13px;
|
|
132
|
+
text-align: left;
|
|
133
|
+
white-space: pre-wrap;
|
|
134
|
+
word-break: break-word;
|
|
135
|
+
}
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
_TEMPLATE = """\
|
|
139
|
+
<!DOCTYPE html>
|
|
140
|
+
<html lang="en">
|
|
141
|
+
<head>
|
|
142
|
+
<meta charset="utf-8">
|
|
143
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
144
|
+
<title>{title} | insightfactory.ai</title>
|
|
145
|
+
<style>
|
|
146
|
+
{styles}
|
|
147
|
+
</style>
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
<main style="--pill: var({pill_colour}); --pill-soft: var({pill_soft});">
|
|
151
|
+
{logo}
|
|
152
|
+
<span class="pill"><span class="dot"></span>{pill}</span>
|
|
153
|
+
<h1>{heading}</h1>
|
|
154
|
+
<p>{message}</p>
|
|
155
|
+
{detail}
|
|
156
|
+
</main>
|
|
157
|
+
</body>
|
|
158
|
+
</html>
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def render(
|
|
163
|
+
*,
|
|
164
|
+
title: str,
|
|
165
|
+
pill: str,
|
|
166
|
+
heading: str,
|
|
167
|
+
message: str,
|
|
168
|
+
detail: str = "",
|
|
169
|
+
ok: bool = True,
|
|
170
|
+
) -> bytes:
|
|
171
|
+
"""Render one callback page, ready to write to the socket."""
|
|
172
|
+
return _TEMPLATE.format(
|
|
173
|
+
title=title,
|
|
174
|
+
styles=STYLES,
|
|
175
|
+
logo=logo_svg(),
|
|
176
|
+
pill=pill,
|
|
177
|
+
pill_colour="--success" if ok else "--error",
|
|
178
|
+
pill_soft="--success-soft" if ok else "--error-soft",
|
|
179
|
+
heading=heading,
|
|
180
|
+
message=message,
|
|
181
|
+
detail=detail,
|
|
182
|
+
).encode("utf-8")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def success_page() -> bytes:
|
|
186
|
+
return render(
|
|
187
|
+
title="Login complete",
|
|
188
|
+
pill="Connected",
|
|
189
|
+
heading="Authentication successful",
|
|
190
|
+
message="You can close this tab and return to the terminal.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def failure_page(error: str, description: str) -> bytes:
|
|
195
|
+
return render(
|
|
196
|
+
title="Login failed",
|
|
197
|
+
pill="Not connected",
|
|
198
|
+
heading="Authentication failed",
|
|
199
|
+
message="Close this tab and run <code>if-cli login</code> again.",
|
|
200
|
+
detail=f"<pre>{error}: {description}</pre>" if error or description else "",
|
|
201
|
+
ok=False,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def state_mismatch_page() -> bytes:
|
|
206
|
+
return render(
|
|
207
|
+
title="Login callback ignored",
|
|
208
|
+
pill="Not connected",
|
|
209
|
+
heading="Callback ignored",
|
|
210
|
+
message=(
|
|
211
|
+
"The callback state did not match the one this login started with, "
|
|
212
|
+
"so it was discarded. Close this tab and run <code>if-cli login</code> again."
|
|
213
|
+
),
|
|
214
|
+
ok=False,
|
|
215
|
+
)
|
if_cli/cli.py
CHANGED
|
@@ -53,6 +53,8 @@ def _parse_args_with_positionals(argv: list[str], *, options: Options) -> tuple[
|
|
|
53
53
|
"""Parse argv allowing options and positionals in any order (Node util.parseArgs parity)."""
|
|
54
54
|
values: dict[str, Any] = {}
|
|
55
55
|
for name, spec in options.items():
|
|
56
|
+
if spec.get("repeat"):
|
|
57
|
+
die(f"internal error: option '--{name}' has repeat=True, which allow_positionals parsing does not support")
|
|
56
58
|
if spec["type"] == "boolean":
|
|
57
59
|
values[name] = spec.get("default", False)
|
|
58
60
|
else:
|
|
@@ -136,6 +138,10 @@ def parse_args(
|
|
|
136
138
|
dest = name.replace("-", "_")
|
|
137
139
|
if spec["type"] == "boolean":
|
|
138
140
|
parser.add_argument(*flags, dest=dest, action="store_true", default=spec.get("default", False))
|
|
141
|
+
elif spec.get("repeat"):
|
|
142
|
+
# `action="append"` with `default=None` (rather than a spec-level `[]`) so
|
|
143
|
+
# unset stays None and a shared mutable default can't leak across calls.
|
|
144
|
+
parser.add_argument(*flags, dest=dest, action="append", default=None)
|
|
139
145
|
else:
|
|
140
146
|
parser.add_argument(*flags, dest=dest, default=spec.get("default"))
|
|
141
147
|
namespace = parser.parse_args(argv)
|
if_cli/commands/mcp.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from if_cli.cli import Options, parse_args
|
|
7
|
+
from if_cli.config import config_file, get_profile, list_factories, load_config, parse_factory_section
|
|
8
|
+
from if_cli.http import parse_timeout_seconds
|
|
9
|
+
from if_cli.runtime import die
|
|
10
|
+
|
|
11
|
+
MCP_OPTIONS: Options = {
|
|
12
|
+
"env": {"type": "string", "repeat": True},
|
|
13
|
+
"writable": {"type": "string", "repeat": True},
|
|
14
|
+
"read-only": {"type": "boolean"},
|
|
15
|
+
"catalog": {"type": "string"},
|
|
16
|
+
"allow-tool": {"type": "string", "repeat": True},
|
|
17
|
+
"timeout": {"type": "string", "default": "120"},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
MCP_USAGE = (
|
|
21
|
+
"usage: if-cli mcp [FACTORY] [--env CODE=PROFILE ...]\n"
|
|
22
|
+
" [--writable CODE ... | --read-only] [--catalog CODE]\n"
|
|
23
|
+
" [--allow-tool NAME ...] [--timeout SECONDS]\n"
|
|
24
|
+
"\n"
|
|
25
|
+
"Runs one MCP server over stdio that fronts several factory environments: every\n"
|
|
26
|
+
"tool gets a required 'environment' argument, routed to that environment's own\n"
|
|
27
|
+
"if-cli profile. FACTORY names a [factory NAME] section in the config file,\n"
|
|
28
|
+
"supplying its environments, --writable and --catalog; flags override the\n"
|
|
29
|
+
"section. Without FACTORY, --env is required at least once. --catalog defaults\n"
|
|
30
|
+
"to the first environment; environments not passed to --writable are read-only;\n"
|
|
31
|
+
"--read-only forces every environment read-only and cannot be combined with\n"
|
|
32
|
+
"--writable.\n"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
MISSING_MCP_EXTRA = (
|
|
36
|
+
"if-cli mcp needs the mcp extra — install with: uv tool install 'insightfactory-cli[mcp]'\n"
|
|
37
|
+
"(or: uvx --from 'insightfactory-cli[mcp]' if-cli mcp ...)"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_env_mapping(raw: str) -> tuple[str, str]:
|
|
42
|
+
code, separator, profile_name = raw.partition("=")
|
|
43
|
+
if not separator or not code or not profile_name:
|
|
44
|
+
die(f"--env must be CODE=PROFILE (received '{raw}')\n\n{MCP_USAGE}")
|
|
45
|
+
return code, profile_name
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _stray_positional(rest: list[str]) -> str | None:
|
|
49
|
+
"""Find a bare (non-flag) token in `rest`, skipping tokens consumed as a
|
|
50
|
+
preceding flag's value. `rest` is `argv` with a leading FACTORY already
|
|
51
|
+
peeled off, so any bare token found here is a second, misplaced one."""
|
|
52
|
+
index = 0
|
|
53
|
+
while index < len(rest):
|
|
54
|
+
token = rest[index]
|
|
55
|
+
if token == "--":
|
|
56
|
+
return rest[index + 1] if index + 1 < len(rest) else None
|
|
57
|
+
if token.startswith("--"):
|
|
58
|
+
name, has_inline_value, _value = token[2:].partition("=")
|
|
59
|
+
spec = MCP_OPTIONS.get(name)
|
|
60
|
+
if spec is not None and spec["type"] != "boolean" and not has_inline_value:
|
|
61
|
+
index += 2
|
|
62
|
+
continue
|
|
63
|
+
index += 1
|
|
64
|
+
continue
|
|
65
|
+
if token.startswith("-") and len(token) > 1:
|
|
66
|
+
index += 1
|
|
67
|
+
continue
|
|
68
|
+
return token
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def mcp_command(argv: list[str]) -> None:
|
|
73
|
+
if "-h" in argv or "--help" in argv:
|
|
74
|
+
sys.stdout.write(MCP_USAGE)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
factory_name: str | None = None
|
|
78
|
+
rest = argv
|
|
79
|
+
if argv and not argv[0].startswith("-"):
|
|
80
|
+
factory_name, *rest = argv
|
|
81
|
+
|
|
82
|
+
stray = _stray_positional(rest)
|
|
83
|
+
if stray is not None:
|
|
84
|
+
die(f"FACTORY must come before the flags: if-cli mcp {stray} --env ...\n\n{MCP_USAGE}")
|
|
85
|
+
|
|
86
|
+
values, _positionals = parse_args(rest, options=MCP_OPTIONS)
|
|
87
|
+
raw_envs: list[str] = values["env"] or []
|
|
88
|
+
raw_writable: list[str] | None = values["writable"]
|
|
89
|
+
read_only: bool = values["read-only"]
|
|
90
|
+
|
|
91
|
+
if read_only and raw_writable is not None:
|
|
92
|
+
die(f"--read-only cannot be combined with --writable\n\n{MCP_USAGE}")
|
|
93
|
+
|
|
94
|
+
config = load_config()
|
|
95
|
+
|
|
96
|
+
# A dict, not a list of pairs: --env "adds or replaces that code's profile"
|
|
97
|
+
# (a factory's own environments first, then each --env in turn), so a
|
|
98
|
+
# repeated code is the way to override one, not a mistake to reject.
|
|
99
|
+
# Overriding an existing code keeps its original position (a dict update,
|
|
100
|
+
# not a delete-and-reinsert), which is what keeps the --catalog default
|
|
101
|
+
# (codes[0]) stable under a partial override.
|
|
102
|
+
mappings: dict[str, str] = {}
|
|
103
|
+
writable_codes: frozenset[str] = frozenset()
|
|
104
|
+
catalog_source: str | None = None
|
|
105
|
+
|
|
106
|
+
if factory_name is not None:
|
|
107
|
+
factory = parse_factory_section(config, factory_name)
|
|
108
|
+
mappings.update(factory["environments"])
|
|
109
|
+
writable_codes = factory["writable"]
|
|
110
|
+
catalog_source = factory["catalog"]
|
|
111
|
+
|
|
112
|
+
for raw in raw_envs:
|
|
113
|
+
code, profile_name = _parse_env_mapping(raw)
|
|
114
|
+
mappings[code] = profile_name
|
|
115
|
+
|
|
116
|
+
if not mappings:
|
|
117
|
+
factories = sorted(list_factories(config))
|
|
118
|
+
hint = f" or name one of the factories in {config_file()}: {', '.join(factories)}" if factories else ""
|
|
119
|
+
die(f"--env is required (at least one){hint}\n\n{MCP_USAGE}")
|
|
120
|
+
codes = list(mappings)
|
|
121
|
+
|
|
122
|
+
if read_only:
|
|
123
|
+
writable_codes = frozenset()
|
|
124
|
+
elif raw_writable is not None:
|
|
125
|
+
writable_codes = frozenset(raw_writable)
|
|
126
|
+
if values["catalog"] is not None:
|
|
127
|
+
catalog_source = values["catalog"]
|
|
128
|
+
if catalog_source is None:
|
|
129
|
+
catalog_source = codes[0]
|
|
130
|
+
|
|
131
|
+
# Checked with find_spec rather than a bare try/except ImportError so a genuine
|
|
132
|
+
# import bug inside if_cli.router propagates instead of reading as a missing extra.
|
|
133
|
+
if importlib.util.find_spec("mcp") is None:
|
|
134
|
+
die(MISSING_MCP_EXTRA)
|
|
135
|
+
from if_cli.router.server import build_router, log, run_server
|
|
136
|
+
|
|
137
|
+
timeout = parse_timeout_seconds(values["timeout"], "--timeout")
|
|
138
|
+
extra_read_only = frozenset(values["allow-tool"] or [])
|
|
139
|
+
|
|
140
|
+
if catalog_source not in codes:
|
|
141
|
+
die(f"--catalog '{catalog_source}' is not one of the --env codes: {', '.join(codes)}")
|
|
142
|
+
|
|
143
|
+
unknown_writable = sorted(code for code in writable_codes if code not in codes)
|
|
144
|
+
if unknown_writable:
|
|
145
|
+
named = ", ".join(f"'{code}'" for code in unknown_writable)
|
|
146
|
+
die(f"--writable {named} not among the --env codes: {', '.join(codes)}")
|
|
147
|
+
|
|
148
|
+
profiles = {code: get_profile(config, profile_name) for code, profile_name in mappings.items()}
|
|
149
|
+
|
|
150
|
+
label = factory_name if factory_name is not None else "environments"
|
|
151
|
+
parts = [
|
|
152
|
+
f"{code}={mappings[code]} {profiles[code]['host']}{' (writable)' if code in writable_codes else ''}"
|
|
153
|
+
for code in codes
|
|
154
|
+
]
|
|
155
|
+
log(f"{label}: {'; '.join(parts)}; catalog={catalog_source}")
|
|
156
|
+
|
|
157
|
+
router = build_router(
|
|
158
|
+
profiles=profiles,
|
|
159
|
+
writable_codes=writable_codes,
|
|
160
|
+
catalog_source=catalog_source,
|
|
161
|
+
extra_read_only=extra_read_only,
|
|
162
|
+
timeout=timeout,
|
|
163
|
+
)
|
|
164
|
+
run_server(router)
|
if_cli/commands/profiles.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Literal, TypedDict
|
|
|
8
8
|
from if_cli.cache import TokenCache, is_fresh, load_cache
|
|
9
9
|
from if_cli.cli import Options, parse_args
|
|
10
10
|
from if_cli.colour import Colour, colourise
|
|
11
|
-
from if_cli.config import config_file, get_profile, load_config
|
|
11
|
+
from if_cli.config import config_file, get_profile, is_factory_section, load_config
|
|
12
12
|
from if_cli.runtime import format_expiry
|
|
13
13
|
|
|
14
14
|
TokenStatus = Literal["valid", "expired", "none", "unknown"]
|
|
@@ -41,6 +41,8 @@ def summarize_profiles(
|
|
|
41
41
|
) -> list[ProfileSummary]:
|
|
42
42
|
summaries: list[ProfileSummary] = []
|
|
43
43
|
for section in config:
|
|
44
|
+
if is_factory_section(section):
|
|
45
|
+
continue
|
|
44
46
|
name = "DEFAULT" if section == "" else section
|
|
45
47
|
try:
|
|
46
48
|
profile = get_profile(config, name)
|
if_cli/config.py
CHANGED
|
@@ -23,6 +23,17 @@ class Profile(TypedDict):
|
|
|
23
23
|
organization: str | None
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class Factory(TypedDict):
|
|
27
|
+
name: str
|
|
28
|
+
environments: list[tuple[str, str]]
|
|
29
|
+
writable: frozenset[str]
|
|
30
|
+
catalog: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
FACTORY_SECTION_PREFIX = "factory "
|
|
34
|
+
FACTORY_RESERVED_KEYS = {"writable", "catalog"}
|
|
35
|
+
|
|
36
|
+
|
|
26
37
|
def config_dir() -> str:
|
|
27
38
|
if "INSIGHTFACTORY_CONFIG_DIR" in os.environ:
|
|
28
39
|
return os.environ["INSIGHTFACTORY_CONFIG_DIR"]
|
|
@@ -163,7 +174,82 @@ def resolve_profile_name(flag: str | None = None) -> str:
|
|
|
163
174
|
return name
|
|
164
175
|
|
|
165
176
|
|
|
177
|
+
def is_factory_section(name: str) -> bool:
|
|
178
|
+
return name.startswith(FACTORY_SECTION_PREFIX)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def list_factories(config: dict[str, dict[str, str]]) -> list[str]:
|
|
182
|
+
return [section.removeprefix(FACTORY_SECTION_PREFIX) for section in config if is_factory_section(section)]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_factory_section(config: dict[str, dict[str, str]], name: str) -> Factory:
|
|
186
|
+
"""Parse a `[factory <name>]` section on its own: no check that a `writable`/
|
|
187
|
+
`catalog` code is among its environments, and none of its profiles are resolved.
|
|
188
|
+
|
|
189
|
+
A caller that means to merge `--env`/`--writable`/`--catalog` overrides on top
|
|
190
|
+
(`commands/mcp.py`) validates the merged result once, afterwards, rather than
|
|
191
|
+
validating this unmerged section only to re-validate the effective mapping a
|
|
192
|
+
second time. `get_factory` below runs both steps for a caller that has no
|
|
193
|
+
overrides to merge.
|
|
194
|
+
"""
|
|
195
|
+
section_key = f"{FACTORY_SECTION_PREFIX}{name}"
|
|
196
|
+
if section_key not in config:
|
|
197
|
+
known = sorted(list_factories(config))
|
|
198
|
+
hint = f"known factories: {', '.join(known)}" if known else "no factory sections defined"
|
|
199
|
+
die(
|
|
200
|
+
f"factory '{name}' not found in {config_file()} — add a [factory {name}] section with "
|
|
201
|
+
f"<code> = <profile> lines ({hint})"
|
|
202
|
+
)
|
|
203
|
+
section = config[section_key]
|
|
204
|
+
|
|
205
|
+
environments = [(code, profile_name) for code, profile_name in section.items() if code not in FACTORY_RESERVED_KEYS]
|
|
206
|
+
if not environments:
|
|
207
|
+
die(f"factory '{name}' in {config_file()} has no environments — add <code> = <profile> lines")
|
|
208
|
+
for code, profile_name in environments:
|
|
209
|
+
if not profile_name.strip():
|
|
210
|
+
die(
|
|
211
|
+
f"factory '{name}' in {config_file()}: environment '{code}' has an empty profile "
|
|
212
|
+
f"(found '{code} =' with no value)"
|
|
213
|
+
)
|
|
214
|
+
codes = [code for code, _profile_name in environments]
|
|
215
|
+
|
|
216
|
+
writable = frozenset(code.strip() for code in section.get("writable", "").split(",") if code.strip())
|
|
217
|
+
catalog_value = section.get("catalog")
|
|
218
|
+
catalog = catalog_value.strip() if catalog_value is not None and catalog_value.strip() else codes[0]
|
|
219
|
+
|
|
220
|
+
return {"name": name, "environments": environments, "writable": writable, "catalog": catalog}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_factory(config: dict[str, dict[str, str]], name: str) -> Factory:
|
|
224
|
+
"""Parse and fully validate a `[factory <name>]` section: every profile it
|
|
225
|
+
references exists, and its own `writable`/`catalog` codes are among its own
|
|
226
|
+
environments. A caller that merges overrides on top of the section should use
|
|
227
|
+
`parse_factory_section` instead, and validate the merged result once."""
|
|
228
|
+
factory = parse_factory_section(config, name)
|
|
229
|
+
codes = [code for code, _profile_name in factory["environments"]]
|
|
230
|
+
|
|
231
|
+
unknown_writable = sorted(code for code in factory["writable"] if code not in codes)
|
|
232
|
+
if unknown_writable:
|
|
233
|
+
named = ", ".join(f"'{code}'" for code in unknown_writable)
|
|
234
|
+
die(f"factory '{name}' in {config_file()}: writable {named} not among its environments: {', '.join(codes)}")
|
|
235
|
+
|
|
236
|
+
if factory["catalog"] not in codes:
|
|
237
|
+
die(
|
|
238
|
+
f"factory '{name}' in {config_file()}: catalog '{factory['catalog']}' is not among its environments: "
|
|
239
|
+
f"{', '.join(codes)}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
for _code, profile_name in factory["environments"]:
|
|
243
|
+
get_profile(config, profile_name)
|
|
244
|
+
|
|
245
|
+
return factory
|
|
246
|
+
|
|
247
|
+
|
|
166
248
|
def get_profile(config: dict[str, dict[str, str]], name: str) -> Profile:
|
|
249
|
+
if is_factory_section(name):
|
|
250
|
+
die(
|
|
251
|
+
f"'{name}' is a factory section, not a profile; use: if-cli mcp {name.removeprefix(FACTORY_SECTION_PREFIX)}"
|
|
252
|
+
)
|
|
167
253
|
section_key = "" if name == "DEFAULT" else name
|
|
168
254
|
if section_key not in config:
|
|
169
255
|
die(f"profile '{name}' not found in {config_file()} — run: if-cli login -p {name} --host <factory-url>")
|
if_cli/main.py
CHANGED
|
@@ -7,6 +7,7 @@ from if_cli.commands.api import api_command
|
|
|
7
7
|
from if_cli.commands.config import config_command
|
|
8
8
|
from if_cli.commands.login import login_command
|
|
9
9
|
from if_cli.commands.logout import logout_command
|
|
10
|
+
from if_cli.commands.mcp import mcp_command
|
|
10
11
|
from if_cli.commands.profiles import profiles_command
|
|
11
12
|
from if_cli.commands.set_token import set_token_command
|
|
12
13
|
from if_cli.commands.token import token_command
|
|
@@ -30,6 +31,12 @@ commands:
|
|
|
30
31
|
[-p profile] [-X METHOD] [-d DATA] [--timeout SECONDS] <path>
|
|
31
32
|
routes [-p profile] [--timeout SECONDS] [filter]
|
|
32
33
|
describe [-p profile] [--timeout SECONDS] METHOD <path>
|
|
34
|
+
mcp run one MCP server that routes across factory environments
|
|
35
|
+
[FACTORY] [--env CODE=PROFILE ...]
|
|
36
|
+
[--writable CODE ... | --read-only] [--catalog CODE]
|
|
37
|
+
[--allow-tool NAME ...] [--timeout SECONDS]
|
|
38
|
+
FACTORY names a [factory NAME] config section; flags override it
|
|
39
|
+
(needs the mcp extra: uv tool install 'insightfactory-cli[mcp]')
|
|
33
40
|
|
|
34
41
|
profile selection: -p flag, then ${ENV_PROFILE}, then [DEFAULT].
|
|
35
42
|
request timeout: --timeout flag, then ${ENV_REQUEST_TIMEOUT}, then 30 seconds.
|
|
@@ -60,6 +67,9 @@ def run_cli(argv: list[str]) -> None:
|
|
|
60
67
|
if command == "api":
|
|
61
68
|
api_command(rest)
|
|
62
69
|
return
|
|
70
|
+
if command == "mcp":
|
|
71
|
+
mcp_command(rest)
|
|
72
|
+
return
|
|
63
73
|
if command in {None, "-h", "--help"}:
|
|
64
74
|
import sys
|
|
65
75
|
|