Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ JST is a Cargo workspace with three crates:

```text
jst natural language request
→ POST /translate
OpenAI-compatible LLM API
hosted provider: POST /translate → OpenAI-compatible LLM API
Apple provider (macOS 27 beta): bundled Swift helper → FoundationModels system model
→ command + concrete effect description + optional semantic parts
→ local denylist OR dangerous model effects
→ optional interactive session: explain, revise, manually replace,
Expand Down Expand Up @@ -42,6 +42,17 @@ complete replacement command and recalculate its effects. The replacement goes
through the same server validation, local denylist, terminal-safety checks, and
explicit approval loop as the initial command.

`--provider apple` is an explicit macOS 27.0-beta-or-later option. The CLI
serializes the normal system and user prompts to the adjacent
`jst-apple-intelligence` executable over stdin and reads a structured response
from stdout. The Swift helper uses `FoundationModels` directly and checks
`SystemLanguageModel.default.availability` before making a request. No Rust
FFI, provider key, request logging, JST-server quota, or network hop is
involved. The helper is built and signed beside the universal Rust executable;
the release package must keep both files together. Homebrew places the helper
in its private `libexec` directory, which the CLI also locates. Linux and
Windows releases do not contain the helper and retain the hosted provider flow.

Choosing `e` opens a prefilled inline editor with the cursor at the end. Enter
counts as execution approval and the edit remains entirely local: it is never
sent to the server or model. Safe edits run immediately; edits that match the
Expand Down Expand Up @@ -89,7 +100,8 @@ successful translations only.
## Workspace

```text
crates/cli/src/main.rs argument parsing, API calls, interactive loop, execution
crates/cli/src/main.rs argument parsing, provider calls, interactive loop, execution
crates/apple-intelligence/main.swift macOS 27 FoundationModels helper
crates/cli/src/installation.rs anonymous installation ID persistence
crates/cli/src/safety.rs deterministic destructive-command denylist
crates/server/src/main.rs HTTP server and routes
Expand All @@ -110,4 +122,5 @@ JST_API_URL=http://localhost:8080/translate cargo run -p jst-cli -- pwd
```

The CLI contains no provider credentials. Release binaries can be built and
signed as ordinary Rust executables.
signed as ordinary Rust executables. macOS packages also contain the separately
signed Apple Intelligence helper, built by `scripts/build-apple-intelligence-helper.sh`.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] }
tokio = { version = "1", features = ["io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,51 @@ Use `--dry` to print a generated command and exit without running it:
jst --dry show the current directory
```

### Apple Intelligence (macOS 27 beta)

On macOS 27.0 beta or later, use the on-device Apple Intelligence model instead
of the hosted JST server. It is opt-in; the hosted provider remains JST's
unchanged default:

```sh
jst --provider apple --dry show the current directory
jst --provider apple -i find files larger than 500 MB
jst --provider apple --status
```

If you use it regularly, set the provider once in your shell configuration:

```sh
export JST_PROVIDER=apple
jst show the current directory
```

Use `--provider server` on an individual command to override that setting.

This option requires a Mac and Apple Intelligence configuration for which the
system model is available. `jst --provider apple --status` reports the model's
availability before any translation. The request, generated command, and
revision instructions stay on the Mac: this provider does not contact the JST
server, consume its quota, or use an API key.

On macOS 26 or earlier (and on non-macOS platforms), selecting Apple mode exits
before launching the helper with a clear macOS-27 requirement. Run JST without
the Apple provider, or use `--provider server`, to keep using the hosted model.

Mac release archives include a `jst-apple-intelligence` companion executable;
Linux and Windows archives do not. Homebrew installs it privately and wires it
up automatically. For a manual macOS install, keep the two archive executables
together in the same directory on your `PATH`:

```sh
install -m 755 jst-*/jst jst-*/jst-apple-intelligence ~/.local/bin/
```

The Rust CLI sends the companion a JSON request and receives a structured JSON
response; the companion calls Apple's `FoundationModels` framework directly.
This keeps the beta framework out of Rust and avoids maintaining Swift
bindings. The hosted JST server is available on every supported platform.

### Review and refine

Use `-i` or `--interactive` to inspect and refine a command before anything
Expand Down
210 changes: 210 additions & 0 deletions crates/apple-intelligence/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import Foundation
import FoundationModels

// This executable is intentionally a tiny process boundary. jst talks to it
// using JSON so the Rust CLI does not need Swift bindings or a Rust wrapper for
// Apple's beta-only FoundationModels framework.

private struct Request: Decodable {
let systemPrompt: String
let userPrompt: String
let explain: Bool
}

private struct Response: Encodable {
let command: String
let effects: Effects
let matchesRequest: Bool
let explanation: String
let parts: [Part]

enum CodingKeys: String, CodingKey {
case command
case effects
case matchesRequest = "matches_request"
case explanation
case parts
}
}

private struct Effects: Encodable {
let readsData: Bool
let modifiesData: Bool
let deletesData: Bool
let usesNetwork: Bool
let changesRemoteData: Bool
let changesProcesses: Bool
let installsSoftware: Bool
let usesPrivilege: Bool
let executesRemoteCode: Bool

enum CodingKeys: String, CodingKey {
case readsData = "reads_data"
case modifiesData = "modifies_data"
case deletesData = "deletes_data"
case usesNetwork = "uses_network"
case changesRemoteData = "changes_remote_data"
case changesProcesses = "changes_processes"
case installsSoftware = "installs_software"
case usesPrivilege = "uses_privilege"
case executesRemoteCode = "executes_remote_code"
}
}

private struct Part: Encodable {
let fragment: String
let meaning: String
let source: String
}

@Generable(description: "A complete, safety-described shell-command translation for JST.")
private struct GeneratedTranslation {
@Guide(description: "One complete executable shell command. Use '# unable to translate' when no safe, compatible translation is possible.")
var command: String

@Guide(description: "Concrete effects of running the command.")
var effects: GeneratedEffects

@Guide(description: "True only when the command completely implements the request.")
var matchesRequest: Bool

@Guide(description: "A short standalone explanation of what the command does.")
var explanation: String
}

@Generable(description: "A complete, safety-described shell-command translation for JST with semantic command fragments.")
private struct GeneratedDetailedTranslation {
@Guide(description: "One complete executable shell command. Use '# unable to translate' when no safe, compatible translation is possible.")
var command: String

@Guide(description: "Concrete effects of running the command.")
var effects: GeneratedEffects

@Guide(description: "True only when the command completely implements the request.")
var matchesRequest: Bool

@Guide(description: "A short standalone explanation of what the command does.")
var explanation: String

@Guide(description: "One to eight command fragments in order. Their fragments must concatenate exactly to command.", .maximumCount(8))
var parts: [GeneratedPart]
}

@Generable(description: "Concrete effects of a shell command.")
private struct GeneratedEffects {
var readsData: Bool
var modifiesData: Bool
var deletesData: Bool
var usesNetwork: Bool
var changesRemoteData: Bool
var changesProcesses: Bool
var installsSoftware: Bool
var usesPrivilege: Bool
var executesRemoteCode: Bool
}

@Generable(description: "A semantic fragment of the generated command.")
private struct GeneratedPart {
var fragment: String
var meaning: String
var source: String
}

@main
private struct AppleIntelligenceHelper {
static func main() async {
do {
if CommandLine.arguments.dropFirst().first == "--status" {
try writeJSON(["status": availabilityStatus()])
return
}

guard case .available = SystemLanguageModel.default.availability else {
throw HelperError.unavailable(availabilityStatus())
}

let request = try JSONDecoder().decode(
Request.self,
from: FileHandle.standardInput.readDataToEndOfFile()
)
let session = LanguageModelSession(instructions: request.systemPrompt)
let response: Response
if request.explain {
let generated = try await session.respond(
to: request.userPrompt,
generating: GeneratedDetailedTranslation.self
).content
response = Response(
command: generated.command,
effects: effects(for: generated.effects),
matchesRequest: generated.matchesRequest,
explanation: generated.explanation,
parts: generated.parts.map {
Part(fragment: $0.fragment, meaning: $0.meaning, source: $0.source)
}
)
} else {
let generated = try await session.respond(
to: request.userPrompt,
generating: GeneratedTranslation.self
).content
response = Response(
command: generated.command,
effects: effects(for: generated.effects),
matchesRequest: generated.matchesRequest,
explanation: generated.explanation,
parts: []
)
}
try writeJSON(response)
} catch {
FileHandle.standardError.write(
Data(("jst-apple-intelligence: \(error.localizedDescription)\n").utf8)
)
exit(1)
}
}

private static func availabilityStatus() -> String {
switch SystemLanguageModel.default.availability {
case .available:
return "available"
case .unavailable(let reason):
return "unavailable: \(reason)"
@unknown default:
return "unavailable: unknown reason"
}
}

private static func writeJSON<T: Encodable>(_ value: T) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(value)
FileHandle.standardOutput.write(data)
}

private static func effects(for generated: GeneratedEffects) -> Effects {
Effects(
readsData: generated.readsData,
modifiesData: generated.modifiesData,
deletesData: generated.deletesData,
usesNetwork: generated.usesNetwork,
changesRemoteData: generated.changesRemoteData,
changesProcesses: generated.changesProcesses,
installsSoftware: generated.installsSoftware,
usesPrivilege: generated.usesPrivilege,
executesRemoteCode: generated.executesRemoteCode
)
}
}

private enum HelperError: LocalizedError {
case unavailable(String)

var errorDescription: String? {
switch self {
case .unavailable(let status):
return "Apple Intelligence is \(status)"
}
}
}
3 changes: 2 additions & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ path = "src/main.rs"

[dependencies]
jst-shared = { path = "../shared" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
reqwest.workspace = true
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "env"] }
regex = "1"
getrandom = "0.3"
crossterm = "0.28"
Loading
Loading