Skip to content

Add fm: Apple Foundation Models CLI#1

Open
planbnet wants to merge 4 commits into
masterfrom
fm-foundationmodels
Open

Add fm: Apple Foundation Models CLI#1
planbnet wants to merge 4 commits into
masterfrom
fm-foundationmodels

Conversation

@planbnet

@planbnet planbnet commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Adds the fm command, exposing Apple Foundation Models (on-device 'system' and Private Cloud Compute 'pcc').

The command was introduced by Apple in macOS 27 Golden Gate and allows CLI access to the on-device Apple Foundation Model as well as Apple's new Private Cloud Compute model: https://wwdc.ai/2026/334

The subcommands respond / chat / token-count and available work like they to in the new macOS 27 system tool.

Besides that, I added fm agent an interactive coding agent with tool calling (read/write/edit files, grep, glob, list_dir, run_shell, web_search, read_url). Defaults to the on-device model; pcc is opt-in and falls back to system. run_shell uses ios_system's interpreter (ios_popen) on device. fm agent was inspired by https://github.com/mikeypdev/a-agent but does not need any API key and can run completely with the on device model (for very simpletasks though).

Sources live in a-Shell/fm_cmd/. Adds the private-cloud-compute entitlement (required for the pcc model).

Caveats:

@planbnet planbnet force-pushed the fm-foundationmodels branch from 2cf5734 to e78f895 Compare June 16, 2026 22:14
@planbnet planbnet changed the title Add fm: Apple Foundation Models CLI (respond/chat/agent/schema) Jun 17, 2026
Adds the 'fm' command, registered via ios_system, exposing Apple
Foundation Models (on-device 'system' and Private Cloud Compute 'pcc'):

- respond / chat / token-count / schema / available
- agent: interactive coding agent with tool calling (read/write/edit files,
  grep, glob, list_dir, run_shell, web_search, read_url). Defaults to the
  on-device model; pcc is opt-in and falls back to system. run_shell uses
  ios_system's interpreter (ios_popen) on device. Ctrl-C cancels generation.

Sources live in a-Shell/fm_cmd/. Adds the private-cloud-compute entitlement
(required for the pcc model). No signing/team or bundle-id changes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new fm CLI command to a-Shell that exposes Apple Foundation Models (system + Private Cloud Compute) and includes an interactive “agent” mode with tool-calling.

Changes:

  • Introduces a-Shell/fm_cmd/* implementing respond, chat, token-count, available, and agent subcommands (with streaming and Ctrl‑C cancellation).
  • Registers fm with ios_system (replaceCommand("fm", "fm_main", true)), and adds the PCC entitlement.
  • Updates the Xcode project to include the new fm_cmd Swift sources.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
a-Shell/fm_cmd/Output.swift Adds stream resolution helpers and ANSI utilities.
a-Shell/fm_cmd/Interrupt.swift Implements SIGINT-based cancellation for async model work.
a-Shell/fm_cmd/Help.swift Adds user-facing help text for all fm subcommands.
a-Shell/fm_cmd/FoundationModelsExecution.swift Implements model execution for respond/chat/token-count/available with availability guards.
a-Shell/fm_cmd/FMCommand.swift Adds dispatcher + ios_system entry point (fm_main).
a-Shell/fm_cmd/Commands.swift Adds argument parsing for all subcommands.
a-Shell/fm_cmd/AgentTools.swift Implements agent tool runner (files, grep/glob, shell, web).
a-Shell/fm_cmd/AgentFMTools.swift Wraps runner tools into FoundationModels Tool types.
a-Shell/fm_cmd/AgentDisplay.swift Adds terminal UI rendering + spinner/panels for agent mode.
a-Shell/fm_cmd/AgentCommand.swift Implements agent REPL + streaming + fallback behaviors.
a-Shell/AppDelegate.swift Registers fm command with ios_system.
a-Shell/a-Shell.entitlements Adds Private Cloud Compute entitlement.
a-Shell.xcodeproj/project.pbxproj Adds fm_cmd group + file refs + build phase entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread a-Shell/fm_cmd/FoundationModelsExecution.swift
Comment on lines +149 to +155
case "-m", "--model":
guard let val = parser.next() else {
fputs("Error: '--model' requires a value.\n", error)
return 64
}
opts.model = val
case "-i", "--instructions":
Comment on lines +255 to +261
case "-m", "--model":
guard let val = parser.next() else {
fputs("Error: '--model' requires a value.\n", error)
return 64
}
opts.model = val
default:
Comment on lines +59 to +62
let start = (args["start_line"] as? Int).map { max(1, $0) } ?? 1
let end = (args["end_line"] as? Int).map { min(total, $0) } ?? total

let selected = lines[(start-1)..<end]
Comment on lines +46 to +59
func spin(_ message: String) {
q.async { [weak self] in
guard let self else { return }
self._clearSpin()
self.spinMsg = message
self.spinStart = Date()
self.spinIdx = 0
let t = DispatchSource.makeTimerSource(queue: self.q)
t.schedule(deadline: .now(), repeating: .milliseconds(120))
t.setEventHandler { [weak self] in self?._drawSpin() }
self.spinTimer = t
t.resume()
}
}
Comment thread a-Shell.xcodeproj/project.pbxproj Outdated
Comment on lines +975 to +984
FM0002000000000000000001 /* Commands.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000001 /* Commands.swift */; };
FM0002000000000000000002 /* FMCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000002 /* FMCommand.swift */; };
FM0002000000000000000003 /* FoundationModelsExecution.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000003 /* FoundationModelsExecution.swift */; };
FM0002000000000000000004 /* Help.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000004 /* Help.swift */; };
FM0002000000000000000005 /* Output.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000005 /* Output.swift */; };
FM0002000000000000000007 /* AgentCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000007 /* AgentCommand.swift */; };
FM0002000000000000000008 /* AgentDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000008 /* AgentDisplay.swift */; };
FM0002000000000000000009 /* AgentFMTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM0001000000000000000009 /* AgentFMTools.swift */; };
FM000200000000000000000A /* AgentTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM000100000000000000000A /* AgentTools.swift */; };
FM000200000000000000000C /* Interrupt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FM000100000000000000000C /* Interrupt.swift */; };
Comment on lines +367 to +368
/// Build a `LanguageModelSession` using concrete model types to stay at iOS 26+
/// for the system model path and iOS 27+ for PCC.
- token-count: include --instructions in the counted text (was ignored).
- chat / available: validate --model (reject unknown values, like respond).
- read_file: guard start_line > end_line and start beyond EOF (no trap).
- agent spinner: on non-TTY output, print one status line instead of
  spamming a new line every 120ms.
- Regenerate the fm_cmd project object IDs as valid 24-char hex.
- Fix a stale availability doc comment.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Comment on lines +30 to +32
public static func isTerminal(_ file: UnsafeMutablePointer<FILE>) -> Bool {
isatty(fileno(file)) != 0
}
Comment on lines +13 to +15
final class ErrorBox {
var error: Error?
}
Comment on lines +17 to +41
private var fmInterruptFlag: sig_atomic_t = 0

private func fmInterruptHandler(_ sig: Int32) {
fmInterruptFlag = 1
}

/// Runs `operation` while Ctrl-C cancels its Task. Returns `true` if it ran to
/// completion, `false` if it was interrupted by SIGINT.
@discardableResult
func runInterruptible(_ operation: @escaping () async -> Void) async -> Bool {
fmInterruptFlag = 0
let previous = signal(SIGINT, fmInterruptHandler)
defer { signal(SIGINT, previous) }

let op = Task { await operation() }
let watcher = Task {
while !Task.isCancelled {
if fmInterruptFlag != 0 { op.cancel(); return }
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1s
}
}
await op.value
watcher.cancel()
return fmInterruptFlag == 0
}
Comment on lines +180 to +204
private static func _execShell(command: String, timeout: Int) -> String {
let tmpPath = NSTemporaryDirectory() + "fm-shell-\(UUID().uuidString).out"
guard let fp = fopen(tmpPath, "w+") else {
return "Error: could not create a temporary file to capture output"
}

let savedOut = thread_stdout
let savedErr = thread_stderr
thread_stdout = fp
thread_stderr = fp

let pid = ios_fork()
_ = ios_system(command)
fflush(fp)
ios_waitpid(pid)
ios_releaseThreadId(pid)

thread_stdout = savedOut
thread_stderr = savedErr
fclose(fp)

let data = (try? Data(contentsOf: URL(fileURLWithPath: tmpPath))) ?? Data()
try? FileManager.default.removeItem(atPath: tmpPath)
return _truncateOutput(String(decoding: data, as: UTF8.self))
}
planbnet added 2 commits June 17, 2026 17:05
Pass images as Attachment(cgImage:) (decoded up front with ImageIO,
preserving EXIF orientation) instead of Attachment(imageURL:). The
URL initializer defers loading until the model processes the prompt;
decoding up front fails fast on unreadable images and avoids a
deferred-load stall. ImageIO/CoreGraphics only, no UIKit/AppKit.
The CGImage pre-decode didn't fix the image hang (a FoundationModels beta
limitation, not the attachment path), so revert to the simpler, cleaner
URL-based attachment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants