@zokizuan/satori-cli 0.3.2 → 0.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/LICENSE +1 -1
- package/README.md +63 -18
- package/assets/skills/satori/SKILL.md +61 -0
- package/dist/args.d.ts +1 -1
- package/dist/args.js +3 -3
- package/dist/doctor.js +39 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -3
- package/dist/install.d.ts +11 -1
- package/dist/install.js +472 -57
- package/package.json +5 -4
- package/assets/skills/satori-indexing/SKILL.md +0 -38
- package/assets/skills/satori-navigation/SKILL.md +0 -39
- package/assets/skills/satori-search/SKILL.md +0 -39
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,31 +1,64 @@
|
|
|
1
1
|
# @zokizuan/satori-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installer and shell client for Satori MCP. Use this package to configure supported MCP clients, check provider setup, and call Satori tools from a terminal without starting a resident MCP client.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y @zokizuan/satori-cli@0.
|
|
9
|
-
npx -y @zokizuan/satori-cli@0.
|
|
10
|
-
npx -y @zokizuan/satori-cli@0.3.2 install --client all --dry-run
|
|
11
|
-
npx -y @zokizuan/satori-cli@0.3.2 uninstall --client codex
|
|
12
|
-
npx -y @zokizuan/satori-cli@0.3.2 doctor
|
|
8
|
+
npx -y @zokizuan/satori-cli@0.4.1 install --client all
|
|
9
|
+
npx -y @zokizuan/satori-cli@0.4.1 doctor
|
|
13
10
|
```
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
Supported clients are `codex`, `claude`, `opencode`, and `all`.
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
command
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
The installer performs package resolution once, stores the MCP server under `~/.satori/mcp-runtime/`, writes a stable launcher at `~/.satori/bin/satori-mcp.js`, and writes client-specific config that starts the launcher directly with Node. Resident MCP startup should not run `npx` or require a custom long startup timeout.
|
|
15
|
+
|
|
16
|
+
Treat `~/.satori/` as installer-owned state. The public setup path is the installer command above, not manual copying of runtime cache paths into each harness.
|
|
17
|
+
|
|
18
|
+
The installer only manages Satori-owned config and the first-party workflow skill:
|
|
19
|
+
|
|
20
|
+
- `satori`
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx -y @zokizuan/satori-cli@0.4.1 install --client codex
|
|
26
|
+
npx -y @zokizuan/satori-cli@0.4.1 install --client claude
|
|
27
|
+
npx -y @zokizuan/satori-cli@0.4.1 install --client opencode
|
|
28
|
+
npx -y @zokizuan/satori-cli@0.4.1 install --client all --dry-run
|
|
29
|
+
npx -y @zokizuan/satori-cli@0.4.1 uninstall --client codex
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`doctor` checks Node, package visibility, provider env, and Milvus env without starting an MCP client.
|
|
33
|
+
|
|
34
|
+
Typical first run:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx -y @zokizuan/satori-cli@0.4.1 install --client all
|
|
38
|
+
npx -y @zokizuan/satori-cli@0.4.1 doctor
|
|
39
|
+
# restart your MCP client
|
|
22
40
|
```
|
|
23
41
|
|
|
24
|
-
The installer only
|
|
42
|
+
The installer writes launcher config only. Runtime provider settings are read when the MCP client starts.
|
|
43
|
+
|
|
44
|
+
Supported client installs expose the Satori runtime variable names in the client config:
|
|
45
|
+
|
|
46
|
+
- Codex writes active `env_vars` forwarding plus an optional commented `[mcp_servers.satori.env]` template in `~/.codex/config.toml`.
|
|
47
|
+
- Claude Code writes `mcpServers.satori.env` in `~/.claude.json` with `${VAR:-}` pass-through values.
|
|
48
|
+
- OpenCode writes `mcp.satori.environment` in `~/.config/opencode/opencode.json` with `{env:VAR}` pass-through values.
|
|
25
49
|
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
50
|
+
If you want a client to store literal values, replace the generated pass-through value for that client. In Codex, uncomment or add this table outside the installer-managed launcher block so reinstalls keep your edits:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
[mcp_servers.satori.env]
|
|
54
|
+
EMBEDDING_PROVIDER = "VoyageAI"
|
|
55
|
+
EMBEDDING_MODEL = "voyage-4-large"
|
|
56
|
+
EMBEDDING_OUTPUT_DIMENSION = "1024"
|
|
57
|
+
VOYAGEAI_API_KEY = "pa-..."
|
|
58
|
+
VOYAGEAI_RERANKER_MODEL = "rerank-2.5"
|
|
59
|
+
MILVUS_ADDRESS = "https://your-zilliz-endpoint"
|
|
60
|
+
MILVUS_TOKEN = "your-zilliz-token"
|
|
61
|
+
```
|
|
29
62
|
|
|
30
63
|
## Direct Tool Calls
|
|
31
64
|
|
|
@@ -39,11 +72,23 @@ satori-cli search_codebase --path /abs/repo --query "auth flow"
|
|
|
39
72
|
|
|
40
73
|
Global flags such as `--startup-timeout-ms`, `--call-timeout-ms`, `--format`, and `--debug` must appear before the command token.
|
|
41
74
|
|
|
42
|
-
|
|
75
|
+
## Runtime Requirements
|
|
76
|
+
|
|
77
|
+
Indexing and search require an embedding provider plus a Milvus-compatible backend. MCP startup and `tools list` do not require those credentials; provider-backed tool calls return `MISSING_PROVIDER_CONFIG` when setup is incomplete.
|
|
78
|
+
|
|
79
|
+
Common local setup:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
EMBEDDING_PROVIDER=Ollama
|
|
83
|
+
EMBEDDING_MODEL=nomic-embed-text
|
|
84
|
+
OLLAMA_HOST=http://127.0.0.1:11434
|
|
85
|
+
MILVUS_ADDRESS=localhost:19530
|
|
86
|
+
```
|
|
43
87
|
|
|
44
88
|
## Development
|
|
45
89
|
|
|
46
90
|
```bash
|
|
47
91
|
pnpm --filter @zokizuan/satori-cli build
|
|
48
92
|
pnpm --filter @zokizuan/satori-cli test
|
|
93
|
+
pnpm run release:smoke:cli
|
|
49
94
|
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: satori
|
|
3
|
+
description: Use when working with Satori MCP for code search, exact navigation, call graph context, bounded reads, indexing, sync, reindex, or stale index recovery.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Satori
|
|
7
|
+
|
|
8
|
+
Use this skill when a task needs Satori MCP for code discovery, navigation, or index lifecycle work.
|
|
9
|
+
|
|
10
|
+
## Tools
|
|
11
|
+
|
|
12
|
+
Satori exposes exactly six MCP tools:
|
|
13
|
+
|
|
14
|
+
1. `list_codebases`
|
|
15
|
+
2. `manage_index`
|
|
16
|
+
3. `search_codebase`
|
|
17
|
+
4. `file_outline`
|
|
18
|
+
5. `call_graph`
|
|
19
|
+
6. `read_file`
|
|
20
|
+
|
|
21
|
+
## Default Workflow
|
|
22
|
+
|
|
23
|
+
1. Use `manage_index(action="status", path=...)` when index state is unknown.
|
|
24
|
+
2. If the codebase is not indexed, use `manage_index(action="create", path=...)`.
|
|
25
|
+
3. Search the requested path with `search_codebase(path=..., query=..., scope="runtime", resultMode="grouped", groupBy="symbol", rankingMode="auto_changed_first")`.
|
|
26
|
+
4. Use `file_outline(resolveMode="exact", symbolIdExact|symbolLabelExact)` to lock exact symbol spans when identity is available.
|
|
27
|
+
5. If `callGraphHint.supported=true`, call `call_graph(path=..., symbolRef=..., direction="both", depth=1)`.
|
|
28
|
+
6. Use `read_file(path=..., open_symbol=...)` or deterministic line spans for final evidence before editing.
|
|
29
|
+
|
|
30
|
+
## Search Rules
|
|
31
|
+
|
|
32
|
+
- Start with natural-language intent, not filenames.
|
|
33
|
+
- Default to `scope="runtime"`.
|
|
34
|
+
- Use operators only when useful: `lang:`, `path:`, `-path:`, `must:`, `exclude:`.
|
|
35
|
+
- Pass the user's requested path; if Satori resolves an indexed parent, follow returned fallback payloads exactly.
|
|
36
|
+
- Treat warnings as usable-but-degraded results, not fatal errors.
|
|
37
|
+
- Use `debug=true` only when ranking or filter explanations are required.
|
|
38
|
+
|
|
39
|
+
## Navigation Rules
|
|
40
|
+
|
|
41
|
+
- Treat `navigationFallback` as authoritative. Do not invent spans.
|
|
42
|
+
- `open_symbol` must resolve deterministically. Do not guess on ambiguity.
|
|
43
|
+
- Prefer `read_file(mode="annotated")` when outline metadata helps.
|
|
44
|
+
- Follow continuation hints when plain reads are truncated.
|
|
45
|
+
- Read the relevant implementation and call sites before editing behavior.
|
|
46
|
+
|
|
47
|
+
## Index Rules
|
|
48
|
+
|
|
49
|
+
- If any tool returns `requires_reindex`, run `manage_index(action="reindex")`, then retry the original call. Do not substitute `sync`.
|
|
50
|
+
- Use `manage_index(action="sync")` for freshness convergence and ignore-rule updates.
|
|
51
|
+
- Never call `manage_index(action="clear")` unless the user explicitly requests destructive reset.
|
|
52
|
+
- Respect blocked and actively indexing states instead of forcing retries blindly.
|
|
53
|
+
- `MISSING_PROVIDER_CONFIG` is active only when it appears as the tool response `code` or `reason`. If it appears inside search results, it may just be matched code content.
|
|
54
|
+
|
|
55
|
+
## Status Handling
|
|
56
|
+
|
|
57
|
+
- `not_indexed`: create the index.
|
|
58
|
+
- `not_ready` with indexing reason: check status and wait for terminal completion.
|
|
59
|
+
- `requires_reindex`: reindex before trusting search or navigation.
|
|
60
|
+
- `unsupported`: fall back to deterministic `read_file` spans when supplied by `navigationFallback`.
|
|
61
|
+
- Noise mitigation hint: update `.satoriignore`, wait debounce, rerun search, and use `manage_index(action="sync")` only for immediate convergence.
|
package/dist/args.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface ResolveRawArgsOptions {
|
|
|
49
49
|
stdin?: NodeJS.ReadStream;
|
|
50
50
|
stdinTimeoutMs: number;
|
|
51
51
|
}
|
|
52
|
-
export type InstallClient = "all" | "claude" | "codex";
|
|
52
|
+
export type InstallClient = "all" | "claude" | "codex" | "opencode";
|
|
53
53
|
export declare function parseCliArgs(argv: string[]): ParsedCliInput;
|
|
54
54
|
export declare function resolveRawArguments(rawArgsMode: RawArgsMode, options: ResolveRawArgsOptions): Promise<Record<string, unknown>>;
|
|
55
55
|
export declare function parseWrapperArgumentsFromSchema(toolName: string, inputSchema: unknown, wrapperArgs: string[]): Record<string, unknown>;
|
package/dist/args.js
CHANGED
|
@@ -20,7 +20,7 @@ function stripFlagPrefix(token) {
|
|
|
20
20
|
}
|
|
21
21
|
function parseGlobalOptions(argv) {
|
|
22
22
|
const globals = {
|
|
23
|
-
startupTimeoutMs:
|
|
23
|
+
startupTimeoutMs: 30000,
|
|
24
24
|
callTimeoutMs: 600000,
|
|
25
25
|
format: "json",
|
|
26
26
|
debug: false,
|
|
@@ -116,8 +116,8 @@ function parseInstallCommand(kind, args) {
|
|
|
116
116
|
const token = args[i];
|
|
117
117
|
if (token === "--client") {
|
|
118
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);
|
|
119
|
+
if (next !== "all" && next !== "claude" && next !== "codex" && next !== "opencode") {
|
|
120
|
+
throw new CliError("E_USAGE", "--client must be one of: all, claude, codex, opencode.", 2);
|
|
121
121
|
}
|
|
122
122
|
client = next;
|
|
123
123
|
i += 1;
|
package/dist/doctor.js
CHANGED
|
@@ -7,6 +7,32 @@ function parseNodeMajor(version) {
|
|
|
7
7
|
function selectedProvider(env) {
|
|
8
8
|
return env.EMBEDDING_PROVIDER || "VoyageAI";
|
|
9
9
|
}
|
|
10
|
+
function defaultModelForProvider(provider) {
|
|
11
|
+
switch (provider) {
|
|
12
|
+
case "OpenAI":
|
|
13
|
+
return "text-embedding-3-small";
|
|
14
|
+
case "VoyageAI":
|
|
15
|
+
return "voyage-4-large";
|
|
16
|
+
case "Gemini":
|
|
17
|
+
return "gemini-embedding-001";
|
|
18
|
+
case "Ollama":
|
|
19
|
+
return "nomic-embed-text";
|
|
20
|
+
default:
|
|
21
|
+
return "voyage-4-large";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function selectedModel(env, provider) {
|
|
25
|
+
if (provider === "Ollama") {
|
|
26
|
+
return env.OLLAMA_MODEL || env.EMBEDDING_MODEL || defaultModelForProvider(provider);
|
|
27
|
+
}
|
|
28
|
+
return env.EMBEDDING_MODEL || defaultModelForProvider(provider);
|
|
29
|
+
}
|
|
30
|
+
function selectedDimension(env, provider) {
|
|
31
|
+
if (env.EMBEDDING_OUTPUT_DIMENSION) {
|
|
32
|
+
return env.EMBEDDING_OUTPUT_DIMENSION;
|
|
33
|
+
}
|
|
34
|
+
return provider === "VoyageAI" ? "1024" : "provider default";
|
|
35
|
+
}
|
|
10
36
|
function requiredEmbeddingEnv(provider) {
|
|
11
37
|
switch (provider) {
|
|
12
38
|
case "OpenAI":
|
|
@@ -62,17 +88,25 @@ export function runDoctor(options = {}) {
|
|
|
62
88
|
nextSteps.push("Verify npm can access @zokizuan/satori-mcp from this machine.");
|
|
63
89
|
}
|
|
64
90
|
const provider = selectedProvider(env);
|
|
91
|
+
addCheck(checks, "embedding_provider", "ok", `Embedding provider: ${provider}.`);
|
|
92
|
+
addCheck(checks, "embedding_model", "ok", `Embedding model: ${selectedModel(env, provider)}.`);
|
|
93
|
+
addCheck(checks, "embedding_dimension", "ok", `Embedding output dimension: ${selectedDimension(env, provider)}.`);
|
|
65
94
|
const requiredKey = requiredEmbeddingEnv(provider);
|
|
66
95
|
if (requiredKey && !env[requiredKey]) {
|
|
67
96
|
addCheck(checks, "embedding_provider_env", "error", `${provider} requires ${requiredKey}.`);
|
|
68
|
-
|
|
97
|
+
if (provider === "VoyageAI") {
|
|
98
|
+
nextSteps.push("Set VOYAGEAI_API_KEY from the Voyage AI dashboard API keys page.");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
nextSteps.push(`Set ${requiredKey}.`);
|
|
102
|
+
}
|
|
69
103
|
}
|
|
70
104
|
else {
|
|
71
105
|
addCheck(checks, "embedding_provider_env", "ok", requiredKey ? `${requiredKey} is present.` : `${provider} does not require an API key.`);
|
|
72
106
|
}
|
|
73
107
|
if (!env.MILVUS_ADDRESS) {
|
|
74
108
|
addCheck(checks, "milvus_address", "error", "MILVUS_ADDRESS is required for index/search/clear operations.");
|
|
75
|
-
nextSteps.push("Set MILVUS_ADDRESS.");
|
|
109
|
+
nextSteps.push("Set MILVUS_ADDRESS to a Zilliz Cloud public endpoint or local Milvus address such as localhost:19530.");
|
|
76
110
|
}
|
|
77
111
|
else {
|
|
78
112
|
addCheck(checks, "milvus_address", "ok", "MILVUS_ADDRESS is present.");
|
|
@@ -83,6 +117,9 @@ export function runDoctor(options = {}) {
|
|
|
83
117
|
else {
|
|
84
118
|
addCheck(checks, "milvus_token", "ok", "MILVUS_TOKEN is not set; local/unauthenticated Milvus endpoints are supported.");
|
|
85
119
|
}
|
|
120
|
+
if (nextSteps.length > 0) {
|
|
121
|
+
nextSteps.push("Restart your MCP client after changing Satori environment variables.");
|
|
122
|
+
}
|
|
86
123
|
return {
|
|
87
124
|
status: overallStatus(checks),
|
|
88
125
|
checks,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type ManagedRuntimeCommand } from "./install.js";
|
|
2
3
|
import type { DoctorResult } from "./doctor.js";
|
|
3
4
|
interface RunCliOptions {
|
|
4
5
|
writeStdout?: (text: string) => void;
|
|
@@ -12,6 +13,7 @@ interface RunCliOptions {
|
|
|
12
13
|
callTimeoutMs?: number;
|
|
13
14
|
cwd?: string;
|
|
14
15
|
installabilityVerifier?: () => string | Promise<string>;
|
|
16
|
+
installRuntimeCommand?: ManagedRuntimeCommand;
|
|
15
17
|
doctorRunner?: (options: {
|
|
16
18
|
env: NodeJS.ProcessEnv;
|
|
17
19
|
}) => DoctorResult;
|
package/dist/index.js
CHANGED
|
@@ -45,8 +45,8 @@ function buildHelpPayload() {
|
|
|
45
45
|
return {
|
|
46
46
|
usage: "satori-cli <command>",
|
|
47
47
|
commands: [
|
|
48
|
-
"install [--client all|codex|claude] [--dry-run]",
|
|
49
|
-
"uninstall [--client all|codex|claude] [--dry-run]",
|
|
48
|
+
"install [--client all|codex|claude|opencode] [--dry-run]",
|
|
49
|
+
"uninstall [--client all|codex|claude|opencode] [--dry-run]",
|
|
50
50
|
"doctor",
|
|
51
51
|
"tools list",
|
|
52
52
|
"tool call <toolName> --args-json '<json>'",
|
|
@@ -211,11 +211,14 @@ export async function runCli(argv, options = {}) {
|
|
|
211
211
|
return result.status === "error" ? 1 : 0;
|
|
212
212
|
}
|
|
213
213
|
if (parsed.command.kind === "install" || parsed.command.kind === "uninstall") {
|
|
214
|
+
let packageSpecifier;
|
|
214
215
|
if (parsed.command.kind === "install") {
|
|
215
|
-
await (options.installabilityVerifier || verifyManagedPackageInstallability)();
|
|
216
|
+
packageSpecifier = await (options.installabilityVerifier || verifyManagedPackageInstallability)();
|
|
216
217
|
}
|
|
217
218
|
const result = executeInstallCommand(parsed.command, {
|
|
218
219
|
homeDir: effectiveEnv.HOME,
|
|
220
|
+
packageSpecifier,
|
|
221
|
+
runtimeCommand: options.installRuntimeCommand,
|
|
219
222
|
});
|
|
220
223
|
emitJson(writers, result);
|
|
221
224
|
if (parsed.globals.format === "text") {
|
package/dist/install.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import type { InstallClient } from "./args.js";
|
|
3
|
+
type ExecFileSyncLike = typeof execFileSync;
|
|
2
4
|
type ClientName = Exclude<InstallClient, "all">;
|
|
5
|
+
export interface ManagedRuntimeCommand {
|
|
6
|
+
command: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
}
|
|
3
9
|
export interface InstallCommandInput {
|
|
4
10
|
kind: "install" | "uninstall";
|
|
5
11
|
client: InstallClient;
|
|
@@ -9,13 +15,17 @@ export interface InstallCommandOptions {
|
|
|
9
15
|
homeDir?: string;
|
|
10
16
|
packageSpecifier?: string;
|
|
11
17
|
skillAssetRoot?: string;
|
|
18
|
+
runtimeCommand?: ManagedRuntimeCommand;
|
|
19
|
+
execFileSyncImpl?: ExecFileSyncLike;
|
|
12
20
|
}
|
|
13
21
|
export interface ClientInstallResult {
|
|
14
22
|
client: ClientName;
|
|
15
23
|
configPath: string;
|
|
16
|
-
skillsPath
|
|
24
|
+
skillsPath?: string;
|
|
25
|
+
instructionsPath?: string;
|
|
17
26
|
configChanged: boolean;
|
|
18
27
|
skillsChanged: boolean;
|
|
28
|
+
instructionsChanged: boolean;
|
|
19
29
|
status: "updated" | "unchanged";
|
|
20
30
|
dryRun: boolean;
|
|
21
31
|
}
|
package/dist/install.js
CHANGED
|
@@ -1,13 +1,71 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
5
7
|
import { CliError } from "./errors.js";
|
|
6
8
|
import { resolveManagedPackageSpecifier } from "./managed-package.js";
|
|
7
9
|
const MANAGED_BLOCK_START = "# >>> satori-cli managed satori start >>>";
|
|
8
10
|
const MANAGED_BLOCK_END = "# <<< satori-cli managed satori end <<<";
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
+
const CODEX_ENV_TEMPLATE_START = "# >>> satori-cli optional satori env template >>>";
|
|
12
|
+
const CODEX_ENV_TEMPLATE_END = "# <<< satori-cli optional satori env template <<<";
|
|
13
|
+
const INSTRUCTIONS_BLOCK_START = "<!-- satori-mcp:start -->";
|
|
14
|
+
const INSTRUCTIONS_BLOCK_END = "<!-- satori-mcp:end -->";
|
|
15
|
+
const OWNED_SKILL_DIRS = ["satori"];
|
|
16
|
+
const MANAGED_RUNTIME_DIR = "mcp-runtime";
|
|
17
|
+
const MANAGED_BIN_DIR = "bin";
|
|
18
|
+
const MANAGED_LAUNCHER_FILE = "satori-mcp.js";
|
|
19
|
+
const SATORI_RUNTIME_ENV_VARS = [
|
|
20
|
+
"EMBEDDING_PROVIDER",
|
|
21
|
+
"EMBEDDING_MODEL",
|
|
22
|
+
"EMBEDDING_OUTPUT_DIMENSION",
|
|
23
|
+
"OPENAI_API_KEY",
|
|
24
|
+
"OPENAI_BASE_URL",
|
|
25
|
+
"VOYAGEAI_API_KEY",
|
|
26
|
+
"VOYAGEAI_RERANKER_MODEL",
|
|
27
|
+
"GEMINI_API_KEY",
|
|
28
|
+
"GEMINI_BASE_URL",
|
|
29
|
+
"OLLAMA_HOST",
|
|
30
|
+
"OLLAMA_MODEL",
|
|
31
|
+
"MILVUS_ADDRESS",
|
|
32
|
+
"MILVUS_TOKEN",
|
|
33
|
+
"READ_FILE_MAX_LINES",
|
|
34
|
+
"MCP_ENABLE_WATCHER",
|
|
35
|
+
"MCP_WATCH_DEBOUNCE_MS",
|
|
36
|
+
];
|
|
37
|
+
const CODEX_ENV_TEMPLATE_LINES = [
|
|
38
|
+
CODEX_ENV_TEMPLATE_START,
|
|
39
|
+
"# Optional direct Codex env values. Uncomment/fill these if you prefer",
|
|
40
|
+
"# ~/.codex/config.toml to store Satori runtime settings directly.",
|
|
41
|
+
"# This template is outside the launcher block so reinstall keeps edits.",
|
|
42
|
+
"# [mcp_servers.satori.env]",
|
|
43
|
+
"# EMBEDDING_PROVIDER = \"VoyageAI\"",
|
|
44
|
+
"# EMBEDDING_MODEL = \"voyage-4-large\"",
|
|
45
|
+
"# EMBEDDING_OUTPUT_DIMENSION = \"1024\"",
|
|
46
|
+
"# VOYAGEAI_API_KEY = \"pa-...\"",
|
|
47
|
+
"# VOYAGEAI_RERANKER_MODEL = \"rerank-2.5\"",
|
|
48
|
+
"# MILVUS_ADDRESS = \"https://your-zilliz-endpoint\"",
|
|
49
|
+
"# MILVUS_TOKEN = \"your-zilliz-token\"",
|
|
50
|
+
CODEX_ENV_TEMPLATE_END,
|
|
51
|
+
];
|
|
52
|
+
const OPENCODE_INSTRUCTIONS = `# Satori MCP
|
|
53
|
+
|
|
54
|
+
This project uses Satori MCP for semantic code search, deterministic navigation, and index lifecycle management.
|
|
55
|
+
|
|
56
|
+
## Priority Order
|
|
57
|
+
1. \`search_codebase\` - find candidate code by behavior, concept, or symbol
|
|
58
|
+
2. \`file_outline\` - lock exact symbol spans before reading or editing
|
|
59
|
+
3. \`call_graph\` - inspect callers and callees when supported
|
|
60
|
+
4. \`read_file\` - open exact spans or fallback windows
|
|
61
|
+
5. \`manage_index\` - create, sync, reindex, or inspect index status
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
- Prefer Satori tools before grep/glob for code discovery.
|
|
65
|
+
- If a tool returns \`requires_reindex\`, run \`manage_index(action="reindex")\` and retry the original call.
|
|
66
|
+
- Treat \`navigationFallback\` as authoritative when call graph is unavailable.
|
|
67
|
+
- Read the relevant implementation and call sites before editing behavior.
|
|
68
|
+
`;
|
|
11
69
|
function resolveDefaultSkillAssetRoot() {
|
|
12
70
|
const currentFile = fileURLToPath(import.meta.url);
|
|
13
71
|
return path.resolve(path.dirname(currentFile), "..", "assets", "skills");
|
|
@@ -26,12 +84,26 @@ function resolveClientTargets(homeDir) {
|
|
|
26
84
|
{
|
|
27
85
|
client: "codex",
|
|
28
86
|
configPath: path.join(homeDir, ".codex", "config.toml"),
|
|
29
|
-
|
|
87
|
+
companion: {
|
|
88
|
+
kind: "skills",
|
|
89
|
+
path: path.join(homeDir, ".codex", "skills"),
|
|
90
|
+
},
|
|
30
91
|
},
|
|
31
92
|
{
|
|
32
93
|
client: "claude",
|
|
33
|
-
configPath: path.join(homeDir, ".claude
|
|
34
|
-
|
|
94
|
+
configPath: path.join(homeDir, ".claude.json"),
|
|
95
|
+
companion: {
|
|
96
|
+
kind: "skills",
|
|
97
|
+
path: path.join(homeDir, ".claude", "skills"),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
client: "opencode",
|
|
102
|
+
configPath: path.join(homeDir, ".config", "opencode", "opencode.json"),
|
|
103
|
+
companion: {
|
|
104
|
+
kind: "instructions",
|
|
105
|
+
path: path.join(homeDir, ".config", "opencode", "AGENTS.md"),
|
|
106
|
+
},
|
|
35
107
|
},
|
|
36
108
|
];
|
|
37
109
|
}
|
|
@@ -60,29 +132,213 @@ function normalizeTrailingNewline(value) {
|
|
|
60
132
|
function escapeRegExp(value) {
|
|
61
133
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
62
134
|
}
|
|
63
|
-
function
|
|
135
|
+
function toTomlString(value) {
|
|
136
|
+
return JSON.stringify(value);
|
|
137
|
+
}
|
|
138
|
+
function buildTomlArray(values) {
|
|
139
|
+
return `[${values.map(toTomlString).join(", ")}]`;
|
|
140
|
+
}
|
|
141
|
+
function runtimeEnvMap(valueForName) {
|
|
142
|
+
return Object.fromEntries(SATORI_RUNTIME_ENV_VARS.map((name) => [name, valueForName(name)]));
|
|
143
|
+
}
|
|
144
|
+
function objectValue(value) {
|
|
145
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
function mergeRuntimeEnv(existing, defaults) {
|
|
151
|
+
return {
|
|
152
|
+
...defaults,
|
|
153
|
+
...(objectValue(existing) ?? {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function packageNameFromSpecifier(packageSpecifier) {
|
|
157
|
+
if (packageSpecifier.startsWith("@")) {
|
|
158
|
+
const versionMarker = packageSpecifier.indexOf("@", 1);
|
|
159
|
+
return versionMarker === -1 ? packageSpecifier : packageSpecifier.slice(0, versionMarker);
|
|
160
|
+
}
|
|
161
|
+
const versionMarker = packageSpecifier.indexOf("@");
|
|
162
|
+
return versionMarker === -1 ? packageSpecifier : packageSpecifier.slice(0, versionMarker);
|
|
163
|
+
}
|
|
164
|
+
function safeRuntimeDirName(packageSpecifier) {
|
|
165
|
+
return packageSpecifier.replace(/[^A-Za-z0-9._@-]+/g, "-");
|
|
166
|
+
}
|
|
167
|
+
function resolveRuntimeRoot(homeDir, packageSpecifier) {
|
|
168
|
+
return path.join(homeDir, ".satori", MANAGED_RUNTIME_DIR, safeRuntimeDirName(packageSpecifier));
|
|
169
|
+
}
|
|
170
|
+
function resolveRuntimePackageRoot(homeDir, packageSpecifier) {
|
|
171
|
+
return path.join(resolveRuntimeRoot(homeDir, packageSpecifier), "node_modules", ...packageNameFromSpecifier(packageSpecifier).split("/"));
|
|
172
|
+
}
|
|
173
|
+
function resolveRuntimeEntryPath(packageRoot, packageJson) {
|
|
174
|
+
const bin = packageJson?.bin;
|
|
175
|
+
let relativeEntry = "dist/index.js";
|
|
176
|
+
if (bin && typeof bin === "object" && !Array.isArray(bin) && typeof bin.satori === "string") {
|
|
177
|
+
relativeEntry = bin.satori;
|
|
178
|
+
}
|
|
179
|
+
else if (typeof bin === "string") {
|
|
180
|
+
relativeEntry = bin;
|
|
181
|
+
}
|
|
182
|
+
else if (typeof packageJson?.main === "string") {
|
|
183
|
+
relativeEntry = packageJson.main;
|
|
184
|
+
}
|
|
185
|
+
return path.resolve(packageRoot, relativeEntry);
|
|
186
|
+
}
|
|
187
|
+
function resolveLauncherPath(homeDir) {
|
|
188
|
+
return path.join(homeDir, ".satori", MANAGED_BIN_DIR, MANAGED_LAUNCHER_FILE);
|
|
189
|
+
}
|
|
190
|
+
function plannedManagedRuntimeCommand(homeDir, packageSpecifier) {
|
|
191
|
+
return {
|
|
192
|
+
command: process.execPath,
|
|
193
|
+
args: [resolveRuntimeEntryPath(resolveRuntimePackageRoot(homeDir, packageSpecifier))],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function managedClientCommand(homeDir) {
|
|
197
|
+
return {
|
|
198
|
+
command: process.execPath,
|
|
199
|
+
args: [resolveLauncherPath(homeDir)],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function buildLauncherScript(runtimeCommand) {
|
|
203
|
+
return [
|
|
204
|
+
"#!/usr/bin/env node",
|
|
205
|
+
"",
|
|
206
|
+
"const { spawn } = require(\"node:child_process\");",
|
|
207
|
+
"",
|
|
208
|
+
`const command = ${JSON.stringify(runtimeCommand.command)};`,
|
|
209
|
+
`const baseArgs = ${JSON.stringify(runtimeCommand.args)};`,
|
|
210
|
+
"const child = spawn(command, [...baseArgs, ...process.argv.slice(2)], {",
|
|
211
|
+
" stdio: \"inherit\",",
|
|
212
|
+
" env: process.env,",
|
|
213
|
+
"});",
|
|
214
|
+
"",
|
|
215
|
+
"child.on(\"error\", (error) => {",
|
|
216
|
+
" console.error(`Failed to start Satori MCP runtime: ${error.message}`);",
|
|
217
|
+
" process.exit(1);",
|
|
218
|
+
"});",
|
|
219
|
+
"",
|
|
220
|
+
"child.on(\"exit\", (code, signal) => {",
|
|
221
|
+
" if (signal) {",
|
|
222
|
+
" console.error(`Satori MCP runtime exited from signal ${signal}`);",
|
|
223
|
+
" process.exit(1);",
|
|
224
|
+
" }",
|
|
225
|
+
" process.exit(code ?? 0);",
|
|
226
|
+
"});",
|
|
227
|
+
"",
|
|
228
|
+
].join("\n");
|
|
229
|
+
}
|
|
230
|
+
function writeTextFileAtomic(filePath, content, mode) {
|
|
231
|
+
ensureParentDir(filePath);
|
|
232
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
233
|
+
fs.writeFileSync(tempPath, content, "utf8");
|
|
234
|
+
if (mode !== undefined) {
|
|
235
|
+
fs.chmodSync(tempPath, mode);
|
|
236
|
+
}
|
|
237
|
+
fs.renameSync(tempPath, filePath);
|
|
238
|
+
}
|
|
239
|
+
function prepareLauncherInstall(homeDir, runtimeCommand) {
|
|
240
|
+
const launcherPath = resolveLauncherPath(homeDir);
|
|
241
|
+
const current = readTextIfExists(launcherPath);
|
|
242
|
+
const next = buildLauncherScript(runtimeCommand);
|
|
243
|
+
return {
|
|
244
|
+
changed: current !== next,
|
|
245
|
+
apply: () => {
|
|
246
|
+
if (current === next) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
writeTextFileAtomic(launcherPath, next, 0o755);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function npmOutput(error) {
|
|
254
|
+
if (!(error instanceof Error)) {
|
|
255
|
+
return String(error);
|
|
256
|
+
}
|
|
257
|
+
const stdout = "stdout" in error && typeof error.stdout === "string"
|
|
258
|
+
? error.stdout
|
|
259
|
+
: "";
|
|
260
|
+
const stderr = "stderr" in error && typeof error.stderr === "string"
|
|
261
|
+
? error.stderr
|
|
262
|
+
: "";
|
|
263
|
+
return `${stdout}\n${stderr}\n${error.message}`.trim();
|
|
264
|
+
}
|
|
265
|
+
function installManagedRuntimeCommand(homeDir, packageSpecifier, execImpl) {
|
|
266
|
+
const runtimeRoot = resolveRuntimeRoot(homeDir, packageSpecifier);
|
|
267
|
+
ensureDir(runtimeRoot);
|
|
268
|
+
try {
|
|
269
|
+
execImpl("npm", [
|
|
270
|
+
"install",
|
|
271
|
+
"--prefix",
|
|
272
|
+
runtimeRoot,
|
|
273
|
+
"--omit=dev",
|
|
274
|
+
"--no-package-lock",
|
|
275
|
+
"--ignore-scripts",
|
|
276
|
+
"--no-audit",
|
|
277
|
+
"--no-fund",
|
|
278
|
+
packageSpecifier,
|
|
279
|
+
], {
|
|
280
|
+
encoding: "utf8",
|
|
281
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
throw new CliError("E_USAGE", `Failed to install Satori MCP runtime package ${packageSpecifier} into ${runtimeRoot}. ${npmOutput(error)}`, 2);
|
|
286
|
+
}
|
|
287
|
+
const packageRoot = resolveRuntimePackageRoot(homeDir, packageSpecifier);
|
|
288
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
289
|
+
let packageJson;
|
|
290
|
+
try {
|
|
291
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
throw new CliError("E_USAGE", `Installed Satori MCP runtime is missing package metadata at ${packageJsonPath}: ${error.message}`, 2);
|
|
295
|
+
}
|
|
296
|
+
const command = {
|
|
297
|
+
command: process.execPath,
|
|
298
|
+
args: [resolveRuntimeEntryPath(packageRoot, packageJson)],
|
|
299
|
+
};
|
|
300
|
+
if (!fs.existsSync(command.args[0])) {
|
|
301
|
+
throw new CliError("E_USAGE", `Installed Satori MCP runtime entry does not exist: ${command.args[0]}`, 2);
|
|
302
|
+
}
|
|
303
|
+
return command;
|
|
304
|
+
}
|
|
305
|
+
function buildCodexManagedBlock(runtimeCommand) {
|
|
64
306
|
return [
|
|
65
307
|
MANAGED_BLOCK_START,
|
|
66
308
|
"[mcp_servers.satori]",
|
|
67
|
-
|
|
68
|
-
`args =
|
|
69
|
-
|
|
309
|
+
`command = ${toTomlString(runtimeCommand.command)}`,
|
|
310
|
+
`args = ${buildTomlArray(runtimeCommand.args)}`,
|
|
311
|
+
"# Satori reads provider/vector settings from environment at MCP startup.",
|
|
312
|
+
"# env_vars forwards these names from Codex's parent environment when set.",
|
|
313
|
+
`env_vars = ${buildTomlArray([...SATORI_RUNTIME_ENV_VARS])}`,
|
|
70
314
|
MANAGED_BLOCK_END,
|
|
71
315
|
"",
|
|
72
316
|
].join("\n");
|
|
73
317
|
}
|
|
318
|
+
function buildCodexEnvTemplateBlock() {
|
|
319
|
+
return `${CODEX_ENV_TEMPLATE_LINES.join("\n")}\n`;
|
|
320
|
+
}
|
|
321
|
+
function codexHasSatoriEnvTable(content) {
|
|
322
|
+
return /^\s*\[mcp_servers\.satori\.env\]\s*$/m.test(content);
|
|
323
|
+
}
|
|
324
|
+
function ensureCodexEnvTemplate(content) {
|
|
325
|
+
if (content.includes(CODEX_ENV_TEMPLATE_START) || codexHasSatoriEnvTable(content)) {
|
|
326
|
+
return content;
|
|
327
|
+
}
|
|
328
|
+
return `${normalizeTrailingNewline(content)}\n${buildCodexEnvTemplateBlock()}`;
|
|
329
|
+
}
|
|
74
330
|
function codexHasUnmanagedSatoriSection(content) {
|
|
75
331
|
if (!content.includes("[mcp_servers.satori]")) {
|
|
76
332
|
return false;
|
|
77
333
|
}
|
|
78
334
|
return !(content.includes(MANAGED_BLOCK_START) && content.includes(MANAGED_BLOCK_END));
|
|
79
335
|
}
|
|
80
|
-
function prepareCodexInstall(filePath,
|
|
336
|
+
function prepareCodexInstall(filePath, runtimeCommand) {
|
|
81
337
|
const current = readTextIfExists(filePath) ?? "";
|
|
82
338
|
if (codexHasUnmanagedSatoriSection(current)) {
|
|
83
339
|
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);
|
|
84
340
|
}
|
|
85
|
-
const managedBlock = buildCodexManagedBlock(
|
|
341
|
+
const managedBlock = buildCodexManagedBlock(runtimeCommand);
|
|
86
342
|
let next = current;
|
|
87
343
|
if (current.includes(MANAGED_BLOCK_START) && current.includes(MANAGED_BLOCK_END)) {
|
|
88
344
|
next = current.replace(new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}\\n?`, "m"), managedBlock);
|
|
@@ -93,6 +349,7 @@ function prepareCodexInstall(filePath, packageSpecifier) {
|
|
|
93
349
|
else {
|
|
94
350
|
next = `${normalizeTrailingNewline(current)}\n${managedBlock}`;
|
|
95
351
|
}
|
|
352
|
+
next = ensureCodexEnvTemplate(next);
|
|
96
353
|
return {
|
|
97
354
|
changed: next !== current,
|
|
98
355
|
apply: () => {
|
|
@@ -146,45 +403,39 @@ function parseJsonObject(filePath) {
|
|
|
146
403
|
}
|
|
147
404
|
return parsed;
|
|
148
405
|
}
|
|
149
|
-
function buildClaudeServerConfig(
|
|
406
|
+
function buildClaudeServerConfig(runtimeCommand, existing) {
|
|
150
407
|
return {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
408
|
+
type: "stdio",
|
|
409
|
+
command: runtimeCommand.command,
|
|
410
|
+
args: runtimeCommand.args,
|
|
411
|
+
env: mergeRuntimeEnv(existing?.env, runtimeEnvMap((name) => `\${${name}:-}`)),
|
|
154
412
|
};
|
|
155
413
|
}
|
|
156
|
-
function
|
|
157
|
-
return typeof value === "string" &&
|
|
414
|
+
function isManagedLauncherPath(value) {
|
|
415
|
+
return typeof value === "string" && value.replace(/\\/g, "/").endsWith(`/.satori/${MANAGED_BIN_DIR}/${MANAGED_LAUNCHER_FILE}`);
|
|
416
|
+
}
|
|
417
|
+
function isManagedCommandParts(command, args) {
|
|
418
|
+
if (!Array.isArray(args)) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
const entryPath = args[0];
|
|
422
|
+
return typeof command === "string"
|
|
423
|
+
&& command.length > 0
|
|
424
|
+
&& args.length === 1
|
|
425
|
+
&& isManagedLauncherPath(entryPath);
|
|
158
426
|
}
|
|
159
427
|
function isManagedClaudeEntry(value) {
|
|
160
428
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
161
429
|
return false;
|
|
162
430
|
}
|
|
163
431
|
const entry = value;
|
|
164
|
-
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
if (entry.timeout !== MANAGED_TIMEOUT_MS) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
if (!Array.isArray(entry.args)) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
if (entry.args.length === 2) {
|
|
174
|
-
return entry.args[0] === "-y" && isManagedPackageSpecifier(entry.args[1]);
|
|
175
|
-
}
|
|
176
|
-
if (entry.args.length === 4) {
|
|
177
|
-
return entry.args[0] === "-y"
|
|
178
|
-
&& entry.args[1] === "--package"
|
|
179
|
-
&& isManagedPackageSpecifier(entry.args[2])
|
|
180
|
-
&& entry.args[3] === "satori";
|
|
181
|
-
}
|
|
182
|
-
return false;
|
|
432
|
+
return isManagedCommandParts(entry.command, entry.args);
|
|
183
433
|
}
|
|
184
|
-
function prepareClaudeInstall(filePath,
|
|
434
|
+
function prepareClaudeInstall(filePath, runtimeCommand) {
|
|
185
435
|
const currentObject = parseJsonObject(filePath);
|
|
186
436
|
const currentSerialized = JSON.stringify(currentObject);
|
|
187
|
-
const
|
|
437
|
+
const existingSatori = objectValue(currentObject.mcpServers?.satori);
|
|
438
|
+
const desiredServer = buildClaudeServerConfig(runtimeCommand, existingSatori);
|
|
188
439
|
const mcpServersValue = currentObject.mcpServers;
|
|
189
440
|
let mcpServers;
|
|
190
441
|
if (mcpServersValue === undefined) {
|
|
@@ -196,14 +447,14 @@ function prepareClaudeInstall(filePath, packageSpecifier) {
|
|
|
196
447
|
else {
|
|
197
448
|
throw new CliError("E_USAGE", `Expected mcpServers to be an object in ${filePath}.`, 2);
|
|
198
449
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
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);
|
|
450
|
+
if (mcpServers.satori !== undefined && !isManagedClaudeEntry(mcpServers.satori)) {
|
|
451
|
+
throw new CliError("E_USAGE", `Refusing to overwrite unmanaged Satori config in ${filePath}. Remove mcpServers.satori manually or align it to the managed Satori form first.`, 2);
|
|
202
452
|
}
|
|
203
453
|
mcpServers.satori = {
|
|
204
454
|
...existingSatori,
|
|
205
455
|
...desiredServer,
|
|
206
456
|
};
|
|
457
|
+
delete mcpServers.satori.timeout;
|
|
207
458
|
currentObject.mcpServers = mcpServers;
|
|
208
459
|
const next = `${JSON.stringify(currentObject, null, 2)}\n`;
|
|
209
460
|
return {
|
|
@@ -245,6 +496,88 @@ function prepareClaudeUninstall(filePath) {
|
|
|
245
496
|
},
|
|
246
497
|
};
|
|
247
498
|
}
|
|
499
|
+
function parseJsoncObject(filePath, content) {
|
|
500
|
+
const errors = [];
|
|
501
|
+
const parsed = parseJsonc(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
502
|
+
if (errors.length > 0) {
|
|
503
|
+
throw new CliError("E_USAGE", `Failed to parse JSONC config at ${filePath}.`, 2);
|
|
504
|
+
}
|
|
505
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
506
|
+
throw new CliError("E_USAGE", `Expected top-level JSON object in ${filePath}.`, 2);
|
|
507
|
+
}
|
|
508
|
+
return parsed;
|
|
509
|
+
}
|
|
510
|
+
function buildOpenCodeServerConfig(runtimeCommand, existing) {
|
|
511
|
+
return {
|
|
512
|
+
enabled: true,
|
|
513
|
+
type: "local",
|
|
514
|
+
command: [runtimeCommand.command, ...runtimeCommand.args],
|
|
515
|
+
environment: mergeRuntimeEnv(existing?.environment, runtimeEnvMap((name) => `{env:${name}}`)),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function isManagedOpenCodeEntry(value) {
|
|
519
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
const entry = value;
|
|
523
|
+
if (Array.isArray(entry.command)) {
|
|
524
|
+
const [command, ...args] = entry.command;
|
|
525
|
+
return isManagedCommandParts(command, args);
|
|
526
|
+
}
|
|
527
|
+
return isManagedCommandParts(entry.command, entry.args);
|
|
528
|
+
}
|
|
529
|
+
function mutateJsonc(filePath, current, pathSegments, value) {
|
|
530
|
+
const edits = modify(current, pathSegments, value, {
|
|
531
|
+
formattingOptions: {
|
|
532
|
+
insertSpaces: true,
|
|
533
|
+
tabSize: 2,
|
|
534
|
+
eol: "\n",
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
const next = applyEdits(current, edits);
|
|
538
|
+
return {
|
|
539
|
+
changed: next !== current,
|
|
540
|
+
apply: () => {
|
|
541
|
+
if (next === current) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
ensureParentDir(filePath);
|
|
545
|
+
fs.writeFileSync(filePath, next.endsWith("\n") ? next : `${next}\n`, "utf8");
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function prepareOpenCodeInstall(filePath, runtimeCommand) {
|
|
550
|
+
const current = readTextIfExists(filePath) ?? "{}\n";
|
|
551
|
+
const currentObject = parseJsoncObject(filePath, current);
|
|
552
|
+
const mcpValue = currentObject.mcp;
|
|
553
|
+
if (mcpValue !== undefined && (!mcpValue || typeof mcpValue !== "object" || Array.isArray(mcpValue))) {
|
|
554
|
+
throw new CliError("E_USAGE", `Expected mcp to be an object in ${filePath}.`, 2);
|
|
555
|
+
}
|
|
556
|
+
const existingSatori = mcpValue?.satori;
|
|
557
|
+
if (existingSatori !== undefined && !isManagedOpenCodeEntry(existingSatori)) {
|
|
558
|
+
throw new CliError("E_USAGE", `Refusing to overwrite unmanaged Satori config in ${filePath}. Remove mcp.satori manually or align it to the managed Satori form first.`, 2);
|
|
559
|
+
}
|
|
560
|
+
return mutateJsonc(filePath, current, ["mcp", "satori"], buildOpenCodeServerConfig(runtimeCommand, objectValue(existingSatori)));
|
|
561
|
+
}
|
|
562
|
+
function prepareOpenCodeUninstall(filePath) {
|
|
563
|
+
const current = readTextIfExists(filePath);
|
|
564
|
+
if (!current) {
|
|
565
|
+
return { changed: false, apply: () => { } };
|
|
566
|
+
}
|
|
567
|
+
const currentObject = parseJsoncObject(filePath, current);
|
|
568
|
+
const mcpValue = currentObject.mcp;
|
|
569
|
+
if (!mcpValue || typeof mcpValue !== "object" || Array.isArray(mcpValue)) {
|
|
570
|
+
return { changed: false, apply: () => { } };
|
|
571
|
+
}
|
|
572
|
+
const existingSatori = mcpValue.satori;
|
|
573
|
+
if (existingSatori === undefined) {
|
|
574
|
+
return { changed: false, apply: () => { } };
|
|
575
|
+
}
|
|
576
|
+
if (!isManagedOpenCodeEntry(existingSatori)) {
|
|
577
|
+
throw new CliError("E_USAGE", `Refusing to remove unmanaged Satori config in ${filePath}. Remove mcp.satori manually instead.`, 2);
|
|
578
|
+
}
|
|
579
|
+
return mutateJsonc(filePath, current, ["mcp", "satori"], undefined);
|
|
580
|
+
}
|
|
248
581
|
function prepareSkillInstall(skillsPath, skillAssetRoot) {
|
|
249
582
|
const writes = [];
|
|
250
583
|
let changed = false;
|
|
@@ -284,24 +617,92 @@ function prepareSkillRemoval(skillsPath) {
|
|
|
284
617
|
},
|
|
285
618
|
};
|
|
286
619
|
}
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
620
|
+
function buildManagedInstructionsBlock() {
|
|
621
|
+
return [
|
|
622
|
+
INSTRUCTIONS_BLOCK_START,
|
|
623
|
+
OPENCODE_INSTRUCTIONS.trim(),
|
|
624
|
+
INSTRUCTIONS_BLOCK_END,
|
|
625
|
+
"",
|
|
626
|
+
].join("\n");
|
|
627
|
+
}
|
|
628
|
+
function prepareInstructionsInstall(filePath) {
|
|
629
|
+
const current = readTextIfExists(filePath) ?? "";
|
|
630
|
+
const block = buildManagedInstructionsBlock();
|
|
631
|
+
let next = current;
|
|
632
|
+
if (current.includes(INSTRUCTIONS_BLOCK_START) && current.includes(INSTRUCTIONS_BLOCK_END)) {
|
|
633
|
+
next = current.replace(new RegExp(`${escapeRegExp(INSTRUCTIONS_BLOCK_START)}[\\s\\S]*?${escapeRegExp(INSTRUCTIONS_BLOCK_END)}\\n?`, "m"), block);
|
|
634
|
+
}
|
|
635
|
+
else if (current.trim().length === 0) {
|
|
636
|
+
next = block;
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
next = `${normalizeTrailingNewline(current)}\n${block}`;
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
changed: next !== current,
|
|
643
|
+
apply: () => {
|
|
644
|
+
if (next === current) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
ensureParentDir(filePath);
|
|
648
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function prepareInstructionsRemoval(filePath) {
|
|
653
|
+
const current = readTextIfExists(filePath);
|
|
654
|
+
if (!current || !current.includes(INSTRUCTIONS_BLOCK_START) || !current.includes(INSTRUCTIONS_BLOCK_END)) {
|
|
655
|
+
return { changed: false, apply: () => { } };
|
|
656
|
+
}
|
|
657
|
+
const next = current
|
|
658
|
+
.replace(new RegExp(`\\n?${escapeRegExp(INSTRUCTIONS_BLOCK_START)}[\\s\\S]*?${escapeRegExp(INSTRUCTIONS_BLOCK_END)}\\n?`, "m"), "\n")
|
|
659
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
660
|
+
.replace(/^\n+/, "");
|
|
661
|
+
return {
|
|
662
|
+
changed: next !== current,
|
|
663
|
+
apply: () => {
|
|
664
|
+
if (next === current) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function prepareCompanionMutation(companion, command, skillAssetRoot) {
|
|
672
|
+
if (companion.kind === "skills") {
|
|
673
|
+
return command.kind === "install"
|
|
674
|
+
? prepareSkillInstall(companion.path, skillAssetRoot)
|
|
675
|
+
: prepareSkillRemoval(companion.path);
|
|
676
|
+
}
|
|
677
|
+
return command.kind === "install"
|
|
678
|
+
? prepareInstructionsInstall(companion.path)
|
|
679
|
+
: prepareInstructionsRemoval(companion.path);
|
|
680
|
+
}
|
|
681
|
+
function prepareConfigMutation(target, command, runtimeCommand) {
|
|
682
|
+
if (target.client === "codex") {
|
|
683
|
+
return command.kind === "install"
|
|
684
|
+
? prepareCodexInstall(target.configPath, runtimeCommand)
|
|
685
|
+
: prepareCodexUninstall(target.configPath);
|
|
686
|
+
}
|
|
687
|
+
if (target.client === "claude") {
|
|
688
|
+
return command.kind === "install"
|
|
689
|
+
? prepareClaudeInstall(target.configPath, runtimeCommand)
|
|
294
690
|
: prepareClaudeUninstall(target.configPath);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
691
|
+
}
|
|
692
|
+
return command.kind === "install"
|
|
693
|
+
? prepareOpenCodeInstall(target.configPath, runtimeCommand)
|
|
694
|
+
: prepareOpenCodeUninstall(target.configPath);
|
|
695
|
+
}
|
|
696
|
+
function prepareMutation(target, command, runtimeCommand, skillAssetRoot) {
|
|
697
|
+
const configMutation = prepareConfigMutation(target, command, runtimeCommand);
|
|
698
|
+
const companionMutation = prepareCompanionMutation(target.companion, command, skillAssetRoot);
|
|
298
699
|
return {
|
|
299
700
|
target,
|
|
300
701
|
configChanged: configMutation.changed,
|
|
301
|
-
|
|
702
|
+
companionChanged: companionMutation.changed,
|
|
302
703
|
apply: () => {
|
|
303
704
|
configMutation.apply();
|
|
304
|
-
|
|
705
|
+
companionMutation.apply();
|
|
305
706
|
},
|
|
306
707
|
};
|
|
307
708
|
}
|
|
@@ -309,8 +710,20 @@ export function executeInstallCommand(command, options = {}) {
|
|
|
309
710
|
const homeDir = options.homeDir ?? os.homedir();
|
|
310
711
|
const packageSpecifier = options.packageSpecifier ?? resolveDefaultPackageSpecifier();
|
|
311
712
|
const skillAssetRoot = options.skillAssetRoot ?? resolveDefaultSkillAssetRoot();
|
|
312
|
-
const
|
|
713
|
+
const plannedRuntimeCommand = options.runtimeCommand ?? plannedManagedRuntimeCommand(homeDir, packageSpecifier);
|
|
714
|
+
const clientCommand = managedClientCommand(homeDir);
|
|
715
|
+
let launcherMutation = command.kind === "install"
|
|
716
|
+
? prepareLauncherInstall(homeDir, plannedRuntimeCommand)
|
|
717
|
+
: { changed: false, apply: () => { } };
|
|
718
|
+
const prepared = selectTargets(homeDir, command.client).map((target) => (prepareMutation(target, command, clientCommand, skillAssetRoot)));
|
|
313
719
|
if (!command.dryRun) {
|
|
720
|
+
if (command.kind === "install" && !options.runtimeCommand) {
|
|
721
|
+
const installedRuntimeCommand = installManagedRuntimeCommand(homeDir, packageSpecifier, options.execFileSyncImpl ?? execFileSync);
|
|
722
|
+
launcherMutation = prepareLauncherInstall(homeDir, installedRuntimeCommand);
|
|
723
|
+
}
|
|
724
|
+
if (command.kind === "install") {
|
|
725
|
+
launcherMutation.apply();
|
|
726
|
+
}
|
|
314
727
|
for (const mutation of prepared) {
|
|
315
728
|
mutation.apply();
|
|
316
729
|
}
|
|
@@ -322,10 +735,12 @@ export function executeInstallCommand(command, options = {}) {
|
|
|
322
735
|
results: prepared.map((mutation) => ({
|
|
323
736
|
client: mutation.target.client,
|
|
324
737
|
configPath: mutation.target.configPath,
|
|
325
|
-
skillsPath: mutation.target.
|
|
738
|
+
skillsPath: mutation.target.companion.kind === "skills" ? mutation.target.companion.path : undefined,
|
|
739
|
+
instructionsPath: mutation.target.companion.kind === "instructions" ? mutation.target.companion.path : undefined,
|
|
326
740
|
configChanged: mutation.configChanged,
|
|
327
|
-
skillsChanged: mutation.
|
|
328
|
-
|
|
741
|
+
skillsChanged: mutation.target.companion.kind === "skills" ? mutation.companionChanged : false,
|
|
742
|
+
instructionsChanged: mutation.target.companion.kind === "instructions" ? mutation.companionChanged : false,
|
|
743
|
+
status: mutation.configChanged || mutation.companionChanged || launcherMutation.changed ? "updated" : "unchanged",
|
|
329
744
|
dryRun: command.dryRun,
|
|
330
745
|
})),
|
|
331
746
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zokizuan/satori-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Shell CLI for Satori MCP installation and skill-based workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"satori-cli": "dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
13
|
-
"
|
|
12
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
13
|
+
"jsonc-parser": "^3.3.1",
|
|
14
|
+
"@zokizuan/satori-mcp": "4.11.1"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"@types/node": "^20.0.0",
|
|
17
|
-
"tsx": "^4.
|
|
18
|
+
"tsx": "^4.22.4",
|
|
18
19
|
"typescript": "^5.0.0"
|
|
19
20
|
},
|
|
20
21
|
"files": [
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: satori-indexing
|
|
3
|
-
description: Use when Satori codebases are not indexed, stale, blocked, still indexing, or need lifecycle 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
|
-
- Use `status` and `list_codebases` freely; provider credentials are only needed for provider-backed lifecycle actions.
|
|
31
|
-
|
|
32
|
-
## Status Handling
|
|
33
|
-
|
|
34
|
-
- `requires_reindex`: run `manage_index(action="reindex")`.
|
|
35
|
-
- `not_ready` with indexing reason: check status and wait for terminal completion.
|
|
36
|
-
- `not_indexed`: create the index.
|
|
37
|
-
- `MISSING_PROVIDER_CONFIG`: run `satori-cli doctor` when available, then set missing provider or Milvus env before retrying create/reindex/sync/search.
|
|
38
|
-
- Ignore-rule noise mitigation: update `.satoriignore`, wait debounce, and run `sync` for immediate convergence.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: satori-navigation
|
|
3
|
-
description: Use when search has returned candidate code and exact spans, symbol reads, or call relationships are needed.
|
|
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
|
-
For lifecycle remediation (`requires_reindex`, `not_indexed`, indexing waits), switch to `satori-indexing`.
|
|
18
|
-
|
|
19
|
-
## Workflow
|
|
20
|
-
|
|
21
|
-
1. Use grouped `search_codebase` results as the starting point.
|
|
22
|
-
2. Use `file_outline(resolveMode="exact", symbolIdExact|symbolLabelExact)` to lock the symbol span when exact identity is available.
|
|
23
|
-
3. If `callGraphHint.supported=true`, call `call_graph(path=..., symbolRef=..., direction="both", depth=1)`.
|
|
24
|
-
4. If `callGraphHint.supported=false`, execute `navigationFallback.readSpan.args` exactly.
|
|
25
|
-
5. Use `read_file(path=..., open_symbol=...)` or deterministic line spans for the final read.
|
|
26
|
-
|
|
27
|
-
## Rules
|
|
28
|
-
|
|
29
|
-
- Treat `navigationFallback` as authoritative. Do not invent spans.
|
|
30
|
-
- `open_symbol` must resolve deterministically. Do not guess on ambiguity.
|
|
31
|
-
- `read_file(mode="annotated")` is preferred when outline metadata is useful.
|
|
32
|
-
- Follow continuation hints when plain reads are truncated.
|
|
33
|
-
- Other tools do not run search freshness; remediate stale index state before trusting navigation.
|
|
34
|
-
|
|
35
|
-
## Remediation
|
|
36
|
-
|
|
37
|
-
- `requires_reindex`: switch to `satori-indexing` and reindex before retrying navigation.
|
|
38
|
-
- `not_ready` or `not_indexed`: switch to `satori-indexing`; wait or create before retrying navigation.
|
|
39
|
-
- `unsupported`: fall back to deterministic `read_file` spans when supplied by `navigationFallback`.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: satori-search
|
|
3
|
-
description: Use when finding code by behavior, concept, or symbol before opening files or falling back to 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=...)` when index state is unknown.
|
|
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 the user-requested path 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
|
-
- Pass the user's requested path; if Satori resolves an indexed parent, follow returned `navigationFallback` exactly.
|
|
30
|
-
- Treat warnings as usable-but-degraded results, not fatal errors.
|
|
31
|
-
- Use `debug=true` only when ranking or filter explanations are required.
|
|
32
|
-
|
|
33
|
-
## Remediation
|
|
34
|
-
|
|
35
|
-
- `requires_reindex`: run `manage_index(action="reindex")`, not `sync`.
|
|
36
|
-
- `not_ready` with indexing reason: wait or check `manage_index(action="status")`.
|
|
37
|
-
- `not_indexed`: run `manage_index(action="create")` on the repository root or requested indexed root.
|
|
38
|
-
- `MISSING_PROVIDER_CONFIG` is active only when it appears as the tool response `code` or `reason`. If it appears inside `search_codebase` results, it may just be matched code content.
|
|
39
|
-
- Noise mitigation hint: update `.satoriignore`, wait debounce, rerun search, and use `manage_index(action="sync")` only for immediate convergence.
|