@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 +1 -1
- package/assets/skills/satori-indexing/SKILL.md +36 -0
- package/assets/skills/satori-navigation/SKILL.md +36 -0
- package/assets/skills/satori-search/SKILL.md +36 -0
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +35 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +15 -1
- package/dist/cli/install.d.ts +30 -0
- package/dist/cli/install.js +327 -0
- package/dist/core/handlers.d.ts +2 -0
- package/dist/core/handlers.js +34 -4
- package/dist/core/sync.d.ts +4 -0
- package/dist/core/sync.js +21 -6
- package/dist/tools/read_file.js +15 -0
- package/package.json +7 -4
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/
|
|
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.
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -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 {
|
package/dist/cli/index.d.ts
CHANGED
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
|
-
...
|
|
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
|
package/dist/core/handlers.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/handlers.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|
package/dist/core/sync.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
574
|
+
async refreshWatchersFromWatchList() {
|
|
563
575
|
if (!this.watchEnabled || !this.watcherModeStarted) {
|
|
564
576
|
return;
|
|
565
577
|
}
|
|
566
|
-
const
|
|
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 (!
|
|
580
|
+
if (!watchableCodebases.has(watchedPath)) {
|
|
569
581
|
await this.unregisterCodebaseWatcher(watchedPath);
|
|
570
582
|
}
|
|
571
583
|
}
|
|
572
|
-
for (const codebasePath of
|
|
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.
|
|
596
|
+
await this.refreshWatchersFromWatchList();
|
|
582
597
|
console.log(`[SYNC-WATCH] Watcher mode enabled.`);
|
|
583
598
|
}
|
|
584
599
|
async stopWatcherMode() {
|
package/dist/tools/read_file.js
CHANGED
|
@@ -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
|
+
"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.
|
|
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
|
}
|