@biaoo/tiangong-wiki 0.3.6 → 0.3.7

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`, a dedicated Codex home under the current user's home directory, and `WIKI_AGENT_MODEL=gpt-5.5`. Before enabling that mode, run `CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login` on macOS/Linux, or set `$env:CODEX_HOME = "$env:USERPROFILE\.codex-tiangong-wiki"` before `codex login` on Windows PowerShell.
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`、当前用户 home 目录下的专用 Codex home 和 `WIKI_AGENT_MODEL=gpt-5.5`。启用前,macOS/Linux 执行 `CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login`;Windows PowerShell 先设置 `$env:CODEX_HOME = "$env:USERPROFILE\.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,24 @@ 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 on macOS/Linux:
15
+ # CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login
16
+ # On Windows PowerShell:
17
+ # $env:CODEX_HOME = "$env:USERPROFILE\.codex-tiangong-wiki"; codex login
18
+ WIKI_AGENT_AUTH_MODE=codex-login
19
+ # Optional. Leave unset to use the current user's default: <home>/.codex-tiangong-wiki
20
+ # WIKI_AGENT_CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki
21
+ #
22
+ # To use an API key instead of Codex login:
23
+ # WIKI_AGENT_AUTH_MODE=api-key
24
+ # WIKI_AGENT_BASE_URL=https://api.openai.com/v1
25
+ # WIKI_AGENT_API_KEY=sk-...
26
+ WIKI_AGENT_MODEL=gpt-5.5
27
+ WIKI_AGENT_BATCH_SIZE=5
28
+ WIKI_AGENT_SANDBOX_MODE=danger-full-access
12
29
 
13
30
  VAULT_SOURCE=local
14
31
  # When VAULT_SOURCE=synology, also set:
@@ -63,7 +63,9 @@ export function registerCheckConfigCommand(program) {
63
63
  embeddingConfigured: embeddingClient !== null,
64
64
  agentEnabled: wikiAgent.enabled,
65
65
  agentConfigured: wikiAgent.configured,
66
+ agentAuthMode: wikiAgent.authMode,
66
67
  agentBaseUrl: wikiAgent.baseUrl ?? "",
68
+ agentCodexHome: wikiAgent.codexHome ?? "",
67
69
  agentModel: wikiAgent.model ?? "",
68
70
  agentBatchSize: wikiAgent.batchSize,
69
71
  agentWorkflowTimeoutSeconds: wikiAgent.workflowTimeoutSeconds,
@@ -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,89 @@
1
1
  import path from "node:path";
2
- import { Codex } from "@openai/codex-sdk";
2
+ import { mkdirSync } from "node:fs";
3
+ import childProcess from "node:child_process";
4
+ import { syncBuiltinESMExports } from "node:module";
3
5
  import { readWorkflowResult } from "./workflow-result.js";
4
6
  import { resolveAgentSettings } from "./paths.js";
5
7
  import { readTextFileSync, writeTextFileSync } from "../utils/fs.js";
6
8
  import { AppError } from "../utils/errors.js";
7
9
  export const CODEX_WORKFLOW_VERSION = "2026-04-07";
10
+ const hiddenWindowsSpawnPatch = Symbol.for("tiangong-wiki.hiddenWindowsSpawnPatch");
11
+ function isSpawnOptions(value) {
12
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+ function addHiddenWindowDefault(options) {
15
+ if (!isSpawnOptions(options)) {
16
+ return { windowsHide: true };
17
+ }
18
+ return { ...options, windowsHide: options.windowsHide ?? true };
19
+ }
20
+ export function createHiddenWindowsSpawn(spawnFn) {
21
+ return ((command, argsOrOptions, options) => {
22
+ if (Array.isArray(argsOrOptions)) {
23
+ return spawnFn(command, argsOrOptions, addHiddenWindowDefault(options));
24
+ }
25
+ if (isSpawnOptions(argsOrOptions) && options === undefined) {
26
+ return spawnFn(command, addHiddenWindowDefault(argsOrOptions));
27
+ }
28
+ if (argsOrOptions === undefined && options === undefined) {
29
+ return spawnFn(command, addHiddenWindowDefault(undefined));
30
+ }
31
+ return spawnFn(command, argsOrOptions, addHiddenWindowDefault(options));
32
+ });
33
+ }
34
+ function installHiddenWindowsSpawnPatch(platform = process.platform) {
35
+ if (platform !== "win32") {
36
+ return;
37
+ }
38
+ const currentSpawn = childProcess.spawn;
39
+ if (currentSpawn[hiddenWindowsSpawnPatch]) {
40
+ return;
41
+ }
42
+ const patchedSpawn = createHiddenWindowsSpawn(childProcess.spawn);
43
+ patchedSpawn[hiddenWindowsSpawnPatch] = true;
44
+ childProcess.spawn = patchedSpawn;
45
+ syncBuiltinESMExports();
46
+ }
47
+ let codexSdkModulePromise = null;
48
+ async function loadCodexSdk() {
49
+ // The SDK captures child_process.spawn during module import and currently
50
+ // does not expose windowsHide; install the Windows default before loading it.
51
+ installHiddenWindowsSpawnPatch();
52
+ codexSdkModulePromise ??= import("@openai/codex-sdk");
53
+ return codexSdkModulePromise;
54
+ }
8
55
  function normalizeEnv(input) {
56
+ const agentSettings = resolveAgentSettings(input.env);
57
+ if (agentSettings.authMode === "codex-login" && agentSettings.codexHome) {
58
+ mkdirSync(agentSettings.codexHome, { recursive: true });
59
+ }
9
60
  const normalized = {};
10
61
  for (const [key, value] of Object.entries({
11
62
  ...process.env,
12
63
  ...input.env,
13
- ...(input.env?.WIKI_AGENT_API_KEY && !input.env.OPENAI_API_KEY ? { OPENAI_API_KEY: input.env.WIKI_AGENT_API_KEY } : {}),
64
+ ...(agentSettings.authMode === "api-key" && agentSettings.apiKey && !input.env?.OPENAI_API_KEY
65
+ ? { OPENAI_API_KEY: agentSettings.apiKey }
66
+ : {}),
67
+ ...(agentSettings.authMode === "codex-login" && agentSettings.codexHome
68
+ ? { CODEX_HOME: agentSettings.codexHome }
69
+ : {}),
14
70
  })) {
15
71
  if (typeof value === "string") {
16
72
  normalized[key] = value;
17
73
  }
18
74
  }
75
+ if (agentSettings.authMode === "codex-login") {
76
+ delete normalized.OPENAI_API_KEY;
77
+ delete normalized.CODEX_API_KEY;
78
+ }
19
79
  normalized.PATH = [input.skillArtifactsPath, normalized.PATH].filter(Boolean).join(path.delimiter);
20
80
  return normalized;
21
81
  }
22
- function createCodexClient(input) {
82
+ async function createCodexClient(input) {
83
+ const agentSettings = resolveAgentSettings(input.env);
23
84
  const env = normalizeEnv(input);
24
- const baseUrl = input.env?.WIKI_AGENT_BASE_URL?.trim();
25
- const apiKey = input.env?.WIKI_AGENT_API_KEY?.trim();
85
+ const baseUrl = agentSettings.authMode === "api-key" ? agentSettings.baseUrl : null;
86
+ const apiKey = agentSettings.authMode === "api-key" ? agentSettings.apiKey : null;
26
87
  const options = {
27
88
  apiKey: apiKey || undefined,
28
89
  env,
@@ -43,6 +104,7 @@ function createCodexClient(input) {
43
104
  },
44
105
  };
45
106
  }
107
+ const { Codex } = await loadCodexSdk();
46
108
  return new Codex(options);
47
109
  }
48
110
  function persistWorkflowThreadId(queueItemPath, threadId) {
@@ -117,7 +179,7 @@ export class CodexSdkWorkflowRunner {
117
179
  // The SDK can only continue a thread by sending a new input, so queue retries
118
180
  // must not automatically resume real workflow threads inline.
119
181
  async startWorkflow(input) {
120
- const codex = createCodexClient(input);
182
+ const codex = await createCodexClient(input);
121
183
  const thread = codex.startThread({
122
184
  model: input.model ?? undefined,
123
185
  modelReasoningEffort: "low",
@@ -133,7 +195,7 @@ export class CodexSdkWorkflowRunner {
133
195
  return { threadId, mode: "start" };
134
196
  }
135
197
  async resumeWorkflow(threadId, input) {
136
- const codex = createCodexClient(input);
198
+ const codex = await createCodexClient(input);
137
199
  const thread = codex.resumeThread(threadId, {
138
200
  model: input.model ?? undefined,
139
201
  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
  }
@@ -4,9 +4,9 @@ import { fileURLToPath } from "node:url";
4
4
  import { confirm, input, password, select } from "@inquirer/prompts";
5
5
  import { DEFAULT_WIKI_ENV_FILE, getCliEnvironmentInfo, parseEnvFile, serializeEnvEntries } from "./cli-env.js";
6
6
  import { resolveTemplateFilePath, loadConfig } from "./config.js";
7
- import { EmbeddingClient } from "./embedding.js";
7
+ import { DEFAULT_EMBEDDING_DIMENSIONS, EmbeddingClient } from "./embedding.js";
8
8
  import { writeGlobalConfig } from "./global-config.js";
9
- import { parseVaultHashMode, parseWikiAgentSandboxMode, resolveAgentSettings } from "./paths.js";
9
+ import { DEFAULT_WIKI_AGENT_MODEL, defaultWikiAgentCodexHome, parseVaultHashMode, parseWikiAgentAuthMode, parseWikiAgentSandboxMode, resolveAgentSettings, } from "./paths.js";
10
10
  import { loadSynologyConfigFromEnv, normalizeSynologyRemotePath, withSynologyClient } from "./synology.js";
11
11
  import { ensureWikiSkillInstall, formatParserSkills, inspectSkillInstall, installParserSkill, OPTIONAL_PARSER_SKILLS, parseParserSkillSelection, parseParserSkills, resolveWorkspaceRootFromWikiPath, resolveWorkspaceSkillPath, resolveWorkspaceSkillPaths, } from "./workspace-skills.js";
12
12
  import { scaffoldWorkspaceAssets } from "./workspace-bootstrap.js";
@@ -32,8 +32,10 @@ const MANAGED_ENV_KEYS = new Set([
32
32
  "EMBEDDING_MODEL",
33
33
  "EMBEDDING_DIMENSIONS",
34
34
  "WIKI_AGENT_ENABLED",
35
+ "WIKI_AGENT_AUTH_MODE",
35
36
  "WIKI_AGENT_BASE_URL",
36
37
  "WIKI_AGENT_API_KEY",
38
+ "WIKI_AGENT_CODEX_HOME",
37
39
  "WIKI_AGENT_MODEL",
38
40
  "WIKI_AGENT_BATCH_SIZE",
39
41
  "WIKI_AGENT_SANDBOX_MODE",
@@ -86,6 +88,17 @@ function safeAgentSandboxMode(rawValue) {
86
88
  return "danger-full-access";
87
89
  }
88
90
  }
91
+ function safeAgentAuthMode(env) {
92
+ try {
93
+ if (env.WIKI_AGENT_AUTH_MODE && env.WIKI_AGENT_AUTH_MODE.trim()) {
94
+ return parseWikiAgentAuthMode(env.WIKI_AGENT_AUTH_MODE);
95
+ }
96
+ }
97
+ catch {
98
+ return env.WIKI_AGENT_API_KEY?.trim() ? "api-key" : "codex-login";
99
+ }
100
+ return env.WIKI_AGENT_API_KEY?.trim() ? "api-key" : "codex-login";
101
+ }
89
102
  function safeBooleanFlag(rawValue, defaultValue) {
90
103
  if (rawValue === undefined || rawValue.trim().length === 0) {
91
104
  return defaultValue;
@@ -120,6 +133,12 @@ function validateWikiPath(rawValue) {
120
133
  }
121
134
  return null;
122
135
  }
136
+ function validateAbsolutePath(rawValue, label) {
137
+ if (!path.isAbsolute(rawValue.trim())) {
138
+ return `${label} must be an absolute path.`;
139
+ }
140
+ return null;
141
+ }
123
142
  class InquirerPromptDriver {
124
143
  inputStream;
125
144
  outputStream;
@@ -368,6 +387,7 @@ function getPathDefaults(env, cwd) {
368
387
  const configPath = env.WIKI_CONFIG_PATH ? path.resolve(env.WIKI_CONFIG_PATH) : path.join(wikiRoot, "wiki.config.json");
369
388
  const templatesPath = env.WIKI_TEMPLATES_PATH ? path.resolve(env.WIKI_TEMPLATES_PATH) : path.join(wikiRoot, "templates");
370
389
  const defaultHashMode = vaultSource === "synology" ? "mtime" : "content";
390
+ const agentAuthMode = safeAgentAuthMode(env);
371
391
  return {
372
392
  envFilePath: env.WIKI_ENV_FILE ? path.resolve(cwd, env.WIKI_ENV_FILE) : path.join(cwd, DEFAULT_WIKI_ENV_FILE),
373
393
  vaultSource,
@@ -388,11 +408,13 @@ function getPathDefaults(env, cwd) {
388
408
  embeddingBaseUrl: env.EMBEDDING_BASE_URL ?? env.OPENROUTER_BASE_URL ?? "https://api.openai.com/v1",
389
409
  embeddingApiKey: env.EMBEDDING_API_KEY ?? env.OPENROUTER_API_KEY ?? null,
390
410
  embeddingModel: env.EMBEDDING_MODEL ?? env.OPENROUTER_EMBEDDING_MODEL ?? "text-embedding-3-small",
391
- embeddingDimensions: env.EMBEDDING_DIMENSIONS ?? "384",
411
+ embeddingDimensions: env.EMBEDDING_DIMENSIONS ?? String(DEFAULT_EMBEDDING_DIMENSIONS),
392
412
  agentEnabled: (env.WIKI_AGENT_ENABLED ?? "").trim().toLowerCase() === "true",
393
- agentBaseUrl: env.WIKI_AGENT_BASE_URL ?? "https://api.openai.com/v1",
413
+ agentAuthMode,
414
+ agentBaseUrl: env.WIKI_AGENT_BASE_URL ?? (agentAuthMode === "api-key" ? "https://api.openai.com/v1" : null),
394
415
  agentApiKey: env.WIKI_AGENT_API_KEY ?? null,
395
- agentModel: env.WIKI_AGENT_MODEL ?? null,
416
+ agentCodexHome: env.WIKI_AGENT_CODEX_HOME ?? defaultWikiAgentCodexHome(),
417
+ agentModel: env.WIKI_AGENT_MODEL ?? DEFAULT_WIKI_AGENT_MODEL,
396
418
  agentBatchSize: env.WIKI_AGENT_BATCH_SIZE ?? "5",
397
419
  agentSandboxMode: safeAgentSandboxMode(env.WIKI_AGENT_SANDBOX_MODE),
398
420
  parserSkills: parseParserSkills(env.WIKI_PARSER_SKILLS, { strict: false }),
@@ -417,7 +439,7 @@ async function collectEmbeddingSettings(driver, ctx, defaults, env) {
417
439
  required: true,
418
440
  });
419
441
  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") });
442
+ const embeddingDimensions = await promptText(driver, "EMBEDDING_DIMENSIONS", defaults.embeddingDimensions ?? String(DEFAULT_EMBEDDING_DIMENSIONS), { validator: (value) => validateNonNegativeInteger(value, "EMBEDDING_DIMENSIONS") });
421
443
  const shouldProbe = await promptYesNo(driver, "Probe the embedding endpoint now?", false);
422
444
  if (shouldProbe) {
423
445
  try {
@@ -457,19 +479,50 @@ async function collectAgentSettings(driver, ctx, defaults) {
457
479
  if (!enabled) {
458
480
  return {
459
481
  agentEnabled: false,
482
+ agentAuthMode: null,
460
483
  agentBaseUrl: null,
461
484
  agentApiKey: null,
485
+ agentCodexHome: null,
462
486
  agentModel: null,
463
487
  agentBatchSize: null,
464
488
  agentSandboxMode: null,
465
489
  };
466
490
  }
467
491
  writeWarning(ctx.output, "Warning: danger-full-access grants full access to the runtime workspace.");
492
+ const agentAuthMode = await driver.select({
493
+ message: "WIKI_AGENT_AUTH_MODE",
494
+ defaultValue: defaults.agentAuthMode ?? "codex-login",
495
+ choices: [
496
+ {
497
+ value: "codex-login",
498
+ label: "codex-login",
499
+ description: "Use a local Codex ChatGPT login stored under WIKI_AGENT_CODEX_HOME.",
500
+ },
501
+ {
502
+ value: "api-key",
503
+ label: "api-key",
504
+ description: "Use WIKI_AGENT_API_KEY and optional WIKI_AGENT_BASE_URL.",
505
+ },
506
+ ],
507
+ });
508
+ const authSettings = agentAuthMode === "api-key"
509
+ ? {
510
+ agentBaseUrl: await promptText(driver, "WIKI_AGENT_BASE_URL", defaults.agentBaseUrl ?? "https://api.openai.com/v1", { validator: (value) => validateUrl(value, "WIKI_AGENT_BASE_URL") }),
511
+ agentApiKey: await promptPassword(driver, "WIKI_AGENT_API_KEY", defaults.agentApiKey ?? "", {
512
+ required: true,
513
+ }),
514
+ agentCodexHome: null,
515
+ }
516
+ : {
517
+ agentBaseUrl: null,
518
+ agentApiKey: null,
519
+ agentCodexHome: await promptText(driver, "WIKI_AGENT_CODEX_HOME", defaults.agentCodexHome ?? defaultWikiAgentCodexHome(), { validator: (value) => validateAbsolutePath(value, "WIKI_AGENT_CODEX_HOME") }),
520
+ };
468
521
  return {
469
522
  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 }),
523
+ agentAuthMode,
524
+ ...authSettings,
525
+ agentModel: await promptText(driver, "WIKI_AGENT_MODEL", defaults.agentModel ?? DEFAULT_WIKI_AGENT_MODEL, { required: true }),
473
526
  agentBatchSize: await promptText(driver, "WIKI_AGENT_BATCH_SIZE", defaults.agentBatchSize ?? "5", { validator: (value) => validateNonNegativeInteger(value, "WIKI_AGENT_BATCH_SIZE") }),
474
527
  agentSandboxMode: await driver.select({
475
528
  message: "WIKI_AGENT_SANDBOX_MODE",
@@ -561,7 +614,13 @@ function buildSetupSummary(values) {
561
614
  lines.push(` EMBEDDING_DIMENSIONS: ${values.embeddingDimensions}`);
562
615
  }
563
616
  if (values.agentEnabled) {
564
- lines.push(` WIKI_AGENT_BASE_URL: ${values.agentBaseUrl}`);
617
+ lines.push(` WIKI_AGENT_AUTH_MODE: ${values.agentAuthMode}`);
618
+ if (values.agentAuthMode === "api-key") {
619
+ lines.push(` WIKI_AGENT_BASE_URL: ${values.agentBaseUrl}`);
620
+ }
621
+ if (values.agentAuthMode === "codex-login") {
622
+ lines.push(` WIKI_AGENT_CODEX_HOME: ${values.agentCodexHome}`);
623
+ }
565
624
  lines.push(` WIKI_AGENT_MODEL: ${values.agentModel}`);
566
625
  lines.push(` WIKI_AGENT_BATCH_SIZE: ${values.agentBatchSize}`);
567
626
  lines.push(` WIKI_AGENT_SANDBOX_MODE: ${values.agentSandboxMode}`);
@@ -598,8 +657,10 @@ function writeSetupEnvFile(values) {
598
657
  ["EMBEDDING_MODEL", values.embeddingEnabled ? values.embeddingModel : null],
599
658
  ["EMBEDDING_DIMENSIONS", values.embeddingEnabled ? values.embeddingDimensions : null],
600
659
  ["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],
660
+ ["WIKI_AGENT_AUTH_MODE", values.agentEnabled ? values.agentAuthMode : null],
661
+ ["WIKI_AGENT_BASE_URL", values.agentEnabled && values.agentAuthMode === "api-key" ? values.agentBaseUrl : null],
662
+ ["WIKI_AGENT_API_KEY", values.agentEnabled && values.agentAuthMode === "api-key" ? values.agentApiKey : null],
663
+ ["WIKI_AGENT_CODEX_HOME", values.agentEnabled && values.agentAuthMode === "codex-login" ? values.agentCodexHome : null],
603
664
  ["WIKI_AGENT_MODEL", values.agentEnabled ? values.agentModel : null],
604
665
  ["WIKI_AGENT_BATCH_SIZE", values.agentEnabled ? values.agentBatchSize : null],
605
666
  ["WIKI_AGENT_SANDBOX_MODE", values.agentEnabled ? values.agentSandboxMode : null],
@@ -908,7 +969,9 @@ function inspectAgent(checks, env) {
908
969
  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
970
  return;
910
971
  }
911
- collectDoctorCheck(checks, "ok", "agent", `Automatic vault processing is enabled with model ${settings.model}.`);
972
+ collectDoctorCheck(checks, "ok", "agent", settings.authMode === "codex-login"
973
+ ? `Automatic vault processing is enabled with model ${settings.model} via Codex login at ${settings.codexHome}.`
974
+ : `Automatic vault processing is enabled with model ${settings.model} via API key auth.`);
912
975
  }
913
976
  catch (error) {
914
977
  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-tiangong-wiki");
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.7",
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,18 +75,20 @@ 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 | Dedicated Codex home directory. Leave unset to use the current user's default `${HOME}/.codex-tiangong-wiki`; 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`) |
@@ -122,6 +124,20 @@ Always run `tiangong-wiki lint --path <page-id> --format json` after mutations.
122
124
 
123
125
  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
126
 
127
+ ### Windows opens "choose an app" instead of running tiangong-wiki
128
+
129
+ In Windows native shells, invoke the npm command shim with the `.cmd` suffix:
130
+
131
+ ```powershell
132
+ tiangong-wiki.cmd daemon status
133
+ tiangong-wiki.cmd sync
134
+ tiangong-wiki.cmd lint --format json
135
+ ```
136
+
137
+ 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.
138
+
139
+ 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.
140
+
125
141
  ### Codex workflow sandbox fails to initialize
126
142
 
127
143
  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 +146,44 @@ If the agent workflow fails with `bwrap`, `unshare`, `uid_map`, or similar sandb
130
146
 
131
147
  ## LLM Provider Setup
132
148
 
133
- ### OpenAI (default)
149
+ ### Codex login (recommended local setup)
150
+
151
+ Create and log in to a dedicated Codex home for the wiki service:
152
+
153
+ macOS/Linux:
154
+
155
+ ```bash
156
+ mkdir -p "$HOME/.codex-tiangong-wiki"
157
+ CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login
158
+ CODEX_HOME="$HOME/.codex-tiangong-wiki" codex login status
159
+ ```
160
+
161
+ Windows PowerShell:
162
+
163
+ ```powershell
164
+ New-Item -ItemType Directory -Force "$env:USERPROFILE\.codex-tiangong-wiki" | Out-Null
165
+ $env:CODEX_HOME = "$env:USERPROFILE\.codex-tiangong-wiki"
166
+ codex login
167
+ codex login status
168
+ ```
169
+
170
+ Then configure the wiki agent:
171
+
172
+ ```env
173
+ WIKI_AGENT_ENABLED=true
174
+ WIKI_AGENT_AUTH_MODE=codex-login
175
+ # Optional. Leave unset to use the current user's default dedicated Codex home.
176
+ # WIKI_AGENT_CODEX_HOME=/absolute/path/to/.codex-tiangong-wiki
177
+ WIKI_AGENT_MODEL=gpt-5.5
178
+ ```
179
+
180
+ ### OpenAI API key
134
181
 
135
182
  ```env
136
183
  WIKI_AGENT_ENABLED=true
184
+ WIKI_AGENT_AUTH_MODE=api-key
137
185
  WIKI_AGENT_API_KEY=sk-...
138
- WIKI_AGENT_MODEL=gpt-5.4
186
+ WIKI_AGENT_MODEL=gpt-5.5
139
187
  ```
140
188
 
141
189
  ### vLLM (self-hosted)
@@ -144,6 +192,7 @@ Requires vLLM **v0.8.5+** for Responses API support (`/v1/responses`).
144
192
 
145
193
  ```env
146
194
  WIKI_AGENT_ENABLED=true
195
+ WIKI_AGENT_AUTH_MODE=api-key
147
196
  WIKI_AGENT_BASE_URL=http://<host>:<port>/v1
148
197
  WIKI_AGENT_API_KEY=<your-token>
149
198
  WIKI_AGENT_MODEL=Qwen/Qwen3.5-397B-A17B-GPTQ-Int4