narraeon 0.1.0 → 0.1.1-oidc.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.
package/README.md CHANGED
@@ -17,7 +17,7 @@ The model handles the meaning of the story: it portrays characters, responds to
17
17
 
18
18
  ## Requirements
19
19
 
20
- - Node.js **24.12.0 or newer**
20
+ - Either Docker Engine / Docker Desktop, or Node.js **24.12.0 or newer**
21
21
  - A modern browser
22
22
  - For AI play or setting improvement, a model endpoint and API credential compatible with one of the supported adapters:
23
23
  - OpenAI Responses API
@@ -46,6 +46,22 @@ npx narraeon --help
46
46
 
47
47
  The server listens only on `127.0.0.1`. If the chosen port already hosts a compatible Narraeon instance, the CLI reuses it. If another program owns the port, startup fails instead of attaching to an unknown service.
48
48
 
49
+ Alternatively, run the release image from GitHub Container Registry:
50
+
51
+ ```bash
52
+ docker run --detach --name narraeon \
53
+ --publish 127.0.0.1:4317:4317 \
54
+ --volume narraeon-data:/var/lib/narraeon \
55
+ ghcr.io/narraeon/narraeon:latest
56
+ ```
57
+
58
+ Then open <http://127.0.0.1:4317>. The named volume preserves worlds,
59
+ configuration, credentials, logs, and recovery state across container
60
+ recreation. Keep the host-side address as `127.0.0.1`; publishing the port on
61
+ all interfaces would unnecessarily expose a local, single-user Runtime.
62
+ Stable releases move `latest`, prereleases move `next`, and every release also
63
+ has an exact version tag.
64
+
49
65
  ## Run from source
50
66
 
51
67
  ```bash
@@ -136,6 +152,10 @@ Narraeon uses the operating system's standard per-user application directories.
136
152
  | `NARRAEON_LOG_ROOT` | Runtime logs and AI failure diagnostics |
137
153
  | `NARRAEON_PORT` | Local web port, default `4317` |
138
154
 
155
+ The container image maps all three storage roots under
156
+ `/var/lib/narraeon`, which is why the Docker example persists that single
157
+ directory as a volume.
158
+
139
159
  The browser talks only to the same-machine Runtime. Local-first does not mean that remote model inference is offline: prompts, selected world material, tool exchanges, and generated text are sent to the provider you configure. API keys are stored in the local configuration and are not returned to the browser after saving.
140
160
 
141
161
  When a provider, response-format, Runtime-tool, or candidate-check error occurs, Narraeon creates a JSONL incident under `NARRAEON_LOG_ROOT/ai-failures`. It preserves the raw provider exchange and later recovery attempts, including reasoning or thinking actually returned by the provider. It does not invent hidden reasoning or record API keys and request headers. The files can still contain private prompts, world content, tool arguments, and provider-returned reasoning, so treat the log directory as sensitive.
package/README.zh-CN.md CHANGED
@@ -17,7 +17,7 @@ Narraeon 是一个用于开放式 AI 角色扮演的本地优先工作区。你
17
17
 
18
18
  ## 运行要求
19
19
 
20
- - Node.js **24.12.0 或更高版本**
20
+ - Docker Engine/Docker Desktop,或 Node.js **24.12.0 或更高版本**,二选一
21
21
  - 现代浏览器
22
22
  - 如果要进行 AI 游玩或设定完善,还需要与以下任一适配器兼容的模型端点和 API 凭据:
23
23
  - OpenAI Responses API
@@ -46,6 +46,17 @@ npx narraeon --help
46
46
 
47
47
  服务只监听 `127.0.0.1`。如果选定端口已经运行兼容的 Narraeon,CLI 会直接复用;如果端口被其他程序占用,启动会明确失败,不会连接未知服务。
48
48
 
49
+ 也可以直接运行 GitHub Container Registry 上的发布镜像:
50
+
51
+ ```bash
52
+ docker run --detach --name narraeon \
53
+ --publish 127.0.0.1:4317:4317 \
54
+ --volume narraeon-data:/var/lib/narraeon \
55
+ ghcr.io/narraeon/narraeon:latest
56
+ ```
57
+
58
+ 然后打开 <http://127.0.0.1:4317>。命名卷会在重建容器后继续保留世界、配置、凭据、日志和恢复数据。宿主机一侧应继续绑定 `127.0.0.1`;绑定所有网卡会把本地单用户 Runtime 不必要地暴露出去。稳定版会移动 `latest`,预发布版会移动 `next`,每次发布还会保留精确版本标签。
59
+
49
60
  ## 从源码运行
50
61
 
51
62
  ```bash
@@ -136,6 +147,8 @@ Narraeon 默认使用当前操作系统的标准用户应用目录。如果要
136
147
  | `NARRAEON_LOG_ROOT` | Runtime 日志和 AI 失败诊断 |
137
148
  | `NARRAEON_PORT` | 本地 Web 端口,默认为 `4317` |
138
149
 
150
+ 容器镜像把前三类存储目录统一放在 `/var/lib/narraeon` 下,因此 Docker 示例只需要持久化这一个目录。
151
+
139
152
  浏览器只与同机 Runtime 通信。“本地优先”不等于远程模型推理也离线:提示词、选中的世界材料、工具交换和生成文本会发送到你配置的 Provider。API Key 保存在本地配置中,保存后不会再返回给浏览器。
140
153
 
141
154
  发生 Provider、响应格式、Runtime 工具或候选检查错误时,Narraeon 会在 `NARRAEON_LOG_ROOT/ai-failures` 下创建 JSONL 事故记录。它会保存原始 Provider 交换和后续恢复尝试,包括 Provider 真实返回的 reasoning/thinking。它不会伪造隐藏思考,也不会记录 API Key 和请求 header。不过日志仍可能包含私密提示词、世界内容、工具参数和 Provider 返回的思考,因此应当把该目录视为敏感数据。
@@ -2,7 +2,7 @@ import { createConnection } from "node:net";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { parseArgs } from "node:util";
4
4
  import { resolveAppPaths } from "../runtime/config/appPaths.js";
5
- import { parseWebPort, startWebServer, } from "../server/startWebServer.js";
5
+ import { parseWebHost, parseWebPort, startWebServer, } from "../server/startWebServer.js";
6
6
  export class NarraeonCliUsageError extends Error {
7
7
  }
8
8
  export async function runNarraeonCli(input) {
@@ -29,6 +29,7 @@ export async function runNarraeonCli(input) {
29
29
  server = await start({
30
30
  paths: resolveAppPaths(input.environment),
31
31
  staticRoot: input.staticRoot,
32
+ host: command.host,
32
33
  port: command.port,
33
34
  });
34
35
  }
@@ -93,7 +94,9 @@ function parseCliCommand(args, environment) {
93
94
  if (parsed.positionals.length !== 1 || parsed.positionals[0] !== "web")
94
95
  throw new NarraeonCliUsageError(`Unknown command: ${parsed.positionals.join(" ") || "(empty)"}`);
95
96
  let port;
97
+ let host;
96
98
  try {
99
+ host = parseWebHost(environment.NARRAEON_HOST);
97
100
  port = parseWebPort(parsed.values.port ?? environment.NARRAEON_PORT, parsed.values.port === undefined ? "NARRAEON_PORT" : "--port");
98
101
  }
99
102
  catch (error) {
@@ -101,6 +104,7 @@ function parseCliCommand(args, environment) {
101
104
  }
102
105
  return {
103
106
  kind: "web",
107
+ host,
104
108
  port,
105
109
  openBrowser: parsed.values.open !== false,
106
110
  };
@@ -51,7 +51,10 @@ export async function aggregateChatModelStream(body, onDelta) {
51
51
  reasoningContent += delta.reasoning_content;
52
52
  onDelta?.({ kind: "reasoning", text: delta.reasoning_content });
53
53
  }
54
- if (delta.tool_calls === undefined)
54
+ // Gateways in front of a Chat Completions Model often write `null` where
55
+ // the protocol omits a field, both for the whole tool-call list and for
56
+ // individual fragment fields. A null carries no continuation content.
57
+ if (delta.tool_calls === undefined || delta.tool_calls === null)
55
58
  continue;
56
59
  if (!Array.isArray(delta.tool_calls))
57
60
  throw new Error("Chat SSE tool_calls delta is invalid");
@@ -69,26 +72,26 @@ export async function aggregateChatModelStream(body, onDelta) {
69
72
  type: "function",
70
73
  arguments: "",
71
74
  };
72
- if (fragmentValue.id !== undefined) {
75
+ if (isPresent(fragmentValue.id)) {
73
76
  if (typeof fragmentValue.id !== "string")
74
77
  throw new Error("Chat SSE tool-call id is invalid");
75
78
  current.id += fragmentValue.id;
76
79
  }
77
- if (fragmentValue.type !== undefined) {
80
+ if (isPresent(fragmentValue.type)) {
78
81
  if (fragmentValue.type !== "function")
79
82
  throw new Error("Chat SSE tool-call type is invalid");
80
83
  current.type = fragmentValue.type;
81
84
  }
82
- if (fragmentValue.function !== undefined) {
85
+ if (isPresent(fragmentValue.function)) {
83
86
  if (!isRecord(fragmentValue.function))
84
87
  throw new Error("Chat SSE tool-call function is invalid");
85
88
  const function_ = fragmentValue.function;
86
- if (function_.name !== undefined) {
89
+ if (isPresent(function_.name)) {
87
90
  if (typeof function_.name !== "string")
88
91
  throw new Error("Chat SSE tool-call name is invalid");
89
92
  current.name += function_.name;
90
93
  }
91
- if (function_.arguments !== undefined) {
94
+ if (isPresent(function_.arguments)) {
92
95
  if (typeof function_.arguments !== "string")
93
96
  throw new Error("Chat SSE tool-call arguments are invalid");
94
97
  current.arguments += function_.arguments;
@@ -362,6 +365,9 @@ function mergeUsage(target, value) {
362
365
  function stringOrUndefined(value) {
363
366
  return typeof value === "string" ? value : undefined;
364
367
  }
368
+ function isPresent(value) {
369
+ return value !== undefined && value !== null;
370
+ }
365
371
  function isRecord(value) {
366
372
  return typeof value === "object" && value !== null && !Array.isArray(value);
367
373
  }
@@ -1,11 +1,13 @@
1
1
  import { resolve } from "node:path";
2
2
  import { resolveAppPaths } from "../runtime/config/appPaths.js";
3
- import { parseWebPort, startWebServer } from "./startWebServer.js";
3
+ import { parseWebHost, parseWebPort, startWebServer, } from "./startWebServer.js";
4
+ const host = parseWebHost(process.env.NARRAEON_HOST);
4
5
  const port = parseWebPort(process.env.NARRAEON_PORT);
5
6
  const paths = resolveAppPaths(process.env);
6
7
  const server = await startWebServer({
7
8
  paths,
8
9
  staticRoot: resolve("dist/web"),
10
+ host,
9
11
  port,
10
12
  });
11
13
  installGracefulShutdown(server);
@@ -1,6 +1,8 @@
1
1
  import { createRuntime } from "./createRuntime.js";
2
2
  import { createServer } from "./createServer.js";
3
3
  export const defaultWebPort = 4317;
4
+ export const defaultWebHost = "127.0.0.1";
5
+ export const containerWebHost = "0.0.0.0";
4
6
  export async function startWebServer(input) {
5
7
  const runtime = await createRuntime(input.paths);
6
8
  const server = await createServer({
@@ -10,7 +12,7 @@ export async function startWebServer(input) {
10
12
  ...(input.logger === undefined ? {} : { logger: input.logger }),
11
13
  });
12
14
  try {
13
- await server.listen({ host: "127.0.0.1", port: input.port });
15
+ await server.listen({ host: input.host, port: input.port });
14
16
  return server;
15
17
  }
16
18
  catch (error) {
@@ -18,6 +20,13 @@ export async function startWebServer(input) {
18
20
  throw error;
19
21
  }
20
22
  }
23
+ export function parseWebHost(value, source = "NARRAEON_HOST") {
24
+ if (value === undefined)
25
+ return defaultWebHost;
26
+ if (value === defaultWebHost || value === containerWebHost)
27
+ return value;
28
+ throw new Error(`${source} must be ${defaultWebHost} or ${containerWebHost}: ${value}`);
29
+ }
21
30
  export function parseWebPort(value, source = "NARRAEON_PORT") {
22
31
  if (value === undefined)
23
32
  return defaultWebPort;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "narraeon",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-oidc.1",
4
4
  "description": "A local-first workspace for living worlds.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -44,6 +44,7 @@
44
44
  "prepack": "npm run build",
45
45
  "start": "node src/server/main.ts",
46
46
  "test": "npm run test:unit && npm run test:release && npm run test:e2e",
47
+ "test:container": "docker build --tag narraeon:release-smoke . && node scripts/smoke-container.mjs --image narraeon:release-smoke",
47
48
  "test:e2e": "npm run build && playwright test",
48
49
  "test:package": "node scripts/smoke-package.mjs",
49
50
  "test:release": "node --test scripts/check-release.test.mjs",