@biaoo/tiangong-wiki 0.3.6 → 0.3.8

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
@@ -86,6 +86,8 @@ tiangong-wiki sync # index Markdown pages
86
86
 
87
87
  That means commands still work best from inside a workspace, but they can also run from outside the workspace after setup, or target a specific workspace explicitly with `--env-file`.
88
88
 
89
+ For automatic vault processing, new setup runs default to `WIKI_AGENT_AUTH_MODE=codex-login`, the current user's standard Codex home (`~/.codex`, or the user profile `.codex` directory on Windows), and `WIKI_AGENT_MODEL=gpt-5.5`. If you want an isolated Codex home, set `WIKI_AGENT_CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki` and first run `CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki codex login`.
90
+
89
91
  ```bash
90
92
  tiangong-wiki find --type concept --status active # structured query
91
93
  tiangong-wiki fts "Bayesian" # full-text search
package/README.zh-CN.md CHANGED
@@ -86,6 +86,8 @@ tiangong-wiki sync # 索引 Markdown 文件
86
86
 
87
87
  这意味着命令仍然最适合在 workspace 内执行;但 setup 之后,即使在 workspace 外运行,也可以通过默认配置正常工作,或者通过 `--env-file` 显式指定目标工作区。
88
88
 
89
+ 如果启用自动 vault 处理,新的 setup 默认使用 `WIKI_AGENT_AUTH_MODE=codex-login`、当前用户标准 Codex home(Linux/macOS 为 `~/.codex`,Windows 为用户目录下的 `.codex`)和 `WIKI_AGENT_MODEL=gpt-5.5`。如果需要隔离目录,可手动设置 `WIKI_AGENT_CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki`,并先执行 `CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki codex login`。
90
+
89
91
  ```bash
90
92
  tiangong-wiki find --type concept --status active # 结构化查询
91
93
  tiangong-wiki fts "贝叶斯" # 全文搜索
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tiangong-wiki-skill
3
- description: "Use when you need to retrieve historically accumulated knowledge, methods, or behavioral patterns before answering or acting; when a conversation or workflow produces durable insights worth preserving for future reuse; or when you discover existing wiki content that is outdated or incorrect and needs correction."
3
+ description: "Use when you need to retrieve historically accumulated knowledge, methods, or behavioral patterns before answering or acting; when a conversation or workflow produces durable insights worth preserving for future reuse; or when existing wiki content is outdated or incorrect. On Windows native shells or Codex automation, invoke the CLI as tiangong-wiki.cmd."
4
4
  ---
5
5
 
6
6
  # Wiki Skill
@@ -9,6 +9,20 @@ description: "Use when you need to retrieve historically accumulated knowledge,
9
9
 
10
10
  Use the local wiki as the **durable knowledge layer** — not just for the current conversation, but for all future work. Query first, then read or edit the Markdown files that remain the source of truth.
11
11
 
12
+ ## CLI Entrypoint
13
+
14
+ Use `tiangong-wiki <command>` on macOS, Linux, WSL, and Git Bash.
15
+
16
+ On Windows native shells such as PowerShell, Command Prompt, background daemon tasks, or Codex worker automation, use the npm command shim explicitly:
17
+
18
+ ```powershell
19
+ tiangong-wiki.cmd doctor
20
+ tiangong-wiki.cmd sync
21
+ tiangong-wiki.cmd lint --format json
22
+ ```
23
+
24
+ Do not invoke the suffixless `tiangong-wiki` executable from Windows native shells. npm also installs that shebang script for POSIX-like environments, but Windows may treat it as an unknown file and open the "choose an app" dialog instead of executing the CLI.
25
+
12
26
  ## When to Use
13
27
 
14
28
  Activate this skill in three scenarios:
@@ -8,7 +8,23 @@ WIKI_SYNC_INTERVAL=86400
8
8
  EMBEDDING_BASE_URL=https://api.openai.com/v1
9
9
  EMBEDDING_API_KEY=your-embedding-api-key
10
10
  EMBEDDING_MODEL=text-embedding-3-small
11
- EMBEDDING_DIMENSIONS=384
11
+ EMBEDDING_DIMENSIONS=1536
12
+
13
+ WIKI_AGENT_ENABLED=false
14
+ # For local Codex login auth, run `codex login` once as the same user.
15
+ WIKI_AGENT_AUTH_MODE=codex-login
16
+ # Optional. Leave unset to use the current user's standard Codex home: <home>/.codex
17
+ # For an isolated directory, set this to an absolute path and first run:
18
+ # CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki codex login
19
+ # WIKI_AGENT_CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki
20
+ #
21
+ # To use an API key instead of Codex login:
22
+ # WIKI_AGENT_AUTH_MODE=api-key
23
+ # WIKI_AGENT_BASE_URL=https://api.openai.com/v1
24
+ # WIKI_AGENT_API_KEY=sk-...
25
+ WIKI_AGENT_MODEL=gpt-5.5
26
+ WIKI_AGENT_BATCH_SIZE=5
27
+ WIKI_AGENT_SANDBOX_MODE=danger-full-access
12
28
 
13
29
  VAULT_SOURCE=local
14
30
  # When VAULT_SOURCE=synology, also set:
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { inspectWikiAgentCodexLogin } from "../core/agent-auth.js";
2
3
  import { loadRuntimeConfig } from "../core/runtime.js";
3
4
  import { EmbeddingClient } from "../core/embedding.js";
4
5
  import { getWikiAgentStatus } from "../core/vault-processing.js";
@@ -20,6 +21,17 @@ export function registerCheckConfigCommand(program) {
20
21
  if (wikiAgent.enabled && wikiAgent.missing.length > 0) {
21
22
  throw new AppError(`WIKI_AGENT_ENABLED=true but missing required settings: ${wikiAgent.missing.join(", ")}`, "config");
22
23
  }
24
+ const agentCodexLogin = inspectWikiAgentCodexLogin(wikiAgent);
25
+ if (!agentCodexLogin.ready) {
26
+ throw new AppError([
27
+ agentCodexLogin.summary,
28
+ agentCodexLogin.recommendation,
29
+ ].filter(Boolean).join(" "), "config");
30
+ }
31
+ const safeWikiAgent = {
32
+ ...wikiAgent,
33
+ apiKey: wikiAgent.apiKey ? "<redacted>" : null,
34
+ };
23
35
  const templateChecks = Object.keys(config.templates).map((pageType) => {
24
36
  const templatePath = resolveTemplateFilePath(config, paths.wikiRoot, pageType);
25
37
  return {
@@ -44,7 +56,8 @@ export function registerCheckConfigCommand(program) {
44
56
  templatesPath: paths.templatesPath,
45
57
  configVersion: config.configVersion,
46
58
  embeddingConfigured: embeddingClient !== null,
47
- agentProcessing: wikiAgent,
59
+ agentProcessing: safeWikiAgent,
60
+ agentCodexLogin,
48
61
  probe,
49
62
  templateChecks,
50
63
  };
@@ -63,11 +76,15 @@ export function registerCheckConfigCommand(program) {
63
76
  embeddingConfigured: embeddingClient !== null,
64
77
  agentEnabled: wikiAgent.enabled,
65
78
  agentConfigured: wikiAgent.configured,
79
+ agentAuthMode: wikiAgent.authMode,
66
80
  agentBaseUrl: wikiAgent.baseUrl ?? "",
81
+ agentCodexHome: wikiAgent.codexHome ?? "",
67
82
  agentModel: wikiAgent.model ?? "",
68
83
  agentBatchSize: wikiAgent.batchSize,
69
84
  agentWorkflowTimeoutSeconds: wikiAgent.workflowTimeoutSeconds,
70
85
  agentMissing: wikiAgent.missing.join(", "),
86
+ agentCodexLoginReady: agentCodexLogin.checked ? agentCodexLogin.ready : "",
87
+ agentCodexLoginAuthJson: agentCodexLogin.authJsonPath ?? "",
71
88
  probe,
72
89
  }),
73
90
  "",
@@ -0,0 +1,101 @@
1
+ import { statSync } from "node:fs";
2
+ import path from "node:path";
3
+ function inspectPathKind(targetPath) {
4
+ try {
5
+ const stats = statSync(targetPath);
6
+ if (stats.isDirectory()) {
7
+ return { kind: "directory" };
8
+ }
9
+ if (stats.isFile()) {
10
+ return { kind: "file" };
11
+ }
12
+ return { kind: "other" };
13
+ }
14
+ catch (error) {
15
+ const code = error.code;
16
+ if (code === "ENOENT" || code === "ENOTDIR") {
17
+ return { kind: "missing" };
18
+ }
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return { kind: "inaccessible", errorMessage: message };
21
+ }
22
+ }
23
+ function codexLoginRecommendation(codexHome) {
24
+ return `Run \`CODEX_HOME=${codexHome} codex login\` before starting automatic vault processing.`;
25
+ }
26
+ export function inspectWikiAgentCodexLogin(settings) {
27
+ if (!settings.enabled || settings.authMode !== "codex-login") {
28
+ return {
29
+ checked: false,
30
+ ready: true,
31
+ codexHome: settings.codexHome,
32
+ authJsonPath: null,
33
+ summary: "Codex login auth is not enabled.",
34
+ };
35
+ }
36
+ if (!settings.codexHome) {
37
+ return {
38
+ checked: true,
39
+ ready: false,
40
+ codexHome: null,
41
+ authJsonPath: null,
42
+ summary: "WIKI_AGENT_CODEX_HOME is not configured for codex-login auth.",
43
+ recommendation: "Set WIKI_AGENT_CODEX_HOME or rerun `tiangong-wiki setup`.",
44
+ };
45
+ }
46
+ const codexHome = settings.codexHome;
47
+ const authJsonPath = path.join(codexHome, "auth.json");
48
+ const home = inspectPathKind(codexHome);
49
+ if (home.kind === "missing") {
50
+ return {
51
+ checked: true,
52
+ ready: false,
53
+ codexHome,
54
+ authJsonPath,
55
+ summary: `WIKI_AGENT_CODEX_HOME does not exist: ${codexHome}`,
56
+ recommendation: codexLoginRecommendation(codexHome),
57
+ };
58
+ }
59
+ if (home.kind !== "directory") {
60
+ return {
61
+ checked: true,
62
+ ready: false,
63
+ codexHome,
64
+ authJsonPath,
65
+ summary: home.kind === "inaccessible"
66
+ ? `WIKI_AGENT_CODEX_HOME cannot be inspected: ${codexHome} (${home.errorMessage})`
67
+ : `WIKI_AGENT_CODEX_HOME is not a directory: ${codexHome}`,
68
+ recommendation: codexLoginRecommendation(codexHome),
69
+ };
70
+ }
71
+ const authJson = inspectPathKind(authJsonPath);
72
+ if (authJson.kind === "missing") {
73
+ return {
74
+ checked: true,
75
+ ready: false,
76
+ codexHome,
77
+ authJsonPath,
78
+ summary: `Codex login auth.json was not found under WIKI_AGENT_CODEX_HOME: ${authJsonPath}`,
79
+ recommendation: codexLoginRecommendation(codexHome),
80
+ };
81
+ }
82
+ if (authJson.kind !== "file") {
83
+ return {
84
+ checked: true,
85
+ ready: false,
86
+ codexHome,
87
+ authJsonPath,
88
+ summary: authJson.kind === "inaccessible"
89
+ ? `Codex login auth.json cannot be inspected: ${authJsonPath} (${authJson.errorMessage})`
90
+ : `Codex login auth.json is not a file: ${authJsonPath}`,
91
+ recommendation: codexLoginRecommendation(codexHome),
92
+ };
93
+ }
94
+ return {
95
+ checked: true,
96
+ ready: true,
97
+ codexHome,
98
+ authJsonPath,
99
+ summary: `Codex login auth is available at ${codexHome}.`,
100
+ };
101
+ }
@@ -27,7 +27,27 @@ function unquoteEnvValue(rawValue) {
27
27
  return value;
28
28
  }
29
29
  if (value.startsWith('"') && value.endsWith('"')) {
30
- return value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
30
+ const inner = value.slice(1, -1);
31
+ let output = "";
32
+ for (let index = 0; index < inner.length; index += 1) {
33
+ const current = inner[index];
34
+ if (current !== "\\" || index === inner.length - 1) {
35
+ output += current;
36
+ continue;
37
+ }
38
+ const next = inner[index + 1];
39
+ index += 1;
40
+ if (next === "n") {
41
+ output += "\n";
42
+ }
43
+ else if (next === '"' || next === "\\") {
44
+ output += next;
45
+ }
46
+ else {
47
+ output += `\\${next}`;
48
+ }
49
+ }
50
+ return output;
31
51
  }
32
52
  if (value.startsWith("'") && value.endsWith("'")) {
33
53
  return value.slice(1, -1);
@@ -1,28 +1,85 @@
1
1
  import path from "node:path";
2
- import { Codex } from "@openai/codex-sdk";
2
+ import childProcess from "node:child_process";
3
+ import { syncBuiltinESMExports } from "node:module";
3
4
  import { readWorkflowResult } from "./workflow-result.js";
4
5
  import { resolveAgentSettings } from "./paths.js";
5
6
  import { readTextFileSync, writeTextFileSync } from "../utils/fs.js";
6
7
  import { AppError } from "../utils/errors.js";
7
8
  export const CODEX_WORKFLOW_VERSION = "2026-04-07";
9
+ const hiddenWindowsSpawnPatch = Symbol.for("tiangong-wiki.hiddenWindowsSpawnPatch");
10
+ function isSpawnOptions(value) {
11
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ function addHiddenWindowDefault(options) {
14
+ if (!isSpawnOptions(options)) {
15
+ return { windowsHide: true };
16
+ }
17
+ return { ...options, windowsHide: options.windowsHide ?? true };
18
+ }
19
+ export function createHiddenWindowsSpawn(spawnFn) {
20
+ return ((command, argsOrOptions, options) => {
21
+ if (Array.isArray(argsOrOptions)) {
22
+ return spawnFn(command, argsOrOptions, addHiddenWindowDefault(options));
23
+ }
24
+ if (isSpawnOptions(argsOrOptions) && options === undefined) {
25
+ return spawnFn(command, addHiddenWindowDefault(argsOrOptions));
26
+ }
27
+ if (argsOrOptions === undefined && options === undefined) {
28
+ return spawnFn(command, addHiddenWindowDefault(undefined));
29
+ }
30
+ return spawnFn(command, argsOrOptions, addHiddenWindowDefault(options));
31
+ });
32
+ }
33
+ function installHiddenWindowsSpawnPatch(platform = process.platform) {
34
+ if (platform !== "win32") {
35
+ return;
36
+ }
37
+ const currentSpawn = childProcess.spawn;
38
+ if (currentSpawn[hiddenWindowsSpawnPatch]) {
39
+ return;
40
+ }
41
+ const patchedSpawn = createHiddenWindowsSpawn(childProcess.spawn);
42
+ patchedSpawn[hiddenWindowsSpawnPatch] = true;
43
+ childProcess.spawn = patchedSpawn;
44
+ syncBuiltinESMExports();
45
+ }
46
+ let codexSdkModulePromise = null;
47
+ async function loadCodexSdk() {
48
+ // The SDK captures child_process.spawn during module import and currently
49
+ // does not expose windowsHide; install the Windows default before loading it.
50
+ installHiddenWindowsSpawnPatch();
51
+ codexSdkModulePromise ??= import("@openai/codex-sdk");
52
+ return codexSdkModulePromise;
53
+ }
8
54
  function normalizeEnv(input) {
55
+ const agentSettings = resolveAgentSettings(input.env);
9
56
  const normalized = {};
10
57
  for (const [key, value] of Object.entries({
11
58
  ...process.env,
12
59
  ...input.env,
13
- ...(input.env?.WIKI_AGENT_API_KEY && !input.env.OPENAI_API_KEY ? { OPENAI_API_KEY: input.env.WIKI_AGENT_API_KEY } : {}),
60
+ ...(agentSettings.authMode === "api-key" && agentSettings.apiKey && !input.env?.OPENAI_API_KEY
61
+ ? { OPENAI_API_KEY: agentSettings.apiKey }
62
+ : {}),
63
+ ...(agentSettings.authMode === "codex-login" && agentSettings.codexHome
64
+ ? { CODEX_HOME: agentSettings.codexHome }
65
+ : {}),
14
66
  })) {
15
67
  if (typeof value === "string") {
16
68
  normalized[key] = value;
17
69
  }
18
70
  }
71
+ if (agentSettings.authMode === "codex-login") {
72
+ delete normalized.OPENAI_API_KEY;
73
+ delete normalized.CODEX_API_KEY;
74
+ }
19
75
  normalized.PATH = [input.skillArtifactsPath, normalized.PATH].filter(Boolean).join(path.delimiter);
20
76
  return normalized;
21
77
  }
22
- function createCodexClient(input) {
78
+ async function createCodexClient(input) {
79
+ const agentSettings = resolveAgentSettings(input.env);
23
80
  const env = normalizeEnv(input);
24
- const baseUrl = input.env?.WIKI_AGENT_BASE_URL?.trim();
25
- const apiKey = input.env?.WIKI_AGENT_API_KEY?.trim();
81
+ const baseUrl = agentSettings.authMode === "api-key" ? agentSettings.baseUrl : null;
82
+ const apiKey = agentSettings.authMode === "api-key" ? agentSettings.apiKey : null;
26
83
  const options = {
27
84
  apiKey: apiKey || undefined,
28
85
  env,
@@ -43,6 +100,7 @@ function createCodexClient(input) {
43
100
  },
44
101
  };
45
102
  }
103
+ const { Codex } = await loadCodexSdk();
46
104
  return new Codex(options);
47
105
  }
48
106
  function persistWorkflowThreadId(queueItemPath, threadId) {
@@ -117,7 +175,7 @@ export class CodexSdkWorkflowRunner {
117
175
  // The SDK can only continue a thread by sending a new input, so queue retries
118
176
  // must not automatically resume real workflow threads inline.
119
177
  async startWorkflow(input) {
120
- const codex = createCodexClient(input);
178
+ const codex = await createCodexClient(input);
121
179
  const thread = codex.startThread({
122
180
  model: input.model ?? undefined,
123
181
  modelReasoningEffort: "low",
@@ -133,7 +191,7 @@ export class CodexSdkWorkflowRunner {
133
191
  return { threadId, mode: "start" };
134
192
  }
135
193
  async resumeWorkflow(threadId, input) {
136
- const codex = createCodexClient(input);
194
+ const codex = await createCodexClient(input);
137
195
  const thread = codex.resumeThread(threadId, {
138
196
  model: input.model ?? undefined,
139
197
  modelReasoningEffort: "low",
@@ -1,5 +1,6 @@
1
1
  import { sha256Text } from "../utils/fs.js";
2
2
  import { AppError } from "../utils/errors.js";
3
+ export const DEFAULT_EMBEDDING_DIMENSIONS = 1536;
3
4
  export class EmbeddingClient {
4
5
  settings;
5
6
  constructor(settings) {
@@ -9,7 +10,7 @@ export class EmbeddingClient {
9
10
  const baseUrl = env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL;
10
11
  const apiKey = env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY;
11
12
  const model = env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL;
12
- const rawDimensions = env.EMBEDDING_DIMENSIONS ?? "384";
13
+ const rawDimensions = env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS);
13
14
  if (!baseUrl || !apiKey || !model) {
14
15
  return null;
15
16
  }
@@ -2,11 +2,12 @@ import { accessSync, constants, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { confirm, input, password, select } from "@inquirer/prompts";
5
+ import { inspectWikiAgentCodexLogin } from "./agent-auth.js";
5
6
  import { DEFAULT_WIKI_ENV_FILE, getCliEnvironmentInfo, parseEnvFile, serializeEnvEntries } from "./cli-env.js";
6
7
  import { resolveTemplateFilePath, loadConfig } from "./config.js";
7
- import { EmbeddingClient } from "./embedding.js";
8
+ import { DEFAULT_EMBEDDING_DIMENSIONS, EmbeddingClient } from "./embedding.js";
8
9
  import { writeGlobalConfig } from "./global-config.js";
9
- import { parseVaultHashMode, parseWikiAgentSandboxMode, resolveAgentSettings } from "./paths.js";
10
+ import { DEFAULT_WIKI_AGENT_MODEL, defaultWikiAgentCodexHome, parseVaultHashMode, parseWikiAgentAuthMode, parseWikiAgentSandboxMode, resolveAgentSettings, } from "./paths.js";
10
11
  import { loadSynologyConfigFromEnv, normalizeSynologyRemotePath, withSynologyClient } from "./synology.js";
11
12
  import { ensureWikiSkillInstall, formatParserSkills, inspectSkillInstall, installParserSkill, OPTIONAL_PARSER_SKILLS, parseParserSkillSelection, parseParserSkills, resolveWorkspaceRootFromWikiPath, resolveWorkspaceSkillPath, resolveWorkspaceSkillPaths, } from "./workspace-skills.js";
12
13
  import { scaffoldWorkspaceAssets } from "./workspace-bootstrap.js";
@@ -32,8 +33,10 @@ const MANAGED_ENV_KEYS = new Set([
32
33
  "EMBEDDING_MODEL",
33
34
  "EMBEDDING_DIMENSIONS",
34
35
  "WIKI_AGENT_ENABLED",
36
+ "WIKI_AGENT_AUTH_MODE",
35
37
  "WIKI_AGENT_BASE_URL",
36
38
  "WIKI_AGENT_API_KEY",
39
+ "WIKI_AGENT_CODEX_HOME",
37
40
  "WIKI_AGENT_MODEL",
38
41
  "WIKI_AGENT_BATCH_SIZE",
39
42
  "WIKI_AGENT_SANDBOX_MODE",
@@ -86,6 +89,17 @@ function safeAgentSandboxMode(rawValue) {
86
89
  return "danger-full-access";
87
90
  }
88
91
  }
92
+ function safeAgentAuthMode(env) {
93
+ try {
94
+ if (env.WIKI_AGENT_AUTH_MODE && env.WIKI_AGENT_AUTH_MODE.trim()) {
95
+ return parseWikiAgentAuthMode(env.WIKI_AGENT_AUTH_MODE);
96
+ }
97
+ }
98
+ catch {
99
+ return env.WIKI_AGENT_API_KEY?.trim() ? "api-key" : "codex-login";
100
+ }
101
+ return env.WIKI_AGENT_API_KEY?.trim() ? "api-key" : "codex-login";
102
+ }
89
103
  function safeBooleanFlag(rawValue, defaultValue) {
90
104
  if (rawValue === undefined || rawValue.trim().length === 0) {
91
105
  return defaultValue;
@@ -120,6 +134,12 @@ function validateWikiPath(rawValue) {
120
134
  }
121
135
  return null;
122
136
  }
137
+ function validateAbsolutePath(rawValue, label) {
138
+ if (!path.isAbsolute(rawValue.trim())) {
139
+ return `${label} must be an absolute path.`;
140
+ }
141
+ return null;
142
+ }
123
143
  class InquirerPromptDriver {
124
144
  inputStream;
125
145
  outputStream;
@@ -368,6 +388,7 @@ function getPathDefaults(env, cwd) {
368
388
  const configPath = env.WIKI_CONFIG_PATH ? path.resolve(env.WIKI_CONFIG_PATH) : path.join(wikiRoot, "wiki.config.json");
369
389
  const templatesPath = env.WIKI_TEMPLATES_PATH ? path.resolve(env.WIKI_TEMPLATES_PATH) : path.join(wikiRoot, "templates");
370
390
  const defaultHashMode = vaultSource === "synology" ? "mtime" : "content";
391
+ const agentAuthMode = safeAgentAuthMode(env);
371
392
  return {
372
393
  envFilePath: env.WIKI_ENV_FILE ? path.resolve(cwd, env.WIKI_ENV_FILE) : path.join(cwd, DEFAULT_WIKI_ENV_FILE),
373
394
  vaultSource,
@@ -388,11 +409,13 @@ function getPathDefaults(env, cwd) {
388
409
  embeddingBaseUrl: env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL ?? "https://api.openai.com/v1",
389
410
  embeddingApiKey: env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY ?? null,
390
411
  embeddingModel: env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL ?? "text-embedding-3-small",
391
- embeddingDimensions: env.EMBEDDING_DIMENSIONS ?? "384",
412
+ embeddingDimensions: env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS),
392
413
  agentEnabled: (env.WIKI_AGENT_ENABLED ?? "").trim().toLowerCase() === "true",
393
- agentBaseUrl: env.WIKI_AGENT_BASE_URL ?? "https://api.openai.com/v1",
414
+ agentAuthMode,
415
+ agentBaseUrl: env.WIKI_AGENT_BASE_URL ?? (agentAuthMode === "api-key" ? "https://api.openai.com/v1" : null),
394
416
  agentApiKey: env.WIKI_AGENT_API_KEY ?? null,
395
- agentModel: env.WIKI_AGENT_MODEL ?? null,
417
+ agentCodexHome: env.WIKI_AGENT_CODEX_HOME ?? defaultWikiAgentCodexHome(),
418
+ agentModel: env.WIKI_AGENT_MODEL ?? DEFAULT_WIKI_AGENT_MODEL,
396
419
  agentBatchSize: env.WIKI_AGENT_BATCH_SIZE ?? "5",
397
420
  agentSandboxMode: safeAgentSandboxMode(env.WIKI_AGENT_SANDBOX_MODE),
398
421
  parserSkills: parseParserSkills(env.WIKI_PARSER_SKILLS, { strict: false }),
@@ -417,7 +440,7 @@ async function collectEmbeddingSettings(driver, ctx, defaults, env) {
417
440
  required: true,
418
441
  });
419
442
  const embeddingModel = await promptText(driver, "EMBEDDING_MODEL", defaults.embeddingModel ?? "text-embedding-3-small", { required: true });
420
- const embeddingDimensions = await promptText(driver, "EMBEDDING_DIMENSIONS", defaults.embeddingDimensions ?? "384", { validator: (value) => validateNonNegativeInteger(value, "EMBEDDING_DIMENSIONS") });
443
+ const embeddingDimensions = await promptText(driver, "EMBEDDING_DIMENSIONS", defaults.embeddingDimensions ?? String(DEFAULT_EMBEDDING_DIMENSIONS), { validator: (value) => validateNonNegativeInteger(value, "EMBEDDING_DIMENSIONS") });
421
444
  const shouldProbe = await promptYesNo(driver, "Probe the embedding endpoint now?", false);
422
445
  if (shouldProbe) {
423
446
  try {
@@ -457,19 +480,50 @@ async function collectAgentSettings(driver, ctx, defaults) {
457
480
  if (!enabled) {
458
481
  return {
459
482
  agentEnabled: false,
483
+ agentAuthMode: null,
460
484
  agentBaseUrl: null,
461
485
  agentApiKey: null,
486
+ agentCodexHome: null,
462
487
  agentModel: null,
463
488
  agentBatchSize: null,
464
489
  agentSandboxMode: null,
465
490
  };
466
491
  }
467
492
  writeWarning(ctx.output, "Warning: danger-full-access grants full access to the runtime workspace.");
493
+ const agentAuthMode = await driver.select({
494
+ message: "WIKI_AGENT_AUTH_MODE",
495
+ defaultValue: defaults.agentAuthMode ?? "codex-login",
496
+ choices: [
497
+ {
498
+ value: "codex-login",
499
+ label: "codex-login",
500
+ description: "Use the current user's Codex login by default (~/.codex); set WIKI_AGENT_CODEX_HOME for isolation.",
501
+ },
502
+ {
503
+ value: "api-key",
504
+ label: "api-key",
505
+ description: "Use WIKI_AGENT_API_KEY and optional WIKI_AGENT_BASE_URL.",
506
+ },
507
+ ],
508
+ });
509
+ const authSettings = agentAuthMode === "api-key"
510
+ ? {
511
+ agentBaseUrl: await promptText(driver, "WIKI_AGENT_BASE_URL", defaults.agentBaseUrl ?? "https://api.openai.com/v1", { validator: (value) => validateUrl(value, "WIKI_AGENT_BASE_URL") }),
512
+ agentApiKey: await promptPassword(driver, "WIKI_AGENT_API_KEY", defaults.agentApiKey ?? "", {
513
+ required: true,
514
+ }),
515
+ agentCodexHome: null,
516
+ }
517
+ : {
518
+ agentBaseUrl: null,
519
+ agentApiKey: null,
520
+ agentCodexHome: await promptText(driver, "WIKI_AGENT_CODEX_HOME", defaults.agentCodexHome ?? defaultWikiAgentCodexHome(), { validator: (value) => validateAbsolutePath(value, "WIKI_AGENT_CODEX_HOME") }),
521
+ };
468
522
  return {
469
523
  agentEnabled: true,
470
- agentBaseUrl: await promptText(driver, "WIKI_AGENT_BASE_URL", defaults.agentBaseUrl ?? "https://api.openai.com/v1", { validator: (value) => validateUrl(value, "WIKI_AGENT_BASE_URL") }),
471
- agentApiKey: await promptPassword(driver, "WIKI_AGENT_API_KEY", defaults.agentApiKey ?? "", { required: true }),
472
- agentModel: await promptText(driver, "WIKI_AGENT_MODEL", defaults.agentModel ?? "", { required: true }),
524
+ agentAuthMode,
525
+ ...authSettings,
526
+ agentModel: await promptText(driver, "WIKI_AGENT_MODEL", defaults.agentModel ?? DEFAULT_WIKI_AGENT_MODEL, { required: true }),
473
527
  agentBatchSize: await promptText(driver, "WIKI_AGENT_BATCH_SIZE", defaults.agentBatchSize ?? "5", { validator: (value) => validateNonNegativeInteger(value, "WIKI_AGENT_BATCH_SIZE") }),
474
528
  agentSandboxMode: await driver.select({
475
529
  message: "WIKI_AGENT_SANDBOX_MODE",
@@ -561,7 +615,13 @@ function buildSetupSummary(values) {
561
615
  lines.push(` EMBEDDING_DIMENSIONS: ${values.embeddingDimensions}`);
562
616
  }
563
617
  if (values.agentEnabled) {
564
- lines.push(` WIKI_AGENT_BASE_URL: ${values.agentBaseUrl}`);
618
+ lines.push(` WIKI_AGENT_AUTH_MODE: ${values.agentAuthMode}`);
619
+ if (values.agentAuthMode === "api-key") {
620
+ lines.push(` WIKI_AGENT_BASE_URL: ${values.agentBaseUrl}`);
621
+ }
622
+ if (values.agentAuthMode === "codex-login") {
623
+ lines.push(` WIKI_AGENT_CODEX_HOME: ${values.agentCodexHome}`);
624
+ }
565
625
  lines.push(` WIKI_AGENT_MODEL: ${values.agentModel}`);
566
626
  lines.push(` WIKI_AGENT_BATCH_SIZE: ${values.agentBatchSize}`);
567
627
  lines.push(` WIKI_AGENT_SANDBOX_MODE: ${values.agentSandboxMode}`);
@@ -598,8 +658,10 @@ function writeSetupEnvFile(values) {
598
658
  ["EMBEDDING_MODEL", values.embeddingEnabled ? values.embeddingModel : null],
599
659
  ["EMBEDDING_DIMENSIONS", values.embeddingEnabled ? values.embeddingDimensions : null],
600
660
  ["WIKI_AGENT_ENABLED", values.agentEnabled ? "true" : "false"],
601
- ["WIKI_AGENT_BASE_URL", values.agentEnabled ? values.agentBaseUrl : null],
602
- ["WIKI_AGENT_API_KEY", values.agentEnabled ? values.agentApiKey : null],
661
+ ["WIKI_AGENT_AUTH_MODE", values.agentEnabled ? values.agentAuthMode : null],
662
+ ["WIKI_AGENT_BASE_URL", values.agentEnabled && values.agentAuthMode === "api-key" ? values.agentBaseUrl : null],
663
+ ["WIKI_AGENT_API_KEY", values.agentEnabled && values.agentAuthMode === "api-key" ? values.agentApiKey : null],
664
+ ["WIKI_AGENT_CODEX_HOME", values.agentEnabled && values.agentAuthMode === "codex-login" ? values.agentCodexHome : null],
603
665
  ["WIKI_AGENT_MODEL", values.agentEnabled ? values.agentModel : null],
604
666
  ["WIKI_AGENT_BATCH_SIZE", values.agentEnabled ? values.agentBatchSize : null],
605
667
  ["WIKI_AGENT_SANDBOX_MODE", values.agentEnabled ? values.agentSandboxMode : null],
@@ -731,6 +793,11 @@ export async function runSetupWizard(env = process.env, options = {}) {
731
793
  `- Example: cd ${JSON.stringify(workspaceRoot)} && tiangong-wiki init`,
732
794
  "- Run `tiangong-wiki doctor` to validate the generated configuration.",
733
795
  "- Run `tiangong-wiki init` to create index.db and perform the first sync.",
796
+ ...(values.agentEnabled && values.agentAuthMode === "codex-login" && values.agentCodexHome
797
+ ? [
798
+ `- Codex login auth uses ${values.agentCodexHome}; if needed, run \`CODEX_HOME=${values.agentCodexHome} codex login\` before starting the daemon.`,
799
+ ]
800
+ : []),
734
801
  ...(values.vaultSource === "synology"
735
802
  ? ["- Protect `.wiki.env` carefully because it now stores Synology credentials."]
736
803
  : []),
@@ -908,7 +975,16 @@ function inspectAgent(checks, env) {
908
975
  collectDoctorCheck(checks, "error", "agent", `Automatic vault processing is enabled but missing: ${settings.missing.join(", ")}`, "Set the missing WIKI_AGENT_* values in `.wiki.env` or rerun `tiangong-wiki setup`.");
909
976
  return;
910
977
  }
911
- collectDoctorCheck(checks, "ok", "agent", `Automatic vault processing is enabled with model ${settings.model}.`);
978
+ if (settings.authMode === "codex-login") {
979
+ const codexLogin = inspectWikiAgentCodexLogin(settings);
980
+ if (!codexLogin.ready) {
981
+ collectDoctorCheck(checks, "error", "agent", codexLogin.summary, codexLogin.recommendation);
982
+ return;
983
+ }
984
+ }
985
+ collectDoctorCheck(checks, "ok", "agent", settings.authMode === "codex-login"
986
+ ? `Automatic vault processing is enabled with model ${settings.model} via Codex login at ${settings.codexHome}.`
987
+ : `Automatic vault processing is enabled with model ${settings.model} via API key auth.`);
912
988
  }
913
989
  catch (error) {
914
990
  const message = error instanceof Error ? error.message : String(error);
@@ -1,8 +1,10 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { AppError } from "../utils/errors.js";
4
5
  const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
5
6
  const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
7
+ export const DEFAULT_WIKI_AGENT_MODEL = "gpt-5.5";
6
8
  function parseBooleanFlag(label, rawValue, defaultValue) {
7
9
  if (rawValue === undefined) {
8
10
  return defaultValue;
@@ -75,6 +77,19 @@ function requireAbsolutePath(label, rawValue) {
75
77
  }
76
78
  return path.resolve(rawValue);
77
79
  }
80
+ function normalizeOptionalAbsolutePath(label, rawValue) {
81
+ const value = rawValue?.trim();
82
+ if (!value) {
83
+ return null;
84
+ }
85
+ if (!path.isAbsolute(value)) {
86
+ throw new AppError(`${label} must be an absolute path: ${rawValue}`, "config");
87
+ }
88
+ return path.resolve(value);
89
+ }
90
+ export function defaultWikiAgentCodexHome() {
91
+ return path.join(os.homedir(), ".codex");
92
+ }
78
93
  export function parseVaultHashMode(raw) {
79
94
  const value = (raw ?? "content").trim().toLowerCase();
80
95
  if (value === "content" || value === "mtime") {
@@ -89,6 +104,13 @@ export function parseWikiAgentBackend(raw) {
89
104
  }
90
105
  throw new AppError(`WIKI_AGENT_BACKEND must be "codex-workflow", got ${raw}`, "config");
91
106
  }
107
+ export function parseWikiAgentAuthMode(raw) {
108
+ const value = (raw ?? "api-key").trim().toLowerCase();
109
+ if (value === "api-key" || value === "codex-login") {
110
+ return value;
111
+ }
112
+ throw new AppError(`WIKI_AGENT_AUTH_MODE must be "api-key" or "codex-login", got ${raw}`, "config");
113
+ }
92
114
  export function parseWikiAgentSandboxMode(raw) {
93
115
  const value = (raw ?? "danger-full-access").trim().toLowerCase();
94
116
  if (value === "danger-full-access" || value === "workspace-write") {
@@ -98,28 +120,33 @@ export function parseWikiAgentSandboxMode(raw) {
98
120
  }
99
121
  export function resolveAgentSettings(env = process.env, options = {}) {
100
122
  const enabled = parseBooleanFlag("WIKI_AGENT_ENABLED", env.WIKI_AGENT_ENABLED, false);
101
- const baseUrl = normalizeOptionalUrl(env.WIKI_AGENT_BASE_URL);
102
- const apiKey = env.WIKI_AGENT_API_KEY?.trim() || null;
103
- const model = env.WIKI_AGENT_MODEL?.trim() || null;
123
+ const authMode = parseWikiAgentAuthMode(env.WIKI_AGENT_AUTH_MODE);
124
+ const rawBaseUrl = normalizeOptionalUrl(env.WIKI_AGENT_BASE_URL);
125
+ const baseUrl = authMode === "api-key" ? rawBaseUrl : null;
126
+ const rawApiKey = env.WIKI_AGENT_API_KEY?.trim() || null;
127
+ const apiKey = authMode === "api-key" ? rawApiKey : null;
128
+ const codexHome = authMode === "codex-login"
129
+ ? normalizeOptionalAbsolutePath("WIKI_AGENT_CODEX_HOME", env.WIKI_AGENT_CODEX_HOME) ?? defaultWikiAgentCodexHome()
130
+ : null;
131
+ const model = env.WIKI_AGENT_MODEL?.trim() || DEFAULT_WIKI_AGENT_MODEL;
104
132
  const batchSize = parseNonNegativeInteger(env.WIKI_AGENT_BATCH_SIZE, 5, "WIKI_AGENT_BATCH_SIZE");
105
133
  const sandboxMode = parseWikiAgentSandboxMode(env.WIKI_AGENT_SANDBOX_MODE);
106
134
  const workflowTimeoutSeconds = parsePositiveInteger(env.WIKI_WORKFLOW_TIMEOUT, 600, "WIKI_WORKFLOW_TIMEOUT");
107
135
  const missing = [];
108
136
  if (enabled) {
109
- if (!apiKey) {
137
+ if (authMode === "api-key" && !apiKey) {
110
138
  missing.push("WIKI_AGENT_API_KEY");
111
139
  }
112
- if (!model) {
113
- missing.push("WIKI_AGENT_MODEL");
114
- }
115
140
  }
116
141
  if (options.strict && enabled && missing.length > 0) {
117
142
  throw new AppError(`WIKI_AGENT_ENABLED=true but missing required settings: ${missing.join(", ")}`, "config");
118
143
  }
119
144
  return {
120
145
  enabled,
146
+ authMode,
121
147
  baseUrl,
122
148
  apiKey,
149
+ codexHome,
123
150
  model,
124
151
  batchSize,
125
152
  sandboxMode,
@@ -1,11 +1,11 @@
1
1
  import { loadConfig } from "./config.js";
2
2
  import { openDb } from "./db.js";
3
- import { EmbeddingClient } from "./embedding.js";
3
+ import { DEFAULT_EMBEDDING_DIMENSIONS, EmbeddingClient } from "./embedding.js";
4
4
  import { resolveRuntimePaths } from "./paths.js";
5
5
  export function getEmbeddingDimensionFromEnv(env = process.env) {
6
- const raw = env.EMBEDDING_DIMENSIONS ?? "384";
6
+ const raw = env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS);
7
7
  const value = Number.parseInt(raw, 10);
8
- return Number.isFinite(value) && value > 0 ? value : 384;
8
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_EMBEDDING_DIMENSIONS;
9
9
  }
10
10
  export function loadRuntimeConfig(env = process.env) {
11
11
  const paths = resolveRuntimePaths(env);
@@ -23,21 +23,25 @@ function getExistingFtsSql(db) {
23
23
  .get();
24
24
  return row?.sql ?? null;
25
25
  }
26
- function resolveBundledSimpleExtensionPath(packageRoot) {
27
- const byArch = SIMPLE_ASSET_MAP[process.platform];
26
+ export function resolveBundledSimpleExtensionRelativePath(platform = process.platform, arch = process.arch) {
27
+ const byArch = SIMPLE_ASSET_MAP[platform];
28
28
  if (!byArch) {
29
- throw new AppError(`Bundled simple extension is not available for platform ${process.platform}-${process.arch}.`, "config", {
30
- platform: process.platform,
31
- arch: process.arch,
29
+ throw new AppError(`Bundled simple extension is not available for platform ${platform}-${arch}.`, "config", {
30
+ platform,
31
+ arch,
32
32
  });
33
33
  }
34
- const relativePath = byArch[process.arch];
34
+ const relativePath = byArch[arch];
35
35
  if (!relativePath) {
36
- throw new AppError(`Bundled simple extension is not available for platform ${process.platform}-${process.arch}.`, "config", {
37
- platform: process.platform,
38
- arch: process.arch,
36
+ throw new AppError(`Bundled simple extension is not available for platform ${platform}-${arch}.`, "config", {
37
+ platform,
38
+ arch,
39
39
  });
40
40
  }
41
+ return relativePath;
42
+ }
43
+ function resolveBundledSimpleExtensionPath(packageRoot) {
44
+ const relativePath = resolveBundledSimpleExtensionRelativePath();
41
45
  const extensionPath = path.join(packageRoot, relativePath);
42
46
  if (!pathExistsSync(extensionPath)) {
43
47
  throw new AppError(`Bundled simple extension not found: ${extensionPath}`, "runtime", {
package/dist/core/sync.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from "./config.js";
2
2
  import { clearAllIndexedData, getMeta, openDb, resetVectorTable, setMetaValues, } from "./db.js";
3
- import { EmbeddingClient } from "./embedding.js";
3
+ import { DEFAULT_EMBEDDING_DIMENSIONS, EmbeddingClient } from "./embedding.js";
4
4
  import { applyChanges, scanPages, scanSpecificPages } from "./indexer.js";
5
5
  import { resolveRuntimePaths } from "./paths.js";
6
6
  import { collectVaultFiles, syncVaultIndex } from "./vault.js";
@@ -8,9 +8,9 @@ import { AppError } from "../utils/errors.js";
8
8
  import { pathExistsSync } from "../utils/fs.js";
9
9
  import { makeSyncId, toOffsetIso } from "../utils/time.js";
10
10
  function getEmbeddingDimension(env) {
11
- const raw = env.EMBEDDING_DIMENSIONS ?? "384";
11
+ const raw = env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS);
12
12
  const value = Number.parseInt(raw, 10);
13
- return Number.isFinite(value) && value > 0 ? value : 384;
13
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_EMBEDDING_DIMENSIONS;
14
14
  }
15
15
  function getEmbeddingTargets(db, embedAll, insertedIds, summaryChangedIds) {
16
16
  const rows = db.prepare("SELECT rowid, id, summary_text AS summaryText, embedding_status AS embeddingStatus FROM pages").all();
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { CODEX_WORKFLOW_VERSION, createDefaultWorkflowRunner, } from "./codex-workflow.js";
5
5
  import { loadConfig } from "./config.js";
6
6
  import { openDb } from "./db.js";
7
+ import { DEFAULT_EMBEDDING_DIMENSIONS } from "./embedding.js";
7
8
  import { resolveAgentSettings, resolveRuntimePaths } from "./paths.js";
8
9
  import { assertTemplateEvolutionAllowed, resolveTemplateEvolutionSettings } from "./template-evolution.js";
9
10
  import { ensureLocalVaultFile } from "./vault.js";
@@ -817,7 +818,7 @@ function prepareCodexWorkflowInput(paths, item, file, localFilePath, env, allowT
817
818
  queueItemPath: artifacts.queueItemPath,
818
819
  resultPath: artifacts.resultPath,
819
820
  skillArtifactsPath: artifacts.skillArtifactsPath,
820
- model: env.WIKI_AGENT_MODEL ?? null,
821
+ model: resolveAgentSettings(env).model,
821
822
  env,
822
823
  },
823
824
  };
@@ -1017,7 +1018,7 @@ async function processClaimedQueueItem(input) {
1017
1018
  export function getVaultQueueSnapshot(env = process.env, status) {
1018
1019
  const paths = resolveRuntimePaths(env);
1019
1020
  const config = loadConfig(paths.configPath);
1020
- const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384, paths.packageRoot);
1021
+ const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS), 10) || DEFAULT_EMBEDDING_DIMENSIONS, paths.packageRoot);
1021
1022
  try {
1022
1023
  const items = fetchQueueItemsByStatus(db, status);
1023
1024
  const counts = db.prepare(`
@@ -1045,7 +1046,7 @@ export function getVaultQueueSnapshot(env = process.env, status) {
1045
1046
  export function getVaultQueueItem(env = process.env, fileId) {
1046
1047
  const paths = resolveRuntimePaths(env);
1047
1048
  const config = loadConfig(paths.configPath);
1048
- const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384, paths.packageRoot);
1049
+ const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS), 10) || DEFAULT_EMBEDDING_DIMENSIONS, paths.packageRoot);
1049
1050
  try {
1050
1051
  return fetchQueueItemByFileId(db, fileId);
1051
1052
  }
@@ -1067,7 +1068,7 @@ export async function processVaultQueueBatch(env = process.env, options = {}) {
1067
1068
  }
1068
1069
  const paths = resolveRuntimePaths(env);
1069
1070
  const config = loadConfig(paths.configPath);
1070
- const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? "384", 10) || 384, paths.packageRoot);
1071
+ const { db } = openDb(paths.dbPath, config, Number.parseInt(env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS), 10) || DEFAULT_EMBEDDING_DIMENSIONS, paths.packageRoot);
1071
1072
  try {
1072
1073
  const result = {
1073
1074
  enabled: true,
@@ -340,6 +340,7 @@ function extractPdfText(filePath) {
340
340
  const result = execFileSync("/usr/bin/mdls", ["-raw", "-name", "kMDItemTextContent", filePath], {
341
341
  encoding: "utf8",
342
342
  stdio: ["pipe", "pipe", "pipe"],
343
+ windowsHide: true,
343
344
  });
344
345
  const text = (result || "").trim();
345
346
  if (text && text !== "(null)")
@@ -350,6 +351,7 @@ function extractPdfText(filePath) {
350
351
  const result = execFileSync("/usr/bin/strings", ["-n", "6", filePath], {
351
352
  encoding: "utf8",
352
353
  stdio: ["pipe", "pipe", "pipe"],
354
+ windowsHide: true,
353
355
  });
354
356
  const lines = result.split("\n").map((l) => l.trim()).filter(Boolean).slice(0, 400);
355
357
  if (lines.length)
@@ -5,6 +5,9 @@ import { sha256Text } from "../utils/fs.js";
5
5
  function shellSingleQuote(value) {
6
6
  return `'${value.replace(/'/g, `'\\''`)}'`;
7
7
  }
8
+ function batchSetLiteral(value) {
9
+ return value.replace(/%/g, "%%").replace(/"/g, '""');
10
+ }
8
11
  function resolveNodeExecutable() {
9
12
  const currentExec = path.basename(process.execPath).toLowerCase();
10
13
  if (currentExec === "node" || currentExec.startsWith("node")) {
@@ -59,6 +62,7 @@ export function buildVaultWorkflowPrompt(input) {
59
62
  "",
60
63
  "Workspace-local skills are available from WORKSPACE_ROOT through normal Codex skill discovery.",
61
64
  "A local tiangong-wiki CLI launcher is already available on PATH for this run.",
65
+ "On Windows native shells, use `tiangong-wiki.cmd` instead of the suffixless `tiangong-wiki` command.",
62
66
  "",
63
67
  "The tiangong-wiki CLI provides these discovery and search capabilities:",
64
68
  "- `tiangong-wiki type list` / `tiangong-wiki type show <type>` — discover registered page types and their purpose",
@@ -193,6 +197,7 @@ export function ensureWorkflowArtifactSet(paths, input) {
193
197
  ensureDirSync(artifacts.rootDir);
194
198
  ensureDirSync(artifacts.skillArtifactsPath);
195
199
  const wikiCliWrapperPath = path.join(artifacts.skillArtifactsPath, "tiangong-wiki");
200
+ const wikiCliCmdWrapperPath = path.join(artifacts.skillArtifactsPath, "tiangong-wiki.cmd");
196
201
  const nodeExecutable = resolveNodeExecutable();
197
202
  const cliEntrypoint = path.join(paths.packageRoot, "dist", "index.js");
198
203
  writeTextFileSync(artifacts.queueItemPath, `${JSON.stringify(input.queueItem, null, 2)}\n`);
@@ -214,6 +219,22 @@ export function ensureWorkflowArtifactSet(paths, input) {
214
219
  "",
215
220
  ].join("\n"));
216
221
  chmodSync(wikiCliWrapperPath, 0o755);
222
+ writeTextFileSync(wikiCliCmdWrapperPath, [
223
+ "@echo off",
224
+ "setlocal",
225
+ "if not defined WIKI_CLI_NODE set \"WIKI_CLI_NODE=%~dp0node.exe\"",
226
+ `if not exist "%WIKI_CLI_NODE%" set "WIKI_CLI_NODE=${batchSetLiteral(nodeExecutable)}"`,
227
+ "if not defined WIKI_CLI_ENTRYPOINT (",
228
+ ` set "WIKI_CLI_ENTRYPOINT=${batchSetLiteral(cliEntrypoint)}"`,
229
+ ")",
230
+ "if not exist \"%WIKI_CLI_ENTRYPOINT%\" (",
231
+ " echo tiangong-wiki CLI entrypoint not found: %WIKI_CLI_ENTRYPOINT% 1>&2",
232
+ " exit /b 127",
233
+ ")",
234
+ "\"%WIKI_CLI_NODE%\" \"%WIKI_CLI_ENTRYPOINT%\" %*",
235
+ "exit /b %ERRORLEVEL%",
236
+ "",
237
+ ].join("\r\n"));
217
238
  writeTextFileSync(artifacts.promptPath, input.promptMarkdown ??
218
239
  [
219
240
  "# Vault To Wiki Workflow",
@@ -25,8 +25,8 @@ function canRead(filePath) {
25
25
  accessSync(filePath, constants.R_OK);
26
26
  return true;
27
27
  }
28
- function getNpxCommand() {
29
- return process.platform === "win32" ? "npx.cmd" : "npx";
28
+ export function getNpxCommand(platform = process.platform) {
29
+ return platform === "win32" ? "npx.cmd" : "npx";
30
30
  }
31
31
  export function resolveWorkspaceRootFromWikiPath(wikiPath) {
32
32
  return path.resolve(wikiPath, "..", "..");
@@ -186,6 +186,9 @@ function replaceSkillDirectory(targetPath, sourcePath) {
186
186
  ensureDirSync(targetPath);
187
187
  copyDirectoryContentsSync(sourcePath, targetPath);
188
188
  }
189
+ function linkWorkspaceSkill(sourcePath, targetPath) {
190
+ symlinkSync(sourcePath, targetPath, process.platform === "win32" ? "junction" : "dir");
191
+ }
189
192
  function createManagedSkillMetadata(descriptor, baselineHash, command) {
190
193
  return {
191
194
  version: 1,
@@ -255,7 +258,7 @@ function createWikiDescriptor(wikiPath, packageRoot) {
255
258
  }
256
259
  function renderCommand(command, args) {
257
260
  return [command, ...args]
258
- .map((part) => (/[A-Za-z0-9_./:@+-]+/.test(part) ? part : JSON.stringify(part)))
261
+ .map((part) => (/^[A-Za-z0-9_./:@+-]+$/.test(part) ? part : JSON.stringify(part)))
259
262
  .join(" ");
260
263
  }
261
264
  export function buildExternalSkillInstallInvocation(source, skillName) {
@@ -267,6 +270,18 @@ export function buildExternalSkillInstallInvocation(source, skillName) {
267
270
  rendered: renderCommand(command, args),
268
271
  };
269
272
  }
273
+ export function buildExternalSkillInstallSpawnInvocation(invocation, platform = process.platform, env = process.env) {
274
+ if (platform !== "win32") {
275
+ return {
276
+ command: invocation.command,
277
+ args: invocation.args,
278
+ };
279
+ }
280
+ return {
281
+ command: env.ComSpec?.trim() || "cmd.exe",
282
+ args: ["/d", "/c", "call", invocation.command, ...invocation.args],
283
+ };
284
+ }
270
285
  function installManagedExternalSkill(descriptor, options = {}) {
271
286
  if (descriptor.sourceKind === "workspace-package") {
272
287
  throw new AppError("Workspace package skills must be installed via ensureWikiSkillInstall.", "config");
@@ -285,10 +300,12 @@ function installManagedExternalSkill(descriptor, options = {}) {
285
300
  }
286
301
  const workspaceRoot = getWorkspaceRootForSkillPath(descriptor.skillPath);
287
302
  options.output?.write(`Installing skill ${descriptor.name} from ${descriptor.source}...\n`);
288
- const result = spawnSync(invocation.command, invocation.args, {
303
+ const spawnInvocation = buildExternalSkillInstallSpawnInvocation(invocation);
304
+ const result = spawnSync(spawnInvocation.command, spawnInvocation.args, {
289
305
  cwd: workspaceRoot,
290
306
  env: options.env ?? process.env,
291
307
  encoding: "utf8",
308
+ windowsHide: true,
292
309
  });
293
310
  if (result.error) {
294
311
  throw new AppError(`failed to install skill ${descriptor.name}: ${result.error.message}`, "runtime", {
@@ -610,7 +627,7 @@ export function ensureWikiSkillInstall(wikiPath, packageRoot) {
610
627
  };
611
628
  }
612
629
  unlinkSync(paths.wikiSkillPath);
613
- symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
630
+ linkWorkspaceSkill(packageRoot, paths.wikiSkillPath);
614
631
  return {
615
632
  sourcePath: packageRoot,
616
633
  skillPath: paths.wikiSkillPath,
@@ -619,7 +636,7 @@ export function ensureWikiSkillInstall(wikiPath, packageRoot) {
619
636
  }
620
637
  if (existing.readable) {
621
638
  rmSync(paths.wikiSkillPath, { recursive: true, force: true });
622
- symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
639
+ linkWorkspaceSkill(packageRoot, paths.wikiSkillPath);
623
640
  return {
624
641
  sourcePath: packageRoot,
625
642
  skillPath: paths.wikiSkillPath,
@@ -631,7 +648,7 @@ export function ensureWikiSkillInstall(wikiPath, packageRoot) {
631
648
  skillPath: paths.wikiSkillPath,
632
649
  });
633
650
  }
634
- symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
651
+ linkWorkspaceSkill(packageRoot, paths.wikiSkillPath);
635
652
  return {
636
653
  sourcePath: packageRoot,
637
654
  skillPath: paths.wikiSkillPath,
@@ -16,6 +16,7 @@ function runGit(paths, actor, args) {
16
16
  const result = spawnSync("git", ["-C", paths.wikiRoot, ...args], {
17
17
  encoding: "utf8",
18
18
  env: buildGitEnv(actor),
19
+ windowsHide: true,
19
20
  });
20
21
  return {
21
22
  status: result.status,
@@ -18,39 +18,40 @@ export function getCurrentInvocation() {
18
18
  args: [argv1],
19
19
  };
20
20
  }
21
- export function spawnDetachedCurrentProcess(extraArgs, options = {}) {
22
- const invocation = getCurrentInvocation();
21
+ export function buildDetachedSpawnOptions(options = {}) {
23
22
  const stdio = options.logFile
24
23
  ? ["ignore", openSync(options.logFile, "a"), openSync(options.logFile, "a")]
25
24
  : "ignore";
26
- const child = spawn(invocation.command, [...invocation.args, ...extraArgs], {
25
+ return {
27
26
  detached: true,
28
27
  stdio,
29
28
  env: options.env ?? process.env,
30
- });
29
+ windowsHide: true,
30
+ };
31
+ }
32
+ export function spawnDetachedCurrentProcess(extraArgs, options = {}) {
33
+ const invocation = getCurrentInvocation();
34
+ const child = spawn(invocation.command, [...invocation.args, ...extraArgs], buildDetachedSpawnOptions(options));
31
35
  child.unref();
32
36
  return child.pid;
33
37
  }
34
- export function openTarget(target) {
35
- let command = "";
36
- let args = [];
37
- if (process.platform === "darwin") {
38
- command = "open";
39
- args = [target];
40
- }
41
- else if (process.platform === "win32") {
42
- command = "cmd";
43
- args = ["/c", "start", "", target];
38
+ export function buildOpenTargetInvocation(target, platform = process.platform) {
39
+ if (platform === "darwin") {
40
+ return { command: "open", args: [target] };
44
41
  }
45
- else {
46
- command = "xdg-open";
47
- args = [target];
42
+ if (platform === "win32") {
43
+ return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", target] };
48
44
  }
45
+ return { command: "xdg-open", args: [target] };
46
+ }
47
+ export function openTarget(target) {
48
+ const { command, args } = buildOpenTargetInvocation(target);
49
49
  try {
50
50
  const child = spawn(command, args, {
51
51
  detached: true,
52
52
  stdio: "ignore",
53
53
  shell: false,
54
+ windowsHide: true,
54
55
  });
55
56
  child.unref();
56
57
  }
@@ -15,6 +15,6 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@modelcontextprotocol/sdk": "^1.29.0",
18
- "zod": "^4.3.6"
18
+ "zod": "^4.4.2"
19
19
  }
20
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biaoo/tiangong-wiki",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Local-first wiki index and query engine for Markdown knowledge pages (Tiangong Wiki).",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -30,7 +30,7 @@
30
30
  "SKILL.md"
31
31
  ],
32
32
  "engines": {
33
- "node": ">=18"
33
+ "node": ">=22.13.0"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "npm run build:cli && npm run build:dashboard && npm run build:mcp-server",
@@ -48,25 +48,25 @@
48
48
  "@antv/g6": "^5.1.0",
49
49
  "@fontsource/jetbrains-mono": "^5.2.8",
50
50
  "@fontsource/space-grotesk": "^5.2.10",
51
- "@inquirer/prompts": "^7.10.1",
51
+ "@inquirer/prompts": "^8.4.2",
52
52
  "@modelcontextprotocol/sdk": "^1.29.0",
53
- "@openai/codex-sdk": "^0.118.0",
53
+ "@openai/codex-sdk": "^0.128.0",
54
54
  "adm-zip": "^0.5.17",
55
- "better-sqlite3": "^12.8.0",
55
+ "better-sqlite3": "^12.9.0",
56
56
  "commander": "^14.0.3",
57
57
  "gray-matter": "^4.0.3",
58
58
  "preact": "^10.29.1",
59
59
  "sqlite-vec": "^0.1.9",
60
- "zod": "^4.3.6"
60
+ "zod": "^4.4.2"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@preact/preset-vite": "^2.10.5",
64
64
  "@types/adm-zip": "^0.5.8",
65
65
  "@types/better-sqlite3": "^7.6.13",
66
- "@types/node": "^24.5.2",
66
+ "@types/node": "^25.6.0",
67
67
  "tsx": "^4.21.0",
68
- "typescript": "^6.0.2",
69
- "vite": "^8.0.7",
70
- "vitest": "^4.1.2"
68
+ "typescript": "^6.0.3",
69
+ "vite": "^8.0.10",
70
+ "vitest": "^4.1.5"
71
71
  }
72
72
  }
@@ -13,6 +13,14 @@ npx @biaoo/tiangong-wiki <command> [options]
13
13
  npm run dev -- <command> [options]
14
14
  ```
15
15
 
16
+ On Windows native shells (PowerShell, Command Prompt, scheduled/background tasks, and Codex worker automation), use the npm `.cmd` shim explicitly:
17
+
18
+ ```powershell
19
+ tiangong-wiki.cmd <command> [options]
20
+ ```
21
+
22
+ Do not use the suffixless `tiangong-wiki` command in Windows native shells. npm installs it for POSIX-like environments such as macOS, Linux, WSL, and Git Bash; Windows may try to open it as an unknown file instead of executing it.
23
+
16
24
  Global workspace resolution priority:
17
25
 
18
26
  1. `--env-file <path>`
@@ -75,24 +75,28 @@ For the full single-host deployment baseline, see [centralized-service-deploymen
75
75
  | `EMBEDDING_BASE_URL` | Yes | Embedding API base URL |
76
76
  | `EMBEDDING_API_KEY` | Yes | Embedding API key |
77
77
  | `EMBEDDING_MODEL` | Yes | Embedding model name |
78
- | `EMBEDDING_DIMENSIONS` | No | Vector dimensions (default: model-dependent) |
78
+ | `EMBEDDING_DIMENSIONS` | No | Vector dimensions (default: `1536`, matching the default OpenAI `text-embedding-3-small`; set explicitly only when using a different dimension profile) |
79
79
 
80
80
  ### Agent (Agentic Workflow)
81
81
 
82
- The agent uses [Codex SDK](https://www.npmjs.com/package/@openai/codex-sdk) to process vault items. When `WIKI_AGENT_BASE_URL` is set, a custom `model_provider` is injected to override any global `~/.codex/config.toml` settings.
82
+ The agent uses [Codex SDK](https://www.npmjs.com/package/@openai/codex-sdk) to process vault items. It supports API-key auth and local Codex login auth. In `api-key` mode, when `WIKI_AGENT_BASE_URL` is set, a custom `model_provider` is injected to override any global `~/.codex/config.toml` settings. In `codex-login` mode, the workflow uses `WIKI_AGENT_CODEX_HOME` as `CODEX_HOME` and does not pass API key environment variables to Codex.
83
83
 
84
84
  | Variable | Required | Description |
85
85
  | --- | --- | --- |
86
86
  | `WIKI_AGENT_ENABLED` | No | Enable agentic workflow (`true` / `false`, default: `false`) |
87
- | `WIKI_AGENT_BASE_URL` | No | LLM API base URL (e.g. `https://api.openai.com/v1`). When set, overrides global Codex config |
88
- | `WIKI_AGENT_API_KEY` | If enabled | API key for the LLM provider |
89
- | `WIKI_AGENT_MODEL` | No | Model name (e.g. `gpt-5.4`, `Qwen/Qwen3.5-397B-A17B-GPTQ-Int4`) |
87
+ | `WIKI_AGENT_AUTH_MODE` | No | Auth mode: `api-key` or `codex-login`. Runtime default is `api-key` for backwards compatibility; `tiangong-wiki setup` defaults new agent configs to `codex-login` |
88
+ | `WIKI_AGENT_CODEX_HOME` | No | Codex home directory. Leave unset to use the current user's standard `${HOME}/.codex` (or the user profile `.codex` directory on Windows); if set in `.wiki.env`, use a real absolute path because shell variables are not expanded there |
89
+ | `WIKI_AGENT_BASE_URL` | No | LLM API base URL for `api-key` mode (e.g. `https://api.openai.com/v1`). When set, overrides global Codex config |
90
+ | `WIKI_AGENT_API_KEY` | In `api-key` mode | API key for the LLM provider |
91
+ | `WIKI_AGENT_MODEL` | No | Model name (default: `gpt-5.5`; e.g. `Qwen/Qwen3.5-397B-A17B-GPTQ-Int4`) |
90
92
  | `WIKI_AGENT_BATCH_SIZE` | No | Max concurrent vault queue workers per cycle (default: `5`) |
91
93
  | `WIKI_AGENT_SANDBOX_MODE` | No | Codex sandbox mode: `danger-full-access` (default) or `workspace-write` |
92
94
  | `WIKI_PARSER_SKILLS` | No | Comma-separated parser skill list (e.g. `pdf,docx,pptx,xlsx`) |
93
95
 
94
96
  `tiangong-wiki setup` now prompts for `WIKI_AGENT_SANDBOX_MODE` when automatic vault processing is enabled. The default is `danger-full-access`, and the setup wizard highlights that this mode grants full runtime access.
95
97
 
98
+ When `WIKI_AGENT_ENABLED=true` and `WIKI_AGENT_AUTH_MODE=codex-login`, `tiangong-wiki doctor` and `tiangong-wiki check-config` verify that `WIKI_AGENT_CODEX_HOME` exists and contains `auth.json`. They report the path and remediation command, but never print token or auth file contents.
99
+
96
100
  Queue items that fail workflow execution are auto-retried up to 3 times. After that they remain in `error` until you manually retry them from the dashboard / queue tooling, or a later vault sync requeues the file because the source changed.
97
101
 
98
102
  ---
@@ -122,6 +126,20 @@ Always run `tiangong-wiki lint --path <page-id> --format json` after mutations.
122
126
 
123
127
  Parser skills must be installed under `<workspace-root>/.agents/skills/`. Run `tiangong-wiki skill` to inspect installed skills. Use `tiangong-wiki skill update --all` to update.
124
128
 
129
+ ### Windows opens "choose an app" instead of running tiangong-wiki
130
+
131
+ In Windows native shells, invoke the npm command shim with the `.cmd` suffix:
132
+
133
+ ```powershell
134
+ tiangong-wiki.cmd daemon status
135
+ tiangong-wiki.cmd sync
136
+ tiangong-wiki.cmd lint --format json
137
+ ```
138
+
139
+ Avoid bare `tiangong-wiki` in PowerShell, Command Prompt, daemon scripts, and Codex worker automation. npm installs a suffixless shebang script for POSIX-like environments, but Windows native shells do not execute it the same way as macOS, Linux, WSL, or Git Bash.
140
+
141
+ Vault workflow artifacts also include a workspace-local `tiangong-wiki.cmd` launcher. If a Windows agent opens a new command window or an app chooser while processing vault items, verify that it is calling `tiangong-wiki.cmd`, not the suffixless `tiangong-wiki` wrapper.
142
+
125
143
  ### Codex workflow sandbox fails to initialize
126
144
 
127
145
  If the agent workflow fails with `bwrap`, `unshare`, `uid_map`, or similar sandbox startup errors, switch `WIKI_AGENT_SANDBOX_MODE` to `danger-full-access`. Use `workspace-write` only when you explicitly want that sandbox mode and know the host supports it.
@@ -130,12 +148,60 @@ If the agent workflow fails with `bwrap`, `unshare`, `uid_map`, or similar sandb
130
148
 
131
149
  ## LLM Provider Setup
132
150
 
133
- ### OpenAI (default)
151
+ ### Codex login (recommended local setup)
152
+
153
+ By default, use the current user's standard Codex home:
154
+
155
+ macOS/Linux:
156
+
157
+ ```bash
158
+ codex login
159
+ codex login status
160
+ ```
161
+
162
+ Windows PowerShell:
163
+
164
+ ```powershell
165
+ codex login
166
+ codex login status
167
+ ```
168
+
169
+ For an isolated Codex home, set `WIKI_AGENT_CODEX_HOME` to an absolute path and log in there first:
170
+
171
+ macOS/Linux:
172
+
173
+ ```bash
174
+ mkdir -p "$HOME/.codex-tiangong-wiki"
175
+ CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login
176
+ CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login status
177
+ ```
178
+
179
+ Windows PowerShell:
180
+
181
+ ```powershell
182
+ New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex-tiangong-wiki" | Out-Null
183
+ $env:CODEX_HOME = "$env:USERPROFILE\.codex-tiangong-wiki"
184
+ codex login
185
+ codex login status
186
+ ```
187
+
188
+ Then configure the wiki agent:
189
+
190
+ ```env
191
+ WIKI_AGENT_ENABLED=true
192
+ WIKI_AGENT_AUTH_MODE=codex-login
193
+ # Optional. Leave unset to use the current user's standard ~/.codex.
194
+ # WIKI_AGENT_CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki
195
+ WIKI_AGENT_MODEL=gpt-5.5
196
+ ```
197
+
198
+ ### OpenAI API key
134
199
 
135
200
  ```env
136
201
  WIKI_AGENT_ENABLED=true
202
+ WIKI_AGENT_AUTH_MODE=api-key
137
203
  WIKI_AGENT_API_KEY=sk-...
138
- WIKI_AGENT_MODEL=gpt-5.4
204
+ WIKI_AGENT_MODEL=gpt-5.5
139
205
  ```
140
206
 
141
207
  ### vLLM (self-hosted)
@@ -144,6 +210,7 @@ Requires vLLM **v0.8.5+** for Responses API support (`/v1/responses`).
144
210
 
145
211
  ```env
146
212
  WIKI_AGENT_ENABLED=true
213
+ WIKI_AGENT_AUTH_MODE=api-key
147
214
  WIKI_AGENT_BASE_URL=http://<host>:<port>/v1
148
215
  WIKI_AGENT_API_KEY=<your-token>
149
216
  WIKI_AGENT_MODEL=Qwen/Qwen3.5-397B-A17B-GPTQ-Int4