@zokizuan/satori-mcp 4.3.1 → 4.4.0

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
@@ -189,7 +189,7 @@ env = { EMBEDDING_PROVIDER = "VoyageAI", EMBEDDING_MODEL = "voyage-4-large", EMB
189
189
  "mcpServers": {
190
190
  "satori": {
191
191
  "command": "node",
192
- "args": ["/absolute/path/to/claude-context/packages/mcp/dist/index.js"],
192
+ "args": ["/absolute/path/to/satori/packages/mcp/dist/index.js"],
193
193
  "timeout": 180000,
194
194
  "env": {
195
195
  "EMBEDDING_PROVIDER": "VoyageAI",
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: satori-indexing
3
+ description: Index lifecycle and remediation for Satori. Use when codebases are not indexed, stale, blocked, or need freshness recovery.
4
+ ---
5
+
6
+ # Satori Indexing
7
+
8
+ Use this skill when the task is to create, reindex, sync, inspect readiness, or recover from stale index state.
9
+
10
+ ## Tools
11
+
12
+ Use only:
13
+ 1. `list_codebases`
14
+ 2. `manage_index`
15
+
16
+ ## Workflow
17
+
18
+ 1. Use `list_codebases` for a global view of tracked roots.
19
+ 2. Use `manage_index(action="status", path=...)` for the specific codebase.
20
+ 3. Use `manage_index(action="create", path=...)` when the codebase is not indexed.
21
+ 4. Use `manage_index(action="reindex", path=...)` only for compatibility gates or explicit rebuilds.
22
+ 5. Use `manage_index(action="sync", path=...)` for freshness convergence and ignore-rule updates.
23
+
24
+ ## Rules
25
+
26
+ - If any tool returns `requires_reindex`, stop and reindex. Do not substitute `sync`.
27
+ - Never call `manage_index(action="clear")` unless the user explicitly requests destructive reset.
28
+ - Treat ignore-only churn as a `sync` problem first.
29
+ - Respect blocked and indexing states instead of forcing retries blindly.
30
+
31
+ ## Status Handling
32
+
33
+ - `requires_reindex`: run `manage_index(action="reindex")`.
34
+ - `not_ready` with indexing reason: check status and wait for terminal completion.
35
+ - `not_indexed`: create the index.
36
+ - Ignore-rule noise mitigation: update `.satoriignore`, wait debounce, and run `sync` for immediate convergence.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: satori-navigation
3
+ description: Deterministic symbol navigation with Satori. Use after search results are found to lock exact spans and inspect call relationships.
4
+ ---
5
+
6
+ # Satori Navigation
7
+
8
+ Use this skill after `search_codebase` has returned candidate results and you need exact symbol/file navigation.
9
+
10
+ ## Tools
11
+
12
+ Use only:
13
+ 1. `file_outline`
14
+ 2. `call_graph`
15
+ 3. `read_file`
16
+
17
+ ## Workflow
18
+
19
+ 1. Use grouped `search_codebase` results as the starting point.
20
+ 2. If `callGraphHint.supported=true`, call `call_graph(path=..., symbolRef=..., direction="both", depth=1)`.
21
+ 3. If `callGraphHint.supported=false`, execute `navigationFallback.readSpan.args` exactly.
22
+ 4. Use `file_outline(resolveMode="exact", symbolIdExact|symbolLabelExact)` to lock the symbol span.
23
+ 5. Use `read_file(path=..., open_symbol=...)` or deterministic line spans for the final read.
24
+
25
+ ## Rules
26
+
27
+ - Treat `navigationFallback` as authoritative. Do not invent spans.
28
+ - `open_symbol` must resolve deterministically. Do not guess on ambiguity.
29
+ - `read_file(mode="annotated")` is preferred when outline metadata is useful.
30
+ - Follow continuation hints when plain reads are truncated.
31
+
32
+ ## Remediation
33
+
34
+ - `requires_reindex`: reindex before retrying navigation.
35
+ - `not_ready`: wait for indexing to finish.
36
+ - `unsupported`: fall back to deterministic `read_file` spans when supplied by `navigationFallback`.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: satori-search
3
+ description: Semantic-first code search with Satori. Use for intent-based code discovery before file reads or grep.
4
+ ---
5
+
6
+ # Satori Search
7
+
8
+ Use this skill when the task is to find where behavior lives, identify candidate symbols, or narrow the search space before deeper navigation.
9
+
10
+ ## Tools
11
+
12
+ Use only:
13
+ 1. `list_codebases`
14
+ 2. `manage_index`
15
+ 3. `search_codebase`
16
+
17
+ ## Workflow
18
+
19
+ 1. Check readiness with `manage_index(action="status", path=...)`.
20
+ 2. If not indexed, use `manage_index(action="create", path=...)`.
21
+ 3. If `requires_reindex` appears, stop and use `manage_index(action="reindex", path=...)`, then retry.
22
+ 4. Search with `search_codebase(path=..., query=..., scope="runtime", resultMode="grouped", groupBy="symbol", rankingMode="auto_changed_first")`.
23
+
24
+ ## Search Rules
25
+
26
+ - Start with natural-language intent, not filenames.
27
+ - Default to `scope="runtime"`.
28
+ - Use operators only when needed: `lang:`, `path:`, `-path:`, `must:`, `exclude:`.
29
+ - Treat warnings as usable-but-degraded results, not fatal errors.
30
+ - Use `debug=true` only when ranking or filter explanations are required.
31
+
32
+ ## Remediation
33
+
34
+ - `requires_reindex`: run `manage_index(action="reindex")`, not `sync`.
35
+ - `not_ready` with indexing reason: wait or check `manage_index(action="status")`.
36
+ - Noise mitigation hint: update `.satoriignore`, wait debounce, rerun search, and use `manage_index(action="sync")` only for immediate convergence.
@@ -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 {
@@ -3,6 +3,7 @@ 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>;
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ 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";
9
10
  import { resolveServerEntryPath } from "./resolve-server-entry.js";
10
11
  const MANAGE_INDEX_MIN_POLL_TIMEOUT_MS = 10 * 60 * 1000;
11
12
  function firstText(result) {
@@ -42,6 +43,8 @@ function buildHelpPayload() {
42
43
  return {
43
44
  usage: "satori-cli <command>",
44
45
  commands: [
46
+ "install [--client all|codex|claude] [--dry-run]",
47
+ "uninstall [--client all|codex|claude] [--dry-run]",
45
48
  "tools list",
46
49
  "tool call <toolName> --args-json '<json>'",
47
50
  "tool call <toolName> --args-file <path>",
@@ -164,6 +167,7 @@ export async function runCli(argv, options = {}) {
164
167
  writeStdout: options.writeStdout || ((text) => process.stdout.write(text)),
165
168
  writeStderr: options.writeStderr || ((text) => process.stderr.write(text)),
166
169
  };
170
+ const effectiveEnv = options.env || process.env;
167
171
  let parsedFormat = "json";
168
172
  let parsedCommandKind = null;
169
173
  try {
@@ -190,11 +194,21 @@ export async function runCli(argv, options = {}) {
190
194
  }
191
195
  return 0;
192
196
  }
197
+ if (parsed.command.kind === "install" || parsed.command.kind === "uninstall") {
198
+ const result = executeInstallCommand(parsed.command, {
199
+ homeDir: effectiveEnv.HOME,
200
+ });
201
+ emitJson(writers, result);
202
+ if (parsed.globals.format === "text") {
203
+ writers.writeStderr(`satori-cli ${parsed.command.kind} completed for ${parsed.command.client}.\n`);
204
+ }
205
+ return 0;
206
+ }
193
207
  const session = await connectCliMcpSession({
194
208
  command: options.serverCommand || process.execPath,
195
209
  args: options.serverArgs || resolveDefaultServerArgs(),
196
210
  env: {
197
- ...process.env,
211
+ ...effectiveEnv,
198
212
  ...options.serverEnv,
199
213
  SATORI_RUN_MODE: "cli",
200
214
  },
@@ -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,327 @@
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", "${packageSpecifier}"]`,
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", packageSpecifier],
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 !== 2) {
172
+ return false;
173
+ }
174
+ return entry.args[0] === "-y"
175
+ && typeof entry.args[1] === "string"
176
+ && /^@zokizuan\/satori-mcp@.+$/.test(entry.args[1]);
177
+ }
178
+ function prepareClaudeInstall(filePath, packageSpecifier) {
179
+ const currentObject = parseJsonObject(filePath);
180
+ const currentSerialized = JSON.stringify(currentObject);
181
+ const desiredServer = buildClaudeServerConfig(packageSpecifier);
182
+ const mcpServersValue = currentObject.mcpServers;
183
+ let mcpServers;
184
+ if (mcpServersValue === undefined) {
185
+ mcpServers = {};
186
+ }
187
+ else if (mcpServersValue && typeof mcpServersValue === "object" && !Array.isArray(mcpServersValue)) {
188
+ mcpServers = { ...mcpServersValue };
189
+ }
190
+ else {
191
+ throw new CliError("E_USAGE", `Expected mcpServers to be an object in ${filePath}.`, 2);
192
+ }
193
+ const existingSatori = mcpServers.satori;
194
+ if (existingSatori !== undefined && !isManagedClaudeEntry(existingSatori)) {
195
+ 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);
196
+ }
197
+ mcpServers.satori = {
198
+ ...existingSatori,
199
+ ...desiredServer,
200
+ };
201
+ currentObject.mcpServers = mcpServers;
202
+ const next = `${JSON.stringify(currentObject, null, 2)}\n`;
203
+ return {
204
+ changed: JSON.stringify(currentObject) !== currentSerialized,
205
+ apply: () => {
206
+ if (JSON.stringify(currentObject) === currentSerialized) {
207
+ return;
208
+ }
209
+ ensureParentDir(filePath);
210
+ fs.writeFileSync(filePath, next, "utf8");
211
+ },
212
+ };
213
+ }
214
+ function prepareClaudeUninstall(filePath) {
215
+ const currentObject = parseJsonObject(filePath);
216
+ const mcpServersValue = currentObject.mcpServers;
217
+ if (!mcpServersValue || typeof mcpServersValue !== "object" || Array.isArray(mcpServersValue)) {
218
+ return { changed: false, apply: () => { } };
219
+ }
220
+ const mcpServers = { ...mcpServersValue };
221
+ if (!Object.prototype.hasOwnProperty.call(mcpServers, "satori")) {
222
+ return { changed: false, apply: () => { } };
223
+ }
224
+ if (!isManagedClaudeEntry(mcpServers.satori)) {
225
+ throw new CliError("E_USAGE", `Refusing to remove unmanaged Satori config in ${filePath}. Remove mcpServers.satori manually instead.`, 2);
226
+ }
227
+ delete mcpServers.satori;
228
+ if (Object.keys(mcpServers).length === 0) {
229
+ delete currentObject.mcpServers;
230
+ }
231
+ else {
232
+ currentObject.mcpServers = mcpServers;
233
+ }
234
+ const next = `${JSON.stringify(currentObject, null, 2)}\n`;
235
+ return {
236
+ changed: true,
237
+ apply: () => {
238
+ fs.writeFileSync(filePath, next, "utf8");
239
+ },
240
+ };
241
+ }
242
+ function prepareSkillInstall(skillsPath, skillAssetRoot) {
243
+ const writes = [];
244
+ let changed = false;
245
+ for (const skillDirName of OWNED_SKILL_DIRS) {
246
+ const sourceFile = path.join(skillAssetRoot, skillDirName, "SKILL.md");
247
+ if (!fs.existsSync(sourceFile)) {
248
+ throw new CliError("E_USAGE", `Missing packaged skill asset: ${sourceFile}`, 2);
249
+ }
250
+ const content = fs.readFileSync(sourceFile, "utf8");
251
+ const destinationDir = path.join(skillsPath, skillDirName);
252
+ const destinationFile = path.join(destinationDir, "SKILL.md");
253
+ if (readTextIfExists(destinationFile) !== content) {
254
+ changed = true;
255
+ writes.push({ destinationDir, destinationFile, content });
256
+ }
257
+ }
258
+ return {
259
+ changed,
260
+ apply: () => {
261
+ for (const write of writes) {
262
+ ensureDir(write.destinationDir);
263
+ fs.writeFileSync(write.destinationFile, write.content, "utf8");
264
+ }
265
+ },
266
+ };
267
+ }
268
+ function prepareSkillRemoval(skillsPath) {
269
+ const removals = OWNED_SKILL_DIRS
270
+ .map((skillDirName) => path.join(skillsPath, skillDirName))
271
+ .filter((destinationDir) => fs.existsSync(destinationDir));
272
+ return {
273
+ changed: removals.length > 0,
274
+ apply: () => {
275
+ for (const destinationDir of removals) {
276
+ fs.rmSync(destinationDir, { recursive: true, force: true });
277
+ }
278
+ },
279
+ };
280
+ }
281
+ function prepareMutation(target, command, packageSpecifier, skillAssetRoot) {
282
+ const configMutation = command.kind === "install"
283
+ ? target.client === "codex"
284
+ ? prepareCodexInstall(target.configPath, packageSpecifier)
285
+ : prepareClaudeInstall(target.configPath, packageSpecifier)
286
+ : target.client === "codex"
287
+ ? prepareCodexUninstall(target.configPath)
288
+ : prepareClaudeUninstall(target.configPath);
289
+ const skillsMutation = command.kind === "install"
290
+ ? prepareSkillInstall(target.skillsPath, skillAssetRoot)
291
+ : prepareSkillRemoval(target.skillsPath);
292
+ return {
293
+ target,
294
+ configChanged: configMutation.changed,
295
+ skillsChanged: skillsMutation.changed,
296
+ apply: () => {
297
+ configMutation.apply();
298
+ skillsMutation.apply();
299
+ },
300
+ };
301
+ }
302
+ export function executeInstallCommand(command, options = {}) {
303
+ const homeDir = options.homeDir ?? os.homedir();
304
+ const packageSpecifier = options.packageSpecifier ?? resolveDefaultPackageSpecifier();
305
+ const skillAssetRoot = options.skillAssetRoot ?? resolveDefaultSkillAssetRoot();
306
+ const prepared = selectTargets(homeDir, command.client).map((target) => (prepareMutation(target, command, packageSpecifier, skillAssetRoot)));
307
+ if (!command.dryRun) {
308
+ for (const mutation of prepared) {
309
+ mutation.apply();
310
+ }
311
+ }
312
+ return {
313
+ action: command.kind,
314
+ client: command.client,
315
+ dryRun: command.dryRun,
316
+ results: prepared.map((mutation) => ({
317
+ client: mutation.target.client,
318
+ configPath: mutation.target.configPath,
319
+ skillsPath: mutation.target.skillsPath,
320
+ configChanged: mutation.configChanged,
321
+ skillsChanged: mutation.skillsChanged,
322
+ status: mutation.configChanged || mutation.skillsChanged ? "updated" : "unchanged",
323
+ dryRun: command.dryRun,
324
+ })),
325
+ };
326
+ }
327
+ //# sourceMappingURL=install.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zokizuan/satori-mcp",
3
- "version": "4.3.1",
3
+ "version": "4.4.0",
4
4
  "description": "MCP server for Satori with agent-safe semantic search and indexing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "ignore": "^7.0.5",
16
16
  "zod": "^3.25.55",
17
17
  "zod-to-json-schema": "^3.25.1",
18
- "@zokizuan/satori-core": "1.0.0"
18
+ "@zokizuan/satori-core": "1.1.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.0.0",
@@ -25,6 +25,7 @@
25
25
  "files": [
26
26
  "dist/**/*.js",
27
27
  "dist/**/*.d.ts",
28
+ "assets/skills/**/*.md",
28
29
  "README.md"
29
30
  ],
30
31
  "repository": {
@@ -42,7 +43,7 @@
42
43
  "access": "public"
43
44
  },
44
45
  "scripts": {
45
- "build": "pnpm clean && tsc --build --force && pnpm fix:bin-perms && pnpm docs:generate",
46
+ "build": "pnpm clean && tsc --build --force && pnpm fix:bin-perms && pnpm docs:generate && pnpm manifest:generate",
46
47
  "dev": "tsx --watch src/index.ts",
47
48
  "clean": "rimraf dist",
48
49
  "lint": "eslint src --ext .ts",
@@ -52,7 +53,9 @@
52
53
  "start:with-env": "OPENAI_API_KEY=${OPENAI_API_KEY:your-api-key-here} MILVUS_ADDRESS=${MILVUS_ADDRESS:localhost:19530} tsx src/index.ts",
53
54
  "docs:generate": "tsx scripts/generate-docs.ts",
54
55
  "docs:check": "tsx scripts/generate-docs.ts --check",
56
+ "manifest:generate": "tsx scripts/generate-server-manifest.ts",
57
+ "manifest:check": "tsx scripts/generate-server-manifest.ts --check",
55
58
  "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"
59
+ "test": "pnpm --filter @zokizuan/satori-core build && tsx --test --test-concurrency=1 src/**/*.test.ts"
57
60
  }
58
61
  }