@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 +30 -6
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +35 -1
- package/dist/cli/client.js +3 -1
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +20 -2
- package/dist/cli/install.d.ts +30 -0
- package/dist/cli/install.js +329 -0
- package/dist/cli/package-installability.d.ts +14 -0
- package/dist/cli/package-installability.js +124 -0
- package/dist/config.js +5 -5
- 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 +10 -7
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@
|
|
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@
|
|
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/
|
|
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 (
|
|
217
|
+
## Shell CLI (`@zokizuan/satori-cli`)
|
|
217
218
|
|
|
218
|
-
|
|
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.
|
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/client.js
CHANGED
|
@@ -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.
|
|
84
|
+
version: "1.1.1",
|
|
83
85
|
});
|
|
84
86
|
const session = new CliMcpSession(client, transport, options.callTimeoutMs, options.writeStderr);
|
|
85
87
|
session.wireStderr();
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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
|
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,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zokizuan/satori-mcp",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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
|
-
"
|
|
56
|
-
"
|
|
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
|
}
|