@zokizuan/satori-mcp 4.3.1 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ MCP server for Satori — agent-safe semantic code search and indexing.
17
17
  - Zod-first tool schemas converted to MCP JSON Schema for `ListTools`
18
18
  - Auto-generated tool docs from live tool schemas
19
19
  - `read_file` line-range retrieval with default large-file truncation guard and optional `mode="annotated"` metadata envelope
20
- - Optional proactive sync watcher mode (debounced filesystem events)
20
+ - Optional proactive sync watcher mode (debounced filesystem events for explicitly touched roots in the current session)
21
21
  - Index-time AST scope breadcrumbs (TS/JS/Python) rendered in search output as `🧬 Scope`
22
22
  - Fingerprint schema `dense_v3`/`hybrid_v3` with hard gate for all pre-v3 indexes
23
23
 
@@ -46,6 +46,7 @@ Tool surface is hard-broken to 6 tools. This keeps routing explicit while exposi
46
46
 
47
47
  - Enabled by default. Set `MCP_ENABLE_WATCHER=false` to disable
48
48
  - Debounce window via `MCP_WATCH_DEBOUNCE_MS` (default `5000`)
49
+ - Watchers are session-scoped: startup does not watch every indexed codebase, only roots touched by successful index/search/navigation/read flows in the current session
49
50
  - Watch events reuse the same incremental sync pipeline (`reindexByChange`)
50
51
  - Ignore control files (`.satoriignore`, root `.gitignore`) trigger no-reindex reconciliation:
51
52
  - delete indexed paths now ignored by active rules
@@ -156,7 +157,7 @@ No parameters.
156
157
  "mcpServers": {
157
158
  "satori": {
158
159
  "command": "npx",
159
- "args": ["-y", "@zokizuan/satori-mcp@latest"],
160
+ "args": ["-y", "@zokizuan/satori-mcp@4.4.1"],
160
161
  "timeout": 180000,
161
162
  "env": {
162
163
  "EMBEDDING_PROVIDER": "VoyageAI",
@@ -177,7 +178,7 @@ No parameters.
177
178
  ```toml
178
179
  [mcp_servers.satori]
179
180
  command = "npx"
180
- args = ["-y", "@zokizuan/satori-mcp@latest"]
181
+ args = ["-y", "@zokizuan/satori-mcp@4.4.1"]
181
182
  startup_timeout_ms = 180000
182
183
  env = { EMBEDDING_PROVIDER = "VoyageAI", EMBEDDING_MODEL = "voyage-4-large", EMBEDDING_OUTPUT_DIMENSION = "1024", VOYAGEAI_API_KEY = "your-api-key", VOYAGEAI_RERANKER_MODEL = "rerank-2.5", MILVUS_ADDRESS = "your-milvus-endpoint", MILVUS_TOKEN = "your-milvus-token" }
183
184
  ```
@@ -189,7 +190,7 @@ env = { EMBEDDING_PROVIDER = "VoyageAI", EMBEDDING_MODEL = "voyage-4-large", EMB
189
190
  "mcpServers": {
190
191
  "satori": {
191
192
  "command": "node",
192
- "args": ["/absolute/path/to/claude-context/packages/mcp/dist/index.js"],
193
+ "args": ["/absolute/path/to/satori/packages/mcp/dist/index.js"],
193
194
  "timeout": 180000,
194
195
  "env": {
195
196
  "EMBEDDING_PROVIDER": "VoyageAI",
@@ -213,9 +214,30 @@ Never commit real API keys/tokens into repo config files.
213
214
  pnpm --filter @zokizuan/satori-mcp start
214
215
  ```
215
216
 
216
- ## Shell CLI (`satori-cli`)
217
+ ## Shell CLI (`@zokizuan/satori-cli`)
217
218
 
218
- `@zokizuan/satori-mcp` also ships a shell-first client binary that works without an MCP adapter.
219
+ The shell-first installer/client now lives in a separate package: `@zokizuan/satori-cli`.
220
+
221
+ ### Install / Uninstall
222
+
223
+ Supported installer targets in Phase 1:
224
+ - `codex`
225
+ - `claude`
226
+ - `all`
227
+
228
+ Examples:
229
+
230
+ ```bash
231
+ npx -y @zokizuan/satori-cli@0.1.1 install --client codex
232
+ npx -y @zokizuan/satori-cli@0.1.1 install --client claude
233
+ npx -y @zokizuan/satori-cli@0.1.1 install --client all --dry-run
234
+ npx -y @zokizuan/satori-cli@0.1.1 uninstall --client codex
235
+ ```
236
+
237
+ Install and uninstall run before MCP session startup, only touch Satori-managed config, and copy/remove these packaged skills:
238
+ - `satori-search`
239
+ - `satori-navigation`
240
+ - `satori-indexing`
219
241
 
220
242
  ### Commands
221
243
 
@@ -271,6 +293,8 @@ pnpm --filter @zokizuan/satori-mcp build
271
293
  pnpm --filter @zokizuan/satori-mcp typecheck
272
294
  pnpm --filter @zokizuan/satori-mcp test
273
295
  pnpm --filter @zokizuan/satori-mcp docs:check
296
+ pnpm --filter @zokizuan/satori-cli build
297
+ pnpm --filter @zokizuan/satori-cli test
274
298
  ```
275
299
 
276
300
  `build` automatically runs docs generation from tool schemas.
@@ -19,6 +19,14 @@ export type ParsedCommand = {
19
19
  kind: "help";
20
20
  } | {
21
21
  kind: "version";
22
+ } | {
23
+ kind: "install";
24
+ client: InstallClient;
25
+ dryRun: boolean;
26
+ } | {
27
+ kind: "uninstall";
28
+ client: InstallClient;
29
+ dryRun: boolean;
22
30
  } | {
23
31
  kind: "tools-list";
24
32
  } | {
@@ -39,6 +47,7 @@ export interface ResolveRawArgsOptions {
39
47
  stdin?: NodeJS.ReadStream;
40
48
  stdinTimeoutMs: number;
41
49
  }
50
+ export type InstallClient = "all" | "claude" | "codex";
42
51
  export declare function parseCliArgs(argv: string[]): ParsedCliInput;
43
52
  export declare function resolveRawArguments(rawArgsMode: RawArgsMode, options: ResolveRawArgsOptions): Promise<Record<string, unknown>>;
44
53
  export declare function parseWrapperArgumentsFromSchema(toolName: string, inputSchema: unknown, wrapperArgs: string[]): Record<string, unknown>;
package/dist/cli/args.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { CliError } from "./errors.js";
3
- const RESERVED_SUBCOMMANDS = new Set(["tools", "tool", "help", "version"]);
3
+ const RESERVED_SUBCOMMANDS = new Set(["tools", "tool", "help", "version", "install", "uninstall"]);
4
4
  const PRIMITIVE_TYPES = new Set(["string", "number", "integer", "boolean"]);
5
5
  function parsePositiveInteger(value, flagName) {
6
6
  const parsed = Number.parseInt(value, 10);
@@ -109,6 +109,28 @@ function parseRawArgsMode(args) {
109
109
  }
110
110
  return { rawArgsMode, remaining };
111
111
  }
112
+ function parseInstallCommand(kind, args) {
113
+ let client = "all";
114
+ let dryRun = false;
115
+ for (let i = 0; i < args.length; i += 1) {
116
+ const token = args[i];
117
+ if (token === "--client") {
118
+ const next = args[i + 1];
119
+ if (next !== "all" && next !== "claude" && next !== "codex") {
120
+ throw new CliError("E_USAGE", "--client must be one of: all, claude, codex.", 2);
121
+ }
122
+ client = next;
123
+ i += 1;
124
+ continue;
125
+ }
126
+ if (token === "--dry-run") {
127
+ dryRun = true;
128
+ continue;
129
+ }
130
+ throw new CliError("E_USAGE", `Unknown arguments for ${kind}: ${args.slice(i).join(" ")}`, 2);
131
+ }
132
+ return { kind, client, dryRun };
133
+ }
112
134
  export function parseCliArgs(argv) {
113
135
  const { globals, rest } = parseGlobalOptions(argv);
114
136
  if (rest.length === 0 || rest[0] === "help" || rest.includes("--help") || rest.includes("-h")) {
@@ -123,6 +145,18 @@ export function parseCliArgs(argv) {
123
145
  command: { kind: "version" }
124
146
  };
125
147
  }
148
+ if (rest[0] === "install") {
149
+ return {
150
+ globals,
151
+ command: parseInstallCommand("install", rest.slice(1))
152
+ };
153
+ }
154
+ if (rest[0] === "uninstall") {
155
+ return {
156
+ globals,
157
+ command: parseInstallCommand("uninstall", rest.slice(1))
158
+ };
159
+ }
126
160
  if (rest[0] === "tools") {
127
161
  if (rest.length === 2 && rest[1] === "list") {
128
162
  return {
@@ -39,6 +39,7 @@ export class CliMcpSession {
39
39
  return createTimeout(this.client.callTool({ name, arguments: args }), this.callTimeoutMs, "E_CALL_TIMEOUT", `Timed out after ${this.callTimeoutMs}ms while calling tools/call for '${name}'.`);
40
40
  }
41
41
  async close() {
42
+ const stderr = this.transport.stderr;
42
43
  try {
43
44
  await this.client.close();
44
45
  }
@@ -51,6 +52,7 @@ export class CliMcpSession {
51
52
  catch {
52
53
  // Best-effort close.
53
54
  }
55
+ stderr?.removeAllListeners("data");
54
56
  }
55
57
  logProtocolFailure(error) {
56
58
  if (error instanceof CliError) {
@@ -79,7 +81,7 @@ export async function connectCliMcpSession(options) {
79
81
  });
80
82
  const client = new Client({
81
83
  name: "satori-cli",
82
- version: "1.1.0",
84
+ version: "1.1.1",
83
85
  });
84
86
  const session = new CliMcpSession(client, transport, options.callTimeoutMs, options.writeStderr);
85
87
  session.wireStderr();
@@ -3,12 +3,28 @@ interface RunCliOptions {
3
3
  writeStdout?: (text: string) => void;
4
4
  writeStderr?: (text: string) => void;
5
5
  stdin?: NodeJS.ReadStream;
6
+ env?: NodeJS.ProcessEnv;
6
7
  serverCommand?: string;
7
8
  serverArgs?: string[];
8
9
  serverEnv?: Record<string, string>;
9
10
  startupTimeoutMs?: number;
10
11
  callTimeoutMs?: number;
11
12
  cwd?: string;
13
+ installabilityVerifier?: () => string | Promise<string>;
14
+ connectSession?: (options: {
15
+ command: string;
16
+ args: string[];
17
+ env: Record<string, string | undefined>;
18
+ cwd?: string;
19
+ startupTimeoutMs: number;
20
+ callTimeoutMs: number;
21
+ writeStderr: (text: string) => void;
22
+ }) => Promise<CliSession>;
23
+ }
24
+ interface CliSession {
25
+ listTools(): Promise<any>;
26
+ callTool(name: string, args: Record<string, unknown>): Promise<any>;
27
+ close(): Promise<void>;
12
28
  }
13
29
  export declare function runCli(argv: string[], options?: RunCliOptions): Promise<number>;
14
30
  export declare function isExecutedDirectlyForPaths(moduleUrl: string, entryPath: string | undefined): boolean;
package/dist/cli/index.js CHANGED
@@ -6,6 +6,8 @@ import { parseCliArgs, parseWrapperArgumentsFromSchema, resolveRawArguments } fr
6
6
  import { connectCliMcpSession } from "./client.js";
7
7
  import { asCliError, CliError } from "./errors.js";
8
8
  import { emitError, emitJson, inferManageStatusState, parseStructuredEnvelope } from "./format.js";
9
+ import { executeInstallCommand } from "./install.js";
10
+ import { verifyManagedPackageInstallability } from "./package-installability.js";
9
11
  import { resolveServerEntryPath } from "./resolve-server-entry.js";
10
12
  const MANAGE_INDEX_MIN_POLL_TIMEOUT_MS = 10 * 60 * 1000;
11
13
  function firstText(result) {
@@ -42,6 +44,8 @@ function buildHelpPayload() {
42
44
  return {
43
45
  usage: "satori-cli <command>",
44
46
  commands: [
47
+ "install [--client all|codex|claude] [--dry-run]",
48
+ "uninstall [--client all|codex|claude] [--dry-run]",
45
49
  "tools list",
46
50
  "tool call <toolName> --args-json '<json>'",
47
51
  "tool call <toolName> --args-file <path>",
@@ -164,6 +168,7 @@ export async function runCli(argv, options = {}) {
164
168
  writeStdout: options.writeStdout || ((text) => process.stdout.write(text)),
165
169
  writeStderr: options.writeStderr || ((text) => process.stderr.write(text)),
166
170
  };
171
+ const effectiveEnv = options.env || process.env;
167
172
  let parsedFormat = "json";
168
173
  let parsedCommandKind = null;
169
174
  try {
@@ -190,11 +195,24 @@ export async function runCli(argv, options = {}) {
190
195
  }
191
196
  return 0;
192
197
  }
193
- const session = await connectCliMcpSession({
198
+ if (parsed.command.kind === "install" || parsed.command.kind === "uninstall") {
199
+ if (parsed.command.kind === "install") {
200
+ await (options.installabilityVerifier || verifyManagedPackageInstallability)();
201
+ }
202
+ const result = executeInstallCommand(parsed.command, {
203
+ homeDir: effectiveEnv.HOME,
204
+ });
205
+ emitJson(writers, result);
206
+ if (parsed.globals.format === "text") {
207
+ writers.writeStderr(`satori-cli ${parsed.command.kind} completed for ${parsed.command.client}.\n`);
208
+ }
209
+ return 0;
210
+ }
211
+ const session = await (options.connectSession || connectCliMcpSession)({
194
212
  command: options.serverCommand || process.execPath,
195
213
  args: options.serverArgs || resolveDefaultServerArgs(),
196
214
  env: {
197
- ...process.env,
215
+ ...effectiveEnv,
198
216
  ...options.serverEnv,
199
217
  SATORI_RUN_MODE: "cli",
200
218
  },
@@ -0,0 +1,30 @@
1
+ import type { InstallClient } from "./args.js";
2
+ type ClientName = Exclude<InstallClient, "all">;
3
+ export interface InstallCommandInput {
4
+ kind: "install" | "uninstall";
5
+ client: InstallClient;
6
+ dryRun: boolean;
7
+ }
8
+ export interface InstallCommandOptions {
9
+ homeDir?: string;
10
+ packageSpecifier?: string;
11
+ skillAssetRoot?: string;
12
+ }
13
+ export interface ClientInstallResult {
14
+ client: ClientName;
15
+ configPath: string;
16
+ skillsPath: string;
17
+ configChanged: boolean;
18
+ skillsChanged: boolean;
19
+ status: "updated" | "unchanged";
20
+ dryRun: boolean;
21
+ }
22
+ export interface InstallCommandResult {
23
+ action: "install" | "uninstall";
24
+ client: InstallClient;
25
+ dryRun: boolean;
26
+ results: ClientInstallResult[];
27
+ }
28
+ export declare function executeInstallCommand(command: InstallCommandInput, options?: InstallCommandOptions): InstallCommandResult;
29
+ export {};
30
+ //# sourceMappingURL=install.d.ts.map
@@ -0,0 +1,329 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CliError } from "./errors.js";
6
+ const MANAGED_BLOCK_START = "# >>> satori-cli managed satori start >>>";
7
+ const MANAGED_BLOCK_END = "# <<< satori-cli managed satori end <<<";
8
+ const OWNED_SKILL_DIRS = ["satori-search", "satori-navigation", "satori-indexing"];
9
+ const MANAGED_TIMEOUT_MS = 180000;
10
+ function resolveDefaultSkillAssetRoot() {
11
+ const currentFile = fileURLToPath(import.meta.url);
12
+ return path.resolve(path.dirname(currentFile), "..", "..", "assets", "skills");
13
+ }
14
+ function resolveDefaultPackageSpecifier() {
15
+ try {
16
+ const currentFile = fileURLToPath(import.meta.url);
17
+ const packagePath = path.resolve(path.dirname(currentFile), "..", "..", "package.json");
18
+ const parsed = JSON.parse(fs.readFileSync(packagePath, "utf8"));
19
+ if (typeof parsed.name === "string" && typeof parsed.version === "string") {
20
+ return `${parsed.name}@${parsed.version}`;
21
+ }
22
+ }
23
+ catch {
24
+ // Fall through to hard failure below.
25
+ }
26
+ throw new CliError("E_USAGE", "Unable to resolve the installed Satori package version for CLI install.", 2);
27
+ }
28
+ function resolveClientTargets(homeDir) {
29
+ return [
30
+ {
31
+ client: "codex",
32
+ configPath: path.join(homeDir, ".codex", "config.toml"),
33
+ skillsPath: path.join(homeDir, ".codex", "skills"),
34
+ },
35
+ {
36
+ client: "claude",
37
+ configPath: path.join(homeDir, ".claude", "settings.json"),
38
+ skillsPath: path.join(homeDir, ".claude", "skills"),
39
+ },
40
+ ];
41
+ }
42
+ function selectTargets(homeDir, client) {
43
+ const targets = resolveClientTargets(homeDir);
44
+ if (client === "all") {
45
+ return targets;
46
+ }
47
+ return targets.filter((target) => target.client === client);
48
+ }
49
+ function ensureParentDir(filePath) {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ }
52
+ function ensureDir(dirPath) {
53
+ fs.mkdirSync(dirPath, { recursive: true });
54
+ }
55
+ function readTextIfExists(filePath) {
56
+ if (!fs.existsSync(filePath)) {
57
+ return null;
58
+ }
59
+ return fs.readFileSync(filePath, "utf8");
60
+ }
61
+ function normalizeTrailingNewline(value) {
62
+ return value.endsWith("\n") ? value : `${value}\n`;
63
+ }
64
+ function escapeRegExp(value) {
65
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
+ }
67
+ function buildCodexManagedBlock(packageSpecifier) {
68
+ return [
69
+ MANAGED_BLOCK_START,
70
+ "[mcp_servers.satori]",
71
+ 'command = "npx"',
72
+ `args = ["-y", "--package", "${packageSpecifier}", "satori"]`,
73
+ `startup_timeout_ms = ${MANAGED_TIMEOUT_MS}`,
74
+ MANAGED_BLOCK_END,
75
+ "",
76
+ ].join("\n");
77
+ }
78
+ function codexHasUnmanagedSatoriSection(content) {
79
+ if (!content.includes("[mcp_servers.satori]")) {
80
+ return false;
81
+ }
82
+ return !(content.includes(MANAGED_BLOCK_START) && content.includes(MANAGED_BLOCK_END));
83
+ }
84
+ function prepareCodexInstall(filePath, packageSpecifier) {
85
+ const current = readTextIfExists(filePath) ?? "";
86
+ if (codexHasUnmanagedSatoriSection(current)) {
87
+ throw new CliError("E_USAGE", `Refusing to overwrite unmanaged Satori config in ${filePath}. Remove [mcp_servers.satori] manually or convert it to the managed block first.`, 2);
88
+ }
89
+ const managedBlock = buildCodexManagedBlock(packageSpecifier);
90
+ let next = current;
91
+ if (current.includes(MANAGED_BLOCK_START) && current.includes(MANAGED_BLOCK_END)) {
92
+ next = current.replace(new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}\\n?`, "m"), managedBlock);
93
+ }
94
+ else if (current.trim().length === 0) {
95
+ next = managedBlock;
96
+ }
97
+ else {
98
+ next = `${normalizeTrailingNewline(current)}\n${managedBlock}`;
99
+ }
100
+ return {
101
+ changed: next !== current,
102
+ apply: () => {
103
+ if (next === current) {
104
+ return;
105
+ }
106
+ ensureParentDir(filePath);
107
+ fs.writeFileSync(filePath, next, "utf8");
108
+ },
109
+ };
110
+ }
111
+ function prepareCodexUninstall(filePath) {
112
+ const current = readTextIfExists(filePath);
113
+ if (!current) {
114
+ return { changed: false, apply: () => { } };
115
+ }
116
+ if (codexHasUnmanagedSatoriSection(current)) {
117
+ throw new CliError("E_USAGE", `Refusing to remove unmanaged Satori config in ${filePath}. Remove [mcp_servers.satori] manually instead.`, 2);
118
+ }
119
+ if (!current.includes(MANAGED_BLOCK_START) || !current.includes(MANAGED_BLOCK_END)) {
120
+ return { changed: false, apply: () => { } };
121
+ }
122
+ const next = current
123
+ .replace(new RegExp(`\\n?${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}\\n?`, "m"), "\n")
124
+ .replace(/\n{3,}/g, "\n\n")
125
+ .replace(/^\n+/, "");
126
+ return {
127
+ changed: next !== current,
128
+ apply: () => {
129
+ if (next === current) {
130
+ return;
131
+ }
132
+ fs.writeFileSync(filePath, next, "utf8");
133
+ },
134
+ };
135
+ }
136
+ function parseJsonObject(filePath) {
137
+ const current = readTextIfExists(filePath);
138
+ if (!current) {
139
+ return {};
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(current);
144
+ }
145
+ catch (error) {
146
+ throw new CliError("E_USAGE", `Failed to parse JSON config at ${filePath}: ${error.message}`, 2);
147
+ }
148
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
149
+ throw new CliError("E_USAGE", `Expected top-level JSON object in ${filePath}.`, 2);
150
+ }
151
+ return parsed;
152
+ }
153
+ function buildClaudeServerConfig(packageSpecifier) {
154
+ return {
155
+ command: "npx",
156
+ args: ["-y", "--package", packageSpecifier, "satori"],
157
+ timeout: MANAGED_TIMEOUT_MS,
158
+ };
159
+ }
160
+ function isManagedClaudeEntry(value) {
161
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
162
+ return false;
163
+ }
164
+ const entry = value;
165
+ if (entry.command !== "npx") {
166
+ return false;
167
+ }
168
+ if (entry.timeout !== MANAGED_TIMEOUT_MS) {
169
+ return false;
170
+ }
171
+ if (!Array.isArray(entry.args) || entry.args.length !== 4) {
172
+ return false;
173
+ }
174
+ return entry.args[0] === "-y"
175
+ && entry.args[1] === "--package"
176
+ && typeof entry.args[2] === "string"
177
+ && /^@zokizuan\/satori-mcp@.+$/.test(entry.args[2])
178
+ && entry.args[3] === "satori";
179
+ }
180
+ function prepareClaudeInstall(filePath, packageSpecifier) {
181
+ const currentObject = parseJsonObject(filePath);
182
+ const currentSerialized = JSON.stringify(currentObject);
183
+ const desiredServer = buildClaudeServerConfig(packageSpecifier);
184
+ const mcpServersValue = currentObject.mcpServers;
185
+ let mcpServers;
186
+ if (mcpServersValue === undefined) {
187
+ mcpServers = {};
188
+ }
189
+ else if (mcpServersValue && typeof mcpServersValue === "object" && !Array.isArray(mcpServersValue)) {
190
+ mcpServers = { ...mcpServersValue };
191
+ }
192
+ else {
193
+ throw new CliError("E_USAGE", `Expected mcpServers to be an object in ${filePath}.`, 2);
194
+ }
195
+ const existingSatori = mcpServers.satori;
196
+ if (existingSatori !== undefined && !isManagedClaudeEntry(existingSatori)) {
197
+ throw new CliError("E_USAGE", `Refusing to overwrite unmanaged Satori config in ${filePath}. Remove mcpServers.satori manually or align it to the managed npx form first.`, 2);
198
+ }
199
+ mcpServers.satori = {
200
+ ...existingSatori,
201
+ ...desiredServer,
202
+ };
203
+ currentObject.mcpServers = mcpServers;
204
+ const next = `${JSON.stringify(currentObject, null, 2)}\n`;
205
+ return {
206
+ changed: JSON.stringify(currentObject) !== currentSerialized,
207
+ apply: () => {
208
+ if (JSON.stringify(currentObject) === currentSerialized) {
209
+ return;
210
+ }
211
+ ensureParentDir(filePath);
212
+ fs.writeFileSync(filePath, next, "utf8");
213
+ },
214
+ };
215
+ }
216
+ function prepareClaudeUninstall(filePath) {
217
+ const currentObject = parseJsonObject(filePath);
218
+ const mcpServersValue = currentObject.mcpServers;
219
+ if (!mcpServersValue || typeof mcpServersValue !== "object" || Array.isArray(mcpServersValue)) {
220
+ return { changed: false, apply: () => { } };
221
+ }
222
+ const mcpServers = { ...mcpServersValue };
223
+ if (!Object.prototype.hasOwnProperty.call(mcpServers, "satori")) {
224
+ return { changed: false, apply: () => { } };
225
+ }
226
+ if (!isManagedClaudeEntry(mcpServers.satori)) {
227
+ throw new CliError("E_USAGE", `Refusing to remove unmanaged Satori config in ${filePath}. Remove mcpServers.satori manually instead.`, 2);
228
+ }
229
+ delete mcpServers.satori;
230
+ if (Object.keys(mcpServers).length === 0) {
231
+ delete currentObject.mcpServers;
232
+ }
233
+ else {
234
+ currentObject.mcpServers = mcpServers;
235
+ }
236
+ const next = `${JSON.stringify(currentObject, null, 2)}\n`;
237
+ return {
238
+ changed: true,
239
+ apply: () => {
240
+ fs.writeFileSync(filePath, next, "utf8");
241
+ },
242
+ };
243
+ }
244
+ function prepareSkillInstall(skillsPath, skillAssetRoot) {
245
+ const writes = [];
246
+ let changed = false;
247
+ for (const skillDirName of OWNED_SKILL_DIRS) {
248
+ const sourceFile = path.join(skillAssetRoot, skillDirName, "SKILL.md");
249
+ if (!fs.existsSync(sourceFile)) {
250
+ throw new CliError("E_USAGE", `Missing packaged skill asset: ${sourceFile}`, 2);
251
+ }
252
+ const content = fs.readFileSync(sourceFile, "utf8");
253
+ const destinationDir = path.join(skillsPath, skillDirName);
254
+ const destinationFile = path.join(destinationDir, "SKILL.md");
255
+ if (readTextIfExists(destinationFile) !== content) {
256
+ changed = true;
257
+ writes.push({ destinationDir, destinationFile, content });
258
+ }
259
+ }
260
+ return {
261
+ changed,
262
+ apply: () => {
263
+ for (const write of writes) {
264
+ ensureDir(write.destinationDir);
265
+ fs.writeFileSync(write.destinationFile, write.content, "utf8");
266
+ }
267
+ },
268
+ };
269
+ }
270
+ function prepareSkillRemoval(skillsPath) {
271
+ const removals = OWNED_SKILL_DIRS
272
+ .map((skillDirName) => path.join(skillsPath, skillDirName))
273
+ .filter((destinationDir) => fs.existsSync(destinationDir));
274
+ return {
275
+ changed: removals.length > 0,
276
+ apply: () => {
277
+ for (const destinationDir of removals) {
278
+ fs.rmSync(destinationDir, { recursive: true, force: true });
279
+ }
280
+ },
281
+ };
282
+ }
283
+ function prepareMutation(target, command, packageSpecifier, skillAssetRoot) {
284
+ const configMutation = command.kind === "install"
285
+ ? target.client === "codex"
286
+ ? prepareCodexInstall(target.configPath, packageSpecifier)
287
+ : prepareClaudeInstall(target.configPath, packageSpecifier)
288
+ : target.client === "codex"
289
+ ? prepareCodexUninstall(target.configPath)
290
+ : prepareClaudeUninstall(target.configPath);
291
+ const skillsMutation = command.kind === "install"
292
+ ? prepareSkillInstall(target.skillsPath, skillAssetRoot)
293
+ : prepareSkillRemoval(target.skillsPath);
294
+ return {
295
+ target,
296
+ configChanged: configMutation.changed,
297
+ skillsChanged: skillsMutation.changed,
298
+ apply: () => {
299
+ configMutation.apply();
300
+ skillsMutation.apply();
301
+ },
302
+ };
303
+ }
304
+ export function executeInstallCommand(command, options = {}) {
305
+ const homeDir = options.homeDir ?? os.homedir();
306
+ const packageSpecifier = options.packageSpecifier ?? resolveDefaultPackageSpecifier();
307
+ const skillAssetRoot = options.skillAssetRoot ?? resolveDefaultSkillAssetRoot();
308
+ const prepared = selectTargets(homeDir, command.client).map((target) => (prepareMutation(target, command, packageSpecifier, skillAssetRoot)));
309
+ if (!command.dryRun) {
310
+ for (const mutation of prepared) {
311
+ mutation.apply();
312
+ }
313
+ }
314
+ return {
315
+ action: command.kind,
316
+ client: command.client,
317
+ dryRun: command.dryRun,
318
+ results: prepared.map((mutation) => ({
319
+ client: mutation.target.client,
320
+ configPath: mutation.target.configPath,
321
+ skillsPath: mutation.target.skillsPath,
322
+ configChanged: mutation.configChanged,
323
+ skillsChanged: mutation.skillsChanged,
324
+ status: mutation.configChanged || mutation.skillsChanged ? "updated" : "unchanged",
325
+ dryRun: command.dryRun,
326
+ })),
327
+ };
328
+ }
329
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1,14 @@
1
+ import { execFileSync } from "node:child_process";
2
+ type ExecFileSyncLike = typeof execFileSync;
3
+ export interface PackageInstallabilityOptions {
4
+ packageJsonPath?: string;
5
+ execFileSyncImpl?: ExecFileSyncLike;
6
+ }
7
+ export interface ReleaseSmokeOptions extends PackageInstallabilityOptions {
8
+ packageRoot?: string;
9
+ tempDir?: string;
10
+ }
11
+ export declare function verifyManagedPackageInstallability(options?: PackageInstallabilityOptions): string;
12
+ export declare function runPublishedPackageReleaseSmoke(options?: ReleaseSmokeOptions): void;
13
+ export {};
14
+ //# sourceMappingURL=package-installability.d.ts.map
@@ -0,0 +1,124 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CliError } from "./errors.js";
7
+ function resolveDefaultPackageJsonPath() {
8
+ const currentFile = fileURLToPath(import.meta.url);
9
+ return path.resolve(path.dirname(currentFile), "..", "..", "package.json");
10
+ }
11
+ function readPackageJson(packageJsonPath) {
12
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
13
+ }
14
+ function looksLikeExactVersion(value) {
15
+ return /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(value);
16
+ }
17
+ function npmOutput(error) {
18
+ if (!(error instanceof Error)) {
19
+ return String(error);
20
+ }
21
+ const stdout = "stdout" in error && typeof error.stdout === "string"
22
+ ? error.stdout
23
+ : "";
24
+ const stderr = "stderr" in error && typeof error.stderr === "string"
25
+ ? error.stderr
26
+ : "";
27
+ return `${stdout}\n${stderr}\n${error.message}`.trim();
28
+ }
29
+ function resolveWorkspaceDependencyVersion(packageJsonPath, dependencyName) {
30
+ const packageRoot = path.dirname(packageJsonPath);
31
+ const repoRoot = path.resolve(packageRoot, "..", "..");
32
+ const packagesRoot = path.join(repoRoot, "packages");
33
+ if (!fs.existsSync(packagesRoot)) {
34
+ return null;
35
+ }
36
+ for (const entry of fs.readdirSync(packagesRoot, { withFileTypes: true })) {
37
+ if (!entry.isDirectory()) {
38
+ continue;
39
+ }
40
+ const candidatePath = path.join(packagesRoot, entry.name, "package.json");
41
+ if (!fs.existsSync(candidatePath)) {
42
+ continue;
43
+ }
44
+ const candidate = readPackageJson(candidatePath);
45
+ if (candidate.name === dependencyName && looksLikeExactVersion(candidate.version)) {
46
+ return candidate.version;
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ function assertPublishedVersion(packageName, version, ownerPackageName, ownerPackageVersion, execImpl, relation) {
52
+ try {
53
+ execImpl("npm", ["view", `${packageName}@${version}`, "version", "--json"], {
54
+ encoding: "utf8",
55
+ stdio: ["ignore", "pipe", "pipe"],
56
+ });
57
+ }
58
+ catch (error) {
59
+ if (relation === "self") {
60
+ throw new CliError("E_USAGE", `Cannot install ${ownerPackageName}@${ownerPackageVersion} because that package version is not published on npm. Publish ${ownerPackageName}@${ownerPackageVersion} first or use a local dev server config instead.`, 2);
61
+ }
62
+ throw new CliError("E_USAGE", `Cannot install ${ownerPackageName}@${ownerPackageVersion} because required dependency ${packageName}@${version} is not published on npm. Publish ${packageName}@${version} first, then rerun satori-cli install.`, 2);
63
+ }
64
+ }
65
+ export function verifyManagedPackageInstallability(options = {}) {
66
+ const packageJsonPath = options.packageJsonPath ?? resolveDefaultPackageJsonPath();
67
+ const execImpl = options.execFileSyncImpl ?? execFileSync;
68
+ const pkg = readPackageJson(packageJsonPath);
69
+ const packageSpecifier = `${pkg.name}@${pkg.version}`;
70
+ assertPublishedVersion(pkg.name, pkg.version, pkg.name, pkg.version, execImpl, "self");
71
+ for (const [dependencyName, rawDependencyVersion] of Object.entries(pkg.dependencies ?? {})) {
72
+ const dependencyVersion = looksLikeExactVersion(rawDependencyVersion)
73
+ ? rawDependencyVersion
74
+ : rawDependencyVersion.startsWith("workspace:")
75
+ ? resolveWorkspaceDependencyVersion(packageJsonPath, dependencyName)
76
+ : null;
77
+ if (!dependencyVersion) {
78
+ continue;
79
+ }
80
+ assertPublishedVersion(dependencyName, dependencyVersion, pkg.name, pkg.version, execImpl, "dependency");
81
+ }
82
+ return packageSpecifier;
83
+ }
84
+ export function runPublishedPackageReleaseSmoke(options = {}) {
85
+ const packageJsonPath = options.packageJsonPath ?? resolveDefaultPackageJsonPath();
86
+ const packageRoot = options.packageRoot ?? path.dirname(packageJsonPath);
87
+ const tempDir = options.tempDir ?? os.tmpdir();
88
+ const execImpl = options.execFileSyncImpl ?? execFileSync;
89
+ verifyManagedPackageInstallability({ packageJsonPath, execFileSyncImpl: execImpl });
90
+ const smokePackDir = fs.mkdtempSync(path.join(tempDir, "satori-release-smoke-"));
91
+ const smokeExecDir = fs.mkdtempSync(path.join(tempDir, "satori-release-exec-"));
92
+ const beforeFiles = new Set(fs.readdirSync(smokePackDir));
93
+ execImpl("pnpm", ["pack", "--pack-destination", smokePackDir], {
94
+ cwd: packageRoot,
95
+ encoding: "utf8",
96
+ stdio: ["ignore", "pipe", "pipe"],
97
+ });
98
+ const tarballName = fs.readdirSync(smokePackDir).find((entry) => entry.endsWith(".tgz") && !beforeFiles.has(entry));
99
+ if (!tarballName) {
100
+ throw new CliError("E_USAGE", "Release smoke failed: pnpm pack did not produce a tarball.", 2);
101
+ }
102
+ const tarballPath = path.join(smokePackDir, tarballName);
103
+ try {
104
+ execImpl("npm", ["exec", "--yes", "--package", tarballPath, "--", "satori", "--help"], {
105
+ cwd: smokeExecDir,
106
+ encoding: "utf8",
107
+ env: {
108
+ ...process.env,
109
+ npm_config_package_lock: "false",
110
+ },
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ });
113
+ }
114
+ catch (error) {
115
+ const output = npmOutput(error);
116
+ const pkg = readPackageJson(packageJsonPath);
117
+ throw new CliError("E_USAGE", `Release smoke failed for ${pkg.name}@${pkg.version}. The packed tarball did not start via 'npm exec --yes --package <tarball> -- satori --help'. ${output}`, 2);
118
+ }
119
+ finally {
120
+ fs.rmSync(smokePackDir, { recursive: true, force: true });
121
+ fs.rmSync(smokeExecDir, { recursive: true, force: true });
122
+ }
123
+ }
124
+ //# sourceMappingURL=package-installability.js.map
package/dist/config.js CHANGED
@@ -169,7 +169,7 @@ export function showHelpMessage() {
169
169
  console.log(`
170
170
  Satori MCP Server
171
171
 
172
- Usage: npx @zokizuan/satori-mcp@latest [options]
172
+ Usage: npx -y @zokizuan/satori-mcp@4.4.1 [options]
173
173
 
174
174
  Options:
175
175
  --help, -h Show this help message
@@ -206,16 +206,16 @@ Environment Variables:
206
206
 
207
207
  Examples:
208
208
  # Start MCP server with OpenAI and explicit Milvus address
209
- OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 npx @zokizuan/satori-mcp@latest
209
+ OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 npx -y @zokizuan/satori-mcp@4.4.1
210
210
 
211
211
  # Start MCP server with VoyageAI and specific model
212
- EMBEDDING_PROVIDER=VoyageAI VOYAGEAI_API_KEY=pa-xxx EMBEDDING_MODEL=voyage-4-large MILVUS_TOKEN=your-token npx @zokizuan/satori-mcp@latest
212
+ EMBEDDING_PROVIDER=VoyageAI VOYAGEAI_API_KEY=pa-xxx EMBEDDING_MODEL=voyage-4-large MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.4.1
213
213
 
214
214
  # Start MCP server with Gemini and specific model
215
- EMBEDDING_PROVIDER=Gemini GEMINI_API_KEY=xxx EMBEDDING_MODEL=gemini-embedding-001 MILVUS_TOKEN=your-token npx @zokizuan/satori-mcp@latest
215
+ EMBEDDING_PROVIDER=Gemini GEMINI_API_KEY=xxx EMBEDDING_MODEL=gemini-embedding-001 MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.4.1
216
216
 
217
217
  # Start MCP server with Ollama and specific model
218
- EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_TOKEN=your-token npx @zokizuan/satori-mcp@latest
218
+ EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.4.1
219
219
  `);
220
220
  }
221
221
  //# sourceMappingURL=config.js.map
@@ -24,6 +24,8 @@ export declare class ToolHandlers {
24
24
  private buildReindexHint;
25
25
  private buildCreateHint;
26
26
  private buildStatusHint;
27
+ private touchWatchedCodebase;
28
+ private unwatchCodebase;
27
29
  private buildManageResponseEnvelope;
28
30
  private manageResponseFromEnvelope;
29
31
  private manageResponse;
@@ -153,6 +153,26 @@ export class ToolHandlers {
153
153
  }
154
154
  };
155
155
  }
156
+ async touchWatchedCodebase(codebasePath) {
157
+ const syncManager = this.syncManager;
158
+ if (typeof syncManager.touchWatchedCodebase === 'function') {
159
+ await syncManager.touchWatchedCodebase(codebasePath);
160
+ return;
161
+ }
162
+ if (typeof syncManager.registerCodebaseWatcher === 'function') {
163
+ await syncManager.registerCodebaseWatcher(codebasePath);
164
+ }
165
+ }
166
+ async unwatchCodebase(codebasePath) {
167
+ const syncManager = this.syncManager;
168
+ if (typeof syncManager.unwatchCodebase === 'function') {
169
+ await syncManager.unwatchCodebase(codebasePath);
170
+ return;
171
+ }
172
+ if (typeof syncManager.unregisterCodebaseWatcher === 'function') {
173
+ await syncManager.unregisterCodebaseWatcher(codebasePath);
174
+ }
175
+ }
156
176
  buildManageResponseEnvelope(action, codebasePath, status, humanText, options = {}) {
157
177
  const envelope = {
158
178
  tool: "manage_index",
@@ -1961,7 +1981,7 @@ Agent instructions:
1961
1981
  this.snapshotManager.removeCodebaseCompletely(droppedCodebasePath);
1962
1982
  this.snapshotManager.saveCodebaseSnapshot();
1963
1983
  try {
1964
- await this.syncManager.unregisterCodebaseWatcher(droppedCodebasePath);
1984
+ await this.unwatchCodebase(droppedCodebasePath);
1965
1985
  }
1966
1986
  catch {
1967
1987
  // Best-effort watcher cleanup; dropping cloud collection remains successful.
@@ -2148,7 +2168,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2148
2168
  this.snapshotManager.removeCodebaseCompletely(absolutePath);
2149
2169
  this.snapshotManager.saveCodebaseSnapshot();
2150
2170
  try {
2151
- await this.syncManager.unregisterCodebaseWatcher(absolutePath);
2171
+ await this.unwatchCodebase(absolutePath);
2152
2172
  }
2153
2173
  catch {
2154
2174
  // Best-effort watcher cleanup before force rebuild.
@@ -2221,6 +2241,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2221
2241
  this.snapshotManager.saveCodebaseSnapshot();
2222
2242
  // Track the codebase path for syncing
2223
2243
  trackCodebasePath(absolutePath);
2244
+ await this.touchWatchedCodebase(absolutePath);
2224
2245
  // Start background indexing - now safe to proceed
2225
2246
  this.startBackgroundIndexing(absolutePath, forceReindex);
2226
2247
  const pathInfo = codebasePath !== absolutePath
@@ -2308,7 +2329,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2308
2329
  // Save snapshot after updating codebase lists
2309
2330
  this.snapshotManager.saveCodebaseSnapshot();
2310
2331
  await this.rebuildCallGraphForIndex(absolutePath);
2311
- await this.syncManager.registerCodebaseWatcher(absolutePath);
2332
+ await this.touchWatchedCodebase(absolutePath);
2312
2333
  let message = `Background indexing completed for '${absolutePath}'.\nIndexed ${stats.indexedFiles} files, ${stats.totalChunks} chunks.`;
2313
2334
  if (stats.status === 'limit_reached') {
2314
2335
  message += `\n⚠️ Warning: Indexing stopped because the chunk limit (450,000) was reached. The index may be incomplete.`;
@@ -2931,6 +2952,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2931
2952
  ...(Object.keys(responseHints).length > 0 ? { hints: responseHints } : {}),
2932
2953
  results: rawResults
2933
2954
  };
2955
+ await this.touchWatchedCodebase(effectiveRoot);
2934
2956
  return {
2935
2957
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
2936
2958
  meta: { searchDiagnostics }
@@ -3073,6 +3095,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
3073
3095
  ...(Object.keys(responseHints).length > 0 ? { hints: responseHints } : {}),
3074
3096
  results: visibleGroupedResults
3075
3097
  };
3098
+ await this.touchWatchedCodebase(effectiveRoot);
3076
3099
  return {
3077
3100
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
3078
3101
  meta: { searchDiagnostics }
@@ -3204,6 +3227,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
3204
3227
  content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
3205
3228
  };
3206
3229
  }
3230
+ await this.touchWatchedCodebase(effectiveRoot);
3207
3231
  if (!fs.existsSync(absoluteFile)) {
3208
3232
  const payload = {
3209
3233
  status: 'not_found',
@@ -3524,6 +3548,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
3524
3548
  }]
3525
3549
  };
3526
3550
  }
3551
+ await this.touchWatchedCodebase(effectiveRoot);
3527
3552
  const graph = this.callGraphManager.queryGraph(effectiveRoot, symbolRef, {
3528
3553
  direction,
3529
3554
  depth,
@@ -3610,7 +3635,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
3610
3635
  this.indexingStats = null;
3611
3636
  // Save snapshot after clearing index
3612
3637
  this.snapshotManager.saveCodebaseSnapshot();
3613
- await this.syncManager.unregisterCodebaseWatcher(absolutePath);
3638
+ await this.unwatchCodebase(absolutePath);
3614
3639
  let resultText = `Successfully cleared codebase '${absolutePath}'`;
3615
3640
  const remainingIndexed = this.snapshotManager.getIndexedCodebases().length;
3616
3641
  const remainingIndexing = this.snapshotManager.getIndexingCodebases().length;
@@ -3889,10 +3914,12 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
3889
3914
  : '';
3890
3915
  return this.manageResponse("sync", absolutePath, "error", `Error syncing codebase: coalesced in-flight reconcile failed (${decision.errorMessage}).${fallbackLine}`);
3891
3916
  }
3917
+ await this.touchWatchedCodebase(absolutePath);
3892
3918
  return this.manageResponse("sync", absolutePath, "ok", `🔄 Sync request coalesced for '${absolutePath}'. Reused in-flight sync result.`);
3893
3919
  }
3894
3920
  if (decision.mode === 'reconciled_ignore_change') {
3895
3921
  if (totalChanges === 0 && ignoredDeletes === 0) {
3922
+ await this.touchWatchedCodebase(absolutePath);
3896
3923
  return this.manageResponse("sync", absolutePath, "ok", `✅ Ignore-rule reconciliation completed for '${absolutePath}'. No additional index changes were required.`);
3897
3924
  }
3898
3925
  const resultMessage = `🔄 Incremental sync + ignore-rule reconciliation completed for '${absolutePath}'.\n\n` +
@@ -3900,13 +3927,16 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
3900
3927
  `🧹 Ignored paths removed from index: ${ignoredDeletes}\n` +
3901
3928
  `\nTotal changes: ${totalChanges + ignoredDeletes}`;
3902
3929
  console.log(`[SYNC] ✅ Sync+ignore reconcile completed: +${added}, -${removed}, ~${modified}, ignoredDeleted=${ignoredDeletes}`);
3930
+ await this.touchWatchedCodebase(absolutePath);
3903
3931
  return this.manageResponse("sync", absolutePath, "ok", resultMessage);
3904
3932
  }
3905
3933
  if (totalChanges === 0) {
3934
+ await this.touchWatchedCodebase(absolutePath);
3906
3935
  return this.manageResponse("sync", absolutePath, "ok", `✅ No changes detected for codebase '${absolutePath}'. Index is up to date.`);
3907
3936
  }
3908
3937
  const resultMessage = `🔄 Incremental sync completed for '${absolutePath}'.\n\n📊 Changes:\n+ ${added} file(s) added\n- ${removed} file(s) removed\n~ ${modified} file(s) modified\n\nTotal changes: ${totalChanges}`;
3909
3938
  console.log(`[SYNC] ✅ Sync completed: +${added}, -${removed}, ~${modified}`);
3939
+ await this.touchWatchedCodebase(absolutePath);
3910
3940
  return this.manageResponse("sync", absolutePath, "ok", resultMessage);
3911
3941
  }
3912
3942
  catch (error) {
@@ -53,6 +53,7 @@ export declare class SyncManager {
53
53
  private watcherModeStarted;
54
54
  private watchEnabled;
55
55
  private watchDebounceMs;
56
+ private watchedCodebases;
56
57
  private watchers;
57
58
  private debounceTimers;
58
59
  private watcherIgnoreMatchers;
@@ -87,8 +88,11 @@ export declare class SyncManager {
87
88
  private shouldIgnoreWatchPath;
88
89
  scheduleWatcherSync(codebasePath: string, reason?: WatchSyncReason): void;
89
90
  private handleWatcherError;
91
+ touchWatchedCodebase(codebasePath: string): Promise<void>;
92
+ unwatchCodebase(codebasePath: string): Promise<void>;
90
93
  registerCodebaseWatcher(codebasePath: string): Promise<void>;
91
94
  unregisterCodebaseWatcher(codebasePath: string): Promise<void>;
95
+ refreshWatchersFromWatchList(): Promise<void>;
92
96
  refreshWatchersFromSnapshot(): Promise<void>;
93
97
  startWatcherMode(): Promise<void>;
94
98
  stopWatcherMode(): Promise<void>;
package/dist/core/sync.js CHANGED
@@ -11,6 +11,7 @@ export class SyncManager {
11
11
  this.lastSyncTimes = new Map();
12
12
  this.backgroundSyncTimer = null;
13
13
  this.watcherModeStarted = false;
14
+ this.watchedCodebases = new Set();
14
15
  this.watchers = new Map();
15
16
  this.debounceTimers = new Map();
16
17
  this.watcherIgnoreMatchers = new Map();
@@ -237,7 +238,7 @@ export class SyncManager {
237
238
  try {
238
239
  this.snapshotManager.removeIndexedCodebase(codebasePath);
239
240
  this.snapshotManager.saveCodebaseSnapshot();
240
- await this.unregisterCodebaseWatcher(codebasePath);
241
+ await this.unwatchCodebase(codebasePath);
241
242
  }
242
243
  catch (e) {
243
244
  console.error(`[SYNC] Failed to clean snapshot for '${codebasePath}':`, e);
@@ -488,6 +489,17 @@ export class SyncManager {
488
489
  }
489
490
  console.error(`[SYNC-WATCH] Watcher error for '${codebasePath}':`, error);
490
491
  }
492
+ async touchWatchedCodebase(codebasePath) {
493
+ this.watchedCodebases.add(codebasePath);
494
+ if (!this.watchEnabled || !this.watcherModeStarted) {
495
+ return;
496
+ }
497
+ await this.refreshWatchersFromWatchList();
498
+ }
499
+ async unwatchCodebase(codebasePath) {
500
+ this.watchedCodebases.delete(codebasePath);
501
+ await this.unregisterCodebaseWatcher(codebasePath);
502
+ }
491
503
  async registerCodebaseWatcher(codebasePath) {
492
504
  if (!this.watchEnabled || !this.watcherModeStarted) {
493
505
  return;
@@ -559,26 +571,29 @@ export class SyncManager {
559
571
  console.error(`[SYNC-WATCH] Failed to close watcher for '${codebasePath}':`, error);
560
572
  }
561
573
  }
562
- async refreshWatchersFromSnapshot() {
574
+ async refreshWatchersFromWatchList() {
563
575
  if (!this.watchEnabled || !this.watcherModeStarted) {
564
576
  return;
565
577
  }
566
- const indexedCodebases = new Set(this.snapshotManager.getIndexedCodebases());
578
+ const watchableCodebases = new Set(Array.from(this.watchedCodebases).filter((codebasePath) => this.canScheduleWatchSync(codebasePath)));
567
579
  for (const watchedPath of Array.from(this.watchers.keys())) {
568
- if (!indexedCodebases.has(watchedPath)) {
580
+ if (!watchableCodebases.has(watchedPath)) {
569
581
  await this.unregisterCodebaseWatcher(watchedPath);
570
582
  }
571
583
  }
572
- for (const codebasePath of indexedCodebases) {
584
+ for (const codebasePath of watchableCodebases) {
573
585
  await this.registerCodebaseWatcher(codebasePath);
574
586
  }
575
587
  }
588
+ async refreshWatchersFromSnapshot() {
589
+ await this.refreshWatchersFromWatchList();
590
+ }
576
591
  async startWatcherMode() {
577
592
  if (!this.watchEnabled || this.watcherModeStarted) {
578
593
  return;
579
594
  }
580
595
  this.watcherModeStarted = true;
581
- await this.refreshWatchersFromSnapshot();
596
+ await this.refreshWatchersFromWatchList();
582
597
  console.log(`[SYNC-WATCH] Watcher mode enabled.`);
583
598
  }
584
599
  async stopWatcherMode() {
@@ -102,6 +102,20 @@ function resolveCodebaseRootForFile(absolutePath, ctx) {
102
102
  }
103
103
  return candidates[0].path;
104
104
  }
105
+ async function touchResolvedCodebaseRoot(absolutePath, ctx) {
106
+ const codebaseRoot = resolveCodebaseRootForFile(absolutePath, ctx);
107
+ if (!codebaseRoot) {
108
+ return;
109
+ }
110
+ const syncManager = ctx.syncManager;
111
+ if (typeof syncManager.touchWatchedCodebase === "function") {
112
+ await syncManager.touchWatchedCodebase(codebaseRoot);
113
+ return;
114
+ }
115
+ if (typeof syncManager.registerCodebaseWatcher === "function") {
116
+ await syncManager.registerCodebaseWatcher(codebaseRoot);
117
+ }
118
+ }
105
119
  function resolveIndexingBlockForFile(absolutePath, ctx) {
106
120
  const allCodebases = typeof ctx.snapshotManager?.getAllCodebases === "function"
107
121
  ? ctx.snapshotManager.getAllCodebases()
@@ -200,6 +214,7 @@ export const readFileTool = {
200
214
  }]
201
215
  };
202
216
  }
217
+ await touchResolvedCodebaseRoot(absolutePath, ctx);
203
218
  const content = fs.readFileSync(absolutePath, "utf-8");
204
219
  const lines = splitIntoLines(content);
205
220
  const totalLines = lines.length;
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@zokizuan/satori-mcp",
3
- "version": "4.3.1",
3
+ "version": "4.4.1",
4
4
  "description": "MCP server for Satori with agent-safe semantic search and indexing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
- "satori": "dist/index.js",
10
- "satori-cli": "dist/cli/index.js"
9
+ "satori": "dist/index.js"
11
10
  },
12
11
  "dependencies": {
13
12
  "@modelcontextprotocol/sdk": "^1.12.1",
@@ -15,7 +14,7 @@
15
14
  "ignore": "^7.0.5",
16
15
  "zod": "^3.25.55",
17
16
  "zod-to-json-schema": "^3.25.1",
18
- "@zokizuan/satori-core": "1.0.0"
17
+ "@zokizuan/satori-core": "1.1.1"
19
18
  },
20
19
  "devDependencies": {
21
20
  "@types/node": "^20.0.0",
@@ -42,7 +41,8 @@
42
41
  "access": "public"
43
42
  },
44
43
  "scripts": {
45
- "build": "pnpm clean && tsc --build --force && pnpm fix:bin-perms && pnpm docs:generate",
44
+ "build": "pnpm clean && tsc --build --force && pnpm fix:bin-perms && pnpm docs:generate && pnpm manifest:generate",
45
+ "build:runtime": "pnpm clean && tsc --build --force && pnpm fix:bin-perms",
46
46
  "dev": "tsx --watch src/index.ts",
47
47
  "clean": "rimraf dist",
48
48
  "lint": "eslint src --ext .ts",
@@ -52,7 +52,10 @@
52
52
  "start:with-env": "OPENAI_API_KEY=${OPENAI_API_KEY:your-api-key-here} MILVUS_ADDRESS=${MILVUS_ADDRESS:localhost:19530} tsx src/index.ts",
53
53
  "docs:generate": "tsx scripts/generate-docs.ts",
54
54
  "docs:check": "tsx scripts/generate-docs.ts --check",
55
- "fix:bin-perms": "node -e \"const fs=require('fs');try{fs.chmodSync('dist/index.js',0o755);fs.chmodSync('dist/cli/index.js',0o755);}catch(e){console.error(e);process.exit(1);}\"",
56
- "test": "tsx --test --test-concurrency=1 src/**/*.test.ts"
55
+ "manifest:generate": "tsx scripts/generate-server-manifest.ts",
56
+ "manifest:check": "tsx scripts/generate-server-manifest.ts --check",
57
+ "release:smoke": "tsx scripts/release-smoke.ts",
58
+ "fix:bin-perms": "node -e \"const fs=require('fs');try{fs.chmodSync('dist/index.js',0o755);}catch(e){console.error(e);process.exit(1);}\"",
59
+ "test": "pnpm --filter @zokizuan/satori-core build && node --import tsx --test --test-concurrency=1 src/core/**/*.test.ts src/server/**/*.test.ts src/tools/**/*.test.ts src/cli/**/*.test.ts"
57
60
  }
58
61
  }