tapestry-cli 0.1.0__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.
- tapestry_cli-0.1.0/PKG-INFO +74 -0
- tapestry_cli-0.1.0/README.md +52 -0
- tapestry_cli-0.1.0/pyproject.toml +41 -0
- tapestry_cli-0.1.0/setup.cfg +4 -0
- tapestry_cli-0.1.0/tapestry_cli/__init__.py +7 -0
- tapestry_cli-0.1.0/tapestry_cli/__main__.py +7 -0
- tapestry_cli-0.1.0/tapestry_cli/cli.py +77 -0
- tapestry_cli-0.1.0/tapestry_cli/init.py +449 -0
- tapestry_cli-0.1.0/tapestry_cli/observatory.py +40 -0
- tapestry_cli-0.1.0/tapestry_cli/onboard.py +108 -0
- tapestry_cli-0.1.0/tapestry_cli.egg-info/PKG-INFO +74 -0
- tapestry_cli-0.1.0/tapestry_cli.egg-info/SOURCES.txt +13 -0
- tapestry_cli-0.1.0/tapestry_cli.egg-info/dependency_links.txt +1 -0
- tapestry_cli-0.1.0/tapestry_cli.egg-info/entry_points.txt +3 -0
- tapestry_cli-0.1.0/tapestry_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tapestry-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tapestry command-line interface — onboard projects, inspect state, open the Observatory.
|
|
5
|
+
Author-email: Liz Osborn <lizocontactinfo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Lizo-RoadTown/tapestry
|
|
8
|
+
Project-URL: Repository, https://github.com/Lizo-RoadTown/tapestry
|
|
9
|
+
Project-URL: Documentation, https://tapestry-khaki.vercel.app/
|
|
10
|
+
Keywords: tapestry,agents,coordination,observability,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# tapestry-cli
|
|
24
|
+
|
|
25
|
+
The command-line interface for [Tapestry](https://github.com/Lizo-RoadTown/tapestry) — project intelligence for AI-native teams.
|
|
26
|
+
|
|
27
|
+
Wire a project into the platform, then open the live console. Stdlib-only, no third-party dependencies, no model downloads — it works the moment `pip install` completes, on Windows, macOS, and Linux.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pipx install tapestry-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(or `pip install tapestry-cli`)
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
tapestry onboard Onboard the current directory and write
|
|
41
|
+
.claude/settings.json with the Tapestry plugins enabled
|
|
42
|
+
(the recommended quickstart path).
|
|
43
|
+
tapestry observatory Open the Observatory console in a browser.
|
|
44
|
+
tapestry init Register the project and write .env / .mcp.json /
|
|
45
|
+
.project-intelligence/ (granular path; does not touch
|
|
46
|
+
.claude/settings.json).
|
|
47
|
+
tapestry version Print version and platform info.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pipx install tapestry-cli
|
|
54
|
+
tapestry onboard
|
|
55
|
+
tapestry observatory
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The agents pick up memory, telemetry, and the discipline rules from there. See the [setup guide](https://tapestry-khaki.vercel.app/how-to/set-up-a-new-project/) for the full walkthrough.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
The CLI is URL-env-driven — no hardcoded hostnames. It reads:
|
|
63
|
+
|
|
64
|
+
- `LOOM_MEMORY_MCP_URL` / `LOOM_MEMORY_URL` — the memory MCP endpoint
|
|
65
|
+
- `LOOM_PROJECT_ID` — the project identifier
|
|
66
|
+
- `TAPESTRY_OBSERVATORY_URL` — override the Observatory URL opened by `tapestry observatory`
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
|
|
70
|
+
`tapestry` is the primary entry point; `loom` is kept as a deprecated alias during the transition. The Python package is named `tapestry_cli`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
*Provenance: lifted verbatim from `the-loom/tapestry-cli/` during the Tapestry monorepo migration (Step 5a, 2026-06-21) — the cross-platform replacement for the old PowerShell `new-loom-project.ps1` scaffolder.*
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# tapestry-cli
|
|
2
|
+
|
|
3
|
+
The command-line interface for [Tapestry](https://github.com/Lizo-RoadTown/tapestry) — project intelligence for AI-native teams.
|
|
4
|
+
|
|
5
|
+
Wire a project into the platform, then open the live console. Stdlib-only, no third-party dependencies, no model downloads — it works the moment `pip install` completes, on Windows, macOS, and Linux.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install tapestry-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
(or `pip install tapestry-cli`)
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
tapestry onboard Onboard the current directory and write
|
|
19
|
+
.claude/settings.json with the Tapestry plugins enabled
|
|
20
|
+
(the recommended quickstart path).
|
|
21
|
+
tapestry observatory Open the Observatory console in a browser.
|
|
22
|
+
tapestry init Register the project and write .env / .mcp.json /
|
|
23
|
+
.project-intelligence/ (granular path; does not touch
|
|
24
|
+
.claude/settings.json).
|
|
25
|
+
tapestry version Print version and platform info.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pipx install tapestry-cli
|
|
32
|
+
tapestry onboard
|
|
33
|
+
tapestry observatory
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The agents pick up memory, telemetry, and the discipline rules from there. See the [setup guide](https://tapestry-khaki.vercel.app/how-to/set-up-a-new-project/) for the full walkthrough.
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
The CLI is URL-env-driven — no hardcoded hostnames. It reads:
|
|
41
|
+
|
|
42
|
+
- `LOOM_MEMORY_MCP_URL` / `LOOM_MEMORY_URL` — the memory MCP endpoint
|
|
43
|
+
- `LOOM_PROJECT_ID` — the project identifier
|
|
44
|
+
- `TAPESTRY_OBSERVATORY_URL` — override the Observatory URL opened by `tapestry observatory`
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
`tapestry` is the primary entry point; `loom` is kept as a deprecated alias during the transition. The Python package is named `tapestry_cli`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
*Provenance: lifted verbatim from `the-loom/tapestry-cli/` during the Tapestry monorepo migration (Step 5a, 2026-06-21) — the cross-platform replacement for the old PowerShell `new-loom-project.ps1` scaffolder.*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tapestry-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tapestry command-line interface — onboard projects, inspect state, open the Observatory."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Liz Osborn", email = "lizocontactinfo@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["tapestry", "agents", "coordination", "observability", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
]
|
|
27
|
+
# Stdlib-only at runtime by design. Cross-platform Python (Windows / macOS / Linux),
|
|
28
|
+
# no compiled deps, no model downloads — `tapestry init` works the moment pip install completes.
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
tapestry = "tapestry_cli.cli:main"
|
|
33
|
+
loom = "tapestry_cli.cli:main" # deprecated alias
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/Lizo-RoadTown/tapestry"
|
|
37
|
+
Repository = "https://github.com/Lizo-RoadTown/tapestry"
|
|
38
|
+
Documentation = "https://tapestry-khaki.vercel.app/"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
packages = ["tapestry_cli"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Top-level CLI dispatcher for `tapestry`.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
tapestry onboard — `init` + writes .claude/settings.json with tapestry plugins
|
|
5
|
+
enabled (the 4-step quickstart path)
|
|
6
|
+
tapestry observatory — open the Observatory console in a browser
|
|
7
|
+
tapestry init — register the project, write .env / .mcp.json / .project-intelligence/
|
|
8
|
+
(the granular path — does NOT touch .claude/settings.json)
|
|
9
|
+
tapestry version — print version + diagnostic info
|
|
10
|
+
|
|
11
|
+
Add a new subcommand by:
|
|
12
|
+
1. Creating tapestry_cli/<name>.py with add_arguments(parser) + run(args) -> int
|
|
13
|
+
2. Importing + registering it in SUBCOMMANDS below
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from tapestry_cli import (
|
|
21
|
+
__version__,
|
|
22
|
+
init as init_cmd,
|
|
23
|
+
observatory as observatory_cmd,
|
|
24
|
+
onboard as onboard_cmd,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
SUBCOMMANDS: dict[str, dict] = {
|
|
29
|
+
"onboard": {
|
|
30
|
+
"help": "Onboard the current directory + write .claude/settings.json (recommended).",
|
|
31
|
+
"add_arguments": onboard_cmd.add_arguments,
|
|
32
|
+
"run": onboard_cmd.run,
|
|
33
|
+
},
|
|
34
|
+
"observatory": {
|
|
35
|
+
"help": "Open the Observatory console in a browser.",
|
|
36
|
+
"add_arguments": observatory_cmd.add_arguments,
|
|
37
|
+
"run": observatory_cmd.run,
|
|
38
|
+
},
|
|
39
|
+
"init": {
|
|
40
|
+
"help": "Onboard the current directory as a Tapestry consuming project (no settings.json).",
|
|
41
|
+
"add_arguments": init_cmd.add_arguments,
|
|
42
|
+
"run": init_cmd.run,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _print_version() -> int:
|
|
48
|
+
import platform
|
|
49
|
+
print(f"tapestry-cli {__version__}")
|
|
50
|
+
print(f" python: {platform.python_version()} ({platform.system()} {platform.release()})")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: list[str] | None = None) -> int:
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
prog="tapestry",
|
|
57
|
+
description="Tapestry command-line interface — onboard projects, inspect state, open the Observatory.",
|
|
58
|
+
)
|
|
59
|
+
subs = parser.add_subparsers(dest="command", metavar="<command>")
|
|
60
|
+
subs.add_parser("version", help="Print version and platform info.")
|
|
61
|
+
for name, spec in SUBCOMMANDS.items():
|
|
62
|
+
sub = subs.add_parser(name, help=spec["help"])
|
|
63
|
+
spec["add_arguments"](sub)
|
|
64
|
+
|
|
65
|
+
args = parser.parse_args(argv)
|
|
66
|
+
|
|
67
|
+
if args.command == "version":
|
|
68
|
+
return _print_version()
|
|
69
|
+
if args.command in SUBCOMMANDS:
|
|
70
|
+
return SUBCOMMANDS[args.command]["run"](args)
|
|
71
|
+
|
|
72
|
+
parser.print_help()
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
sys.exit(main())
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""loom init — onboard the current directory as a loom consuming project.
|
|
2
|
+
|
|
3
|
+
The implementation behind `loom init` (and the legacy `scripts/loom_init.py`
|
|
4
|
+
shim). Stdlib-only so it runs equally from PowerShell / bash / zsh on
|
|
5
|
+
Windows / macOS / Linux without extra installs.
|
|
6
|
+
|
|
7
|
+
What it does (mirrors `docs/howto/onboard-a-project.md` Part 1, steps 2-5):
|
|
8
|
+
|
|
9
|
+
1. Pre-check: confirm you're in a directory that looks like a project
|
|
10
|
+
(has .git, or has files, or you pass --force).
|
|
11
|
+
2. Pre-check: confirm the slug isn't already registered for your
|
|
12
|
+
tenant (GET /projects/by-slug/<slug>). Idempotent on rerun.
|
|
13
|
+
3. POST to https://loom-project-registry.onrender.com/projects to
|
|
14
|
+
register the project. Self-host mode: no Bearer token needed;
|
|
15
|
+
server falls back to SELF_HOST_TENANT_ID.
|
|
16
|
+
4. Create .env in the current dir with OTel credentials copied from
|
|
17
|
+
the-loom's .env (so hook events flow to Grafana tagged with this
|
|
18
|
+
new project_id) + LOOM_PROJECT_ID=<slug>.
|
|
19
|
+
5. Create .project-intelligence/ folder per the platform-data-model.
|
|
20
|
+
6. Print confirmation + next-steps.
|
|
21
|
+
|
|
22
|
+
What it does NOT do:
|
|
23
|
+
- Does NOT create a GitHub repo (use `gh repo create` or the
|
|
24
|
+
PowerShell scaffolder for that).
|
|
25
|
+
- Does NOT install the tapestry-discipline Claude Code plugin.
|
|
26
|
+
- Does NOT install skills (Phase 5 SDK install-path future work).
|
|
27
|
+
- Does NOT touch .gitignore (warns if .env not gitignored).
|
|
28
|
+
|
|
29
|
+
Dual-mode:
|
|
30
|
+
- Self-host (default): no --token, server falls back to SELF_HOST_TENANT_ID.
|
|
31
|
+
- Hosted-multitenant: pass --token <jwt>; sent as Bearer to the Registry.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import socket
|
|
39
|
+
import sys
|
|
40
|
+
import time
|
|
41
|
+
import urllib.error
|
|
42
|
+
import urllib.parse
|
|
43
|
+
import urllib.request
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Any, Optional
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
DEFAULT_REGISTRY_URL = "https://loom-project-registry.onrender.com"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _read_loom_env(loom_repo: Path) -> dict[str, str]:
|
|
52
|
+
"""Read the-loom's .env to pull OTel credentials for propagation."""
|
|
53
|
+
env_path = loom_repo / ".env"
|
|
54
|
+
out: dict[str, str] = {}
|
|
55
|
+
if not env_path.is_file():
|
|
56
|
+
return out
|
|
57
|
+
try:
|
|
58
|
+
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
59
|
+
line = line.strip()
|
|
60
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
61
|
+
continue
|
|
62
|
+
key, _, value = line.partition("=")
|
|
63
|
+
key = key.strip()
|
|
64
|
+
value = value.strip()
|
|
65
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
|
66
|
+
value = value[1:-1]
|
|
67
|
+
if key:
|
|
68
|
+
out[key] = value
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _gitignore_has_env(project_dir: Path) -> bool:
|
|
75
|
+
gi = project_dir / ".gitignore"
|
|
76
|
+
if not gi.is_file():
|
|
77
|
+
return False
|
|
78
|
+
try:
|
|
79
|
+
for line in gi.read_text(encoding="utf-8").splitlines():
|
|
80
|
+
if line.strip() == ".env":
|
|
81
|
+
return True
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _warm_registry(registry_url: str, max_wait: float = 90.0) -> None:
|
|
88
|
+
"""Render free-tier services cold-start. Wait for /health up to max_wait."""
|
|
89
|
+
url = registry_url.rstrip("/") + "/health"
|
|
90
|
+
deadline = time.time() + max_wait
|
|
91
|
+
attempt = 0
|
|
92
|
+
while time.time() < deadline:
|
|
93
|
+
attempt += 1
|
|
94
|
+
try:
|
|
95
|
+
with urllib.request.urlopen(
|
|
96
|
+
urllib.request.Request(url=url, method="GET"), timeout=15
|
|
97
|
+
) as resp:
|
|
98
|
+
if resp.status == 200:
|
|
99
|
+
if attempt > 1:
|
|
100
|
+
print(f" Registry warmed up (attempt {attempt}).")
|
|
101
|
+
return
|
|
102
|
+
except (urllib.error.URLError, urllib.error.HTTPError,
|
|
103
|
+
TimeoutError, ConnectionError, OSError):
|
|
104
|
+
pass
|
|
105
|
+
if attempt == 1:
|
|
106
|
+
print(f" Registry cold-starting (Render free tier); waiting up to {int(max_wait)}s...")
|
|
107
|
+
time.sleep(5)
|
|
108
|
+
print(f" WARN: Registry /health didn't respond within {int(max_wait)}s; proceeding anyway.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check_registry(
|
|
112
|
+
registry_url: str, slug: str, token: Optional[str], timeout: float = 60.0,
|
|
113
|
+
) -> Optional[dict[str, Any]]:
|
|
114
|
+
"""GET /projects/by-slug/<slug>. Returns row, None on 404, raises on other errors."""
|
|
115
|
+
url = registry_url.rstrip("/") + f"/projects/by-slug/{urllib.parse.quote(slug)}"
|
|
116
|
+
headers = {}
|
|
117
|
+
if token:
|
|
118
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
119
|
+
req = urllib.request.Request(url=url, headers=headers, method="GET")
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
122
|
+
if resp.status == 200:
|
|
123
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
124
|
+
return None
|
|
125
|
+
except urllib.error.HTTPError as e:
|
|
126
|
+
if e.code == 404:
|
|
127
|
+
return None
|
|
128
|
+
body = e.read().decode("utf-8", errors="replace")[:300]
|
|
129
|
+
raise RuntimeError(f"Registry GET /by-slug/{slug} returned HTTP {e.code}: {body}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _register_project(
|
|
133
|
+
registry_url: str,
|
|
134
|
+
slug: str,
|
|
135
|
+
name: str,
|
|
136
|
+
description: str,
|
|
137
|
+
kind: str,
|
|
138
|
+
token: Optional[str],
|
|
139
|
+
timeout: float = 30.0,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
"""POST /projects. Returns the new row. Raises on non-201."""
|
|
142
|
+
body = json.dumps({
|
|
143
|
+
"slug": slug, "name": name, "description": description, "kind": kind,
|
|
144
|
+
}).encode("utf-8")
|
|
145
|
+
url = registry_url.rstrip("/") + "/projects"
|
|
146
|
+
headers = {"Content-Type": "application/json"}
|
|
147
|
+
if token:
|
|
148
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
149
|
+
req = urllib.request.Request(url=url, data=body, headers=headers, method="POST")
|
|
150
|
+
try:
|
|
151
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
152
|
+
if resp.status != 201:
|
|
153
|
+
raise RuntimeError(f"Registry POST /projects returned HTTP {resp.status}")
|
|
154
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
155
|
+
except urllib.error.HTTPError as e:
|
|
156
|
+
err_body = e.read().decode("utf-8", errors="replace")[:300]
|
|
157
|
+
raise RuntimeError(f"Registry POST /projects returned HTTP {e.code}: {err_body}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _write_env_file(project_dir: Path, slug: str, loom_env: dict[str, str]) -> None:
|
|
161
|
+
"""Create .env with OTel propagation + LOOM_PROJECT_ID. Does NOT overwrite."""
|
|
162
|
+
env_path = project_dir / ".env"
|
|
163
|
+
if env_path.exists():
|
|
164
|
+
print(f" WARN: {env_path} already exists; not overwriting.")
|
|
165
|
+
print(f" Add LOOM_PROJECT_ID={slug} manually if missing.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
lines = [
|
|
169
|
+
"# .env for this consuming project of the-loom.",
|
|
170
|
+
"# Generated by `loom init` — review before using.",
|
|
171
|
+
"# Gitignore me. (See .gitignore — loom warns if not present.)",
|
|
172
|
+
"",
|
|
173
|
+
f"LOOM_PROJECT_ID={slug}",
|
|
174
|
+
"",
|
|
175
|
+
"# OTel credentials propagated from the-loom/.env so this project's",
|
|
176
|
+
"# hook events flow to the same Grafana Cloud stack, tagged with the",
|
|
177
|
+
"# project_id above.",
|
|
178
|
+
]
|
|
179
|
+
for key in (
|
|
180
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
181
|
+
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
|
182
|
+
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
183
|
+
"OTEL_RESOURCE_ATTRIBUTES",
|
|
184
|
+
"OTEL_SERVICE_NAME",
|
|
185
|
+
):
|
|
186
|
+
value = loom_env.get(key)
|
|
187
|
+
if value:
|
|
188
|
+
lines.append(f"{key}={value}")
|
|
189
|
+
lines.append("")
|
|
190
|
+
env_path.write_text("\n".join(lines), encoding="utf-8")
|
|
191
|
+
print(f" wrote {env_path}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Constants for the loom-memory MCP server entry — this is the concrete rule
|
|
195
|
+
# every consuming project must satisfy. See skills_private/concrete-rule/SKILL.md
|
|
196
|
+
# and docs/CORE_DIRECTIVES.md. Format: Claude Code's .mcp.json schema.
|
|
197
|
+
#
|
|
198
|
+
# Env-override precedence for Tapestry migration (PR-prep-2b 2026-06-19):
|
|
199
|
+
# 1. TAPESTRY_MEMORY_MCP_URL: Tapestry-aware full URL; highest precedence
|
|
200
|
+
# 2. LOOM_MEMORY_MCP_URL: pre-Tapestry full URL
|
|
201
|
+
# 3. TAPESTRY_MEMORY_URL: Tapestry-aware bare base; /mcp/memory/ composed
|
|
202
|
+
# 4. LOOM_MEMORY_URL: pre-Tapestry bare base; /mcp/memory/ composed
|
|
203
|
+
# 5. Hardcoded default — the-loom's Render deployment
|
|
204
|
+
LOOM_MEMORY_MCP_URL = os.environ.get(
|
|
205
|
+
"TAPESTRY_MEMORY_MCP_URL",
|
|
206
|
+
os.environ.get(
|
|
207
|
+
"LOOM_MEMORY_MCP_URL",
|
|
208
|
+
f"{os.environ.get('TAPESTRY_MEMORY_URL', os.environ.get('LOOM_MEMORY_URL', 'https://loom-agent-context.onrender.com')).rstrip('/')}/mcp/memory/",
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
LOOM_MEMORY_SERVER_NAME = "loom-memory"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _write_mcp_config(project_dir: Path) -> None:
|
|
215
|
+
"""Ensure `.mcp.json` exists in project_dir with loom-memory wired in.
|
|
216
|
+
|
|
217
|
+
This is **Layer 5** of the concrete-rule defense-in-depth pattern (see
|
|
218
|
+
skills_private/concrete-rule/SKILL.md). Every consuming project MUST
|
|
219
|
+
have loom-memory in its `.mcp.json` so Claude Code sessions can call
|
|
220
|
+
memory_recall / memory_write / memory_read tools regardless of which
|
|
221
|
+
plugins are enabled.
|
|
222
|
+
|
|
223
|
+
Idempotent: if `.mcp.json` exists, merges the loom-memory entry into
|
|
224
|
+
its mcpServers block (preserves all existing servers). If it doesn't
|
|
225
|
+
exist, creates a minimal one with just loom-memory.
|
|
226
|
+
|
|
227
|
+
The plugin's plugin.json (integrations/claude-code/tapestry-discipline/
|
|
228
|
+
.claude-plugin/plugin.json v0.1.8+) ALSO registers loom-memory at the
|
|
229
|
+
user level — Layer 3 of the concrete rule. Both layers exist so that
|
|
230
|
+
if the plugin is disabled or fails, the project-level config still
|
|
231
|
+
provides memory access.
|
|
232
|
+
"""
|
|
233
|
+
mcp_path = project_dir / ".mcp.json"
|
|
234
|
+
loom_memory_entry = {
|
|
235
|
+
"type": "http",
|
|
236
|
+
"url": LOOM_MEMORY_MCP_URL,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if mcp_path.exists():
|
|
240
|
+
try:
|
|
241
|
+
existing = json.loads(mcp_path.read_text(encoding="utf-8"))
|
|
242
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
243
|
+
print(f" WARN: {mcp_path} exists but couldn't be parsed ({e}).")
|
|
244
|
+
print(f" Skipping merge. Add loom-memory manually:")
|
|
245
|
+
print(f" {{\"loom-memory\": {{\"type\": \"http\", \"url\": \"{LOOM_MEMORY_MCP_URL}\"}}}}")
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
servers = existing.setdefault("mcpServers", {})
|
|
249
|
+
if LOOM_MEMORY_SERVER_NAME in servers:
|
|
250
|
+
existing_url = servers[LOOM_MEMORY_SERVER_NAME].get("url", "")
|
|
251
|
+
if existing_url == LOOM_MEMORY_MCP_URL:
|
|
252
|
+
print(f" loom-memory already wired in {mcp_path}")
|
|
253
|
+
return
|
|
254
|
+
print(f" WARN: {mcp_path} has loom-memory pointing at a different URL:")
|
|
255
|
+
print(f" existing: {existing_url}")
|
|
256
|
+
print(f" expected: {LOOM_MEMORY_MCP_URL}")
|
|
257
|
+
print(f" Leaving as-is. Review and reconcile if needed.")
|
|
258
|
+
return
|
|
259
|
+
servers[LOOM_MEMORY_SERVER_NAME] = loom_memory_entry
|
|
260
|
+
mcp_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
|
|
261
|
+
print(f" merged loom-memory into {mcp_path}")
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
# Create fresh
|
|
265
|
+
config = {
|
|
266
|
+
"mcpServers": {
|
|
267
|
+
LOOM_MEMORY_SERVER_NAME: loom_memory_entry,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
mcp_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
271
|
+
print(f" wrote {mcp_path}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _write_project_intelligence(
|
|
275
|
+
project_dir: Path,
|
|
276
|
+
slug: str,
|
|
277
|
+
project_uuid: str,
|
|
278
|
+
project_name: str,
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Create .project-intelligence/ per the platform-data-model. Idempotent."""
|
|
281
|
+
pi = project_dir / ".project-intelligence"
|
|
282
|
+
pi.mkdir(exist_ok=True)
|
|
283
|
+
|
|
284
|
+
agent_profile_path = pi / "agent-profile.json"
|
|
285
|
+
if not agent_profile_path.exists():
|
|
286
|
+
agent_profile_path.write_text(json.dumps({
|
|
287
|
+
"configured_agents": [
|
|
288
|
+
{
|
|
289
|
+
"kind": "claude-code",
|
|
290
|
+
"version": "unknown",
|
|
291
|
+
"capabilities": ["hooks", "mcp", "skills", "plugins"],
|
|
292
|
+
}
|
|
293
|
+
],
|
|
294
|
+
"generated_by": "tapestry_cli.init",
|
|
295
|
+
}, indent=2), encoding="utf-8")
|
|
296
|
+
print(f" wrote {agent_profile_path}")
|
|
297
|
+
|
|
298
|
+
project_context_path = pi / "project-context.json"
|
|
299
|
+
if not project_context_path.exists():
|
|
300
|
+
project_context_path.write_text(json.dumps({
|
|
301
|
+
"project_id": project_uuid,
|
|
302
|
+
"slug": slug,
|
|
303
|
+
"name": project_name,
|
|
304
|
+
"hostname": socket.gethostname(),
|
|
305
|
+
"registered_via": "tapestry_cli.init",
|
|
306
|
+
}, indent=2), encoding="utf-8")
|
|
307
|
+
print(f" wrote {project_context_path}")
|
|
308
|
+
|
|
309
|
+
observatory_config_path = pi / "observatory-config.json"
|
|
310
|
+
if not observatory_config_path.exists():
|
|
311
|
+
observatory_config_path.write_text(json.dumps({
|
|
312
|
+
"telemetry_destinations": [
|
|
313
|
+
{
|
|
314
|
+
"kind": "grafana-cloud-otlp",
|
|
315
|
+
"configured_via": "OTEL_EXPORTER_OTLP_* env vars in .env",
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
"sampling_rules": "default-all",
|
|
319
|
+
}, indent=2), encoding="utf-8")
|
|
320
|
+
print(f" wrote {observatory_config_path}")
|
|
321
|
+
|
|
322
|
+
for sub in ("local-skills", "workflow-candidates", "lessons-learned", "promotion-candidates"):
|
|
323
|
+
d = pi / sub
|
|
324
|
+
d.mkdir(exist_ok=True)
|
|
325
|
+
readme = d / "README.md"
|
|
326
|
+
if not readme.exists():
|
|
327
|
+
readme.write_text(
|
|
328
|
+
f"# {sub}\n\nLocal {sub.replace('-', ' ')} for this project. "
|
|
329
|
+
f"Populated by the agency optimizer during use. See "
|
|
330
|
+
f"docs/proposals/2026-05-25-platform-data-model.md in the-loom "
|
|
331
|
+
f"for the directory contract.\n",
|
|
332
|
+
encoding="utf-8",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
print(f" initialized {pi}/")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def add_arguments(p: argparse.ArgumentParser) -> None:
|
|
339
|
+
"""Attach the init subcommand's args to a parser. Used by cli.py dispatch."""
|
|
340
|
+
p.add_argument("--slug", required=True,
|
|
341
|
+
help="Project slug (kebab-case, lowercase). Used as LOOM_PROJECT_ID.")
|
|
342
|
+
p.add_argument("--name", default=None,
|
|
343
|
+
help="Human-readable project name. Defaults to slug.")
|
|
344
|
+
p.add_argument("--description", default="",
|
|
345
|
+
help="One-sentence project description.")
|
|
346
|
+
p.add_argument("--kind", default="dev", choices=["dev", "archived", "paused"],
|
|
347
|
+
help="Project lifecycle state (default: dev).")
|
|
348
|
+
p.add_argument("--loom-repo", default=None,
|
|
349
|
+
help="Path to the-loom repo (defaults to LOOM_REPO env var, "
|
|
350
|
+
"then $HOME/the-loom or %%USERPROFILE%%/the-loom).")
|
|
351
|
+
p.add_argument("--registry-url", default=DEFAULT_REGISTRY_URL,
|
|
352
|
+
help=f"Project Registry base URL (default: {DEFAULT_REGISTRY_URL}).")
|
|
353
|
+
p.add_argument("--token", default=None,
|
|
354
|
+
help="Optional Bearer token (hosted-multitenant mode).")
|
|
355
|
+
p.add_argument("--force", action="store_true",
|
|
356
|
+
help="Skip the 'looks like a project' pre-check.")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def run(args: argparse.Namespace) -> int:
|
|
360
|
+
"""Execute the init flow. Returns POSIX exit code."""
|
|
361
|
+
project_dir = Path.cwd()
|
|
362
|
+
name = args.name or args.slug
|
|
363
|
+
|
|
364
|
+
# Locate the-loom repo
|
|
365
|
+
loom_repo_str = args.loom_repo or os.environ.get("LOOM_REPO") or str(Path.home() / "the-loom")
|
|
366
|
+
loom_repo = Path(loom_repo_str).expanduser().resolve()
|
|
367
|
+
if not (loom_repo / "render.yaml").is_file():
|
|
368
|
+
print(f"error: --loom-repo path doesn't look like the-loom (no render.yaml at {loom_repo}).", file=sys.stderr)
|
|
369
|
+
print(f" Pass --loom-repo /path/to/the-loom explicitly, or set LOOM_REPO env var.", file=sys.stderr)
|
|
370
|
+
return 1
|
|
371
|
+
|
|
372
|
+
print(f"==> loom init")
|
|
373
|
+
print(f" project dir: {project_dir}")
|
|
374
|
+
print(f" slug: {args.slug}")
|
|
375
|
+
print(f" name: {name}")
|
|
376
|
+
print(f" description: {args.description or '(none)'}")
|
|
377
|
+
print(f" kind: {args.kind}")
|
|
378
|
+
print(f" loom repo: {loom_repo}")
|
|
379
|
+
print(f" registry: {args.registry_url}")
|
|
380
|
+
print(f" auth: {'Bearer token (hosted)' if args.token else 'self-host fallback (no token)'}")
|
|
381
|
+
print()
|
|
382
|
+
|
|
383
|
+
if not args.force:
|
|
384
|
+
has_git = (project_dir / ".git").exists()
|
|
385
|
+
has_files = any(project_dir.iterdir())
|
|
386
|
+
if not has_git and not has_files:
|
|
387
|
+
print(f"error: {project_dir} doesn't look like a project (no .git, no files).", file=sys.stderr)
|
|
388
|
+
print(f" Pass --force to override.", file=sys.stderr)
|
|
389
|
+
return 1
|
|
390
|
+
|
|
391
|
+
print(f"--> [1/4] Check Project Registry for existing slug '{args.slug}'...")
|
|
392
|
+
_warm_registry(args.registry_url)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
existing = _check_registry(args.registry_url, args.slug, args.token)
|
|
396
|
+
except RuntimeError as e:
|
|
397
|
+
print(f"error: registry check failed: {e}", file=sys.stderr)
|
|
398
|
+
return 1
|
|
399
|
+
if existing:
|
|
400
|
+
print(f" IDEMPOTENT: project '{args.slug}' already registered.")
|
|
401
|
+
print(f" UUID: {existing.get('id')}")
|
|
402
|
+
print(f" created_at: {existing.get('created_at')}")
|
|
403
|
+
print(f" Skipping Registry POST; proceeding with local file setup.")
|
|
404
|
+
project_uuid = existing.get("id")
|
|
405
|
+
else:
|
|
406
|
+
print(f"--> [2/4] Register '{args.slug}' with the Project Registry...")
|
|
407
|
+
try:
|
|
408
|
+
row = _register_project(
|
|
409
|
+
args.registry_url, args.slug, name, args.description, args.kind, args.token,
|
|
410
|
+
)
|
|
411
|
+
except RuntimeError as e:
|
|
412
|
+
print(f"error: registry registration failed: {e}", file=sys.stderr)
|
|
413
|
+
return 1
|
|
414
|
+
project_uuid = row.get("id")
|
|
415
|
+
print(f" registered. UUID: {project_uuid}")
|
|
416
|
+
|
|
417
|
+
loom_env = _read_loom_env(loom_repo)
|
|
418
|
+
if not loom_env.get("OTEL_EXPORTER_OTLP_HEADERS"):
|
|
419
|
+
print(f" WARN: {loom_repo}/.env didn't have OTEL_EXPORTER_OTLP_HEADERS;")
|
|
420
|
+
print(f" the generated .env won't have telemetry credentials. Fix manually.")
|
|
421
|
+
|
|
422
|
+
print(f"--> [3/5] Create .env in {project_dir}...")
|
|
423
|
+
_write_env_file(project_dir, args.slug, loom_env)
|
|
424
|
+
|
|
425
|
+
if not _gitignore_has_env(project_dir):
|
|
426
|
+
print(f" WARN: .gitignore does NOT contain '.env'. Add it to prevent")
|
|
427
|
+
print(f" committing OTel credentials.")
|
|
428
|
+
|
|
429
|
+
print(f"--> [4/5] Wire loom-memory MCP server (concrete-rule Layer 5)...")
|
|
430
|
+
_write_mcp_config(project_dir)
|
|
431
|
+
|
|
432
|
+
print(f"--> [5/5] Initialize .project-intelligence/...")
|
|
433
|
+
_write_project_intelligence(project_dir, args.slug, project_uuid or "unknown", name)
|
|
434
|
+
|
|
435
|
+
print()
|
|
436
|
+
print(f"==> Done.")
|
|
437
|
+
print(f" Project '{args.slug}' is now registered with the-loom.")
|
|
438
|
+
print(f" UUID: {project_uuid}")
|
|
439
|
+
print()
|
|
440
|
+
print(f"Next steps:")
|
|
441
|
+
print(f" 1. If you don't already have the tapestry-discipline plugin installed,")
|
|
442
|
+
print(f" see docs/howto/onboard-a-project.md Part 2.")
|
|
443
|
+
print(f" 2. Start a Claude Code session in this directory. The SessionStart")
|
|
444
|
+
print(f" hook (v0.1.7+) will auto-recall relevant memories for this project.")
|
|
445
|
+
print(f" 3. Hook events from this project will flow to Grafana tagged with")
|
|
446
|
+
print(f" project_id={args.slug}.")
|
|
447
|
+
print(f" 4. If .env doesn't exist or is missing OTel creds, copy from")
|
|
448
|
+
print(f" {loom_repo}/.env manually.")
|
|
449
|
+
return 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""`tapestry observatory` — open the Observatory console.
|
|
2
|
+
|
|
3
|
+
The Observatory is the platform's coordination console (one deployed
|
|
4
|
+
instance). This opens it in a browser. Override the URL with
|
|
5
|
+
`--url` or the `TAPESTRY_OBSERVATORY_URL` environment variable.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import webbrowser
|
|
12
|
+
|
|
13
|
+
DEFAULT_URL = "https://tapestry-khaki.vercel.app/observatory"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--url",
|
|
19
|
+
default=os.environ.get("TAPESTRY_OBSERVATORY_URL", DEFAULT_URL),
|
|
20
|
+
help="Console URL (default: the deployed console; override with TAPESTRY_OBSERVATORY_URL).",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--print",
|
|
24
|
+
dest="print_only",
|
|
25
|
+
action="store_true",
|
|
26
|
+
help="Print the URL instead of opening a browser.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(args: argparse.Namespace) -> int:
|
|
31
|
+
url = args.url
|
|
32
|
+
print(f"Observatory console: {url}")
|
|
33
|
+
if args.print_only:
|
|
34
|
+
return 0
|
|
35
|
+
try:
|
|
36
|
+
webbrowser.open(url)
|
|
37
|
+
print("Opened in your default browser.")
|
|
38
|
+
except Exception:
|
|
39
|
+
print("(Couldn't open a browser automatically — open the URL above.)")
|
|
40
|
+
return 0
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""`loom onboard` — `init` + writes `.claude/settings.json` with the tapestry plugins enabled.
|
|
2
|
+
|
|
3
|
+
Wraps `loom init` so a single command does everything `loom init` does PLUS:
|
|
4
|
+
|
|
5
|
+
- writes `.claude/settings.json` with `tapestry-discipline@tapestry` and
|
|
6
|
+
`tapestry-patterns@tapestry` enabled (idempotent merge if the file
|
|
7
|
+
already exists)
|
|
8
|
+
- prints a 2-step epilogue covering the operator actions that have to
|
|
9
|
+
happen inside Claude Code (plugin install + IDE reload)
|
|
10
|
+
|
|
11
|
+
Argument surface is identical to `loom init` — see `init.add_arguments`.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from tapestry_cli import init as init_cmd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# The two plugins the consolidated `tapestry` marketplace ships.
|
|
23
|
+
# These names are the install identifiers (per
|
|
24
|
+
# tapestry/.claude-plugin/marketplace.json), distinct from the runtime
|
|
25
|
+
# emission strings the hook scripts still produce ([loom-discipline]) —
|
|
26
|
+
# see feedback_plugin_install_name_vs_runtime_emission_string_2026_06_22.
|
|
27
|
+
TAPESTRY_PLUGINS = (
|
|
28
|
+
"tapestry-discipline@tapestry",
|
|
29
|
+
"tapestry-patterns@tapestry",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_claude_settings(project_dir: Path) -> None:
|
|
34
|
+
"""Ensure `.claude/settings.json` has both tapestry plugins enabled.
|
|
35
|
+
|
|
36
|
+
Idempotent: merges into existing `enabledPlugins` if the file is
|
|
37
|
+
present; creates a minimal one if absent. Other settings (permissions,
|
|
38
|
+
etc.) are preserved.
|
|
39
|
+
"""
|
|
40
|
+
settings_dir = project_dir / ".claude"
|
|
41
|
+
settings_dir.mkdir(exist_ok=True)
|
|
42
|
+
settings_path = settings_dir / "settings.json"
|
|
43
|
+
|
|
44
|
+
if settings_path.exists():
|
|
45
|
+
try:
|
|
46
|
+
existing = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
47
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
48
|
+
print(f" WARN: {settings_path} exists but couldn't be parsed ({e}).")
|
|
49
|
+
print(f" Skipping merge. Add the plugins manually:")
|
|
50
|
+
for plugin in TAPESTRY_PLUGINS:
|
|
51
|
+
print(f' "{plugin}": true')
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
enabled = existing.setdefault("enabledPlugins", {})
|
|
55
|
+
changed = False
|
|
56
|
+
for plugin in TAPESTRY_PLUGINS:
|
|
57
|
+
if enabled.get(plugin) is not True:
|
|
58
|
+
enabled[plugin] = True
|
|
59
|
+
changed = True
|
|
60
|
+
print(f" enabled {plugin} in {settings_path}")
|
|
61
|
+
if not changed:
|
|
62
|
+
print(f" both tapestry plugins already enabled in {settings_path}")
|
|
63
|
+
return
|
|
64
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n", encoding="utf-8")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Create fresh
|
|
68
|
+
config = {"enabledPlugins": {plugin: True for plugin in TAPESTRY_PLUGINS}}
|
|
69
|
+
settings_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
70
|
+
print(f" wrote {settings_path}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _print_operator_epilogue() -> None:
|
|
74
|
+
"""Print the 2 manual steps that can't be automated from CLI."""
|
|
75
|
+
print()
|
|
76
|
+
print("==> Two more steps (operator):")
|
|
77
|
+
print()
|
|
78
|
+
print(" 1. In Claude Code chat, register the tapestry marketplace + install the plugins:")
|
|
79
|
+
print()
|
|
80
|
+
print(" /plugin marketplace add Lizo-RoadTown/tapestry")
|
|
81
|
+
print(" /plugin install tapestry-discipline@tapestry")
|
|
82
|
+
print(" /plugin install tapestry-patterns@tapestry")
|
|
83
|
+
print()
|
|
84
|
+
print(" 2. Reload the IDE so the new hooks bind:")
|
|
85
|
+
print()
|
|
86
|
+
print(" VS Code: Cmd+Shift+P → 'Developer: Reload Window'")
|
|
87
|
+
print(" Claude Code CLI: exit + restart `claude` in this project")
|
|
88
|
+
print()
|
|
89
|
+
print("That's it. The SessionStart hook will auto-recall memories on next session start.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def add_arguments(p: argparse.ArgumentParser) -> None:
|
|
93
|
+
"""Same argument surface as `loom init` — delegated to it."""
|
|
94
|
+
init_cmd.add_arguments(p)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run(args: argparse.Namespace) -> int:
|
|
98
|
+
"""Run `loom init`, then write `.claude/settings.json`, then print operator steps."""
|
|
99
|
+
rc = init_cmd.run(args)
|
|
100
|
+
if rc != 0:
|
|
101
|
+
return rc
|
|
102
|
+
|
|
103
|
+
print()
|
|
104
|
+
print(f"--> [extra] Write .claude/settings.json with tapestry plugins enabled...")
|
|
105
|
+
_write_claude_settings(Path.cwd())
|
|
106
|
+
|
|
107
|
+
_print_operator_epilogue()
|
|
108
|
+
return 0
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tapestry-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tapestry command-line interface — onboard projects, inspect state, open the Observatory.
|
|
5
|
+
Author-email: Liz Osborn <lizocontactinfo@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Lizo-RoadTown/tapestry
|
|
8
|
+
Project-URL: Repository, https://github.com/Lizo-RoadTown/tapestry
|
|
9
|
+
Project-URL: Documentation, https://tapestry-khaki.vercel.app/
|
|
10
|
+
Keywords: tapestry,agents,coordination,observability,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# tapestry-cli
|
|
24
|
+
|
|
25
|
+
The command-line interface for [Tapestry](https://github.com/Lizo-RoadTown/tapestry) — project intelligence for AI-native teams.
|
|
26
|
+
|
|
27
|
+
Wire a project into the platform, then open the live console. Stdlib-only, no third-party dependencies, no model downloads — it works the moment `pip install` completes, on Windows, macOS, and Linux.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pipx install tapestry-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(or `pip install tapestry-cli`)
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
tapestry onboard Onboard the current directory and write
|
|
41
|
+
.claude/settings.json with the Tapestry plugins enabled
|
|
42
|
+
(the recommended quickstart path).
|
|
43
|
+
tapestry observatory Open the Observatory console in a browser.
|
|
44
|
+
tapestry init Register the project and write .env / .mcp.json /
|
|
45
|
+
.project-intelligence/ (granular path; does not touch
|
|
46
|
+
.claude/settings.json).
|
|
47
|
+
tapestry version Print version and platform info.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pipx install tapestry-cli
|
|
54
|
+
tapestry onboard
|
|
55
|
+
tapestry observatory
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The agents pick up memory, telemetry, and the discipline rules from there. See the [setup guide](https://tapestry-khaki.vercel.app/how-to/set-up-a-new-project/) for the full walkthrough.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
The CLI is URL-env-driven — no hardcoded hostnames. It reads:
|
|
63
|
+
|
|
64
|
+
- `LOOM_MEMORY_MCP_URL` / `LOOM_MEMORY_URL` — the memory MCP endpoint
|
|
65
|
+
- `LOOM_PROJECT_ID` — the project identifier
|
|
66
|
+
- `TAPESTRY_OBSERVATORY_URL` — override the Observatory URL opened by `tapestry observatory`
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
|
|
70
|
+
`tapestry` is the primary entry point; `loom` is kept as a deprecated alias during the transition. The Python package is named `tapestry_cli`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
*Provenance: lifted verbatim from `the-loom/tapestry-cli/` during the Tapestry monorepo migration (Step 5a, 2026-06-21) — the cross-platform replacement for the old PowerShell `new-loom-project.ps1` scaffolder.*
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tapestry_cli/__init__.py
|
|
4
|
+
tapestry_cli/__main__.py
|
|
5
|
+
tapestry_cli/cli.py
|
|
6
|
+
tapestry_cli/init.py
|
|
7
|
+
tapestry_cli/observatory.py
|
|
8
|
+
tapestry_cli/onboard.py
|
|
9
|
+
tapestry_cli.egg-info/PKG-INFO
|
|
10
|
+
tapestry_cli.egg-info/SOURCES.txt
|
|
11
|
+
tapestry_cli.egg-info/dependency_links.txt
|
|
12
|
+
tapestry_cli.egg-info/entry_points.txt
|
|
13
|
+
tapestry_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tapestry_cli
|