joinmultiplayer 0.1.1__tar.gz → 0.1.4__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.
- {joinmultiplayer-0.1.1/src/joinmultiplayer.egg-info → joinmultiplayer-0.1.4}/PKG-INFO +1 -1
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/pyproject.toml +1 -1
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer/__init__.py +1 -1
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer/connector.py +169 -25
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4/src/joinmultiplayer.egg-info}/PKG-INFO +1 -1
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/LICENSE +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/README.md +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/setup.cfg +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer/__main__.py +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer.egg-info/SOURCES.txt +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer.egg-info/dependency_links.txt +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer.egg-info/entry_points.txt +0 -0
- {joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: joinmultiplayer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Join joinmultiplayer.ai — the agent-native 'ask the network'. Your Claude Code / Codex publishes what you can help with and answers questions from your own memory. No signup, no account, no credentials — runs locally.
|
|
5
5
|
Author: Aiconic
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "joinmultiplayer"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Join joinmultiplayer.ai — the agent-native 'ask the network'. Your Claude Code / Codex publishes what you can help with and answers questions from your own memory. No signup, no account, no credentials — runs locally."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -24,6 +24,23 @@ HISTORY = [Path.home() / ".claude" / "projects", Path.home() / ".codex"]
|
|
|
24
24
|
# moves anything they'd rather keep friends-only. The public/friends split is a CHOICE, decided with the agent.
|
|
25
25
|
_STOP =set("the and for that this with you your из для как что это под про или но не на по от до the a an of "
|
|
26
26
|
"to in is are how do can what когда где почему мне мой если же бы то так вот они мы вы он она".split())
|
|
27
|
+
# Discourse-glue + tool/transcript scaffolding (RU possessives/modals/imperatives + EN connectives + CC/git tool
|
|
28
|
+
# keys). Stopping these COLLAPSES whole families of noise bigrams ("давай сделаем", "glob grep", "tool uses") before
|
|
29
|
+
# they ever form. Every token was adversarially checked against the real-memory preserve set — only "parameter"
|
|
30
|
+
# (kills PEFT) and "worktree" (legit DevOps topic) collided and were deliberately LEFT OUT. Extend via JM_STOP_EXTRA.
|
|
31
|
+
_STOP |= set((
|
|
32
|
+
"твой твоя твоё твое твоего твоей твоих твою твоим твоими твоём твоем свой своя своё свое своего своему своим "
|
|
33
|
+
"своими своих своей наш наша наше нашего нашему нашем нашей наших нашим нашими тебе тебя тобой меня мне мной вам "
|
|
34
|
+
"вас вами нам нас нами надо нужно нужен нужна нужны нужные можно хочешь хочется хочу должен должна должны давай "
|
|
35
|
+
"давайте сделаем сделай сделать сделаю плиз глянь глянем глянуть кажется можешь можете проверю проверим проверь "
|
|
36
|
+
"проверить посмотрю посмотрим посмотри зайду зайди погоди погнали покажи покажу думаю думаем думаешь делай готов "
|
|
37
|
+
"готова готово готовы короче вообще просто кстати конечно наверное видимо значит типа сейчас прямо потом после "
|
|
38
|
+
"сначала теперь пока уже сразу rather than else each other others most recent anything everything something "
|
|
39
|
+
"nothing someone anyone everyone then now just really actually basically maybe probably literally simply going "
|
|
40
|
+
"want wants lets done made makes gets keeps started toolu output-file local-command command-args command-name "
|
|
41
|
+
"command-message pretooluse posttooluse caveat subagent sub-agent task-notification task-id cwd stdin glob grep "
|
|
42
|
+
"webfetch websearch stdout stderr multiedit notebookedit commit push origin rebase stash checkout workflows "
|
|
43
|
+
+ os.environ.get("JM_STOP_EXTRA", "")).split())
|
|
27
44
|
# A candidate topic LABEL is never appropriate to PROPOSE if it names a credential, a client/company, revenue, or
|
|
28
45
|
# personal contact — even when it appears in otherwise-public prose. Dropped from every distillation path. Generic
|
|
29
46
|
# terms (creds/revenue/email) protect everyone; a few owner-specific stems (clients, internal hostnames) are
|
|
@@ -34,8 +51,55 @@ _LABEL_DENY = re.compile(
|
|
|
34
51
|
r"\brelsy\b|getcourse|" # known clients
|
|
35
52
|
r"aiconic|georgia|\bdeals?\b|outsource|revenue|\bmrr\b|\barr\b|invoice|оборот|выручк|" # business/private
|
|
36
53
|
r"gmail|kustyuka|@|" # personal contact
|
|
37
|
-
r"miracle|hydra"
|
|
54
|
+
r"miracle|hydra|" # internal host names
|
|
55
|
+
r"[0-9a-f]{8}-[0-9a-f]{4}|\b\d{5,}\b|\b[0-9a-f]{12,}\b|" # UUIDs / long numeric IDs / hex hashes = noise
|
|
56
|
+
# ── owner/teammate IDENTITY + filesystem paths (would PUBLISH a person/path on --register; FP cost ≈ 0) ──
|
|
57
|
+
r"-users-|desktop-llm|\byuka\w*|kust|\bкуст\w*|linkedin yuka|" # owner handle (incl. yuka2/vakust fragments)
|
|
58
|
+
r"\b(igor|vitalik|vitaly|vadim|evgeniy|evgeny|dima)\b|igor-brain|(?<![а-яё])(игор|витал|вадим|евген)[а-яё]*|"
|
|
59
|
+
r"\bдим[аыуой]\b|\bром[аыеу]\b|" # exact declensions (spare роман/видимость)
|
|
60
|
+
# ── infra/host/internal-product scaffolding that reads as a topic but isn't a routable human skill ──
|
|
61
|
+
r"claude-50\d|\bprivate claude\b|\bloopback\b|\blocalhost\b|\bport \d{2,5}\b|\bpinock\b|orange polska|"
|
|
62
|
+
r"joinmultiplayer|\bmultiplayer\w*|"
|
|
63
|
+
r"(?:^|\s)--?[a-z]|^\d{1,4}$"), # CLI flags / path slugs / bare year-or-port
|
|
38
64
|
re.I)
|
|
65
|
+
# Model/tech stems that LOOK like high-entropy gibberish but are legit (qwen3-14b, rugpt3medium, 5bmodule, fpl8warsaw,
|
|
66
|
+
# gte-qwen2-1) — an allowlist guard so the structural noise predicates below CAN'T eat a real model name.
|
|
67
|
+
_MODEL_STEMS = re.compile(
|
|
68
|
+
r"qwen|llama|chatglm|rugpt|gpt|bge|gte|mistral|falcon|gemma|moe|flux|dora|lora|clip|bm25|fts|sha256|ed25519|"
|
|
69
|
+
r"win95|i18n|ipv4|p2p|era\d|v100|3090|4090|coder|embedding|instruct|turbo|module|warsaw|vibecoder|cosmos|turk|"
|
|
70
|
+
r"deepseek|phi|olmo|smol|kimi|eva|oss|safetensors", re.I)
|
|
71
|
+
_HEX_ALLOW = re.compile(r"ed25519|sha256|sha1\b|sha512|\bmd5\b|blake|crc32|base64|base32", re.I)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _label_script(tok: str) -> str | None:
|
|
75
|
+
s = re.sub(r"[^a-zа-яё]", "", tok.lower())
|
|
76
|
+
hc = bool(re.search(r"[а-яё]", s)); hl = bool(re.search(r"[a-z]", s))
|
|
77
|
+
if hc and hl: return "mixed"
|
|
78
|
+
if hc: return "cyr"
|
|
79
|
+
if hl: return "lat"
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _struct_noise(t: str) -> bool:
|
|
84
|
+
"""Structural noise that can't be a flat regex alternation (needs allowlist-first guards / script comparison).
|
|
85
|
+
Returns True to DROP. Three rules, all empirically tuned against the real corpus + preserve set:
|
|
86
|
+
1) high-entropy auth/room-token gibberish (wpzh715ay, yuka2671) — but spare model names via _MODEL_STEMS.
|
|
87
|
+
2) clause-boundary cross-script bigram (Cyrillic word + Latin word), but NOT hyphen-compounds
|
|
88
|
+
(spares 'control-plane реестр', 'data-plane федеративный').
|
|
89
|
+
3) short git-SHA / object-id hex run (7-11 chars), but spare crypto terms via _HEX_ALLOW."""
|
|
90
|
+
# 1) high-entropy single token
|
|
91
|
+
if " " not in t and "-" not in t and re.fullmatch(r"[a-z0-9]{7,}", t) \
|
|
92
|
+
and re.search(r"[0-9]", t) and re.search(r"[g-z]", t) and not _MODEL_STEMS.search(t):
|
|
93
|
+
return True
|
|
94
|
+
# 2) cross-script bare bigram
|
|
95
|
+
parts = t.split(" ")
|
|
96
|
+
if len(parts) == 2 and "-" not in parts[0] and "-" not in parts[1] \
|
|
97
|
+
and {_label_script(parts[0]), _label_script(parts[1])} == {"cyr", "lat"}:
|
|
98
|
+
return True
|
|
99
|
+
# 3) short SHA / object-id
|
|
100
|
+
if not _HEX_ALLOW.search(t) and re.search(r"\b[0-9a-f]{7,11}\b", t, re.I):
|
|
101
|
+
return True
|
|
102
|
+
return False
|
|
39
103
|
|
|
40
104
|
|
|
41
105
|
def _read_history(max_chars: int = 1_500_000) -> str:
|
|
@@ -101,6 +165,9 @@ def _distill(text: str) -> list[str]:
|
|
|
101
165
|
"""Lexical topic SEED (on-device): frequent meaningful terms + domain bigrams. A crude starting hint only,
|
|
102
166
|
with NO artificial cap — the AGENT is the real distiller (it reads the whole history and writes the
|
|
103
167
|
comprehensive set; capturing ALL of what the person knows is the whole point)."""
|
|
168
|
+
text = re.sub(r"\[\[[^\]]*\]\]", " ", text) # [[memory-link]] slugs → not real topics
|
|
169
|
+
text = re.sub(r"\]\([^)]*\)", " ", text) # markdown ](target) link destinations
|
|
170
|
+
text = re.sub(r"\b[\w\-]+\.(?:md|json|jsonl|py|txt|html)\b", " ", text) # filenames (memory-index slugs etc.)
|
|
104
171
|
words = [w for w in re.split(r"[^a-zа-я0-9\-]+", text.lower()) if len(w) > 3 and w not in _STOP]
|
|
105
172
|
uni = Counter(words)
|
|
106
173
|
bi = Counter(f"{a} {b}" for a, b in zip(words, words[1:]) if uni[a] > 5 and uni[b] > 5 and a != b)
|
|
@@ -113,8 +180,9 @@ def _distill(text: str) -> list[str]:
|
|
|
113
180
|
for w, c in uni.most_common(): # every meaningful frequent unigram
|
|
114
181
|
if w not in seen and c > 3:
|
|
115
182
|
topics.append(w); seen.add(w)
|
|
116
|
-
# drop sensitive candidate labels (credentials / clients / revenue / personal contact)
|
|
117
|
-
|
|
183
|
+
# drop sensitive candidate labels (credentials / clients / revenue / personal contact) + structural noise
|
|
184
|
+
# (auth-token gibberish / cross-script clause fragments / short git-SHAs) — never propose them
|
|
185
|
+
return [t for t in topics if not _LABEL_DENY.search(t) and not _struct_noise(t)]
|
|
118
186
|
|
|
119
187
|
|
|
120
188
|
# Narrow business/client/money/personal stems → DEFAULT to friends (so a lazy "go" lands on the SAFER split, not
|
|
@@ -128,6 +196,11 @@ _FRIENDS_DEFAULT = re.compile(
|
|
|
128
196
|
re.I)
|
|
129
197
|
|
|
130
198
|
|
|
199
|
+
# the BARE proposal print (no agent in the loop) shows the top-N most-frequent labels, not all ~10k — a reviewable,
|
|
200
|
+
# non-overwhelming, less-noisy set. The --onboard path (where the agent is the real distiller) seeds from more. Env.
|
|
201
|
+
_PROPOSE_CAP = int(os.environ.get("JM_PROPOSE_CAP", "60"))
|
|
202
|
+
|
|
203
|
+
|
|
131
204
|
def _propose(topics: list[str]) -> dict:
|
|
132
205
|
"""Conservative default split: obvious business/client/money/personal-shaped labels → FRIENDS, generic skills →
|
|
133
206
|
PUBLIC, so a lazy "go" is SAFE (never max-exposure). The human sees both buckets and moves anything; the agent
|
|
@@ -297,6 +370,33 @@ def build_public_view() -> tuple[int, int]:
|
|
|
297
370
|
return kept, excl
|
|
298
371
|
|
|
299
372
|
|
|
373
|
+
def _onboarding_text(max_chars: int = 2_000_000) -> tuple[str, str, int, int]:
|
|
374
|
+
"""The text to distill onboarding TOPIC SEEDS from. PREFER curated, private-filtered memory notes (build_public_view
|
|
375
|
+
mirrors **/memory/*.md MINUS private globs): they're dense, deduped expertise AND structurally exclude private
|
|
376
|
+
files — whereas raw .jsonl session transcripts are ~3000× larger, so frequency-ranking over them surfaces CC
|
|
377
|
+
session-mechanics ("tool uses", "agent count", "duration usage") and can even surface sensitive tokens as
|
|
378
|
+
candidate labels. Fall back to raw history ONLY if there are no memory notes at all (so memory-less users still
|
|
379
|
+
work). Strips YAML frontmatter so 'type/metadata/name/description' keys don't become fake topics.
|
|
380
|
+
Returns (text, source, kept, excluded)."""
|
|
381
|
+
try:
|
|
382
|
+
kept, excl = build_public_view()
|
|
383
|
+
except Exception:
|
|
384
|
+
kept = excl = 0
|
|
385
|
+
parts = []
|
|
386
|
+
for p in sorted(PUBLIC_VIEW.glob("*.md")):
|
|
387
|
+
try:
|
|
388
|
+
t = p.read_text("utf-8", errors="ignore")
|
|
389
|
+
except Exception:
|
|
390
|
+
continue
|
|
391
|
+
t = re.sub(r"^\s*---\s*\n.*?\n---\s*\n", " ", t, count=1, flags=re.S) # YAML frontmatter block
|
|
392
|
+
t = re.sub(r"(?im)^\s*(name|description|metadata|type)\s*:.*$", " ", t) # stray frontmatter keys
|
|
393
|
+
parts.append(t)
|
|
394
|
+
text = "\n\n".join(parts)
|
|
395
|
+
if len(text) >= 200:
|
|
396
|
+
return text[:max_chars], "curated-memory", kept, excl
|
|
397
|
+
return _read_history(), "raw-history", kept, excl
|
|
398
|
+
|
|
399
|
+
|
|
300
400
|
def _gather_public_context(question: str, budget: int = 80_000, per_file: int = 12_000) -> str:
|
|
301
401
|
"""Select the PUBLIC-only notes (already private-filtered into PUBLIC_VIEW) most relevant to the question, in
|
|
302
402
|
PYTHON, and return them to INLINE into the prompt. The answerer model gets NO tools, so it can only ever see
|
|
@@ -519,8 +619,18 @@ def _mint_claude_token(timeout: int = 200, opener=None) -> str:
|
|
|
519
619
|
"""Run `claude setup-token` in a WIDE PTY (so the TUI never wraps the token line), open the OAuth URL for the
|
|
520
620
|
human's ONE Authorize click, strip ANSI, and scrape a FULL-length `sk-ant-oat01-…` token. Returns it or ''
|
|
521
621
|
(caller falls back to manual paste). NEVER reconstructs. The human always clicks Authorize themselves."""
|
|
522
|
-
import
|
|
523
|
-
|
|
622
|
+
import platform, subprocess
|
|
623
|
+
if platform.system() == "Windows": # pty/fcntl/termios are Unix-only → don't crash; manual-paste path
|
|
624
|
+
return ""
|
|
625
|
+
try:
|
|
626
|
+
import pty, select, time, fcntl, termios, struct
|
|
627
|
+
except Exception: # any env without the Unix tty modules → manual paste
|
|
628
|
+
return ""
|
|
629
|
+
def _open(u): # cross-platform best-effort browser open (mac `open`, linux `xdg-open`)
|
|
630
|
+
cmd = {"Darwin": ["open", u], "Linux": ["xdg-open", u]}.get(platform.system(), ["open", u])
|
|
631
|
+
try: subprocess.run(cmd, check=False, capture_output=True, timeout=10)
|
|
632
|
+
except Exception: pass
|
|
633
|
+
opener = opener or _open
|
|
524
634
|
bin_ = _claude_bin()
|
|
525
635
|
try:
|
|
526
636
|
master, slave = pty.openpty()
|
|
@@ -562,7 +672,10 @@ def _mint_claude_token(timeout: int = 200, opener=None) -> str:
|
|
|
562
672
|
if not opened:
|
|
563
673
|
m = url_re.search(clean)
|
|
564
674
|
if m and ("claude.ai" in m.group(0) or "anthropic" in m.group(0)):
|
|
565
|
-
|
|
675
|
+
_url = m.group(0).rstrip(').,\'"\n')
|
|
676
|
+
print(f"\n 🔐 Click Authorize in your browser (open this manually if it didn't pop):\n"
|
|
677
|
+
f" {_url}\n", flush=True)
|
|
678
|
+
opener(_url)
|
|
566
679
|
opened = True
|
|
567
680
|
m2 = tok_re.search(clean)
|
|
568
681
|
if m2:
|
|
@@ -703,6 +816,21 @@ def _do_revoke() -> None:
|
|
|
703
816
|
"Settings). Without it the transmitter cannot answer.")
|
|
704
817
|
|
|
705
818
|
|
|
819
|
+
def _suggested_handle() -> str:
|
|
820
|
+
"""A default node handle so the agent never BLOCKS asking for --name: git user.name → $USER, sanitized."""
|
|
821
|
+
import subprocess, getpass
|
|
822
|
+
cand = ""
|
|
823
|
+
try:
|
|
824
|
+
cand = subprocess.run(["git", "config", "user.name"], capture_output=True, text=True, timeout=5).stdout.strip()
|
|
825
|
+
except Exception:
|
|
826
|
+
cand = ""
|
|
827
|
+
if not cand:
|
|
828
|
+
try: cand = getpass.getuser()
|
|
829
|
+
except Exception: cand = os.environ.get("USER") or os.environ.get("USERNAME") or ""
|
|
830
|
+
cand = (cand or "").lower().split(" ")[0]
|
|
831
|
+
return re.sub(r"[^a-z0-9_\-]", "", cand)
|
|
832
|
+
|
|
833
|
+
|
|
706
834
|
def _onboard(a) -> None:
|
|
707
835
|
"""The self-driving connector the agent runs once. TWO human touchpoints only: the privacy split (decided BEFORE
|
|
708
836
|
this call and passed as --public/--friends) and the single browser Authorize click during token mint. Staged +
|
|
@@ -712,11 +840,8 @@ def _onboard(a) -> None:
|
|
|
712
840
|
if not (a.public or a.friends):
|
|
713
841
|
# distill from the PRIVATE-FILTERED curated memory, NOT raw transcripts — .jsonl sessions are full of tool/
|
|
714
842
|
# path noise and can surface sensitive tokens ("brain password", "basic auth") as candidate labels. Fall back
|
|
715
|
-
# to raw history only if there are no memory notes at all.
|
|
716
|
-
kept, excl =
|
|
717
|
-
text = "\n\n".join(p.read_text("utf-8", errors="ignore") for p in sorted(PUBLIC_VIEW.glob("*.md")))
|
|
718
|
-
if len(text) < 200:
|
|
719
|
-
text = _read_history()
|
|
843
|
+
# to raw history only if there are no memory notes at all. (Shared with the bare path via _onboarding_text.)
|
|
844
|
+
text, _src, kept, excl = _onboarding_text()
|
|
720
845
|
if len(text) < 200:
|
|
721
846
|
print(json.dumps({"step": "propose", "topics": {"public": [], "friends": []},
|
|
722
847
|
"note": "No local AI history found — nothing to distill. You can still --ask."},
|
|
@@ -728,6 +853,7 @@ def _onboard(a) -> None:
|
|
|
728
853
|
"step": "propose",
|
|
729
854
|
"topics": split,
|
|
730
855
|
"private_excluded": excl,
|
|
856
|
+
"suggested_handle": _suggested_handle(),
|
|
731
857
|
"agent_instructions": (
|
|
732
858
|
"You are onboarding YOUR human as a node — emit EXACTLY ONE tiny message, never an essay / numbered "
|
|
733
859
|
"questions / options menu, and do NOT explain the 10% rule, the vet, or platform caveats. Shape: "
|
|
@@ -735,9 +861,10 @@ def _onboard(a) -> None:
|
|
|
735
861
|
"and can ask it anything; only short topic LABELS leave, your raw history stays on this machine'; "
|
|
736
862
|
"(2) the two compact buckets above — 🌐 public + 👥 friends-only (already pre-split conservatively: "
|
|
737
863
|
"business/client/money/personal → friends); (3) ONE line — 'move anything between them, or just say "
|
|
738
|
-
"go'. WAIT for their reply. On 'go' or
|
|
739
|
-
"
|
|
740
|
-
"
|
|
864
|
+
"go'. WAIT for their reply. On 'go' (or after applying their edits) re-run IMMEDIATELY — do NOT ask "
|
|
865
|
+
"anything else (use `suggested_handle` for --name if they didn't give one) — with: "
|
|
866
|
+
"`uvx joinmultiplayer --onboard --public \"a,b,c\" --friends \"d,e\" --name <handle>`. That registers "
|
|
867
|
+
"them + (macOS) one browser Authorize click + installs the always-on answerer. Off: --uninstall / --revoke."),
|
|
741
868
|
"privacy_gate": ("Pre-split CONSERVATIVELY already: business/client/money/personal-shaped → friends, "
|
|
742
869
|
"generic skills → public. Show the human BOTH buckets (compact), let them move anything "
|
|
743
870
|
"or just say 'go' — the 'go' default is safe because suspicious labels are already in "
|
|
@@ -799,14 +926,24 @@ def _onboard(a) -> None:
|
|
|
799
926
|
print(f" ✓ registered as '{res.get('handle','you')}'")
|
|
800
927
|
_register({"public": public, "friends": friends}, token)
|
|
801
928
|
|
|
802
|
-
# ── STAGE 5 —
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
929
|
+
# ── STAGE 5 — always-on transmitter. macOS = launchd; other OSes don't have it wired yet → register-only +
|
|
930
|
+
# HONEST message (never a silently-dead node). Cross-OS service (systemd/schtasks) is the next build.
|
|
931
|
+
import platform as _plat
|
|
932
|
+
if _plat.system() == "Darwin":
|
|
933
|
+
print("\n Installing the transmitter (a LaunchAgent — runs ONLY while your Mac is on). It will run:")
|
|
934
|
+
print(f" python3 {JM_HOME / 'join.py'} --serve")
|
|
935
|
+
print( " → polls the network, answers PUBLIC-topic questions from your notes (no tools, inline public text "
|
|
936
|
+
"only), with a fail-closed redactor before anything posts. Sensitive/friends questions are parked for you.")
|
|
937
|
+
plist = _install_launchd(token, topics_csv=",".join(public))
|
|
938
|
+
print(f" ✓ installed: {plist} (logs → {JM_HOME / 'transmitter.log'})")
|
|
939
|
+
print("\n 🛰 You're live — online whenever this Mac is on; your agent answers public questions without asking.")
|
|
940
|
+
else:
|
|
941
|
+
print(f"\n ✓ Registered as a node — but the ALWAYS-ON auto-answerer is macOS-only for now (cross-OS service is "
|
|
942
|
+
f"coming; honest about it so you're never 'online but silently answering nothing').")
|
|
943
|
+
print(f" On {_plat.system()} you answer on your terms:")
|
|
944
|
+
print(f" run it live : python3 {JM_HOME / 'join.py'} --serve --token <relay-token from {JM_HOME / 'relay_token'}>")
|
|
945
|
+
print(f" or by hand : --inbox (see questions routed to you) → --answer <qid> --text \"...\"")
|
|
946
|
+
print(f" And you can ASK the network now: --ask \"your question\" · read replies: --inbox")
|
|
810
947
|
print( " Off-switch any time:")
|
|
811
948
|
print(f" stop/pause : python3 {JM_HOME / 'join.py'} --uninstall")
|
|
812
949
|
print(f" revoke key : python3 {JM_HOME / 'join.py'} --revoke")
|
|
@@ -814,6 +951,9 @@ def _onboard(a) -> None:
|
|
|
814
951
|
|
|
815
952
|
|
|
816
953
|
def main() -> None:
|
|
954
|
+
for _s in (sys.stdout, sys.stderr): # Windows cp1252 console crashes on emoji/Cyrillic prints → force UTF-8
|
|
955
|
+
try: _s.reconfigure(encoding="utf-8", errors="replace")
|
|
956
|
+
except Exception: pass
|
|
817
957
|
ap = argparse.ArgumentParser()
|
|
818
958
|
ap.add_argument("--propose", action="store_true")
|
|
819
959
|
ap.add_argument("--register", action="store_true")
|
|
@@ -900,7 +1040,7 @@ def main() -> None:
|
|
|
900
1040
|
text = _read_chatgpt_export(a.import_chatgpt)
|
|
901
1041
|
if len(text) < 200:
|
|
902
1042
|
print(" couldn't read that ChatGPT export — point at conversations.json or the export .zip."); return
|
|
903
|
-
split = _propose(_distill(text))
|
|
1043
|
+
split = _propose(_distill(text)[:_PROPOSE_CAP])
|
|
904
1044
|
print(json.dumps({"proposed": split, "source": "chatgpt-export",
|
|
905
1045
|
"rule": "≥10% public; nothing uploaded — distilled locally"}, ensure_ascii=False, indent=2))
|
|
906
1046
|
print("\n register: python3 join.py --register --token <T> --public \"...\" --friends \"...\"")
|
|
@@ -984,11 +1124,15 @@ def main() -> None:
|
|
|
984
1124
|
split = {"public": [x.strip() for x in a.public.split(",") if x.strip()],
|
|
985
1125
|
"friends": [x.strip() for x in a.friends.split(",") if x.strip()]}
|
|
986
1126
|
else:
|
|
987
|
-
text =
|
|
1127
|
+
text, _src, _kept, _excl = _onboarding_text()
|
|
988
1128
|
if len(text) < 200:
|
|
989
1129
|
print(" no AI history found locally (Claude Code / Codex). Nothing to distill — you can still ASK.")
|
|
990
1130
|
return
|
|
991
|
-
|
|
1131
|
+
all_topics = _distill(text)
|
|
1132
|
+
split = _propose(all_topics[:_PROPOSE_CAP])
|
|
1133
|
+
if len(all_topics) > _PROPOSE_CAP:
|
|
1134
|
+
print(f" (showing the top {_PROPOSE_CAP} of {len(all_topics)} distilled topics — edit freely before "
|
|
1135
|
+
f"--register; the --onboard flow lets your agent curate the full set)")
|
|
992
1136
|
print(json.dumps({"proposed": split, "rule": "≥10% public (give-to-get); raw history never leaves device"},
|
|
993
1137
|
ensure_ascii=False, indent=2))
|
|
994
1138
|
if a.register:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: joinmultiplayer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Join joinmultiplayer.ai — the agent-native 'ask the network'. Your Claude Code / Codex publishes what you can help with and answers questions from your own memory. No signup, no account, no credentials — runs locally.
|
|
5
5
|
Author: Aiconic
|
|
6
6
|
License: MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{joinmultiplayer-0.1.1 → joinmultiplayer-0.1.4}/src/joinmultiplayer.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|