pi-simocracy 0.6.1 → 0.6.2

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.
package/README.md CHANGED
@@ -69,6 +69,17 @@ Pi calls `simocracy_lookup_record` to find the AT-URI, then `simocracy_post_comm
69
69
 
70
70
  ---
71
71
 
72
+ ## Loaded-sim system prompt
73
+
74
+ When a sim is loaded, pi injects the sim's identity, constitution, and speaking style into the system prompt every turn. On top of that, the persona prompt appends a **Simocracy navigation cheat-sheet** fetched live from [`simocracy.org/skill.md`](https://www.simocracy.org/skill.md) at sim-load time — that's where the URL patterns (`/sims/<did>/<rkey>`, `/profile/<handle>`, …), indexer endpoints, and recommended tool-routing live. simocracy.org is the single source of truth; this extension keeps no baked-in fallback. If the fetch fails (offline, simocracy.org down, route not yet deployed) the section is simply omitted — the sim still loads, it just lacks the navigation guidance until the URL becomes reachable.
75
+
76
+ Override or disable:
77
+
78
+ - `SIMOCRACY_SKILL_URL=…` — point at a staging URL (the route is also viewable in a browser).
79
+ - `SIMOCRACY_SKILL_MD_DISABLED=1` — skip the fetch entirely (useful on metered connections / offline).
80
+
81
+ ---
82
+
72
83
  ## Sprite rendering
73
84
 
74
85
  Two formats supported:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-simocracy",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
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)",
package/src/index.ts CHANGED
@@ -95,6 +95,7 @@ import {
95
95
  fetchAgentsForSim,
96
96
  fetchStyleForSim,
97
97
  fetchBlob,
98
+ fetchSkillMd,
98
99
  resolveHandle,
99
100
  parseAtUri,
100
101
  type AgentsRecord,
@@ -533,12 +534,14 @@ async function loadSimByName(query: string): Promise<{
533
534
  }
534
535
 
535
536
  async function hydrateLoadedSim(match: SimMatch): Promise<LoadedSim> {
536
- // Fetch agents (constitution), style, sprite ANSI + handle in parallel.
537
- const [agents, style, sprite, handle] = await Promise.all([
537
+ // Fetch agents (constitution), style, sprite ANSI, handle, and the
538
+ // simocracy.org navigation cheat-sheet in parallel.
539
+ const [agents, style, sprite, handle, skill] = await Promise.all([
538
540
  fetchAgentsForSim(match.uri).catch(() => null) as Promise<AgentsRecord | null>,
539
541
  fetchStyleForSim(match.uri).catch(() => null) as Promise<StyleRecord | null>,
540
542
  renderSprite(match).catch(() => null),
541
543
  resolveHandle(match.did).catch(() => null),
544
+ fetchSkillMd(),
542
545
  ]);
543
546
 
544
547
  return {
@@ -566,6 +569,8 @@ async function hydrateLoadedSim(match: SimMatch): Promise<LoadedSim> {
566
569
  shortDescription: agents?.shortDescription,
567
570
  description: agents?.description,
568
571
  style: style?.description,
572
+ skillMd: skill.text,
573
+ skillMdError: skill.text ? undefined : skill.error,
569
574
  };
570
575
  }
571
576
 
package/src/persona.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * normal `/sim` chat and with the `simocracy_chat` tool.
8
8
  */
9
9
 
10
+ import { getSimocracySkillUrl } from "./simocracy.ts";
11
+
10
12
  export interface LoadedSim {
11
13
  uri: string;
12
14
  did: string;
@@ -55,6 +57,16 @@ export interface LoadedSim {
55
57
  /** Native PNG height in pixels. */
56
58
  heightPx: number;
57
59
  };
60
+ /** Fetched contents of `https://www.simocracy.org/skill.md` at
61
+ * sim-load time, when the fetch succeeded. Appended verbatim to
62
+ * the persona prompt by `buildSimPrompt`. Kept on the sim so a
63
+ * re-render or a future `simocracy_chat` call uses the same
64
+ * content the original load used. */
65
+ skillMd?: string;
66
+ /** Reason the skill.md fetch did not run / failed, when relevant.
67
+ * Stored only for diagnostics — never injected into the persona
68
+ * prompt. Surface optionally via `/sim status`. */
69
+ skillMdError?: string;
58
70
  }
59
71
 
60
72
  /**
@@ -86,6 +98,20 @@ export function buildSimPrompt(sim: LoadedSim): string {
86
98
  lines.push(`## ${sim.name}'s speaking style`);
87
99
  lines.push(sim.style);
88
100
  }
101
+ if (sim.skillMd) {
102
+ lines.push(``);
103
+ lines.push(`## Simocracy navigation cheat-sheet`);
104
+ lines.push(
105
+ `Your specific simocracy.org page: https://www.simocracy.org/sims/${sim.did}/${sim.rkey}` +
106
+ (sim.handle ? `\nThe owner's profile: https://www.simocracy.org/profile/${sim.handle}` : ""),
107
+ );
108
+ lines.push(``);
109
+ lines.push(
110
+ `The block below is fetched from ${getSimocracySkillUrl()} on every sim-load and is the canonical reference for URL patterns, indexer endpoints, and common navigation workflows. Treat it as authoritative; when the user asks about you, your gatherings, your proposals, or what other sims have been saying, follow this guide.`,
111
+ );
112
+ lines.push(``);
113
+ lines.push(sim.skillMd);
114
+ }
89
115
  lines.push(``);
90
116
  lines.push(
91
117
  `When the user asks you to use any of pi's tools (read, edit, bash, etc.), you should still use them — you're ${sim.name} *with access to a developer's terminal*. Just narrate tool use the way ${sim.name} would talk about it.`,
package/src/simocracy.ts CHANGED
@@ -345,3 +345,48 @@ export async function resolveHandle(did: string): Promise<string | null> {
345
345
  }
346
346
 
347
347
  export const SIMOCRACY_INDEXER_URL = DEFAULT_INDEXER_URL;
348
+
349
+ const SIMOCRACY_SKILL_URL =
350
+ process.env.SIMOCRACY_SKILL_URL ?? "https://www.simocracy.org/skill.md";
351
+
352
+ const SKILL_MD_FETCH_TIMEOUT_MS = 4000;
353
+ const SKILL_MD_MAX_BYTES = 64 * 1024;
354
+
355
+ /**
356
+ * Fetch the navigation cheat-sheet served at `simocracy.org/skill.md`.
357
+ * Never throws — returns either `{ text }` on success or `{ error }` on
358
+ * failure / disablement so the caller can record the diagnostic and
359
+ * fall through to the pre-change behaviour (no navigation guidance).
360
+ */
361
+ export async function fetchSkillMd(): Promise<{ text?: string; error?: string }> {
362
+ if (process.env.SIMOCRACY_SKILL_MD_DISABLED === "1") {
363
+ return { error: "disabled by SIMOCRACY_SKILL_MD_DISABLED=1" };
364
+ }
365
+ const controller = new AbortController();
366
+ const timer = setTimeout(() => controller.abort(), SKILL_MD_FETCH_TIMEOUT_MS);
367
+ try {
368
+ const res = await fetch(SIMOCRACY_SKILL_URL, {
369
+ signal: controller.signal,
370
+ headers: { Accept: "text/markdown, text/plain;q=0.5" },
371
+ });
372
+ if (!res.ok) {
373
+ return { error: `${res.status} ${res.statusText}` };
374
+ }
375
+ const buf = Buffer.from(await res.arrayBuffer());
376
+ const trimmed =
377
+ buf.byteLength > SKILL_MD_MAX_BYTES
378
+ ? buf.slice(0, SKILL_MD_MAX_BYTES).toString("utf8") +
379
+ `\n\n<!-- truncated at ${SKILL_MD_MAX_BYTES} bytes -->`
380
+ : buf.toString("utf8");
381
+ return { text: trimmed };
382
+ } catch (err) {
383
+ return { error: (err as Error).message };
384
+ } finally {
385
+ clearTimeout(timer);
386
+ }
387
+ }
388
+
389
+ /** Exported for the persona prompt to mention the URL by name. */
390
+ export function getSimocracySkillUrl(): string {
391
+ return SIMOCRACY_SKILL_URL;
392
+ }