pi-simocracy 0.8.0 → 0.8.1

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.
@@ -31,13 +31,14 @@ changes. Zero new Simocracy lexicons.**
31
31
 
32
32
  ## Why no lexicon change
33
33
 
34
- The impactindexer namespace is owned by the GainForest hyperindexer
35
- project, not Simocracy — adding a `sim` StrongRef field to
36
- `org.impactindexer.review.comment` would couple two independent release
37
- cycles together for one cross-app feature. And ATProto records *are*
38
- extensible by structure (unknown fields are preserved by the indexer,
39
- ignored by old readers), but baking a Simocracy-specific concept into
40
- an impact-review lexicon mixes concerns badly.
34
+ The `org.impactindexer.*` namespace is owned by the upstream
35
+ hypercerts-org project, not Simocracy — adding a `sim` StrongRef
36
+ field to `org.impactindexer.review.comment` would couple two
37
+ independent release cycles together for one cross-app feature. And
38
+ ATProto records *are* extensible by structure (unknown fields are
39
+ preserved by the indexer, ignored by old readers), but baking a
40
+ Simocracy-specific concept into an impact-review lexicon mixes
41
+ concerns badly.
41
42
 
42
43
  The `org.simocracy.history` lexicon already has every field we need:
43
44
 
@@ -18,8 +18,10 @@ When pi submits a proposal on behalf of a loaded sim, it writes
18
18
  exact shape simocracy.org's `ProposalFormDialog` already writes
19
19
  today (`title`, `shortDescription`, optional `description` /
20
20
  `workScope` / `contributors` / `image`, `createdAt`). Old readers
21
- (Hyperindexer, the existing webapp) see this as a regular
22
- user-authored proposal graceful degradation.
21
+ that don't know about the history sidecar (Bluesky AppView,
22
+ third-party hypercerts consumers, the upstream Hyperindex network at
23
+ `api.hi.gainforest.app`) see this as a regular user-authored
24
+ proposal — graceful degradation.
23
25
  2. **`org.simocracy.proposalContext`** — sidecar binding the proposal
24
26
  to its parent gathering (StrongRef to `org.simocracy.gathering`)
25
27
  or to a Frontier Tower SF floor (`floorNumber` integer). Required
@@ -46,11 +48,12 @@ its parent.
46
48
 
47
49
  ## Why no lexicon change
48
50
 
49
- The `org.hypercerts.*` namespace is owned by the GainForest
50
- hyperindexer project, not Simocracy — extending
51
- `org.hypercerts.claim.activity` with a `sim` StrongRef field would
52
- couple two independent release cycles together for one cross-app
53
- feature, and the same argument made for comments
51
+ The `org.hypercerts.*` namespace is owned by the
52
+ [`hypercerts-org/hypercerts-lexicon`](https://github.com/hypercerts-org/hypercerts-lexicon)
53
+ project, not Simocracy — extending `org.hypercerts.claim.activity`
54
+ with a `sim` StrongRef field would couple two independent release
55
+ cycles together for one cross-app feature, and the same argument
56
+ made for comments
54
57
  ([`SIM_AUTHORED_COMMENTS.md`](./SIM_AUTHORED_COMMENTS.md)) applies
55
58
  verbatim here.
56
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-simocracy",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Pi extension: load a Simocracy sim into your chat — see its pixel-art sprite render inline in the terminal and roleplay with it.",
5
5
  "type": "module",
6
6
  "author": "David Dao <david@gainforest.earth> (https://github.com/daviddao)",
@@ -52,11 +52,23 @@ export async function runLogin(
52
52
  handle = prompt.trim().replace(/^@/, "");
53
53
  }
54
54
 
55
+ const { CALLBACK_PORT } = await import("./callback-server.ts");
56
+ const isRemote = Boolean(
57
+ process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY,
58
+ );
59
+
55
60
  ctx.ui.notify(
56
- `Signing in with ATProto / Bluesky as @${handle}. Starting loopback OAuth flow on 127.0.0.1:53682… (this is NOT Anthropic auth — pi's built-in /login does that.)`,
61
+ `Signing in with ATProto / Bluesky as @${handle}. Starting loopback OAuth flow on 127.0.0.1:${CALLBACK_PORT}… (this is NOT Anthropic auth — pi's built-in /login does that.)`,
57
62
  "info",
58
63
  );
59
64
 
65
+ if (isRemote) {
66
+ ctx.ui.notify(
67
+ `Detected SSH session. The OAuth redirect goes to 127.0.0.1:${CALLBACK_PORT}, which is THIS remote host's loopback — your local browser can't reach it directly. Open another terminal on your laptop and run: ssh -L ${CALLBACK_PORT}:127.0.0.1:${CALLBACK_PORT} <user>@<this-host> then keep that session open and continue the sign-in in your browser. (VS Code / Cursor Remote: add port ${CALLBACK_PORT} in the Ports panel.) If port ${CALLBACK_PORT} is taken locally, set PI_SIMOCRACY_OAUTH_PORT to a free port on both sides.`,
68
+ "info",
69
+ );
70
+ }
71
+
60
72
  let callback: Awaited<ReturnType<typeof startCallbackServer>>;
61
73
  try {
62
74
  callback = await startCallbackServer();
package/src/index.ts CHANGED
@@ -536,7 +536,7 @@ async function loadSimByName(query: string): Promise<{
536
536
  // it into something actionable.
537
537
  const friendly =
538
538
  msg === "fetch failed" || msg.includes("fetch failed")
539
- ? "could not reach the Simocracy indexer at simocracy-indexer-production.up.railway.app — check your internet connection"
539
+ ? "could not reach the Simocracy indexer at simocracy-indexer.gainforest.id — check your internet connection"
540
540
  : msg;
541
541
  return { matches: [], error: `Indexer search failed: ${friendly}` };
542
542
  }
package/src/lookup.ts CHANGED
@@ -5,11 +5,18 @@
5
5
  * (`lookupRecord`) handles every kind the LLM might want to inspect:
6
6
  * sims, proposals, gatherings, decisions, and individual comments.
7
7
  *
8
- * Two indexers are queried (Simocracy + Hyperindexer) plus the
8
+ * Reads go through one indexer (`simocracy-indexer`) plus the
9
9
  * owner's PDS for direct AT-URI lookups. Sim-attribution for
10
10
  * comments is joined client-side from `org.simocracy.history`
11
11
  * records — same pattern simocracy-v2's notifications system uses.
12
12
  * See `docs/SIM_AUTHORED_COMMENTS.md` for the full design.
13
+ *
14
+ * Pre-2026-05-10 this fanned out across two indexers (Simocracy +
15
+ * Hyperindex). The single-indexer migration
16
+ * (https://pi-eval.vercel.app/reports/simocracy-indexer/drop-hyperindex-migration/)
17
+ * folded `org.hypercerts.*` / `org.impactindexer.*` / `app.certified.*`
18
+ * into the same Simocracy indexer, so this file collapsed to a single
19
+ * URL.
13
20
  */
14
21
 
15
22
  import {
@@ -21,9 +28,6 @@ import {
21
28
  SIMOCRACY_INDEXER_URL,
22
29
  } from "./simocracy.ts";
23
30
 
24
- /** Hyperindexer base URL — handles `org.hypercerts.*` and `org.impactindexer.*`. */
25
- const HYPERINDEXER_URL = "https://api.hi.gainforest.app";
26
-
27
31
  const COLLECTION_SIM = "org.simocracy.sim";
28
32
  const COLLECTION_PROPOSAL = "org.hypercerts.claim.activity";
29
33
  const COLLECTION_GATHERING = "org.simocracy.gathering";
@@ -55,10 +59,16 @@ const KIND_BY_COLLECTION: Record<string, Exclude<LookupKind, "auto">> = {
55
59
  [COLLECTION_COMMENT]: "comment",
56
60
  };
57
61
 
58
- /** Which indexer hosts which collection. */
59
- function indexerForCollection(collection: string): string {
60
- if (collection.startsWith("org.simocracy.")) return SIMOCRACY_INDEXER_URL;
61
- return HYPERINDEXER_URL;
62
+ /**
63
+ * Which indexer hosts which collection.
64
+ *
65
+ * Trivial post-migration — every collection we read lives on the
66
+ * same indexer. Kept as a helper so the call sites read clearly
67
+ * and so a future split (if one ever shows up) only needs to edit
68
+ * one place.
69
+ */
70
+ function indexerForCollection(_collection: string): string {
71
+ return SIMOCRACY_INDEXER_URL;
62
72
  }
63
73
 
64
74
  interface GraphQLNode {
@@ -82,6 +92,8 @@ async function fetchRecordsFromIndexer(
82
92
  collection: string,
83
93
  first: number,
84
94
  ): Promise<GraphQLNode[]> {
95
+ // indexerForCollection returns the same URL for everything
96
+ // post-2026-05-10; the helper survives in case a future split needs it.
85
97
  const url = `${indexerForCollection(collection).replace(/\/+$/, "")}/graphql`;
86
98
  const res = await fetch(url, {
87
99
  method: "POST",
package/src/simocracy.ts CHANGED
@@ -6,7 +6,12 @@
6
6
  * - Resolves blob URLs through the owning DID's PDS.
7
7
  */
8
8
 
9
- const DEFAULT_INDEXER_URL = "https://simocracy-indexer-production.up.railway.app";
9
+ // Canonical Simocracy indexer endpoint. The legacy Railway deployment at
10
+ // `simocracy-indexer-production.up.railway.app` is being phased out — the
11
+ // active instance is bumi-0 (Mac Mini, Zurich) behind a Cloudflare Tunnel.
12
+ // Override via SIMOCRACY_INDEXER_URL if needed. Mirrors simocracy-v2's
13
+ // lib/indexer-utils.ts DEFAULT_INDEXER_URL.
14
+ const DEFAULT_INDEXER_URL = "https://simocracy-indexer.gainforest.id";
10
15
  const COLLECTION_SIM = "org.simocracy.sim";
11
16
  const COLLECTION_AGENTS = "org.simocracy.agents";
12
17
  const COLLECTION_STYLE = "org.simocracy.style";
package/src/writes.ts CHANGED
@@ -433,16 +433,36 @@ export async function createProposal(opts: {
433
433
  shortDescription: shortDescription.slice(0, 300),
434
434
  createdAt: new Date().toISOString(),
435
435
  };
436
+ // `description` and `workScope` are UNION types in the
437
+ // org.hypercerts.claim.activity lexicon — they MUST be wrapped objects
438
+ // with a `$type` discriminator. Plain strings are rejected by lex-gql
439
+ // (silently drops the record from the indexer). Same applies to each
440
+ // `contributorIdentity` (also a union).
436
441
  if (opts.description !== undefined) {
437
442
  const body = opts.description.trim();
438
- if (body) record.description = body;
443
+ if (body) {
444
+ record.description = {
445
+ $type: "org.hypercerts.defs#descriptionString",
446
+ value: body,
447
+ };
448
+ }
439
449
  }
440
450
  if (opts.workScope !== undefined) {
441
451
  const ws = opts.workScope.trim();
442
- if (ws) record.workScope = ws;
452
+ if (ws) {
453
+ record.workScope = {
454
+ $type: "org.hypercerts.claim.activity#workScopeString",
455
+ scope: ws,
456
+ };
457
+ }
443
458
  }
444
459
  if (opts.contributors && opts.contributors.length > 0) {
445
- record.contributors = opts.contributors;
460
+ record.contributors = opts.contributors.map((c) => ({
461
+ contributorIdentity: {
462
+ $type: "org.hypercerts.claim.activity#contributorIdentity",
463
+ identity: c.contributorIdentity,
464
+ },
465
+ }));
446
466
  }
447
467
  if (opts.image) record.image = opts.image;
448
468
  const res = await opts.agent.com.atproto.repo.createRecord({