@tekmidian/pai 0.13.3 → 0.13.5

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.
@@ -6,7 +6,7 @@ import "../db-CmYbAVCD.mjs";
6
6
  import "../helpers-IjZkXBhj.mjs";
7
7
  import "../embeddings-BJPOcbik.mjs";
8
8
  import "../search-HcdKtMla.mjs";
9
- import "../pick-DhF8-WFT.mjs";
9
+ import "../pick-CsHZ8Abv.mjs";
10
10
  import "../kg-extraction-C8DEUHTS.mjs";
11
11
  import "../factory-Q88X1bAN.mjs";
12
12
  import "../config-C8m-tPhP.mjs";
@@ -5,7 +5,7 @@ import "../db-CmYbAVCD.mjs";
5
5
  import "../helpers-IjZkXBhj.mjs";
6
6
  import "../embeddings-BJPOcbik.mjs";
7
7
  import "../search-HcdKtMla.mjs";
8
- import { C as registerDaemonCommands, D as registerProjectsCommands, E as registerRegistryCommands, O as findMovedPath, S as registerBackupCommands, T as registerMemoryCommands, _ as registerObservationCommands, a as cmdPauseAll, b as registerSetupCommand, c as cmdPause, d as registerKgCommands, f as registerTopicCommands, g as registerSkillCommands, h as registerUpdateCommand, i as cmdClearNames, k as resolveIdentifier, l as registerHelpCommand, m as registerNotifyCommands, n as cmdFind, o as cmdGoto, p as registerTaskCommands, r as cmdList, s as cmdEnd, t as cmdPick, u as registerDbCommands, v as registerZettelCommands, w as registerMcpCommands, x as registerRestoreCommands, y as registerObsidianCommands } from "../pick-DhF8-WFT.mjs";
8
+ import { C as registerDaemonCommands, D as registerProjectsCommands, E as registerRegistryCommands, O as findMovedPath, S as registerBackupCommands, T as registerMemoryCommands, _ as registerObservationCommands, a as cmdPauseAll, b as registerSetupCommand, c as cmdPause, d as registerKgCommands, f as registerTopicCommands, g as registerSkillCommands, h as registerUpdateCommand, i as cmdClearNames, k as resolveIdentifier, l as registerHelpCommand, m as registerNotifyCommands, n as cmdFind, o as cmdGoto, p as registerTaskCommands, r as cmdList, s as cmdEnd, t as cmdPick, u as registerDbCommands, v as registerZettelCommands, w as registerMcpCommands, x as registerRestoreCommands, y as registerObsidianCommands } from "../pick-CsHZ8Abv.mjs";
9
9
  import "../kg-extraction-C8DEUHTS.mjs";
10
10
  import "../factory-Q88X1bAN.mjs";
11
11
  import "../config-C8m-tPhP.mjs";
@@ -1898,9 +1898,80 @@ function registerMemoryCommands(memoryCmd, getDb) {
1898
1898
  registerStatsCommands(memoryCmd, getDb);
1899
1899
  }
1900
1900
 
1901
+ //#endregion
1902
+ //#region src/config/claude-json.ts
1903
+ /**
1904
+ * claude-json.ts — safe read/write for ~/.claude.json
1905
+ *
1906
+ * This file is not ours. It holds every MCP server registration plus Claude
1907
+ * Code's own per-project state — on a working machine, hundreds of kilobytes
1908
+ * that no one can reconstruct by hand. PAI only ever adds one key to it.
1909
+ *
1910
+ * The previous implementation (duplicated in mcp.ts and daemon.ts) did:
1911
+ *
1912
+ * try { return JSON.parse(read(path)); } catch { return {}; }
1913
+ *
1914
+ * followed by a full-file write. A malformed or transiently unreadable file
1915
+ * therefore became an empty object, and the next write replaced the user's
1916
+ * entire config with just PAI's entry. No error, no backup, exit code 0.
1917
+ *
1918
+ * The precondition is unlikely, which is exactly the problem: `pai setup` is
1919
+ * advertised as idempotent and safe to re-run, so it is what people run when
1920
+ * something is already broken — the state in which the file is most likely to
1921
+ * be malformed.
1922
+ */
1923
+ const CLAUDE_JSON_PATH = join(homedir(), ".claude.json");
1924
+ const BACKUP_PATH = `${CLAUDE_JSON_PATH}.bak-pai`;
1925
+ /**
1926
+ * Read ~/.claude.json.
1927
+ *
1928
+ * A missing file is a legitimate first-run state and yields {}. Anything else
1929
+ * — malformed JSON, permissions, a half-written file — throws. Returning {}
1930
+ * for an unreadable file is what turns a read problem into data loss on the
1931
+ * next write.
1932
+ */
1933
+ function readClaudeJson() {
1934
+ if (!existsSync(CLAUDE_JSON_PATH)) return {};
1935
+ let raw;
1936
+ try {
1937
+ raw = readFileSync(CLAUDE_JSON_PATH, "utf8");
1938
+ } catch (e) {
1939
+ throw new Error(`Could not read ${CLAUDE_JSON_PATH}: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — writing now would replace your MCP registrations with PAI's alone.`);
1940
+ }
1941
+ try {
1942
+ return JSON.parse(raw);
1943
+ } catch (e) {
1944
+ throw new Error(`${CLAUDE_JSON_PATH} is not valid JSON: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — overwriting it would destroy every MCP server registration it holds.\nFix the file, or move it aside and let Claude Code recreate it, then re-run this command.`);
1945
+ }
1946
+ }
1947
+ /**
1948
+ * Write ~/.claude.json, keeping a backup and never leaving it half-written.
1949
+ *
1950
+ * Writes to a temp file and renames: rename is atomic within a filesystem, so
1951
+ * a crash mid-write leaves the original intact rather than truncated. The
1952
+ * previous direct writeFileSync could truncate the file and then fail.
1953
+ */
1954
+ function writeClaudeJson(data) {
1955
+ const serialized = JSON.stringify(data, null, 2) + "\n";
1956
+ if (existsSync(CLAUDE_JSON_PATH)) try {
1957
+ copyFileSync(CLAUDE_JSON_PATH, BACKUP_PATH);
1958
+ } catch (e) {
1959
+ throw new Error(`Could not back up ${CLAUDE_JSON_PATH} to ${BACKUP_PATH}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
1960
+ }
1961
+ const tmp = `${CLAUDE_JSON_PATH}.tmp-pai-${process.pid}`;
1962
+ try {
1963
+ writeFileSync(tmp, serialized, "utf8");
1964
+ renameSync(tmp, CLAUDE_JSON_PATH);
1965
+ } catch (e) {
1966
+ try {
1967
+ if (existsSync(tmp)) unlinkSync(tmp);
1968
+ } catch {}
1969
+ throw new Error(`Failed to write ${CLAUDE_JSON_PATH}: ${e instanceof Error ? e.message : String(e)}\nThe original is unchanged${existsSync(BACKUP_PATH) ? `; a backup is at ${BACKUP_PATH}` : ""}.`);
1970
+ }
1971
+ }
1972
+
1901
1973
  //#endregion
1902
1974
  //#region src/cli/commands/mcp.ts
1903
- const CLAUDE_JSON_PATH$1 = join(homedir(), ".claude.json");
1904
1975
  /**
1905
1976
  * Resolve the absolute path to the built MCP entry point.
1906
1977
  *
@@ -1911,21 +1982,9 @@ const CLAUDE_JSON_PATH$1 = join(homedir(), ".claude.json");
1911
1982
  function getMcpBinPath() {
1912
1983
  return join(dirname(fileURLToPath(import.meta.url)), "../mcp/index.mjs");
1913
1984
  }
1914
- function readClaudeJson$1() {
1915
- if (!existsSync(CLAUDE_JSON_PATH$1)) return {};
1916
- try {
1917
- const raw = readFileSync(CLAUDE_JSON_PATH$1, "utf8");
1918
- return JSON.parse(raw);
1919
- } catch {
1920
- return {};
1921
- }
1922
- }
1923
- function writeClaudeJson$1(data) {
1924
- writeFileSync(CLAUDE_JSON_PATH$1, JSON.stringify(data, null, 2) + "\n", "utf8");
1925
- }
1926
1985
  function cmdInstall$1() {
1927
1986
  const mcpBin = getMcpBinPath();
1928
- const config = readClaudeJson$1();
1987
+ const config = readClaudeJson();
1929
1988
  if (typeof config.mcpServers !== "object" || config.mcpServers === null) config.mcpServers = {};
1930
1989
  const servers = config.mcpServers;
1931
1990
  if ("pai" in servers) {
@@ -1939,7 +1998,7 @@ function cmdInstall$1() {
1939
1998
  args: [mcpBin]
1940
1999
  };
1941
2000
  try {
1942
- writeClaudeJson$1(config);
2001
+ writeClaudeJson(config);
1943
2002
  } catch (e) {
1944
2003
  console.error(err(`Failed to write ~/.claude.json: ${e}`));
1945
2004
  process.exit(1);
@@ -1958,7 +2017,7 @@ function cmdInstall$1() {
1958
2017
  }
1959
2018
  function cmdStatus$3() {
1960
2019
  const mcpBin = getMcpBinPath();
1961
- const config = readClaudeJson$1();
2020
+ const config = readClaudeJson();
1962
2021
  const servers = typeof config.mcpServers === "object" && config.mcpServers !== null ? config.mcpServers : {};
1963
2022
  const registered = "pai" in servers;
1964
2023
  const binExists = existsSync(mcpBin);
@@ -1997,7 +2056,6 @@ function registerMcpCommands(mcpCmd) {
1997
2056
  //#endregion
1998
2057
  //#region src/cli/commands/daemon.ts
1999
2058
  const HOME$2 = homedir();
2000
- const CLAUDE_JSON_PATH = join(HOME$2, ".claude.json");
2001
2059
  const PLIST_LABEL = "com.pai.pai-daemon";
2002
2060
  const LAUNCH_AGENTS_DIR = join(HOME$2, "Library", "LaunchAgents");
2003
2061
  const PLIST_PATH = join(LAUNCH_AGENTS_DIR, `${PLIST_LABEL}.plist`);
@@ -2017,18 +2075,6 @@ function getDaemonBinPath() {
2017
2075
  function getShimBinPath() {
2018
2076
  return join(dirname(fileURLToPath(import.meta.url)), "../daemon-mcp/index.mjs");
2019
2077
  }
2020
- function readClaudeJson() {
2021
- if (!existsSync(CLAUDE_JSON_PATH)) return {};
2022
- try {
2023
- const raw = readFileSync(CLAUDE_JSON_PATH, "utf8");
2024
- return JSON.parse(raw);
2025
- } catch {
2026
- return {};
2027
- }
2028
- }
2029
- function writeClaudeJson(data) {
2030
- writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(data, null, 2) + "\n", "utf8");
2031
- }
2032
2078
  function generatePlist(daemonBin) {
2033
2079
  return `<?xml version="1.0" encoding="UTF-8"?>
2034
2080
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -2624,6 +2670,86 @@ function registerRestoreCommands(program) {
2624
2670
  });
2625
2671
  }
2626
2672
 
2673
+ //#endregion
2674
+ //#region src/config/json-store.ts
2675
+ /**
2676
+ * json-store.ts — read/write JSON config files without destroying them
2677
+ *
2678
+ * The failure this exists to prevent:
2679
+ *
2680
+ * try { return JSON.parse(read(path)); } catch { return {}; }
2681
+ * ... later ...
2682
+ * write(path, JSON.stringify(ourData));
2683
+ *
2684
+ * An unreadable file becomes an empty object, and the next write makes that
2685
+ * permanent. Silently, exit code 0. This shape appeared three times in this
2686
+ * repo against three different files, and twice in AIBroker.
2687
+ *
2688
+ * The distinction that matters is between *missing* and *unreadable*:
2689
+ *
2690
+ * missing — legitimate first run. Start fresh; writing is safe.
2691
+ * unreadable — the file exists and we could not parse it. Those bytes are
2692
+ * the only copy of something. Never overwrite them.
2693
+ *
2694
+ * Collapsing the second into the first is the bug.
2695
+ *
2696
+ * NOT everything deserves this guard. For a transient buffer — an undelivered
2697
+ * message queue, a cache — starting fresh IS the correct recovery, and
2698
+ * refusing to write would disable the feature permanently. Use `writeJsonAtomic`
2699
+ * alone there: it still prevents a crash from truncating a good file, without
2700
+ * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.
2701
+ */
2702
+ /**
2703
+ * Read a JSON file, distinguishing "absent" from "damaged".
2704
+ *
2705
+ * @param path file to read
2706
+ * @param label how to name it to the user, e.g. "~/.claude.json"
2707
+ * @throws if the file exists but cannot be read or parsed
2708
+ */
2709
+ function readJsonStrict(path, label = path) {
2710
+ if (!existsSync(path)) return {};
2711
+ let raw;
2712
+ try {
2713
+ raw = readFileSync(path, "utf8");
2714
+ } catch (e) {
2715
+ throw new Error(`Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — writing now would replace its contents with ours alone.`);
2716
+ }
2717
+ try {
2718
+ return JSON.parse(raw);
2719
+ } catch (e) {
2720
+ throw new Error(`${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — overwriting it would destroy whatever it holds.\nRepair the file, or move it aside and re-run this command.`);
2721
+ }
2722
+ }
2723
+ /**
2724
+ * Write JSON without risking the existing file.
2725
+ *
2726
+ * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file
2727
+ * and renames. Rename is atomic within a filesystem, so a crash mid-write
2728
+ * leaves the original intact rather than truncated — which is how these files
2729
+ * become corrupt in the first place.
2730
+ */
2731
+ function writeJsonAtomic(path, data, opts = {}) {
2732
+ const { backup = true, label = path } = opts;
2733
+ const serialized = JSON.stringify(data, null, 2) + "\n";
2734
+ const dir = dirname(path);
2735
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
2736
+ if (backup && existsSync(path)) try {
2737
+ copyFileSync(path, `${path}.bak-pai`);
2738
+ } catch (e) {
2739
+ throw new Error(`Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
2740
+ }
2741
+ const tmp = `${path}.tmp-pai-${process.pid}`;
2742
+ try {
2743
+ writeFileSync(tmp, serialized, "utf8");
2744
+ renameSync(tmp, path);
2745
+ } catch (e) {
2746
+ try {
2747
+ if (existsSync(tmp)) unlinkSync(tmp);
2748
+ } catch {}
2749
+ throw new Error(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\nThe original is unchanged.`);
2750
+ }
2751
+ }
2752
+
2627
2753
  //#endregion
2628
2754
  //#region src/cli/commands/setup/utils.ts
2629
2755
  /**
@@ -2695,17 +2821,18 @@ async function promptYesNo(rl, question, defaultYes = true) {
2695
2821
  if (answer === "") return defaultYes;
2696
2822
  return answer.toLowerCase().startsWith("y");
2697
2823
  }
2824
+ /**
2825
+ * This file holds the Postgres connection string, the storage backend choice,
2826
+ * notification routing and any tracker API token — none of which the user can
2827
+ * reconstruct from memory. It previously returned {} on a parse failure and
2828
+ * then overwrote the file, so a damaged config was replaced by whatever the
2829
+ * current command happened to be setting.
2830
+ */
2698
2831
  function readConfigRaw() {
2699
- if (!existsSync(CONFIG_FILE$2)) return {};
2700
- try {
2701
- return JSON.parse(readFileSync(CONFIG_FILE$2, "utf-8"));
2702
- } catch {
2703
- return {};
2704
- }
2832
+ return readJsonStrict(CONFIG_FILE$2, "~/.config/pai/config.json");
2705
2833
  }
2706
2834
  function writeConfigRaw(data) {
2707
- if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
2708
- writeFileSync(CONFIG_FILE$2, JSON.stringify(data, null, 2) + "\n", "utf-8");
2835
+ writeJsonAtomic(CONFIG_FILE$2, data, { label: "~/.config/pai/config.json" });
2709
2836
  }
2710
2837
  function mergeConfig(updates) {
2711
2838
  const current = readConfigRaw();
@@ -3388,27 +3515,18 @@ async function stepTsHooks(rl) {
3388
3515
 
3389
3516
  //#endregion
3390
3517
  //#region src/cli/commands/settings-manager.ts
3518
+ const SETTINGS_FILE = join(join(homedir(), ".claude"), "settings.json");
3391
3519
  /**
3392
- * settings-manager merge-not-overwrite utility for ~/.claude/settings.json
3393
- *
3394
- * Provides safe, idempotent writes to Claude Code's settings.json:
3395
- * - env vars: added only if the key is absent (never overwrites)
3396
- * - hooks: appended per hookType, deduplicated by command string
3397
- * - statusLine: written only if the key is not already present
3520
+ * ~/.claude/settings.json is Claude Code's own file, not ours: hooks, env,
3521
+ * permissions, statusline, enabled plugins. PAI only ever adds to it. Returning
3522
+ * {} for a damaged file and then writing meant a stray parse error could strip
3523
+ * every hook registration the user had.
3398
3524
  */
3399
- const CLAUDE_DIR = join(homedir(), ".claude");
3400
- const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
3401
3525
  function readSettingsJson() {
3402
- if (!existsSync(SETTINGS_FILE)) return {};
3403
- try {
3404
- return JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
3405
- } catch {
3406
- return {};
3407
- }
3526
+ return readJsonStrict(SETTINGS_FILE, "~/.claude/settings.json");
3408
3527
  }
3409
3528
  function writeSettingsJson(data) {
3410
- if (!existsSync(CLAUDE_DIR)) mkdirSync(CLAUDE_DIR, { recursive: true });
3411
- writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n", "utf-8");
3529
+ writeJsonAtomic(SETTINGS_FILE, data, { label: "~/.claude/settings.json" });
3412
3530
  }
3413
3531
  /**
3414
3532
  * Merge env vars — add keys that are absent, never overwrite existing ones.
@@ -6048,7 +6166,7 @@ function stepVerifyRepo() {
6048
6166
  const gitDir = capture("git rev-parse --show-toplevel", getPaiSrcDir());
6049
6167
  if (!gitDir) {
6050
6168
  error("Not a git repository. Cannot update automatically.");
6051
- info("Install PAI via npm to get automatic updates: npm install -g @mnott/pai");
6169
+ info("Install PAI via npm to get automatic updates: npm install -g @tekmidian/pai");
6052
6170
  return null;
6053
6171
  }
6054
6172
  const remote = capture("git remote get-url origin", gitDir);
@@ -9015,4 +9133,4 @@ async function cmdPick(db, opts = {}) {
9015
9133
 
9016
9134
  //#endregion
9017
9135
  export { registerDaemonCommands as C, registerProjectsCommands as D, registerRegistryCommands as E, findMovedPath as O, registerBackupCommands as S, registerMemoryCommands as T, registerObservationCommands as _, cmdPauseAll as a, registerSetupCommand as b, cmdPause as c, registerKgCommands as d, registerTopicCommands as f, registerSkillCommands as g, registerUpdateCommand as h, cmdClearNames as i, resolveIdentifier as k, registerHelpCommand as l, registerNotifyCommands as m, cmdFind as n, cmdGoto as o, registerTaskCommands as p, cmdList as r, cmdEnd as s, cmdPick as t, registerDbCommands as u, registerZettelCommands as v, registerMcpCommands as w, registerRestoreCommands as x, registerObsidianCommands as y };
9018
- //# sourceMappingURL=pick-DhF8-WFT.mjs.map
9136
+ //# sourceMappingURL=pick-CsHZ8Abv.mjs.map