@evandu/super-pi 0.1.113 → 0.1.115

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.
Files changed (28) hide show
  1. package/README.md +16 -0
  2. package/bin/super-pi.js +95 -3
  3. package/dist/client/.super-pi-client-sources.json +3 -3
  4. package/dist/client/assets/{CodeMirrorEditor-moFTk5SE.js → CodeMirrorEditor-DNLm2zT6.js} +2 -2
  5. package/dist/client/assets/{dist-DQjJMys5.js → dist-2zLLUXCo.js} +1 -1
  6. package/dist/client/assets/{dist-BsO40qxQ.js → dist-4qEFEbMF.js} +1 -1
  7. package/dist/client/assets/{dist-DuWhzonJ.js → dist-BLldidtK.js} +1 -1
  8. package/dist/client/assets/{dist-DrbUrVWB.js → dist-BO4YrDmO.js} +1 -1
  9. package/dist/client/assets/{dist-DIfmMr8s.js → dist-BTsfS8CA.js} +1 -1
  10. package/dist/client/assets/{dist-Dnr5BxAi.js → dist-BVCGM9Bd.js} +1 -1
  11. package/dist/client/assets/{dist-BR4s0dNg.js → dist-B_N2ELl5.js} +1 -1
  12. package/dist/client/assets/{dist-TgNJUr7p.js → dist-BoV9nE13.js} +1 -1
  13. package/dist/client/assets/{dist-DPDGd2jl.js → dist-CEJcHfEn.js} +1 -1
  14. package/dist/client/assets/{dist-D9m04q-s.js → dist-CW83qn3P.js} +1 -1
  15. package/dist/client/assets/{dist-CCYnr6ul.js → dist-CezTi1ET.js} +1 -1
  16. package/dist/client/assets/{dist-BUuPRG15.js → dist-DszqHTui.js} +1 -1
  17. package/dist/client/assets/{dist-rhPxFuD1.js → dist-DuHJr7vQ.js} +1 -1
  18. package/dist/client/assets/{dist-Bds_PUYK.js → dist-JI_7suRx.js} +1 -1
  19. package/dist/client/assets/{dist-CL65aMXY.js → dist-pJBZJJnA.js} +1 -1
  20. package/dist/client/assets/{dist-DPvy57Ph.js → dist-zwqOqWyp.js} +1 -1
  21. package/dist/client/assets/{index-BixdV7aQ.js → index-CLoO6IHy.js} +2 -2
  22. package/dist/client/index.html +1 -1
  23. package/dist/extension.js +1 -1
  24. package/dist/server/application/conversation-service.js +8 -0
  25. package/dist/server/index.js +14 -4
  26. package/dist/server/logging.d.ts +32 -0
  27. package/dist/server/logging.js +132 -0
  28. package/package.json +2 -1
@@ -8,7 +8,7 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-BixdV7aQ.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-CLoO6IHy.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-CGXMw9LP.css">
13
13
  </head>
14
14
  <body>
package/dist/extension.js CHANGED
@@ -116,7 +116,7 @@ export default function (pi) {
116
116
  }
117
117
  }
118
118
  if (!ready) {
119
- ctx.ui.notify?.(`Warning: Super Pi server on port ${port} did not respond to health check in time`, "warning");
119
+ ctx.ui.notify?.(`Warning: Super Pi server on port ${port} did not respond to health check in time. Run "super-pi logs" to see why.`, "warning");
120
120
  }
121
121
  }
122
122
  openBrowser(url);
@@ -100,6 +100,8 @@ export class ConversationService {
100
100
  const conversation = state.conversations.find(item => item.id === conversationId);
101
101
  if (!conversation)
102
102
  throw new Error("会话不存在");
103
+ if (conversation.archivedAt)
104
+ throw new Error("已归档的会话无法重命名,请先恢复会话");
103
105
  const workspace = state.workspaces.find(item => item.id === conversation.workspaceId);
104
106
  if (!workspace)
105
107
  throw new Error("会话所属的工作区不存在");
@@ -154,6 +156,9 @@ export class ConversationService {
154
156
  * Renames a conversation.
155
157
  */
156
158
  async renameConversation(conversationId, name) {
159
+ const conversation = this.store.snapshot().conversations.find((item) => item.id === conversationId);
160
+ if (conversation?.archivedAt)
161
+ throw new Error("已归档的会话无法重命名,请先恢复会话");
157
162
  await this.store.renameConversation(conversationId, name);
158
163
  this.broadcaster.broadcastState();
159
164
  }
@@ -368,6 +373,9 @@ export class ConversationService {
368
373
  if (!conversation) {
369
374
  return { ok: false, message: "会话不存在" };
370
375
  }
376
+ if (conversation.archivedAt) {
377
+ return { ok: false, message: "已归档的会话无法分叉,请先恢复会话" };
378
+ }
371
379
  const workspace = snapshot.workspaces.find((item) => item.id === conversation.workspaceId);
372
380
  if (!workspace) {
373
381
  return { ok: false, message: `会话所属的工作区 ${conversation.workspaceId} 已不存在,拒绝分叉` };
@@ -28,12 +28,25 @@ import { ExecService } from "./exec-service.js";
28
28
  import { createHttpServer, getLanIps, isLocalOrPrivateAddress, parseServerArgs } from "./http-server.js";
29
29
  import { WsBroadcaster, WsServer } from "./ws-server.js";
30
30
  import { initRunningVersion } from "./version.js";
31
+ import { installServerLogging } from "./logging.js";
31
32
  import { ToolRegistry } from "./tool-registry.js";
32
33
  import { ToolRunner } from "./tool-runner.js";
33
34
  import { ToolLlmService } from "./tool-llm.js";
34
35
  // ------------------------------------------------------------- Environment & paths
35
36
  const rootDir = resolve(import.meta.dirname, "../..");
36
37
  initRunningVersion(rootDir);
38
+ // ------------------------------------------------------------- The log file
39
+ //
40
+ // THE SERVER IS STARTED DETACHED WITH ITS OUTPUT THROWN AWAY, so anything printed to the console
41
+ // is gone. A crash used to take the process down with no word at all, and the browser just sat there
42
+ // saying "connecting". The log file is installed BEFORE anything else can fail, so the first line in
43
+ // it is always "the server started" and the last line is always "the server stopped".
44
+ const defaultDataDir = !rootDir.includes("node_modules") && existsSync(join(rootDir, ".super-pi"))
45
+ ? join(rootDir, ".super-pi")
46
+ : join(homedir(), ".super-pi");
47
+ const dataDir = resolve(process.env.SUPER_PI_DATA_DIR ?? defaultDataDir);
48
+ const logging = installServerLogging(dataDir);
49
+ logging.write("info", `server starting: pid ${process.pid}, node ${process.versions.node}, data dir ${dataDir}`);
37
50
  // THE SECURITY PATCH, APPLIED WHERE IT CANNOT BREAK AN INSTALL.
38
51
  //
39
52
  // Pi's package pins a vulnerable `brace-expansion` in its own shrinkwrapped tree, and npm will not let
@@ -84,10 +97,6 @@ const envPort = Number.parseInt(process.env.SUPER_PI_PORT ?? "3080", 10);
84
97
  const port = cliArgs.port ?? (Number.isNaN(envPort) ? 3080 : envPort);
85
98
  const host = cliArgs.host ?? process.env.SUPER_PI_HOST ?? "0.0.0.0";
86
99
  const internalHost = (host === "0.0.0.0" || host === "::" || !host) ? "127.0.0.1" : host;
87
- const defaultDataDir = !rootDir.includes("node_modules") && existsSync(join(rootDir, ".super-pi"))
88
- ? join(rootDir, ".super-pi")
89
- : join(homedir(), ".super-pi");
90
- const dataDir = resolve(process.env.SUPER_PI_DATA_DIR ?? defaultDataDir);
91
100
  const agentPromptDir = join(dataDir, "agent-prompts");
92
101
  const clientDir = join(rootDir, "dist", "client");
93
102
  const accessToken = process.env.SUPER_PI_ACCESS_TOKEN;
@@ -326,6 +335,7 @@ const wsServer = new WsServer({
326
335
  httpServer.listen(port, host, () => {
327
336
  const displayHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
328
337
  console.log(`Super Pi is running at http://${displayHost}:${port}`);
338
+ logging.write("info", `listening on ${host}:${port} (pid ${process.pid})`);
329
339
  if (host === "0.0.0.0" || host === "::") {
330
340
  const lanIps = getLanIps();
331
341
  for (const ip of lanIps) {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * THE SERVER'S OWN LOG FILE.
3
+ *
4
+ * Super Pi is started DETACHED, with its standard streams thrown away (`stdio: "ignore"`), because
5
+ * the person who started it is usually gone by the time it matters - the panel runs in a browser tab
6
+ * and the server lives on behind it. The cost of that decision was that when the server DIED, it
7
+ * died silently: the browser sat there saying "connecting" and nobody could say why. A log file is
8
+ * the cheapest possible answer to "how did it die".
9
+ *
10
+ * Two properties matter more than volume here:
11
+ *
12
+ * - EVERY LINE IS WRITTEN SYNCHRONOUSLY. A buffered stream loses exactly the lines you want - the
13
+ * ones written a millisecond before the crash - because the buffer is still in memory when the
14
+ * process dies. `appendFileSync` costs a syscall per line, which is nothing at this volume, and
15
+ * it costs the line that matters.
16
+ * - THE CRASH IS LOGGED BEFORE THE PROCESS LEAVES. `uncaughtException` and `unhandledRejection` are
17
+ * the two ways a Node process dies without saying anything, and both are turned into a stack trace
18
+ * in the log file.
19
+ */
20
+ export declare const SERVER_LOG_NAME = "server.log";
21
+ export interface ServerLogging {
22
+ /** Absolute path of the log file this server writes to. */
23
+ readonly logFile: string;
24
+ /** Absolute path of the liveness file; its timestamp says when the server was last alive. */
25
+ readonly heartbeatFile: string;
26
+ /** Append one line to the log file, independently of the console. */
27
+ write(level: string, message: string): void;
28
+ }
29
+ /**
30
+ * Install the file logger. Returns the handle so the caller can write lines the console never sees.
31
+ */
32
+ export declare function installServerLogging(dataDir: string): ServerLogging;
@@ -0,0 +1,132 @@
1
+ import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * THE SERVER'S OWN LOG FILE.
5
+ *
6
+ * Super Pi is started DETACHED, with its standard streams thrown away (`stdio: "ignore"`), because
7
+ * the person who started it is usually gone by the time it matters - the panel runs in a browser tab
8
+ * and the server lives on behind it. The cost of that decision was that when the server DIED, it
9
+ * died silently: the browser sat there saying "connecting" and nobody could say why. A log file is
10
+ * the cheapest possible answer to "how did it die".
11
+ *
12
+ * Two properties matter more than volume here:
13
+ *
14
+ * - EVERY LINE IS WRITTEN SYNCHRONOUSLY. A buffered stream loses exactly the lines you want - the
15
+ * ones written a millisecond before the crash - because the buffer is still in memory when the
16
+ * process dies. `appendFileSync` costs a syscall per line, which is nothing at this volume, and
17
+ * it costs the line that matters.
18
+ * - THE CRASH IS LOGGED BEFORE THE PROCESS LEAVES. `uncaughtException` and `unhandledRejection` are
19
+ * the two ways a Node process dies without saying anything, and both are turned into a stack trace
20
+ * in the log file.
21
+ */
22
+ export const SERVER_LOG_NAME = "server.log";
23
+ /** Kept small enough to open by eye; the previous file is kept as `server.log.1`. */
24
+ const MAX_LOG_BYTES = 8 * 1024 * 1024;
25
+ function format(level, message) {
26
+ const time = new Date().toISOString();
27
+ // A multi-line message (a stack trace) stays one entry per line, each carrying the same stamp, so
28
+ // that a `tail` still shows where it started.
29
+ return message
30
+ .split("\n")
31
+ .map((line) => `[${time}] [${level}] ${line}`)
32
+ .join("\n");
33
+ }
34
+ function rotateIfLarge(logFile) {
35
+ try {
36
+ if (!existsSync(logFile))
37
+ return;
38
+ if (statSync(logFile).size < MAX_LOG_BYTES)
39
+ return;
40
+ const previous = `${logFile}.1`;
41
+ try {
42
+ renameSync(previous, `${previous}.old`);
43
+ }
44
+ catch { /* no previous file: fine */ }
45
+ renameSync(logFile, previous);
46
+ }
47
+ catch {
48
+ // Rotation is best-effort: a log file that cannot be rotated must not stop the server from
49
+ // starting, and must not stop it from logging.
50
+ }
51
+ }
52
+ /**
53
+ * Install the file logger. Returns the handle so the caller can write lines the console never sees.
54
+ */
55
+ export function installServerLogging(dataDir) {
56
+ const logDir = join(dataDir, "logs");
57
+ try {
58
+ mkdirSync(logDir, { recursive: true });
59
+ }
60
+ catch { /* the write below will report the real problem */ }
61
+ const logFile = join(logDir, SERVER_LOG_NAME);
62
+ rotateIfLarge(logFile);
63
+ /**
64
+ * THE LIVENESS FILE.
65
+ *
66
+ * A log file says what the server said; it cannot say when the server STOPPED, because the lines
67
+ * it would have written to say that are exactly the ones a crash takes away. A native crash (the
68
+ * kind that comes from a terminal being killed) never runs a JavaScript handler at all, so the
69
+ * server cannot even write "I died". This file answers the other half of the question: it is
70
+ * touched every minute while the server lives, so its timestamp says "the server was alive until
71
+ * this moment" - which is how a silent death gets a time on it.
72
+ */
73
+ const heartbeatFile = join(logDir, "heartbeat.txt");
74
+ const touch = () => {
75
+ try {
76
+ writeFileSync(heartbeatFile, `${Date.now()}\n`, "utf8");
77
+ }
78
+ catch { /* best effort */ }
79
+ };
80
+ touch();
81
+ const heartbeat = setInterval(touch, 60_000);
82
+ // The heartbeat must not be the reason the process stays alive.
83
+ heartbeat.unref?.();
84
+ const write = (level, message) => {
85
+ try {
86
+ appendFileSync(logFile, `${format(level, message)}\n`, "utf8");
87
+ }
88
+ catch { /* never let logging crash the server */ }
89
+ };
90
+ // CONSOLE OUTPUT IS TEED INTO THE FILE, NOT REPLACED BY IT.
91
+ //
92
+ // Everything the server already prints (the banner, warnings from the services, the shutdown
93
+ // notice) is worth keeping, and the cheapest way to keep it is to keep printing it and copy it.
94
+ // A service that logs through `console.warn` gets into the file for free.
95
+ const levels = ["log", "info", "warn", "error", "debug"];
96
+ const originals = new Map();
97
+ for (const level of levels) {
98
+ const original = console[level];
99
+ originals.set(level, original);
100
+ console[level] = (...args) => {
101
+ original.apply(console, args);
102
+ write(level === "log" ? "info" : level, args.map((arg) => (typeof arg === "string" ? arg : safeStringify(arg))).join(" "));
103
+ };
104
+ }
105
+ /**
106
+ * THE TWO WAYS A NODE PROCESS DIES WITHOUT A WORD.
107
+ *
108
+ * `uncaughtException` is a throw nobody caught; `unhandledRejection` is a promise nobody awaited.
109
+ * Both used to take the server down silently. Both now write the stack and then end the process -
110
+ * for `uncaughtException` continuing is not safe, because the process state after an uncaught
111
+ * exception is undefined.
112
+ */
113
+ process.on("uncaughtException", (error) => {
114
+ write("fatal", `uncaughtException: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
115
+ process.exit(1);
116
+ });
117
+ process.on("unhandledRejection", (reason) => {
118
+ write("fatal", `unhandledRejection: ${reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)}`);
119
+ });
120
+ process.on("exit", (code) => {
121
+ write("info", `server process exiting with code ${code}`);
122
+ });
123
+ return { logFile, heartbeatFile, write };
124
+ }
125
+ function safeStringify(value) {
126
+ try {
127
+ return JSON.stringify(value);
128
+ }
129
+ catch {
130
+ return String(value);
131
+ }
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,6 +46,7 @@
46
46
  "scripts/pi-model-switch.test.mjs",
47
47
  "scripts/pi-thinking-levels.test.mjs",
48
48
  "scripts/diagnose-cli.test.mjs",
49
+ "scripts/cli-logs.test.mjs",
49
50
  "scripts/lan-access.test.mjs",
50
51
  "scripts/terminal-pty.test.mjs",
51
52
  "scripts/persistence-repository.test.mjs",