psyclaw 0.26.1 → 0.26.3

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.
@@ -8,13 +8,16 @@
8
8
  * 3. 3D 侧影电路方块字 (3D Isometric Shadow Block: ██████╗)
9
9
  * 4. 学术罗马衬线体 (Academic Roman Serif: ╔══╗)
10
10
  *
11
- * Paired with Kaomoji Academic Puppies (✨ / 💤) & 24-Bit TrueColor Gemini Aurora Gradient.
11
+ * Pets are opt-in via /pet and only render when the terminal is wide enough.
12
12
  */
13
- import { readFile, writeFile } from "node:fs/promises";
14
- import { dirname, join } from "node:path";
13
+ import { execFile } from "node:child_process";
14
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
15
+ import { randomUUID } from "node:crypto";
16
+ import { basename, dirname, join } from "node:path";
15
17
  import { fileURLToPath } from "node:url";
16
18
 
17
19
  const NAME = "PsyClaw";
20
+ const LOCKED_PI_VERSION = "0.84.1";
18
21
  // Retain the predecessor Pi profile so existing models, themes, packages, and
19
22
  // skills survive the product rename. PsyClaw's project data remains separate.
20
23
  const CONFIG_DIR = `.psy${"pi"}`;
@@ -54,7 +57,6 @@ const AURORA_STOPS = ["#38bdf8", "#60a5fa", "#818cf8", "#a855f7", "#ec4899", "#2
54
57
  // Colors
55
58
  const WHITE = "\\x1b[1m\\x1b[38;2;255;255;255m";
56
59
  const GLASSES = "\\x1b[1m\\x1b[38;2;56;189;248m";
57
- const ZZZ = "\\x1b[38;2;168;85;247m";
58
60
  const GOLD = "\\x1b[38;2;251;191;36m";
59
61
  const RESET = "\\x1b[0m";
60
62
 
@@ -62,37 +64,6 @@ const RESET = "\\x1b[0m";
62
64
  const PUP_SCHOLAR_TOP = `${WHITE}( ᐡ ${RESET}${GLASSES}⌐■-■${RESET}${WHITE} ᐡ )${RESET} ${GOLD}✨${RESET}`;
63
65
  const PUP_SCHOLAR_BOT = `${WHITE}c( づ 📜 づ )${RESET}`;
64
66
 
65
- const PUP_SLEEP_TOP = `${ZZZ}z z Z${RESET}`;
66
- const PUP_SLEEP_BOT = `${WHITE}( ᐡ ${RESET}${GLASSES}⌐■-■${RESET}${WHITE} ᐡ )${RESET} ${ZZZ}💤${RESET}`;
67
-
68
- const PUP_SCHOLAR_5ROW = [
69
- ` ${WHITE}/ᐢ⑅ᐢ/ ${RESET} ${GOLD}✨${RESET}`,
70
- ` ${WHITE}( ${RESET}${GLASSES}⌐■-■${RESET}${WHITE} )${RESET} `,
71
- ` c${WHITE}(づ 📜 づ${RESET}`,
72
- ` ${WHITE}( " " )${RESET}`,
73
- ` ${WHITE}∪-----∪${RESET} `,
74
- ];
75
-
76
- const PUP_SLEEPING_5ROW = [
77
- ` ${ZZZ}z z Z${RESET} `,
78
- ` ${WHITE}/ᐢ⑅ᐢ/ ${RESET}${ZZZ}💤${RESET} `,
79
- ` ${WHITE}( ${RESET}${GLASSES}⌐■-■${RESET}${WHITE} )~${RESET} `,
80
- `${WHITE}c( - ᴥ - )づ${RESET}`,
81
- ` ${WHITE}(______)'${RESET} `,
82
- ];
83
-
84
- const PUP_SCHOLAR_3ROW = [
85
- ` ${WHITE}/ᐢ⑅ᐢ/ ${RESET} ${GOLD}✨${RESET}`,
86
- ` ${WHITE}( ${RESET}${GLASSES}⌐■-■${RESET}${WHITE} )${RESET} `,
87
- ` c${WHITE}(づ 📜 づ${RESET}`,
88
- ];
89
-
90
- const PUP_SLEEPING_3ROW = [
91
- ` ${ZZZ}z z Z${RESET} `,
92
- ` ${WHITE}/ᐢ⑅ᐢ/ ${RESET}${ZZZ}💤${RESET} `,
93
- ` ${WHITE}( ${RESET}${GLASSES}⌐■-■${RESET}${WHITE} )~${RESET} `,
94
- ];
95
-
96
67
  // 1. 紧凑型对称衬线 I 方块字 (2-Row Compact)
97
68
  const FONT_2ROW_ROW0 = "█▀█ █▀▀ █ █ █▀▀ █ █▀█ █ █";
98
69
  const FONT_2ROW_ROW1 = "█▀▀ ▄██ █ █▄▄ █▄▄ ▄█▀ █▀█";
@@ -109,7 +80,13 @@ const FONT_SLANT_CLASSIC = [
109
80
  ];
110
81
 
111
82
  // 3. 3D 侧影电路方块 (3D Isometric Shadow Block - 5-Row)
112
- const FONT_3D_BLOCK = FONT_SLANT_CLASSIC;
83
+ const FONT_3D_BLOCK = [
84
+ " ██████╗ ███████╗██╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
85
+ " ██╔══██╗██╔════╝╚██╗ ██╔╝██╔════╝██║ ██╔══██╗██║ ██║",
86
+ " ██████╔╝███████╗ ╚████╔╝ ██║ ██║ ███████║██║ █╗ ██║",
87
+ " ██╔═══╝ ╚════██║ ╚██╔╝ ██║ ██║ ██╔══██║██║███╗██║",
88
+ " ██║ ███████║ ██║ ╚██████╗███████╗██║ ██║╚███╔███╔╝",
89
+ ];
113
90
 
114
91
  // 4. 学术罗马衬线体 (Academic Roman Serif - 3-Row)
115
92
  const FONT_SERIF = [
@@ -129,46 +106,57 @@ function buildCustomVariant(fontLines, dogLines = []) {
129
106
  return `[\n${rows.join(",\n")}\n ]`;
130
107
  }
131
108
 
132
- // 组合变体
133
- const VAR_COMPACT_SCHOLAR = `[
134
- \` \\x1b[1m${GRAD_2ROW_0}\\x1b[0m \\x1b[38;2;100;116;139m·\\x1b[0m \\x1b[1m${gradientText("P S Y C L A W", ["#38bdf8", "#818cf8", "#2dd4bf"])}\\x1b[0m \\x1b[38;2;148;163;184mv\${process.env.PSYCLAW_VERSION ?? this.version}\\x1b[0m ${PUP_SCHOLAR_TOP}\`,
135
- \` \\x1b[1m${GRAD_2ROW_1}\\x1b[0m \\x1b[38;2;148;163;184mEvidence-Grounded Social-Science Research Agent\\x1b[0m ${PUP_SCHOLAR_BOT}\`
109
+ // Every default variant is pet-free. Pet variants are selected only when
110
+ // PSYCLAW_PET=1 and the complete row fits the current terminal.
111
+ const VAR_COMPACT = `[
112
+ \` \\x1b[1m${GRAD_2ROW_0}\\x1b[0m \\x1b[38;2;148;163;184mv\${process.env.PSYCLAW_VERSION ?? this.version}\\x1b[0m\`,
113
+ \` \\x1b[1m${GRAD_2ROW_1}\\x1b[0m\`
136
114
  ]`;
137
-
138
- const VAR_COMPACT_SLEEP = `[
139
- \` \\x1b[1m${GRAD_2ROW_0}\\x1b[0m \\x1b[38;2;100;116;139m·\\x1b[0m \\x1b[1m${gradientText("P S Y C L A W", ["#38bdf8", "#818cf8", "#2dd4bf"])}\\x1b[0m \\x1b[38;2;148;163;184mv\${process.env.PSYCLAW_VERSION ?? this.version}\\x1b[0m ${PUP_SLEEP_TOP}\`,
140
- \` \\x1b[1m${GRAD_2ROW_1}\\x1b[0m \\x1b[38;2;148;163;184mEvidence-Grounded Social-Science Research Agent\\x1b[0m ${PUP_SLEEP_BOT}\`
115
+ const VAR_COMPACT_PET = `[
116
+ \` \\x1b[1m${GRAD_2ROW_0}\\x1b[0m ${PUP_SCHOLAR_TOP}\`,
117
+ \` \\x1b[1m${GRAD_2ROW_1}\\x1b[0m ${PUP_SCHOLAR_BOT}\`
141
118
  ]`;
142
-
143
- const VAR_SLANT_CLASSIC_SCHOLAR = buildCustomVariant(FONT_SLANT_CLASSIC, PUP_SCHOLAR_5ROW);
144
- const VAR_SLANT_CLASSIC_SLEEP = buildCustomVariant(FONT_SLANT_CLASSIC, PUP_SLEEPING_5ROW);
145
- const VAR_3D_BLOCK_SCHOLAR = buildCustomVariant(FONT_3D_BLOCK, PUP_SCHOLAR_5ROW);
146
- const VAR_3D_BLOCK_SLEEP = buildCustomVariant(FONT_3D_BLOCK, PUP_SLEEPING_5ROW);
147
- const VAR_SERIF_SCHOLAR = buildCustomVariant(FONT_SERIF, PUP_SCHOLAR_3ROW);
148
- const VAR_SERIF_SLEEP = buildCustomVariant(FONT_SERIF, PUP_SLEEPING_3ROW);
149
- const VAR_SLANT_CLASSIC_NARROW = buildCustomVariant(FONT_SLANT_CLASSIC);
150
- const VAR_SERIF_NARROW = buildCustomVariant(FONT_SERIF);
151
-
119
+ const VAR_SLANT_CLASSIC = buildCustomVariant(FONT_SLANT_CLASSIC);
120
+ const VAR_3D_BLOCK = buildCustomVariant(FONT_3D_BLOCK);
121
+ const VAR_SERIF = buildCustomVariant(FONT_SERIF);
152
122
  const GRAD_PIPELINE = "\\x1b[38;2;45;212;191m• Pipeline :\\x1b[0m \\x1b[1m\\x1b[38;2;56;189;248mintake\\x1b[0m \\x1b[38;2;99;102;241m➔\\x1b[0m \\x1b[1m\\x1b[38;2;129;140;248mcapture\\x1b[0m \\x1b[38;2;168;85;247m➔\\x1b[0m \\x1b[1m\\x1b[38;2;168;85;247mcitation-audit\\x1b[0m \\x1b[38;2;236;72;153m➔\\x1b[0m \\x1b[1m\\x1b[38;2;45;212;191mbrief\\x1b[0m";
153
123
  const GRAD_PROTOCOL = "\\x1b[38;2;45;212;191m• Protocol :\\x1b[0m \\x1b[1m\\x1b[38;2;52;211;153m✔ 2+ Source Cross-Check\\x1b[0m \\x1b[38;2;100;116;139m·\\x1b[0m \\x1b[1m\\x1b[38;2;45;212;191m✔ SHA-256 Provenance Ledger\\x1b[0m";
154
124
  const GRAD_WORKBENCH = "\\x1b[38;2;45;212;191m• Workbench :\\x1b[0m \\x1b[1m\\x1b[38;2;56;189;248mhttp://127.0.0.1:3721\\x1b[0m \\x1b[38;2;168;85;247m(Interactive Panel Active)\\x1b[0m";
155
125
 
156
- const PSYCLAW_DYNAMIC_BANNER = `const wideBannerVariants = [
157
- ${VAR_COMPACT_SCHOLAR}, ${VAR_COMPACT_SLEEP},
158
- ${VAR_SLANT_CLASSIC_SCHOLAR}, ${VAR_SLANT_CLASSIC_SLEEP},
159
- ${VAR_3D_BLOCK_SCHOLAR}, ${VAR_3D_BLOCK_SLEEP},
160
- ${VAR_SERIF_SCHOLAR}, ${VAR_SERIF_SLEEP},
126
+ const PSYCLAW_DYNAMIC_BANNER = `const bannerCandidates = [
127
+ ${VAR_COMPACT}, ${VAR_SLANT_CLASSIC}, ${VAR_3D_BLOCK}, ${VAR_SERIF},
161
128
  ];
162
- const narrowBannerVariants = [${VAR_SLANT_CLASSIC_NARROW}, ${VAR_SERIF_NARROW}];
163
- const BANNER_VARIANTS = (process.stdout.columns ?? 80) >= 96 ? wideBannerVariants : narrowBannerVariants;
164
- const chosenBanner = BANNER_VARIANTS[Math.floor(Math.random() * BANNER_VARIANTS.length)];
129
+ const compactBanner = ${VAR_COMPACT};
130
+ const petBanner = ${VAR_COMPACT_PET};
131
+ const terminalColumns = process.stdout.columns ?? 80;
132
+ const ansiPattern = /\\x1b\\[[0-9;]*m/g;
133
+ const cellWidth = (text) => Array.from(text.replace(ansiPattern, "")).reduce((width, char) => {
134
+ const code = char.codePointAt(0) ?? 0;
135
+ const wide = code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a ||
136
+ (code >= 0x2e80 && code <= 0xa4cf) || (code >= 0xac00 && code <= 0xd7a3) ||
137
+ (code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe10 && code <= 0xfe6f) ||
138
+ (code >= 0xff00 && code <= 0xff60) || (code >= 0x1f300 && code <= 0x1faff));
139
+ return width + (wide ? 2 : 1);
140
+ }, 0);
141
+ const bannerWidth = (rows) => Math.max(...rows.map(cellWidth));
142
+ const fittingBanners = bannerCandidates.filter((rows) => bannerWidth(rows) <= terminalColumns - 4);
143
+ const tinyBanner = [terminalColumns >= 22
144
+ ? \` ψ PsyClaw v\${process.env.PSYCLAW_VERSION ?? this.version}\`
145
+ : " PsyClaw"];
146
+ const defaultPool = fittingBanners.length > 0 ? fittingBanners : [tinyBanner];
147
+ const chosenBanner = process.env.PSYCLAW_PET === "1" && bannerWidth(petBanner) <= terminalColumns - 4
148
+ ? petBanner
149
+ : defaultPool[Math.floor(Math.random() * defaultPool.length)];
150
+ const detailLines = terminalColumns >= 86 ? [
151
+ \` ${GRAD_PIPELINE}\`,
152
+ \` ${GRAD_PROTOCOL}\`,
153
+ ] : [];
154
+ if (terminalColumns >= 76) detailLines.push(\` ${GRAD_WORKBENCH}\`);
165
155
  const logo = [
166
156
  "",
167
157
  ...chosenBanner,
168
158
  "",
169
- \` ${GRAD_PIPELINE}\`,
170
- \` ${GRAD_PROTOCOL}\`,
171
- \` ${GRAD_WORKBENCH}\`,
159
+ ...detailLines,
172
160
  "",
173
161
  ].join("\\n");`;
174
162
 
@@ -230,6 +218,9 @@ const MODE_PATCHES = [
230
218
 
231
219
  async function patchPackageJson(pkgPath) {
232
220
  const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
221
+ if (pkg.version !== LOCKED_PI_VERSION) {
222
+ throw new Error(`Unsupported Pi runtime ${String(pkg.version)}; expected ${LOCKED_PI_VERSION}`);
223
+ }
233
224
  const existing = pkg.piConfig && typeof pkg.piConfig === "object" && !Array.isArray(pkg.piConfig)
234
225
  ? pkg.piConfig
235
226
  : {};
@@ -237,20 +228,51 @@ async function patchPackageJson(pkgPath) {
237
228
  return { applied: false };
238
229
  }
239
230
  pkg.piConfig = { ...existing, name: NAME, configDir: CONFIG_DIR };
240
- await writeFile(pkgPath, `${JSON.stringify(pkg, null, "\t")}\n`, "utf8");
231
+ await atomicReplace(pkgPath, `${JSON.stringify(pkg, null, "\t")}\n`);
241
232
  return { applied: true };
242
233
  }
243
234
 
235
+ async function atomicReplace(path, content, checkJavaScript = false) {
236
+ const suffix = `${process.pid}.${randomUUID()}`;
237
+ const temporary = join(dirname(path), `.${basename(path)}.${suffix}.tmp${checkJavaScript ? ".mjs" : ""}`);
238
+ const backup = join(dirname(path), `.${basename(path)}.${suffix}.bak`);
239
+ let backedUp = false;
240
+ await writeFile(temporary, content, { encoding: "utf8", flag: "wx" });
241
+ try {
242
+ if (checkJavaScript) {
243
+ await new Promise((resolve, reject) => {
244
+ execFile(process.execPath, ["--check", temporary], { windowsHide: true }, (error) => error ? reject(error) : resolve());
245
+ });
246
+ }
247
+ try { await rename(temporary, path); }
248
+ catch (error) {
249
+ if (!new Set(["EEXIST", "EPERM", "EACCES"]).has(error?.code)) throw error;
250
+ await rename(path, backup);
251
+ backedUp = true;
252
+ try { await rename(temporary, path); }
253
+ catch (replaceError) {
254
+ await rename(backup, path).catch(() => undefined);
255
+ throw replaceError;
256
+ }
257
+ }
258
+ } finally {
259
+ await rm(temporary, { force: true }).catch(() => undefined);
260
+ if (backedUp) await rm(backup, { force: true }).catch(() => undefined);
261
+ }
262
+ }
263
+
244
264
  async function applyModePatches(modePath) {
245
265
  let content = await readFile(modePath, "utf8");
246
266
  const applied = [];
247
267
 
248
268
  // Replace the entire generated block. The greedy range also repairs a module
249
269
  // that a previous build appended to, instead of declaring the banner twice.
250
- const dynamicBannerRegex = /(?:const (?:wideBannerVariants|BANNER_VARIANTS)[\s\S]*const logo = \[[\s\S]*?\]\.join\("\\n"\);|const logo = (?:`[\s\S]*?`|\[[\s\S]*?\]\.join\("\\n"\)|theme\.bold[\s\S]*?);)/;
251
- const currentMatch = content.match(dynamicBannerRegex);
270
+ const dynamicBannerRegex = /(?:const (?:bannerCandidates|wideBannerVariants|BANNER_VARIANTS)[\s\S]*?const logo = \[[\s\S]*?\]\.join\("\\n"\);|const logo = (?:`[\s\S]*?`|\[[\s\S]*?\]\.join\("\\n"\)|theme\.bold[^;]*);)/g;
271
+ const matches = [...content.matchAll(dynamicBannerRegex)];
272
+ if (matches.length !== 1) throw new Error(`Expected exactly one Pi startup banner, found ${matches.length}`);
273
+ const currentMatch = matches[0];
252
274
  if (currentMatch && currentMatch[0] !== PSYCLAW_DYNAMIC_BANNER) {
253
- content = content.replace(dynamicBannerRegex, PSYCLAW_DYNAMIC_BANNER);
275
+ content = `${content.slice(0, currentMatch.index)}${PSYCLAW_DYNAMIC_BANNER}${content.slice(currentMatch.index + currentMatch[0].length)}`;
254
276
  applied.push("const logo = ... [Updated Curated 4-Style psyclaw Banner Rotation Pool]");
255
277
  }
256
278
 
@@ -262,7 +284,7 @@ async function applyModePatches(modePath) {
262
284
  }
263
285
  }
264
286
  if (applied.length > 0) {
265
- await writeFile(modePath, content, "utf8");
287
+ await atomicReplace(modePath, content, true);
266
288
  }
267
289
  return applied;
268
290
  }