insightfactory-cli 1.1.2.dev30__tar.gz → 1.1.2.dev31__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/CHANGELOG.md +6 -0
  2. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/PKG-INFO +9 -1
  3. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/README.md +8 -0
  4. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/pyproject.toml +1 -1
  5. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/mcp.py +4 -1
  6. insightfactory_cli-1.1.2.dev31/src/if_cli/router/log.py +20 -0
  7. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/server.py +24 -1
  8. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_mcp_command.py +6 -6
  9. insightfactory_cli-1.1.2.dev31/tests/test_router_log.py +23 -0
  10. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_router_upstream.py +4 -1
  11. insightfactory_cli-1.1.2.dev30/src/if_cli/router/log.py +0 -13
  12. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/.github/workflows/ci.yml +0 -0
  13. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/.github/workflows/claude.yml +0 -0
  14. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/.github/workflows/release.yml +0 -0
  15. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/.gitignore +0 -0
  16. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/.python-version +0 -0
  17. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/AGENTS.md +0 -0
  18. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/CLAUDE.md +0 -0
  19. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/LICENSE +0 -0
  20. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/__init__.py +0 -0
  21. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/__main__.py +0 -0
  22. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/assets/__init__.py +0 -0
  23. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
  24. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/cache.py +0 -0
  25. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/callback_page.py +0 -0
  26. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/cli.py +0 -0
  27. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/colour.py +0 -0
  28. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/__init__.py +0 -0
  29. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/api.py +0 -0
  30. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/config.py +0 -0
  31. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/login.py +0 -0
  32. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/logout.py +0 -0
  33. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/profiles.py +0 -0
  34. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/set_token.py +0 -0
  35. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/commands/token.py +0 -0
  36. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/config.py +0 -0
  37. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/constants.py +0 -0
  38. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/http.py +0 -0
  39. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/main.py +0 -0
  40. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/oauth.py +0 -0
  41. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/__init__.py +0 -0
  42. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/catalog.py +0 -0
  43. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/content.py +0 -0
  44. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/policy.py +0 -0
  45. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/router/upstream.py +0 -0
  46. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/src/if_cli/runtime.py +0 -0
  47. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/__init__.py +0 -0
  48. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/cache_writer.py +0 -0
  49. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/conftest.py +0 -0
  50. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/helpers.py +0 -0
  51. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/servers.py +0 -0
  52. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_api.py +0 -0
  53. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_api_command.py +0 -0
  54. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_cache.py +0 -0
  55. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_cli.py +0 -0
  56. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_config.py +0 -0
  57. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_config_command.py +0 -0
  58. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_login.py +0 -0
  59. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth.py +0 -0
  60. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth_flow.py +0 -0
  61. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_oauth_force_refresh.py +0 -0
  62. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_profiles.py +0 -0
  63. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_programmatic_api.py +0 -0
  64. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_release_tools.py +0 -0
  65. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_router_catalog.py +0 -0
  66. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_router_content.py +0 -0
  67. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_router_policy.py +0 -0
  68. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_router_resources.py +0 -0
  69. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_router_server.py +0 -0
  70. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_runtime.py +0 -0
  71. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_set_token.py +0 -0
  72. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tests/test_token.py +0 -0
  73. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tools/check_release.py +0 -0
  74. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/tools/release_notes.py +0 -0
  75. {insightfactory_cli-1.1.2.dev30 → insightfactory_cli-1.1.2.dev31}/uv.lock +0 -0
@@ -31,6 +31,12 @@ last one has been released, so a cycle's PRs share a section.
31
31
 
32
32
  ### Changed
33
33
 
34
+ - The router's stderr lines now carry the elapsed time since startup, and it logs when
35
+ the catalogue build starts and when it answers its first `initialize`, so an operator
36
+ can tell a slow process apart from a slow factory and a client that called before the
37
+ handshake finished. `if-cli mcp` also now logs a `starting on Python <version>` line
38
+ before doing anything else, so a process that dies during argument parsing, config
39
+ load, or profile lookup still leaves one line on stderr instead of none.
34
40
  - `release-guard` asks every PR into `develop` for a `CHANGELOG.md` entry, and asks
35
41
  for a version bump only when `develop` is still on the version `main` is on. One
36
42
  bump opens a release cycle and the PRs after it add to the section it opened,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: insightfactory-cli
3
- Version: 1.1.2.dev30
3
+ Version: 1.1.2.dev31
4
4
  Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
5
5
  Project-URL: Homepage, https://insightfactory.ai
6
6
  Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
@@ -265,6 +265,14 @@ uv tool install 'insightfactory-cli[mcp]'
265
265
  uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
266
266
  ```
267
267
 
268
+ The first `uvx` launch resolves and downloads the package from PyPI before it runs, which
269
+ added roughly four to six seconds on top of the usual startup in local testing, against
270
+ under two seconds once `uv`'s cache was warm. A client with a fixed handshake budget
271
+ (Claude Code allows 30 seconds) usually has room for that, but a slow network can close
272
+ the gap. Two ways to avoid paying it on every launch: `uv tool install 'insightfactory-cli[mcp]'`
273
+ puts a real script on `PATH` with no resolve step, or run the same `uvx --from ...` command
274
+ by hand once before pointing a client at it, which warms `uv`'s cache for the launches after.
275
+
268
276
  Add it to `.mcp.json`:
269
277
 
270
278
  ```json
@@ -241,6 +241,14 @@ uv tool install 'insightfactory-cli[mcp]'
241
241
  uvx --from 'insightfactory-cli[mcp]' if-cli mcp foundry
242
242
  ```
243
243
 
244
+ The first `uvx` launch resolves and downloads the package from PyPI before it runs, which
245
+ added roughly four to six seconds on top of the usual startup in local testing, against
246
+ under two seconds once `uv`'s cache was warm. A client with a fixed handshake budget
247
+ (Claude Code allows 30 seconds) usually has room for that, but a slow network can close
248
+ the gap. Two ways to avoid paying it on every launch: `uv tool install 'insightfactory-cli[mcp]'`
249
+ puts a real script on `PATH` with no resolve step, or run the same `uvx --from ...` command
250
+ by hand once before pointing a client at it, which warms `uv`'s cache for the launches after.
251
+
244
252
  Add it to `.mcp.json`:
245
253
 
246
254
  ```json
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "insightfactory-cli"
3
- version = "1.1.2.dev30"
3
+ version = "1.1.2.dev31"
4
4
  description = "Profile-based authentication CLI for the InsightFactory Interfaces API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import importlib.util
4
+ import platform
4
5
  import sys
5
6
 
6
7
  from if_cli.cli import Options, parse_args
7
8
  from if_cli.config import config_file, get_profile, list_factories, load_config, parse_factory_section
8
9
  from if_cli.http import parse_timeout_seconds
10
+ from if_cli.router.log import log
9
11
  from if_cli.runtime import die
10
12
 
11
13
  MCP_OPTIONS: Options = {
@@ -75,6 +77,8 @@ def _stray_positional(rest: list[str]) -> str | None:
75
77
 
76
78
 
77
79
  def mcp_command(argv: list[str]) -> None:
80
+ log(f"starting on Python {platform.python_version()}")
81
+
78
82
  if "-h" in argv or "--help" in argv:
79
83
  sys.stdout.write(MCP_USAGE)
80
84
  return
@@ -146,7 +150,6 @@ def mcp_command(argv: list[str]) -> None:
146
150
  # import bug inside if_cli.router propagates instead of reading as a missing extra.
147
151
  if importlib.util.find_spec("mcp") is None:
148
152
  die(MISSING_MCP_EXTRA)
149
- from if_cli.router.log import log
150
153
  from if_cli.router.server import build_router, run_server
151
154
 
152
155
  timeout = parse_timeout_seconds(values["timeout"], "--timeout")
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import time
5
+
6
+ # Captured at import: this module loads before the router does anything else
7
+ # diagnosable, so it stands in for process start. Two `log()` lines can then
8
+ # be subtracted to time the phase between them.
9
+ _START = time.monotonic()
10
+
11
+
12
+ def log(message: str) -> None:
13
+ """Write one timestamped diagnostic line to stderr.
14
+
15
+ The router speaks MCP over stdout, so stderr is the only channel that can
16
+ carry anything to the operator. This lives in its own module because both
17
+ `server` and `upstream` write to it and `server` imports `upstream`.
18
+ """
19
+ elapsed_ms = round((time.monotonic() - _START) * 1000)
20
+ sys.stderr.write(f"[if-cli mcp] +{elapsed_ms}ms {message}\n")
@@ -9,7 +9,7 @@ from typing import Any, TypeVar
9
9
  import anyio
10
10
  import mcp_types as types
11
11
  from mcp.server import Server
12
- from mcp.server.context import ServerRequestContext
12
+ from mcp.server.context import CallNext, HandlerResult, ServerMiddleware, ServerRequestContext
13
13
  from mcp.server.stdio import stdio_server
14
14
  from mcp.shared.exceptions import MCPError
15
15
 
@@ -137,6 +137,7 @@ class Router:
137
137
 
138
138
  async def build_catalog(self) -> list[types.Tool]:
139
139
  """Build the merged catalogue. Safe to call again to pick up a new login."""
140
+ log(f"catalogue: building from {self.config.catalog_source}")
140
141
  catalogues = await self._catalogues()
141
142
  source = catalogues.get(self.config.catalog_source)
142
143
  if source is None:
@@ -536,6 +537,27 @@ def advertised_extensions(router: Router) -> dict[str, dict[str, Any]]:
536
537
  return {SKILLS_EXTENSION: {}} if router.config.skills else {}
537
538
 
538
539
 
540
+ def _log_first_initialize() -> ServerMiddleware[Any]:
541
+ """Log once the router has answered a client's `initialize`.
542
+
543
+ Read against `prime_catalog`'s startup log lines, this tells an operator
544
+ apart a slow process (nothing logged yet), a slow factory (catalogue
545
+ build still running past this point), and a client that called before
546
+ the handshake (a tool call logged before this line ever appears).
547
+ """
548
+ logged = False
549
+
550
+ async def middleware(ctx: ServerRequestContext[Any, Any], call_next: CallNext) -> HandlerResult:
551
+ nonlocal logged
552
+ result = await call_next(ctx)
553
+ if not logged and ctx.method == "initialize":
554
+ logged = True
555
+ log("answered the first initialize")
556
+ return result
557
+
558
+ return middleware
559
+
560
+
539
561
  async def _serve(router: Router) -> None:
540
562
  server: Server[Any] = Server(
541
563
  SERVER_NAME,
@@ -553,6 +575,7 @@ async def _serve(router: Router) -> None:
553
575
  # the handshake rebuilds its capabilities from `server.extensions` once the
554
576
  # protocol version is known, and ignores the ones computed here.
555
577
  server.extensions = advertised_extensions(router)
578
+ server.middleware.append(_log_first_initialize())
556
579
  # Answer `initialize` immediately: the catalogue build runs as a background
557
580
  # task so one hung factory reads as a slow tools/list, not a failed
558
581
  # handshake (clients cap startup time).
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import re
5
6
  import subprocess
6
7
  import sys
7
8
  import time
@@ -802,11 +803,10 @@ def test_mcp_command_logs_the_resolved_factory_mapping_at_startup(tmp_path: Path
802
803
 
803
804
  anyio.run(scenario)
804
805
  stderr_text = stderr_path.read_text(encoding="utf-8")
805
- expected = (
806
- f"[if-cli mcp] foundry: dev=profile-a {server_a.origin} (writable); "
807
- f"tst=profile-b {server_b.origin}; catalog=dev"
806
+ expected = re.escape(
807
+ f"foundry: dev=profile-a {server_a.origin} (writable); tst=profile-b {server_b.origin}; catalog=dev"
808
808
  )
809
- assert expected in stderr_text
809
+ assert re.search(rf"\[if-cli mcp\] \+\d+ms {expected}", stderr_text)
810
810
  finally:
811
811
  server_a.close()
812
812
  server_b.close()
@@ -827,8 +827,8 @@ def test_mcp_command_logs_environments_label_without_a_factory(tmp_path: Path):
827
827
 
828
828
  anyio.run(scenario)
829
829
  stderr_text = stderr_path.read_text(encoding="utf-8")
830
- expected = f"[if-cli mcp] environments: a=profile-a {server.origin} (writable); catalog=a"
831
- assert expected in stderr_text
830
+ expected = re.escape(f"environments: a=profile-a {server.origin} (writable); catalog=a")
831
+ assert re.search(rf"\[if-cli mcp\] \+\d+ms {expected}", stderr_text)
832
832
  finally:
833
833
  server.close()
834
834
 
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from if_cli.router import log as log_module
4
+
5
+
6
+ def test_log_prefixes_the_elapsed_time_since_process_start(monkeypatch, capsys):
7
+ monkeypatch.setattr(log_module, "_START", 10.0)
8
+ monkeypatch.setattr(log_module.time, "monotonic", lambda: 10.25)
9
+
10
+ log_module.log("ready over stdio")
11
+
12
+ assert capsys.readouterr().err == "[if-cli mcp] +250ms ready over stdio\n"
13
+
14
+
15
+ def test_log_keeps_the_grep_prefix_first(monkeypatch, capsys):
16
+ """`[if-cli mcp]` must stay the first thing on the line, timestamp or not:
17
+ it is what an operator (or a script) greps a stdio transcript for."""
18
+ monkeypatch.setattr(log_module, "_START", 0.0)
19
+ monkeypatch.setattr(log_module.time, "monotonic", lambda: 0.0)
20
+
21
+ log_module.log("WARN dropped a connect URL")
22
+
23
+ assert capsys.readouterr().err.startswith("[if-cli mcp] +0ms ")
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import re
4
5
  from typing import Any
5
6
 
6
7
  import pytest
@@ -373,7 +374,9 @@ def test_a_dropped_connect_url_is_reported_on_stderr(capsys):
373
374
  anyio.run(lambda: upstream.call_tool("run_sql_query", {}))
374
375
 
375
376
  stderr = capsys.readouterr().err
376
- assert "[if-cli mcp] WARN dropped a connect URL that is not on https://factory.example" in stderr
377
+ assert re.search(
378
+ r"\[if-cli mcp\] \+\d+ms WARN dropped a connect URL that is not on https://factory\.example", stderr
379
+ )
377
380
  assert "https://phish.example/connect" in stderr
378
381
 
379
382
 
@@ -1,13 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
-
5
-
6
- def log(message: str) -> None:
7
- """Write one diagnostic line to stderr.
8
-
9
- The router speaks MCP over stdout, so stderr is the only channel that can
10
- carry anything to the operator. This lives in its own module because both
11
- `server` and `upstream` write to it and `server` imports `upstream`.
12
- """
13
- sys.stderr.write(f"[if-cli mcp] {message}\n")