@zokizuan/satori-cli 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -31
- package/assets/skills/satori/SKILL.md +61 -0
- package/dist/args.d.ts +1 -1
- package/dist/args.js +3 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -3
- package/dist/install.d.ts +11 -1
- package/dist/install.js +400 -54
- 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/README.md
CHANGED
|
@@ -1,54 +1,63 @@
|
|
|
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
|
-
##
|
|
6
|
-
|
|
7
|
-
- installs and removes Satori MCP client config for supported clients
|
|
8
|
-
- copies packaged first-party skills:
|
|
9
|
-
- `satori-search`
|
|
10
|
-
- `satori-navigation`
|
|
11
|
-
- `satori-indexing`
|
|
12
|
-
- starts a local stdio session against `@zokizuan/satori-mcp` for direct shell workflows
|
|
13
|
-
|
|
14
|
-
## Install / Uninstall
|
|
5
|
+
## Quick Start
|
|
15
6
|
|
|
16
7
|
```bash
|
|
17
|
-
npx -y @zokizuan/satori-cli@0.
|
|
18
|
-
npx -y @zokizuan/satori-cli@0.
|
|
19
|
-
npx -y @zokizuan/satori-cli@0.3.1 install --client all --dry-run
|
|
20
|
-
npx -y @zokizuan/satori-cli@0.3.1 uninstall --client codex
|
|
21
|
-
npx -y @zokizuan/satori-cli@0.3.1 doctor
|
|
8
|
+
npx -y @zokizuan/satori-cli@0.4.0 install --client all
|
|
9
|
+
npx -y @zokizuan/satori-cli@0.4.0 doctor
|
|
22
10
|
```
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
Supported clients are `codex`, `claude`, `opencode`, and `all`.
|
|
25
13
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
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
|
+
The installer only manages Satori-owned config and the first-party workflow skill:
|
|
17
|
+
|
|
18
|
+
- `satori`
|
|
32
19
|
|
|
33
20
|
## Commands
|
|
34
21
|
|
|
35
22
|
```bash
|
|
36
|
-
satori-cli
|
|
23
|
+
npx -y @zokizuan/satori-cli@0.4.0 install --client codex
|
|
24
|
+
npx -y @zokizuan/satori-cli@0.4.0 install --client claude
|
|
25
|
+
npx -y @zokizuan/satori-cli@0.4.0 install --client opencode
|
|
26
|
+
npx -y @zokizuan/satori-cli@0.4.0 install --client all --dry-run
|
|
27
|
+
npx -y @zokizuan/satori-cli@0.4.0 uninstall --client codex
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`doctor` checks Node, package visibility, provider env, and Milvus env without starting an MCP client.
|
|
31
|
+
|
|
32
|
+
## Direct Tool Calls
|
|
33
|
+
|
|
34
|
+
```bash
|
|
37
35
|
satori-cli tools list
|
|
38
|
-
satori-cli tool call
|
|
39
|
-
satori-cli tool call
|
|
40
|
-
satori-cli tool call
|
|
41
|
-
satori-cli
|
|
36
|
+
satori-cli tool call search_codebase --args-json '{"path":"/abs/repo","query":"auth flow"}'
|
|
37
|
+
satori-cli tool call search_codebase --args-file ./args.json
|
|
38
|
+
satori-cli tool call search_codebase --args-json @-
|
|
39
|
+
satori-cli search_codebase --path /abs/repo --query "auth flow"
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
Global flags
|
|
42
|
+
Global flags such as `--startup-timeout-ms`, `--call-timeout-ms`, `--format`, and `--debug` must appear before the command token.
|
|
43
|
+
|
|
44
|
+
## Runtime Requirements
|
|
45
|
+
|
|
46
|
+
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.
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
Common local setup:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
EMBEDDING_PROVIDER=Ollama
|
|
52
|
+
EMBEDDING_MODEL=nomic-embed-text
|
|
53
|
+
OLLAMA_HOST=http://127.0.0.1:11434
|
|
54
|
+
MILVUS_ADDRESS=localhost:19530
|
|
55
|
+
```
|
|
47
56
|
|
|
48
57
|
## Development
|
|
49
58
|
|
|
50
59
|
```bash
|
|
51
60
|
pnpm --filter @zokizuan/satori-cli build
|
|
52
61
|
pnpm --filter @zokizuan/satori-cli test
|
|
53
|
-
pnpm
|
|
62
|
+
pnpm run release:smoke:cli
|
|
54
63
|
```
|
|
@@ -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/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,36 @@
|
|
|
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 INSTRUCTIONS_BLOCK_START = "<!-- satori-mcp:start -->";
|
|
12
|
+
const INSTRUCTIONS_BLOCK_END = "<!-- satori-mcp:end -->";
|
|
13
|
+
const OWNED_SKILL_DIRS = ["satori"];
|
|
14
|
+
const MANAGED_RUNTIME_DIR = "mcp-runtime";
|
|
15
|
+
const MANAGED_BIN_DIR = "bin";
|
|
16
|
+
const MANAGED_LAUNCHER_FILE = "satori-mcp.js";
|
|
17
|
+
const OPENCODE_INSTRUCTIONS = `# Satori MCP
|
|
18
|
+
|
|
19
|
+
This project uses Satori MCP for semantic code search, deterministic navigation, and index lifecycle management.
|
|
20
|
+
|
|
21
|
+
## Priority Order
|
|
22
|
+
1. \`search_codebase\` - find candidate code by behavior, concept, or symbol
|
|
23
|
+
2. \`file_outline\` - lock exact symbol spans before reading or editing
|
|
24
|
+
3. \`call_graph\` - inspect callers and callees when supported
|
|
25
|
+
4. \`read_file\` - open exact spans or fallback windows
|
|
26
|
+
5. \`manage_index\` - create, sync, reindex, or inspect index status
|
|
27
|
+
|
|
28
|
+
## Rules
|
|
29
|
+
- Prefer Satori tools before grep/glob for code discovery.
|
|
30
|
+
- If a tool returns \`requires_reindex\`, run \`manage_index(action="reindex")\` and retry the original call.
|
|
31
|
+
- Treat \`navigationFallback\` as authoritative when call graph is unavailable.
|
|
32
|
+
- Read the relevant implementation and call sites before editing behavior.
|
|
33
|
+
`;
|
|
11
34
|
function resolveDefaultSkillAssetRoot() {
|
|
12
35
|
const currentFile = fileURLToPath(import.meta.url);
|
|
13
36
|
return path.resolve(path.dirname(currentFile), "..", "assets", "skills");
|
|
@@ -26,12 +49,26 @@ function resolveClientTargets(homeDir) {
|
|
|
26
49
|
{
|
|
27
50
|
client: "codex",
|
|
28
51
|
configPath: path.join(homeDir, ".codex", "config.toml"),
|
|
29
|
-
|
|
52
|
+
companion: {
|
|
53
|
+
kind: "skills",
|
|
54
|
+
path: path.join(homeDir, ".codex", "skills"),
|
|
55
|
+
},
|
|
30
56
|
},
|
|
31
57
|
{
|
|
32
58
|
client: "claude",
|
|
33
59
|
configPath: path.join(homeDir, ".claude", "settings.json"),
|
|
34
|
-
|
|
60
|
+
companion: {
|
|
61
|
+
kind: "skills",
|
|
62
|
+
path: path.join(homeDir, ".claude", "skills"),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
client: "opencode",
|
|
67
|
+
configPath: path.join(homeDir, ".config", "opencode", "opencode.json"),
|
|
68
|
+
companion: {
|
|
69
|
+
kind: "instructions",
|
|
70
|
+
path: path.join(homeDir, ".config", "opencode", "AGENTS.md"),
|
|
71
|
+
},
|
|
35
72
|
},
|
|
36
73
|
];
|
|
37
74
|
}
|
|
@@ -60,13 +97,167 @@ function normalizeTrailingNewline(value) {
|
|
|
60
97
|
function escapeRegExp(value) {
|
|
61
98
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
62
99
|
}
|
|
63
|
-
function
|
|
100
|
+
function toTomlString(value) {
|
|
101
|
+
return JSON.stringify(value);
|
|
102
|
+
}
|
|
103
|
+
function buildTomlArray(values) {
|
|
104
|
+
return `[${values.map(toTomlString).join(", ")}]`;
|
|
105
|
+
}
|
|
106
|
+
function packageNameFromSpecifier(packageSpecifier) {
|
|
107
|
+
if (packageSpecifier.startsWith("@")) {
|
|
108
|
+
const versionMarker = packageSpecifier.indexOf("@", 1);
|
|
109
|
+
return versionMarker === -1 ? packageSpecifier : packageSpecifier.slice(0, versionMarker);
|
|
110
|
+
}
|
|
111
|
+
const versionMarker = packageSpecifier.indexOf("@");
|
|
112
|
+
return versionMarker === -1 ? packageSpecifier : packageSpecifier.slice(0, versionMarker);
|
|
113
|
+
}
|
|
114
|
+
function safeRuntimeDirName(packageSpecifier) {
|
|
115
|
+
return packageSpecifier.replace(/[^A-Za-z0-9._@-]+/g, "-");
|
|
116
|
+
}
|
|
117
|
+
function resolveRuntimeRoot(homeDir, packageSpecifier) {
|
|
118
|
+
return path.join(homeDir, ".satori", MANAGED_RUNTIME_DIR, safeRuntimeDirName(packageSpecifier));
|
|
119
|
+
}
|
|
120
|
+
function resolveRuntimePackageRoot(homeDir, packageSpecifier) {
|
|
121
|
+
return path.join(resolveRuntimeRoot(homeDir, packageSpecifier), "node_modules", ...packageNameFromSpecifier(packageSpecifier).split("/"));
|
|
122
|
+
}
|
|
123
|
+
function resolveRuntimeEntryPath(packageRoot, packageJson) {
|
|
124
|
+
const bin = packageJson?.bin;
|
|
125
|
+
let relativeEntry = "dist/index.js";
|
|
126
|
+
if (bin && typeof bin === "object" && !Array.isArray(bin) && typeof bin.satori === "string") {
|
|
127
|
+
relativeEntry = bin.satori;
|
|
128
|
+
}
|
|
129
|
+
else if (typeof bin === "string") {
|
|
130
|
+
relativeEntry = bin;
|
|
131
|
+
}
|
|
132
|
+
else if (typeof packageJson?.main === "string") {
|
|
133
|
+
relativeEntry = packageJson.main;
|
|
134
|
+
}
|
|
135
|
+
return path.resolve(packageRoot, relativeEntry);
|
|
136
|
+
}
|
|
137
|
+
function resolveLauncherPath(homeDir) {
|
|
138
|
+
return path.join(homeDir, ".satori", MANAGED_BIN_DIR, MANAGED_LAUNCHER_FILE);
|
|
139
|
+
}
|
|
140
|
+
function plannedManagedRuntimeCommand(homeDir, packageSpecifier) {
|
|
141
|
+
return {
|
|
142
|
+
command: process.execPath,
|
|
143
|
+
args: [resolveRuntimeEntryPath(resolveRuntimePackageRoot(homeDir, packageSpecifier))],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function managedClientCommand(homeDir) {
|
|
147
|
+
return {
|
|
148
|
+
command: process.execPath,
|
|
149
|
+
args: [resolveLauncherPath(homeDir)],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function buildLauncherScript(runtimeCommand) {
|
|
153
|
+
return [
|
|
154
|
+
"#!/usr/bin/env node",
|
|
155
|
+
"",
|
|
156
|
+
"const { spawn } = require(\"node:child_process\");",
|
|
157
|
+
"",
|
|
158
|
+
`const command = ${JSON.stringify(runtimeCommand.command)};`,
|
|
159
|
+
`const baseArgs = ${JSON.stringify(runtimeCommand.args)};`,
|
|
160
|
+
"const child = spawn(command, [...baseArgs, ...process.argv.slice(2)], {",
|
|
161
|
+
" stdio: \"inherit\",",
|
|
162
|
+
" env: process.env,",
|
|
163
|
+
"});",
|
|
164
|
+
"",
|
|
165
|
+
"child.on(\"error\", (error) => {",
|
|
166
|
+
" console.error(`Failed to start Satori MCP runtime: ${error.message}`);",
|
|
167
|
+
" process.exit(1);",
|
|
168
|
+
"});",
|
|
169
|
+
"",
|
|
170
|
+
"child.on(\"exit\", (code, signal) => {",
|
|
171
|
+
" if (signal) {",
|
|
172
|
+
" console.error(`Satori MCP runtime exited from signal ${signal}`);",
|
|
173
|
+
" process.exit(1);",
|
|
174
|
+
" }",
|
|
175
|
+
" process.exit(code ?? 0);",
|
|
176
|
+
"});",
|
|
177
|
+
"",
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
180
|
+
function writeTextFileAtomic(filePath, content, mode) {
|
|
181
|
+
ensureParentDir(filePath);
|
|
182
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
183
|
+
fs.writeFileSync(tempPath, content, "utf8");
|
|
184
|
+
if (mode !== undefined) {
|
|
185
|
+
fs.chmodSync(tempPath, mode);
|
|
186
|
+
}
|
|
187
|
+
fs.renameSync(tempPath, filePath);
|
|
188
|
+
}
|
|
189
|
+
function prepareLauncherInstall(homeDir, runtimeCommand) {
|
|
190
|
+
const launcherPath = resolveLauncherPath(homeDir);
|
|
191
|
+
const current = readTextIfExists(launcherPath);
|
|
192
|
+
const next = buildLauncherScript(runtimeCommand);
|
|
193
|
+
return {
|
|
194
|
+
changed: current !== next,
|
|
195
|
+
apply: () => {
|
|
196
|
+
if (current === next) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
writeTextFileAtomic(launcherPath, next, 0o755);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function npmOutput(error) {
|
|
204
|
+
if (!(error instanceof Error)) {
|
|
205
|
+
return String(error);
|
|
206
|
+
}
|
|
207
|
+
const stdout = "stdout" in error && typeof error.stdout === "string"
|
|
208
|
+
? error.stdout
|
|
209
|
+
: "";
|
|
210
|
+
const stderr = "stderr" in error && typeof error.stderr === "string"
|
|
211
|
+
? error.stderr
|
|
212
|
+
: "";
|
|
213
|
+
return `${stdout}\n${stderr}\n${error.message}`.trim();
|
|
214
|
+
}
|
|
215
|
+
function installManagedRuntimeCommand(homeDir, packageSpecifier, execImpl) {
|
|
216
|
+
const runtimeRoot = resolveRuntimeRoot(homeDir, packageSpecifier);
|
|
217
|
+
ensureDir(runtimeRoot);
|
|
218
|
+
try {
|
|
219
|
+
execImpl("npm", [
|
|
220
|
+
"install",
|
|
221
|
+
"--prefix",
|
|
222
|
+
runtimeRoot,
|
|
223
|
+
"--omit=dev",
|
|
224
|
+
"--no-package-lock",
|
|
225
|
+
"--ignore-scripts",
|
|
226
|
+
"--no-audit",
|
|
227
|
+
"--no-fund",
|
|
228
|
+
packageSpecifier,
|
|
229
|
+
], {
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
throw new CliError("E_USAGE", `Failed to install Satori MCP runtime package ${packageSpecifier} into ${runtimeRoot}. ${npmOutput(error)}`, 2);
|
|
236
|
+
}
|
|
237
|
+
const packageRoot = resolveRuntimePackageRoot(homeDir, packageSpecifier);
|
|
238
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
239
|
+
let packageJson;
|
|
240
|
+
try {
|
|
241
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
throw new CliError("E_USAGE", `Installed Satori MCP runtime is missing package metadata at ${packageJsonPath}: ${error.message}`, 2);
|
|
245
|
+
}
|
|
246
|
+
const command = {
|
|
247
|
+
command: process.execPath,
|
|
248
|
+
args: [resolveRuntimeEntryPath(packageRoot, packageJson)],
|
|
249
|
+
};
|
|
250
|
+
if (!fs.existsSync(command.args[0])) {
|
|
251
|
+
throw new CliError("E_USAGE", `Installed Satori MCP runtime entry does not exist: ${command.args[0]}`, 2);
|
|
252
|
+
}
|
|
253
|
+
return command;
|
|
254
|
+
}
|
|
255
|
+
function buildCodexManagedBlock(runtimeCommand) {
|
|
64
256
|
return [
|
|
65
257
|
MANAGED_BLOCK_START,
|
|
66
258
|
"[mcp_servers.satori]",
|
|
67
|
-
|
|
68
|
-
`args =
|
|
69
|
-
`startup_timeout_ms = ${MANAGED_TIMEOUT_MS}`,
|
|
259
|
+
`command = ${toTomlString(runtimeCommand.command)}`,
|
|
260
|
+
`args = ${buildTomlArray(runtimeCommand.args)}`,
|
|
70
261
|
MANAGED_BLOCK_END,
|
|
71
262
|
"",
|
|
72
263
|
].join("\n");
|
|
@@ -77,12 +268,12 @@ function codexHasUnmanagedSatoriSection(content) {
|
|
|
77
268
|
}
|
|
78
269
|
return !(content.includes(MANAGED_BLOCK_START) && content.includes(MANAGED_BLOCK_END));
|
|
79
270
|
}
|
|
80
|
-
function prepareCodexInstall(filePath,
|
|
271
|
+
function prepareCodexInstall(filePath, runtimeCommand) {
|
|
81
272
|
const current = readTextIfExists(filePath) ?? "";
|
|
82
273
|
if (codexHasUnmanagedSatoriSection(current)) {
|
|
83
274
|
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
275
|
}
|
|
85
|
-
const managedBlock = buildCodexManagedBlock(
|
|
276
|
+
const managedBlock = buildCodexManagedBlock(runtimeCommand);
|
|
86
277
|
let next = current;
|
|
87
278
|
if (current.includes(MANAGED_BLOCK_START) && current.includes(MANAGED_BLOCK_END)) {
|
|
88
279
|
next = current.replace(new RegExp(`${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}\\n?`, "m"), managedBlock);
|
|
@@ -146,45 +337,36 @@ function parseJsonObject(filePath) {
|
|
|
146
337
|
}
|
|
147
338
|
return parsed;
|
|
148
339
|
}
|
|
149
|
-
function buildClaudeServerConfig(
|
|
340
|
+
function buildClaudeServerConfig(runtimeCommand) {
|
|
150
341
|
return {
|
|
151
|
-
command:
|
|
152
|
-
args:
|
|
153
|
-
timeout: MANAGED_TIMEOUT_MS,
|
|
342
|
+
command: runtimeCommand.command,
|
|
343
|
+
args: runtimeCommand.args,
|
|
154
344
|
};
|
|
155
345
|
}
|
|
156
|
-
function
|
|
157
|
-
return typeof value === "string" &&
|
|
346
|
+
function isManagedLauncherPath(value) {
|
|
347
|
+
return typeof value === "string" && value.replace(/\\/g, "/").endsWith(`/.satori/${MANAGED_BIN_DIR}/${MANAGED_LAUNCHER_FILE}`);
|
|
348
|
+
}
|
|
349
|
+
function isManagedCommandParts(command, args) {
|
|
350
|
+
if (!Array.isArray(args)) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const entryPath = args[0];
|
|
354
|
+
return typeof command === "string"
|
|
355
|
+
&& command.length > 0
|
|
356
|
+
&& args.length === 1
|
|
357
|
+
&& isManagedLauncherPath(entryPath);
|
|
158
358
|
}
|
|
159
359
|
function isManagedClaudeEntry(value) {
|
|
160
360
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
161
361
|
return false;
|
|
162
362
|
}
|
|
163
363
|
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;
|
|
364
|
+
return isManagedCommandParts(entry.command, entry.args);
|
|
183
365
|
}
|
|
184
|
-
function prepareClaudeInstall(filePath,
|
|
366
|
+
function prepareClaudeInstall(filePath, runtimeCommand) {
|
|
185
367
|
const currentObject = parseJsonObject(filePath);
|
|
186
368
|
const currentSerialized = JSON.stringify(currentObject);
|
|
187
|
-
const desiredServer = buildClaudeServerConfig(
|
|
369
|
+
const desiredServer = buildClaudeServerConfig(runtimeCommand);
|
|
188
370
|
const mcpServersValue = currentObject.mcpServers;
|
|
189
371
|
let mcpServers;
|
|
190
372
|
if (mcpServersValue === undefined) {
|
|
@@ -198,12 +380,13 @@ function prepareClaudeInstall(filePath, packageSpecifier) {
|
|
|
198
380
|
}
|
|
199
381
|
const existingSatori = mcpServers.satori;
|
|
200
382
|
if (existingSatori !== undefined && !isManagedClaudeEntry(existingSatori)) {
|
|
201
|
-
throw new CliError("E_USAGE", `Refusing to overwrite unmanaged Satori config in ${filePath}. Remove mcpServers.satori manually or align it to the managed
|
|
383
|
+
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
384
|
}
|
|
203
385
|
mcpServers.satori = {
|
|
204
386
|
...existingSatori,
|
|
205
387
|
...desiredServer,
|
|
206
388
|
};
|
|
389
|
+
delete mcpServers.satori.timeout;
|
|
207
390
|
currentObject.mcpServers = mcpServers;
|
|
208
391
|
const next = `${JSON.stringify(currentObject, null, 2)}\n`;
|
|
209
392
|
return {
|
|
@@ -245,6 +428,87 @@ function prepareClaudeUninstall(filePath) {
|
|
|
245
428
|
},
|
|
246
429
|
};
|
|
247
430
|
}
|
|
431
|
+
function parseJsoncObject(filePath, content) {
|
|
432
|
+
const errors = [];
|
|
433
|
+
const parsed = parseJsonc(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
434
|
+
if (errors.length > 0) {
|
|
435
|
+
throw new CliError("E_USAGE", `Failed to parse JSONC config at ${filePath}.`, 2);
|
|
436
|
+
}
|
|
437
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
438
|
+
throw new CliError("E_USAGE", `Expected top-level JSON object in ${filePath}.`, 2);
|
|
439
|
+
}
|
|
440
|
+
return parsed;
|
|
441
|
+
}
|
|
442
|
+
function buildOpenCodeServerConfig(runtimeCommand) {
|
|
443
|
+
return {
|
|
444
|
+
enabled: true,
|
|
445
|
+
type: "local",
|
|
446
|
+
command: [runtimeCommand.command, ...runtimeCommand.args],
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function isManagedOpenCodeEntry(value) {
|
|
450
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const entry = value;
|
|
454
|
+
if (Array.isArray(entry.command)) {
|
|
455
|
+
const [command, ...args] = entry.command;
|
|
456
|
+
return isManagedCommandParts(command, args);
|
|
457
|
+
}
|
|
458
|
+
return isManagedCommandParts(entry.command, entry.args);
|
|
459
|
+
}
|
|
460
|
+
function mutateJsonc(filePath, current, pathSegments, value) {
|
|
461
|
+
const edits = modify(current, pathSegments, value, {
|
|
462
|
+
formattingOptions: {
|
|
463
|
+
insertSpaces: true,
|
|
464
|
+
tabSize: 2,
|
|
465
|
+
eol: "\n",
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
const next = applyEdits(current, edits);
|
|
469
|
+
return {
|
|
470
|
+
changed: next !== current,
|
|
471
|
+
apply: () => {
|
|
472
|
+
if (next === current) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
ensureParentDir(filePath);
|
|
476
|
+
fs.writeFileSync(filePath, next.endsWith("\n") ? next : `${next}\n`, "utf8");
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function prepareOpenCodeInstall(filePath, runtimeCommand) {
|
|
481
|
+
const current = readTextIfExists(filePath) ?? "{}\n";
|
|
482
|
+
const currentObject = parseJsoncObject(filePath, current);
|
|
483
|
+
const mcpValue = currentObject.mcp;
|
|
484
|
+
if (mcpValue !== undefined && (!mcpValue || typeof mcpValue !== "object" || Array.isArray(mcpValue))) {
|
|
485
|
+
throw new CliError("E_USAGE", `Expected mcp to be an object in ${filePath}.`, 2);
|
|
486
|
+
}
|
|
487
|
+
const existingSatori = mcpValue?.satori;
|
|
488
|
+
if (existingSatori !== undefined && !isManagedOpenCodeEntry(existingSatori)) {
|
|
489
|
+
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);
|
|
490
|
+
}
|
|
491
|
+
return mutateJsonc(filePath, current, ["mcp", "satori"], buildOpenCodeServerConfig(runtimeCommand));
|
|
492
|
+
}
|
|
493
|
+
function prepareOpenCodeUninstall(filePath) {
|
|
494
|
+
const current = readTextIfExists(filePath);
|
|
495
|
+
if (!current) {
|
|
496
|
+
return { changed: false, apply: () => { } };
|
|
497
|
+
}
|
|
498
|
+
const currentObject = parseJsoncObject(filePath, current);
|
|
499
|
+
const mcpValue = currentObject.mcp;
|
|
500
|
+
if (!mcpValue || typeof mcpValue !== "object" || Array.isArray(mcpValue)) {
|
|
501
|
+
return { changed: false, apply: () => { } };
|
|
502
|
+
}
|
|
503
|
+
const existingSatori = mcpValue.satori;
|
|
504
|
+
if (existingSatori === undefined) {
|
|
505
|
+
return { changed: false, apply: () => { } };
|
|
506
|
+
}
|
|
507
|
+
if (!isManagedOpenCodeEntry(existingSatori)) {
|
|
508
|
+
throw new CliError("E_USAGE", `Refusing to remove unmanaged Satori config in ${filePath}. Remove mcp.satori manually instead.`, 2);
|
|
509
|
+
}
|
|
510
|
+
return mutateJsonc(filePath, current, ["mcp", "satori"], undefined);
|
|
511
|
+
}
|
|
248
512
|
function prepareSkillInstall(skillsPath, skillAssetRoot) {
|
|
249
513
|
const writes = [];
|
|
250
514
|
let changed = false;
|
|
@@ -284,24 +548,92 @@ function prepareSkillRemoval(skillsPath) {
|
|
|
284
548
|
},
|
|
285
549
|
};
|
|
286
550
|
}
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
551
|
+
function buildManagedInstructionsBlock() {
|
|
552
|
+
return [
|
|
553
|
+
INSTRUCTIONS_BLOCK_START,
|
|
554
|
+
OPENCODE_INSTRUCTIONS.trim(),
|
|
555
|
+
INSTRUCTIONS_BLOCK_END,
|
|
556
|
+
"",
|
|
557
|
+
].join("\n");
|
|
558
|
+
}
|
|
559
|
+
function prepareInstructionsInstall(filePath) {
|
|
560
|
+
const current = readTextIfExists(filePath) ?? "";
|
|
561
|
+
const block = buildManagedInstructionsBlock();
|
|
562
|
+
let next = current;
|
|
563
|
+
if (current.includes(INSTRUCTIONS_BLOCK_START) && current.includes(INSTRUCTIONS_BLOCK_END)) {
|
|
564
|
+
next = current.replace(new RegExp(`${escapeRegExp(INSTRUCTIONS_BLOCK_START)}[\\s\\S]*?${escapeRegExp(INSTRUCTIONS_BLOCK_END)}\\n?`, "m"), block);
|
|
565
|
+
}
|
|
566
|
+
else if (current.trim().length === 0) {
|
|
567
|
+
next = block;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
next = `${normalizeTrailingNewline(current)}\n${block}`;
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
changed: next !== current,
|
|
574
|
+
apply: () => {
|
|
575
|
+
if (next === current) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
ensureParentDir(filePath);
|
|
579
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function prepareInstructionsRemoval(filePath) {
|
|
584
|
+
const current = readTextIfExists(filePath);
|
|
585
|
+
if (!current || !current.includes(INSTRUCTIONS_BLOCK_START) || !current.includes(INSTRUCTIONS_BLOCK_END)) {
|
|
586
|
+
return { changed: false, apply: () => { } };
|
|
587
|
+
}
|
|
588
|
+
const next = current
|
|
589
|
+
.replace(new RegExp(`\\n?${escapeRegExp(INSTRUCTIONS_BLOCK_START)}[\\s\\S]*?${escapeRegExp(INSTRUCTIONS_BLOCK_END)}\\n?`, "m"), "\n")
|
|
590
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
591
|
+
.replace(/^\n+/, "");
|
|
592
|
+
return {
|
|
593
|
+
changed: next !== current,
|
|
594
|
+
apply: () => {
|
|
595
|
+
if (next === current) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
fs.writeFileSync(filePath, next, "utf8");
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function prepareCompanionMutation(companion, command, skillAssetRoot) {
|
|
603
|
+
if (companion.kind === "skills") {
|
|
604
|
+
return command.kind === "install"
|
|
605
|
+
? prepareSkillInstall(companion.path, skillAssetRoot)
|
|
606
|
+
: prepareSkillRemoval(companion.path);
|
|
607
|
+
}
|
|
608
|
+
return command.kind === "install"
|
|
609
|
+
? prepareInstructionsInstall(companion.path)
|
|
610
|
+
: prepareInstructionsRemoval(companion.path);
|
|
611
|
+
}
|
|
612
|
+
function prepareConfigMutation(target, command, runtimeCommand) {
|
|
613
|
+
if (target.client === "codex") {
|
|
614
|
+
return command.kind === "install"
|
|
615
|
+
? prepareCodexInstall(target.configPath, runtimeCommand)
|
|
616
|
+
: prepareCodexUninstall(target.configPath);
|
|
617
|
+
}
|
|
618
|
+
if (target.client === "claude") {
|
|
619
|
+
return command.kind === "install"
|
|
620
|
+
? prepareClaudeInstall(target.configPath, runtimeCommand)
|
|
294
621
|
: prepareClaudeUninstall(target.configPath);
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
622
|
+
}
|
|
623
|
+
return command.kind === "install"
|
|
624
|
+
? prepareOpenCodeInstall(target.configPath, runtimeCommand)
|
|
625
|
+
: prepareOpenCodeUninstall(target.configPath);
|
|
626
|
+
}
|
|
627
|
+
function prepareMutation(target, command, runtimeCommand, skillAssetRoot) {
|
|
628
|
+
const configMutation = prepareConfigMutation(target, command, runtimeCommand);
|
|
629
|
+
const companionMutation = prepareCompanionMutation(target.companion, command, skillAssetRoot);
|
|
298
630
|
return {
|
|
299
631
|
target,
|
|
300
632
|
configChanged: configMutation.changed,
|
|
301
|
-
|
|
633
|
+
companionChanged: companionMutation.changed,
|
|
302
634
|
apply: () => {
|
|
303
635
|
configMutation.apply();
|
|
304
|
-
|
|
636
|
+
companionMutation.apply();
|
|
305
637
|
},
|
|
306
638
|
};
|
|
307
639
|
}
|
|
@@ -309,8 +641,20 @@ export function executeInstallCommand(command, options = {}) {
|
|
|
309
641
|
const homeDir = options.homeDir ?? os.homedir();
|
|
310
642
|
const packageSpecifier = options.packageSpecifier ?? resolveDefaultPackageSpecifier();
|
|
311
643
|
const skillAssetRoot = options.skillAssetRoot ?? resolveDefaultSkillAssetRoot();
|
|
312
|
-
const
|
|
644
|
+
const plannedRuntimeCommand = options.runtimeCommand ?? plannedManagedRuntimeCommand(homeDir, packageSpecifier);
|
|
645
|
+
const clientCommand = managedClientCommand(homeDir);
|
|
646
|
+
let launcherMutation = command.kind === "install"
|
|
647
|
+
? prepareLauncherInstall(homeDir, plannedRuntimeCommand)
|
|
648
|
+
: { changed: false, apply: () => { } };
|
|
649
|
+
const prepared = selectTargets(homeDir, command.client).map((target) => (prepareMutation(target, command, clientCommand, skillAssetRoot)));
|
|
313
650
|
if (!command.dryRun) {
|
|
651
|
+
if (command.kind === "install" && !options.runtimeCommand) {
|
|
652
|
+
const installedRuntimeCommand = installManagedRuntimeCommand(homeDir, packageSpecifier, options.execFileSyncImpl ?? execFileSync);
|
|
653
|
+
launcherMutation = prepareLauncherInstall(homeDir, installedRuntimeCommand);
|
|
654
|
+
}
|
|
655
|
+
if (command.kind === "install") {
|
|
656
|
+
launcherMutation.apply();
|
|
657
|
+
}
|
|
314
658
|
for (const mutation of prepared) {
|
|
315
659
|
mutation.apply();
|
|
316
660
|
}
|
|
@@ -322,10 +666,12 @@ export function executeInstallCommand(command, options = {}) {
|
|
|
322
666
|
results: prepared.map((mutation) => ({
|
|
323
667
|
client: mutation.target.client,
|
|
324
668
|
configPath: mutation.target.configPath,
|
|
325
|
-
skillsPath: mutation.target.
|
|
669
|
+
skillsPath: mutation.target.companion.kind === "skills" ? mutation.target.companion.path : undefined,
|
|
670
|
+
instructionsPath: mutation.target.companion.kind === "instructions" ? mutation.target.companion.path : undefined,
|
|
326
671
|
configChanged: mutation.configChanged,
|
|
327
|
-
skillsChanged: mutation.
|
|
328
|
-
|
|
672
|
+
skillsChanged: mutation.target.companion.kind === "skills" ? mutation.companionChanged : false,
|
|
673
|
+
instructionsChanged: mutation.target.companion.kind === "instructions" ? mutation.companionChanged : false,
|
|
674
|
+
status: mutation.configChanged || mutation.companionChanged || launcherMutation.changed ? "updated" : "unchanged",
|
|
329
675
|
dryRun: command.dryRun,
|
|
330
676
|
})),
|
|
331
677
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zokizuan/satori-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.0"
|
|
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.
|