feat(companion): add mobile runtime skeleton shell templates

This commit is contained in:
William Valentin
2026-02-26 20:56:43 -08:00
parent d303869866
commit 078c3799ce
20 changed files with 690 additions and 54 deletions
+2 -2
View File
@@ -1736,7 +1736,7 @@ Companion runtime helper:
- `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, optional interval jitter (`jitterRatio`) to spread load (with safe normalization for invalid random samples), `tickNow()` for manual sends, success/error hooks, loop observability (`successCount`, `lastSuccessAt`, `failureCount`, `lastFailure`, `getState()`), and optional auto-stop after repeated failures. - `src/companion/heartbeatLoop.ts` provides `CompanionHeartbeatLoop` for periodic heartbeat scheduling (`publishHeartbeat`) with start/stop safety, optional interval jitter (`jitterRatio`) to spread load (with safe normalization for invalid random samples), `tickNow()` for manual sends, success/error hooks, loop observability (`successCount`, `lastSuccessAt`, `failureCount`, `lastFailure`, `getState()`), and optional auto-stop after repeated failures.
- `src/companion/bootstrapManifest.ts` provides `createCompanionBootstrapManifest()` for generating a typed gateway/node/runtime bootstrap contract used by packaging flows, including optional initial status/location/push payloads. - `src/companion/bootstrapManifest.ts` provides `createCompanionBootstrapManifest()` for generating a typed gateway/node/runtime bootstrap contract used by packaging flows, including optional initial status/location/push payloads.
- `src/companion/releaseBundle.ts` provides `writeCompanionReleaseBundle()` for writing a distributable companion bundle directory (bootstrap JSON + launcher script + README + SHA-256 checksums + release manifest). - `src/companion/releaseBundle.ts` provides `writeCompanionReleaseBundle()` for writing a distributable companion bundle directory (bootstrap JSON + launcher script + README + SHA-256 checksums + release manifest).
- `src/companion/shellTemplate.ts` provides `writeCompanionShellTemplate()` for emitting platform starter shells (macOS/iOS/Android native scaffold snippets + bootstrap JSON). - `src/companion/shellTemplate.ts` provides `writeCompanionShellTemplate()` for emitting platform starter shells (macOS launcher wrapper snippet, iOS/Android bootstrap + runtime skeleton files, and bootstrap JSON).
- `src/companion/releaseVerify.ts` provides `verifyCompanionReleaseBundle()` for validating bundle checksums and optional signature metadata. - `src/companion/releaseVerify.ts` provides `verifyCompanionReleaseBundle()` for validating bundle checksums and optional signature metadata.
- `src/companion/releasePipeline.ts` provides `buildAndVerifyCompanionReleaseBundle()` for build-and-verify automation (including signed bundle flows). - `src/companion/releasePipeline.ts` provides `buildAndVerifyCompanionReleaseBundle()` for build-and-verify automation (including signed bundle flows).
- `src/companion/macosMenuBarApp.ts` provides `generateMacOSMenuBarApp()` for emitting a runnable Swift Package macOS menu-bar companion scaffold. - `src/companion/macosMenuBarApp.ts` provides `generateMacOSMenuBarApp()` for emitting a runnable Swift Package macOS menu-bar companion scaffold.
@@ -1749,7 +1749,7 @@ Minimal companion CLI:
- `flynn companion --export-bootstrap ./companion.bootstrap.json` writes a resolved bootstrap manifest for desktop/mobile companion app packaging (use `-` for stdout). - `flynn companion --export-bootstrap ./companion.bootstrap.json` writes a resolved bootstrap manifest for desktop/mobile companion app packaging (use `-` for stdout).
- `flynn companion --export-release-bundle ./dist/companion-macos` writes a release bundle directory (`companion.bootstrap.json`, `run-companion.sh`, `README.md`, `CHECKSUMS.sha256`, `RELEASE_MANIFEST.json`) and exits. - `flynn companion --export-release-bundle ./dist/companion-macos` writes a release bundle directory (`companion.bootstrap.json`, `run-companion.sh`, `README.md`, `CHECKSUMS.sha256`, `RELEASE_MANIFEST.json`) and exits.
- `flynn companion --export-release-bundle ./dist/companion-macos --signing-key ./keys/release-private.pem --signing-key-id team-k1` also writes `CHECKSUMS.sha256.sig` for signed verification workflows. - `flynn companion --export-release-bundle ./dist/companion-macos --signing-key ./keys/release-private.pem --signing-key-id team-k1` also writes `CHECKSUMS.sha256.sig` for signed verification workflows.
- `flynn companion --platform ios --export-shell-template ./dist/companion-ios-template` writes a platform-native starter template directory (`companion.bootstrap.json`, native starter file, `README.md`) and exits. - `flynn companion --platform ios --export-shell-template ./dist/companion-ios-template` writes a platform-native starter template directory (`companion.bootstrap.json`, bootstrap/runtime starter files, `README.md`) and exits.
- `flynn companion --verify-release-bundle ./dist/companion-macos --verify-signing-key ./keys/release-public.pem --verify-signing-key-id team-k1 --require-signature` verifies checksums and signature metadata before install. - `flynn companion --verify-release-bundle ./dist/companion-macos --verify-signing-key ./keys/release-public.pem --verify-signing-key-id team-k1 --require-signature` verifies checksums and signature metadata before install.
- `pnpm companion:bundle -- --output ./dist/companion-macos --platform macos --signing-key ./keys/release-private.pem --signing-key-id team-k1` builds and verifies a release bundle in one step. - `pnpm companion:bundle -- --output ./dist/companion-macos --platform macos --signing-key ./keys/release-private.pem --signing-key-id team-k1` builds and verifies a release bundle in one step.
- `pnpm companion:reference-apps -- --output ./apps/companion` regenerates macOS/iOS/Android reference app starter shells plus `apps/companion/macos-app` runnable scaffold. - `pnpm companion:reference-apps -- --output ./apps/companion` regenerates macOS/iOS/Android reference app starter shells plus `apps/companion/macos-app` runnable scaffold.
+2
View File
@@ -8,3 +8,5 @@ This directory contains generated companion starter shells for:
- Android shell - Android shell
These are reference starters, not production binaries. Use them as a baseline for app packaging and distribution workflows. These are reference starters, not production binaries. Use them as a baseline for app packaging and distribution workflows.
iOS/Android starter directories include bootstrap plus runtime-skeleton files for register/status/location/push/handoff flows.
@@ -0,0 +1,122 @@
package flynn.companion
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import org.json.JSONArray
import org.json.JSONObject
interface CompanionTransport {
fun connect(url: String, token: String?)
fun disconnect()
fun send(rawMessage: String)
}
data class CompanionStatusPayload(
val platform: String,
val appVersion: String? = null,
val statusText: String? = null,
val batteryPct: Double? = null
)
data class CompanionLocationPayload(
val latitude: Double,
val longitude: Double,
val source: String = "device"
)
class AndroidCompanionRuntime(
private val bootstrap: CompanionBootstrap,
private val transport: CompanionTransport
) {
private var nextRequestId: Int = 1
private val scheduler = Executors.newSingleThreadScheduledExecutor()
private var heartbeatFuture: ScheduledFuture<*>? = null
fun connect() {
transport.connect(bootstrap.gateway.url, bootstrap.gateway.token)
}
fun disconnect() {
heartbeatFuture?.cancel(true)
heartbeatFuture = null
transport.disconnect()
}
fun registerNode() {
val params = JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("role", bootstrap.node.role)
.put("platform", bootstrap.node.platform)
.put("capabilities", JSONArray(bootstrap.node.capabilities))
sendRpc("node.register", params)
}
fun publishStatus(status: CompanionStatusPayload) {
val statusJson = JSONObject()
.put("platform", status.platform)
status.appVersion?.let { statusJson.put("appVersion", it) }
status.statusText?.let { statusJson.put("statusText", it) }
status.batteryPct?.let { statusJson.put("batteryPct", it) }
sendRpc(
"node.status.set",
JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("status", statusJson)
)
}
fun publishLocation(location: CompanionLocationPayload) {
sendRpc(
"node.location.set",
JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put(
"location",
JSONObject()
.put("latitude", location.latitude)
.put("longitude", location.longitude)
.put("source", location.source)
)
)
}
fun registerPushToken(token: String, topic: String? = null) {
val params = JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("provider", "fcm")
.put("token", token)
topic?.let { params.put("topic", it) }
sendRpc("node.push_token.set", params)
}
fun sendHandoff(message: String, sessionId: String? = null) {
val params = JSONObject()
.put("message", message)
.put("awaitResponse", true)
sessionId?.let { params.put("sessionId", it) }
sendRpc("agent.send", params)
}
fun startHeartbeatLoop(statusFactory: () -> CompanionStatusPayload) {
heartbeatFuture?.cancel(true)
val intervalSeconds = bootstrap.runtime.heartbeatSeconds.coerceAtLeast(1).toLong()
heartbeatFuture = scheduler.scheduleAtFixedRate(
{ publishStatus(statusFactory()) },
0L,
intervalSeconds,
TimeUnit.SECONDS
)
}
private fun sendRpc(method: String, params: JSONObject) {
val request = JSONObject()
.put("jsonrpc", "2.0")
.put("id", nextRequestId++)
.put("method", method)
.put("params", params)
transport.send(request.toString())
}
}
// Generated for node: android-reference-shell (android)
+5 -4
View File
@@ -1,11 +1,12 @@
# Flynn Companion android Shell Template # Flynn Companion Android Shell Template
This directory contains a generated starter template for a android companion shell. This directory contains a generated starter template for an Android companion shell.
Files: Files:
- `companion.bootstrap.json`: resolved Flynn companion bootstrap contract - `companion.bootstrap.json`: resolved Flynn companion bootstrap contract
- `CompanionBootstrap.kt`: platform-native starter model/wrapper snippet - `CompanionBootstrap.kt`
- `AndroidCompanionRuntime.kt`
Notes: Notes:
- These templates are intentionally minimal and should be integrated into your app project. - These templates are intentionally minimal and should be integrated into your app project.
- Runtime transport should use Flynn gateway JSON-RPC node methods (`node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`). - Runtime transport should use Flynn gateway JSON-RPC node methods (`node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`, `agent.send`).
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-02-27T04:50:38.373Z", "generatedAt": "2026-02-27T04:56:19.684Z",
"gateway": { "gateway": {
"url": "ws://127.0.0.1:18800" "url": "ws://127.0.0.1:18800"
}, },
@@ -0,0 +1,165 @@
import Foundation
enum CompanionRuntimeError: Error {
case invalidGatewayURL
case notConnected
case encodingFailure
}
private struct JsonRpcRequest<Params: Encodable>: Encodable {
let jsonrpc = "2.0"
let id: Int
let method: String
let params: Params
}
struct CompanionStatusPayload: Encodable {
let platform: String
let appVersion: String?
let statusText: String?
let batteryPct: Double?
}
struct CompanionLocationPayload: Encodable {
let latitude: Double
let longitude: Double
let source: String
}
private struct NodeRegisterParams: Encodable {
let nodeId: String
let role: String
let platform: String
let capabilities: [String]
}
private struct NodeStatusSetParams: Encodable {
let nodeId: String
let status: CompanionStatusPayload
}
private struct NodeLocationSetParams: Encodable {
let nodeId: String
let location: CompanionLocationPayload
}
private struct NodePushTokenSetParams: Encodable {
let nodeId: String
let provider: String
let token: String
let topic: String?
let environment: String?
}
private struct AgentSendParams: Encodable {
let message: String
let sessionId: String?
let awaitResponse: Bool
}
final class IOSCompanionRuntime {
private let bootstrap: CompanionBootstrap
private let session: URLSession
private var socket: URLSessionWebSocketTask?
private var nextRequestId = 1
private var heartbeatLoop: Task<Void, Never>?
init(bootstrap: CompanionBootstrap, session: URLSession = .shared) {
self.bootstrap = bootstrap
self.session = session
}
func connect() throws {
guard socket == nil else { return }
guard let url = URL(string: bootstrap.gateway.url) else {
throw CompanionRuntimeError.invalidGatewayURL
}
var request = URLRequest(url: url)
if let token = bootstrap.gateway.token, !token.isEmpty {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
let task = session.webSocketTask(with: request)
task.resume()
socket = task
}
func disconnect() {
heartbeatLoop?.cancel()
heartbeatLoop = nil
socket?.cancel(with: .goingAway, reason: nil)
socket = nil
}
func registerNode() async throws {
try await sendRpc(
method: "node.register",
params: NodeRegisterParams(
nodeId: bootstrap.node.nodeId,
role: bootstrap.node.role,
platform: bootstrap.node.platform,
capabilities: bootstrap.node.capabilities
)
)
}
func publishStatus(_ status: CompanionStatusPayload) async throws {
try await sendRpc(
method: "node.status.set",
params: NodeStatusSetParams(nodeId: bootstrap.node.nodeId, status: status)
)
}
func publishLocation(_ location: CompanionLocationPayload) async throws {
try await sendRpc(
method: "node.location.set",
params: NodeLocationSetParams(nodeId: bootstrap.node.nodeId, location: location)
)
}
func registerPushToken(token: String, topic: String? = nil, environment: String? = nil) async throws {
try await sendRpc(
method: "node.push_token.set",
params: NodePushTokenSetParams(
nodeId: bootstrap.node.nodeId,
provider: "apns",
token: token,
topic: topic,
environment: environment
)
)
}
func sendHandoff(message: String, sessionId: String? = nil) async throws {
try await sendRpc(
method: "agent.send",
params: AgentSendParams(message: message, sessionId: sessionId, awaitResponse: true)
)
}
func startHeartbeatLoop(statusFactory: @escaping () -> CompanionStatusPayload) {
heartbeatLoop?.cancel()
let interval = UInt64(max(bootstrap.runtime.heartbeatSeconds, 1)) * 1_000_000_000
heartbeatLoop = Task { [weak self] in
while !Task.isCancelled {
guard let self else { return }
try? await self.publishStatus(statusFactory())
try? await Task.sleep(nanoseconds: interval)
}
}
}
private func sendRpc<Params: Encodable>(method: String, params: Params) async throws {
guard let socket else {
throw CompanionRuntimeError.notConnected
}
let request = JsonRpcRequest(id: nextRequestId, method: method, params: params)
nextRequestId += 1
let data = try JSONEncoder().encode(request)
guard let text = String(data: data, encoding: .utf8) else {
throw CompanionRuntimeError.encodingFailure
}
try await socket.send(.string(text))
}
}
// Generated for node: ios-reference-shell (ios)
+5 -4
View File
@@ -1,11 +1,12 @@
# Flynn Companion ios Shell Template # Flynn Companion iOS Shell Template
This directory contains a generated starter template for a ios companion shell. This directory contains a generated starter template for an iOS companion shell.
Files: Files:
- `companion.bootstrap.json`: resolved Flynn companion bootstrap contract - `companion.bootstrap.json`: resolved Flynn companion bootstrap contract
- `CompanionBootstrap.swift`: platform-native starter model/wrapper snippet - `CompanionBootstrap.swift`
- `IOSCompanionRuntime.swift`
Notes: Notes:
- These templates are intentionally minimal and should be integrated into your app project. - These templates are intentionally minimal and should be integrated into your app project.
- Runtime transport should use Flynn gateway JSON-RPC node methods (`node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`). - Runtime transport should use Flynn gateway JSON-RPC node methods (`node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`, `agent.send`).
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-02-27T04:50:38.373Z", "generatedAt": "2026-02-27T04:56:19.684Z",
"gateway": { "gateway": {
"url": "ws://127.0.0.1:18800" "url": "ws://127.0.0.1:18800"
}, },
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-02-27T04:50:38.373Z", "generatedAt": "2026-02-27T04:56:19.684Z",
"gateway": { "gateway": {
"url": "ws://127.0.0.1:18800" "url": "ws://127.0.0.1:18800"
}, },
+4 -4
View File
@@ -1,11 +1,11 @@
# Flynn Companion macos Shell Template # Flynn Companion macOS Shell Template
This directory contains a generated starter template for a macos companion shell. This directory contains a generated starter template for a macOS companion shell.
Files: Files:
- `companion.bootstrap.json`: resolved Flynn companion bootstrap contract - `companion.bootstrap.json`: resolved Flynn companion bootstrap contract
- `MenuBarCompanion.swift`: platform-native starter model/wrapper snippet - `MenuBarCompanion.swift`
Notes: Notes:
- These templates are intentionally minimal and should be integrated into your app project. - These templates are intentionally minimal and should be integrated into your app project.
- Runtime transport should use Flynn gateway JSON-RPC node methods (`node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`). - Runtime transport should use Flynn gateway JSON-RPC node methods (`node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`, `agent.send`).
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"generatedAt": "2026-02-27T04:50:38.373Z", "generatedAt": "2026-02-27T04:56:19.684Z",
"gateway": { "gateway": {
"url": "ws://127.0.0.1:18800" "url": "ws://127.0.0.1:18800"
}, },
+1 -1
View File
@@ -1866,7 +1866,7 @@ For more implementation details, see:
- Companion release bundle helper: `src/companion/releaseBundle.ts` (writes bootstrap JSON + launcher script + README + `CHECKSUMS.sha256` + `RELEASE_MANIFEST.json`; optional `CHECKSUMS.sha256.sig` when a signing key is provided. Launcher performs checksum verification before exec.) - Companion release bundle helper: `src/companion/releaseBundle.ts` (writes bootstrap JSON + launcher script + README + `CHECKSUMS.sha256` + `RELEASE_MANIFEST.json`; optional `CHECKSUMS.sha256.sig` when a signing key is provided. Launcher performs checksum verification before exec.)
- Companion release bundle verifier: `src/companion/releaseVerify.ts` (validates `CHECKSUMS.sha256` and optional signature metadata against a provided public key) - Companion release bundle verifier: `src/companion/releaseVerify.ts` (validates `CHECKSUMS.sha256` and optional signature metadata against a provided public key)
- Companion release automation pipeline: `src/companion/releasePipeline.ts` + `scripts/build-companion-release-bundle.ts` (build-and-verify workflow for deterministic companion artifact generation) - Companion release automation pipeline: `src/companion/releasePipeline.ts` + `scripts/build-companion-release-bundle.ts` (build-and-verify workflow for deterministic companion artifact generation)
- Companion shell template helper: `src/companion/shellTemplate.ts` (writes platform-native starter template files for `macos`, `ios`, and `android` shell scaffolding) - Companion shell template helper: `src/companion/shellTemplate.ts` (writes platform-native starter template files for `macos`, `ios`, and `android` shell scaffolding, including iOS/Android runtime skeletons for `node.register` + status/location/push + `agent.send`)
- Companion macOS menu-bar app scaffold helper: `src/companion/macosMenuBarApp.ts` (writes runnable Swift Package menu-bar app starter surface) - Companion macOS menu-bar app scaffold helper: `src/companion/macosMenuBarApp.ts` (writes runnable Swift Package menu-bar app starter surface)
- Companion reference app exporter: `src/companion/referenceApps.ts` + `scripts/export-companion-reference-apps.ts` (regenerates in-repo platform starter app directories, including `apps/companion/macos-app`) - Companion reference app exporter: `src/companion/referenceApps.ts` + `scripts/export-companion-reference-apps.ts` (regenerates in-repo platform starter app directories, including `apps/companion/macos-app`)
- CI artifact workflow: `.github/workflows/companion-release-bundle.yml` (manual dispatch bundle build/verify/upload pipeline) - CI artifact workflow: `.github/workflows/companion-release-bundle.yml` (manual dispatch bundle build/verify/upload pipeline)
+1 -1
View File
@@ -159,7 +159,7 @@ Gateway streaming UX signals:
- `flynn companion --export-release-bundle <dir>` can emit a distributable shell bundle (bootstrap JSON + launcher + README + SHA-256 checksums) for desktop/mobile packaging pipelines. - `flynn companion --export-release-bundle <dir>` can emit a distributable shell bundle (bootstrap JSON + launcher + README + SHA-256 checksums) for desktop/mobile packaging pipelines.
- `flynn companion --export-release-bundle ... --signing-key <pem>` can additionally emit `CHECKSUMS.sha256.sig` for signed artifact verification pipelines. - `flynn companion --export-release-bundle ... --signing-key <pem>` can additionally emit `CHECKSUMS.sha256.sig` for signed artifact verification pipelines.
- `flynn companion --verify-release-bundle <dir>` can validate bundle checksums and optional signatures before installation or rollout. - `flynn companion --verify-release-bundle <dir>` can validate bundle checksums and optional signatures before installation or rollout.
- `flynn companion --export-shell-template <dir>` can emit platform starter shell templates (macOS/iOS/Android native scaffold files + bootstrap JSON) for reference app bootstrapping. - `flynn companion --export-shell-template <dir>` can emit platform starter shell templates (macOS/iOS/Android native scaffold files + bootstrap JSON), including iOS/Android runtime skeletons for registration, heartbeat/status, location/push updates, and handoff.
- `pnpm companion:bundle -- --output <dir> ...` runs a build-and-verify release pipeline for repeatable companion artifact generation. - `pnpm companion:bundle -- --output <dir> ...` runs a build-and-verify release pipeline for repeatable companion artifact generation.
- `pnpm companion:reference-apps -- --output apps/companion` regenerates in-repo macOS/iOS/Android reference app starter surfaces plus a runnable `macos-app` Swift Package menu-bar scaffold. - `pnpm companion:reference-apps -- --output apps/companion` regenerates in-repo macOS/iOS/Android reference app starter surfaces plus a runnable `macos-app` Swift Package menu-bar scaffold.
- `.github/workflows/companion-release-bundle.yml` provides CI artifact generation for companion release bundles using the same build-and-verify pipeline. - `.github/workflows/companion-release-bundle.yml` provides CI artifact generation for companion release bundles using the same build-and-verify pipeline.
@@ -26,7 +26,7 @@ If you only want the protocol surface, see `docs/api/PROTOCOL.md`.
- Companion release-bundle exports can optionally be signed (`--signing-key`, `--signing-key-id`) to emit `CHECKSUMS.sha256.sig` for distribution trust verification. - Companion release-bundle exports can optionally be signed (`--signing-key`, `--signing-key-id`) to emit `CHECKSUMS.sha256.sig` for distribution trust verification.
- Companion release bundles can be verified before install via `flynn companion --verify-release-bundle <dir>` with optional signature-key checks. - Companion release bundles can be verified before install via `flynn companion --verify-release-bundle <dir>` with optional signature-key checks.
- Companion packaging automation is available via `pnpm companion:bundle -- --output <dir> ...`, which builds and verifies the release bundle in one pass. - Companion packaging automation is available via `pnpm companion:bundle -- --output <dir> ...`, which builds and verifies the release bundle in one pass.
- Companion platform starter scaffolds can be generated via `flynn companion --export-shell-template <dir>` for macOS/iOS/Android reference app bootstrapping. - Companion platform starter scaffolds can be generated via `flynn companion --export-shell-template <dir>` for macOS/iOS/Android reference app bootstrapping, including iOS/Android runtime skeletons that issue `node.register`, `node.status.set`, `node.location.set`, `node.push_token.set`, and `agent.send`.
- Companion reference app directories can be regenerated via `pnpm companion:reference-apps -- --output apps/companion` for repo-shipped starter surfaces, including a runnable `macos-app` Swift Package menu-bar scaffold. - Companion reference app directories can be regenerated via `pnpm companion:reference-apps -- --output apps/companion` for repo-shipped starter surfaces, including a runnable `macos-app` Swift Package menu-bar scaffold.
- CI workflow `.github/workflows/companion-release-bundle.yml` mirrors this pipeline for manual artifact generation/upload. - CI workflow `.github/workflows/companion-release-bundle.yml` mirrors this pipeline for manual artifact generation/upload.
- Companion CLI supports one-shot shell bootstrap metadata for live sessions (`--app-version`/`--status-text`, `--latitude`/`--longitude`, `--push-token`) so desktop/mobile wrappers can initialize node status/location/push in a single launch flow. - Companion CLI supports one-shot shell bootstrap metadata for live sessions (`--app-version`/`--status-text`, `--latitude`/`--longitude`, `--push-token`) so desktop/mobile wrappers can initialize node status/location/push in a single launch flow.
+3 -1
View File
@@ -75,7 +75,9 @@ flynn companion \
Generated files: Generated files:
- `companion.bootstrap.json` - `companion.bootstrap.json`
- platform starter file (`CompanionBootstrap.swift`, `CompanionBootstrap.kt`, or `MenuBarCompanion.swift`) - `MenuBarCompanion.swift` (macOS)
- `CompanionBootstrap.swift` + `IOSCompanionRuntime.swift` (iOS)
- `CompanionBootstrap.kt` + `AndroidCompanionRuntime.kt` (Android)
- `README.md` - `README.md`
## Verify Bundle Integrity ## Verify Bundle Integrity
@@ -6,7 +6,7 @@ Scope: ship the remaining product-layer capabilities that make Flynn feel like a
## Rebaseline (What Is Already Done) ## Rebaseline (What Is Already Done)
Completion update (2026-02-27): all roadmap phases are now implemented, including companion app-surface packaging outputs (bootstrap export, signed/verified release bundles, reference app starter surfaces including a runnable macOS menu-bar Swift Package scaffold, and CI artifact workflow), voice reliability, browser workflow reliability, and onboarding first-success improvements. Completion update (2026-02-27): all roadmap phases are now implemented, including companion app-surface packaging outputs (bootstrap export, signed/verified release bundles, reference app starter surfaces including a runnable macOS menu-bar Swift Package scaffold plus iOS/Android runtime shell skeletons, and CI artifact workflow), voice reliability, browser workflow reliability, and onboarding first-success improvements.
The following were previously treated as gaps but are already implemented in Flynn: The following were previously treated as gaps but are already implemented in Flynn:
@@ -51,7 +51,7 @@ Within 8-10 weeks, ship a stable "Personal Assistant Mode" that supports:
2. Ship a minimal mobile companion shell (iOS + Android) for registration, status, push token, and message handoff. 2. Ship a minimal mobile companion shell (iOS + Android) for registration, status, push token, and message handoff.
3. Add signed release artifacts and installation docs. 3. Add signed release artifacts and installation docs.
Status update (2026-02-27): companion bootstrap-manifest export is now available via `flynn companion --export-bootstrap <path|->` as a packaging contract for desktop/mobile shells, `flynn companion --export-release-bundle <dir>` now emits bundle artifacts (bootstrap JSON + launcher + README + `CHECKSUMS.sha256` + `RELEASE_MANIFEST.json`, optional `CHECKSUMS.sha256.sig` with `--signing-key`), `flynn companion --verify-release-bundle <dir>` now validates checksum/signature artifacts before install, `pnpm companion:bundle -- --output <dir> ...` now provides one-pass build-and-verify automation, `.github/workflows/companion-release-bundle.yml` provides CI artifact build/verify/upload, `flynn companion --export-shell-template <dir>` now emits macOS/iOS/Android starter shell templates, `pnpm companion:reference-apps` now regenerates in-repo macOS/iOS/Android reference app starter directories plus `apps/companion/macos-app` runnable menu-bar scaffold, and `flynn companion` supports one-shot status/location/push bootstrap flags (`--app-version`, `--latitude/--longitude`, `--push-token`) so thin shells can initialize companion metadata in a single run. Status update (2026-02-27): companion bootstrap-manifest export is now available via `flynn companion --export-bootstrap <path|->` as a packaging contract for desktop/mobile shells, `flynn companion --export-release-bundle <dir>` now emits bundle artifacts (bootstrap JSON + launcher + README + `CHECKSUMS.sha256` + `RELEASE_MANIFEST.json`, optional `CHECKSUMS.sha256.sig` with `--signing-key`), `flynn companion --verify-release-bundle <dir>` now validates checksum/signature artifacts before install, `pnpm companion:bundle -- --output <dir> ...` now provides one-pass build-and-verify automation, `.github/workflows/companion-release-bundle.yml` provides CI artifact build/verify/upload, `flynn companion --export-shell-template <dir>` now emits macOS/iOS/Android starter shell templates including iOS/Android runtime skeletons for register/status/location/push/handoff flows, `pnpm companion:reference-apps` now regenerates in-repo macOS/iOS/Android reference app starter directories plus `apps/companion/macos-app` runnable menu-bar scaffold, and `flynn companion` supports one-shot status/location/push bootstrap flags (`--app-version`, `--latitude/--longitude`, `--push-token`) so thin shells can initialize companion metadata in a single run.
### Implementation Anchors ### Implementation Anchors
+12 -4
View File
@@ -7018,13 +7018,21 @@
"status": "completed", "status": "completed",
"date": "2026-02-27", "date": "2026-02-27",
"updated": "2026-02-27", "updated": "2026-02-27",
"summary": "Added platform shell-template export for reference companion apps. `flynn companion --export-shell-template <dir>` now writes platform starter scaffolds for macOS/iOS/Android (`MenuBarCompanion.swift`, `CompanionBootstrap.swift`, `CompanionBootstrap.kt`) plus bootstrap JSON and template README.", "summary": "Expanded platform shell-template export for reference companion apps. `flynn companion --export-shell-template <dir>` now writes macOS/iOS/Android starter scaffolds with iOS/Android runtime skeleton files (`IOSCompanionRuntime.swift`, `AndroidCompanionRuntime.kt`) covering register/status/location/push/handoff JSON-RPC calls, plus bootstrap JSON and template README.",
"files_modified": [ "files_modified": [
"src/companion/shellTemplate.ts", "src/companion/shellTemplate.ts",
"src/companion/shellTemplate.test.ts", "src/companion/shellTemplate.test.ts",
"src/companion/index.ts", "src/companion/index.ts",
"src/cli/companion.ts", "src/cli/companion.ts",
"src/cli/companion.test.ts", "src/cli/companion.test.ts",
"apps/companion/ios/CompanionBootstrap.swift",
"apps/companion/ios/IOSCompanionRuntime.swift",
"apps/companion/ios/README.md",
"apps/companion/ios/companion.bootstrap.json",
"apps/companion/android/CompanionBootstrap.kt",
"apps/companion/android/AndroidCompanionRuntime.kt",
"apps/companion/android/README.md",
"apps/companion/android/companion.bootstrap.json",
"README.md", "README.md",
"docs/api/PROTOCOL.md", "docs/api/PROTOCOL.md",
"docs/architecture/AGENT_DIAGRAM.md", "docs/architecture/AGENT_DIAGRAM.md",
@@ -7033,7 +7041,7 @@
"docs/plans/2026-02-26-personal-assistant-productization-plan.md", "docs/plans/2026-02-26-personal-assistant-productization-plan.md",
"docs/plans/state.json" "docs/plans/state.json"
], ],
"test_status": "pnpm test:run src/cli/companion.test.ts src/companion/shellTemplate.test.ts + pnpm typecheck passing" "test_status": "pnpm test:run src/companion/shellTemplate.test.ts src/companion/referenceApps.test.ts + pnpm typecheck passing"
}, },
"personal-assistant-productization-phase1-companion-signed-release-artifacts": { "personal-assistant-productization-phase1-companion-signed-release-artifacts": {
"status": "completed", "status": "completed",
@@ -7213,7 +7221,7 @@
"tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor", "tier2_completion": "4/4 (100%) — inbound webhooks, vector memory search, Dockerfile, heartbeat monitor",
"tier3_completion": "5/5 (100%) — lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings", "tier3_completion": "5/5 (100%) — lane queue, credential redaction, web UI token dashboard, xAI (Grok) provider, Voyage AI embeddings",
"tier4_completion": "4/4 (100%) — gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes", "tier4_completion": "4/4 (100%) — gateway lock, shell completion, Tailscale Serve/Funnel, DM pairing codes",
"feature_gap_scorecard": "rebaselined 2026-02-26 and finalized 2026-02-27 — channel breadth, setup wizard, browser workflow reliability primitives (wait/assert/extract/retries/checkpoints/guardrails/budgets), companion reconnect/runtime-handoff foundations, companion packaging/distribution primitives (bootstrap export + release-bundle artifacts + checksum manifests + release manifest artifact + optional signatures + verification mode + checksum-gated launcher + one-pass build/verify automation + CI workflow), repo-shipped macOS/iOS/Android reference app starter surfaces plus runnable macOS menu-bar Swift Package scaffold (`apps/companion/macos-app`), one-shot status/location/push shell bootstrap controls, voice reliability hardening (talk controls + TTS fallback/health + interruption-safe cancel semantics), and onboarding first-success funnel improvements are implemented.", "feature_gap_scorecard": "rebaselined 2026-02-26 and finalized 2026-02-27 — channel breadth, setup wizard, browser workflow reliability primitives (wait/assert/extract/retries/checkpoints/guardrails/budgets), companion reconnect/runtime-handoff foundations, companion packaging/distribution primitives (bootstrap export + release-bundle artifacts + checksum manifests + release manifest artifact + optional signatures + verification mode + checksum-gated launcher + one-pass build/verify automation + CI workflow), repo-shipped macOS/iOS/Android reference app starter surfaces plus runnable macOS menu-bar Swift Package scaffold (`apps/companion/macos-app`) and iOS/Android runtime shell skeletons (`IOSCompanionRuntime.swift`, `AndroidCompanionRuntime.kt`), one-shot status/location/push shell bootstrap controls, voice reliability hardening (talk controls + TTS fallback/health + interruption-safe cancel semantics), and onboarding first-success funnel improvements are implemented.",
"operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete — milestone done", "operator_dx_milestone": "Phase 3 (Live Ops Dashboard): 2/2 plans complete — milestone done",
"dashboard_observability": "completed — service health graphs + core service log viewer added to web UI via observability RPCs and bounded backend sampling", "dashboard_observability": "completed — service health graphs + core service log viewer added to web UI via observability RPCs and bounded backend sampling",
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram", "gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
@@ -7246,7 +7254,7 @@
"deeper_surfaces_phase3_companion_canvas_voice": "completed — companion reconnect resilience (auto-reconnect with backoff, pending-wait cancellation on disconnect), canvas artifact persistence (SQLite-backed store, daemon-restart durability), voice TTS fallback coverage (text-only reply on TTS failure, no dropped responses)", "deeper_surfaces_phase3_companion_canvas_voice": "completed — companion reconnect resilience (auto-reconnect with backoff, pending-wait cancellation on disconnect), canvas artifact persistence (SQLite-backed store, daemon-restart durability), voice TTS fallback coverage (text-only reply on TTS failure, no dropped responses)",
"deeper_surfaces_phase4_rollout": "completed — phase 4 rollout and operator readiness plan documented: canary rollout plan by feature flag/surface, explicit rollback playbook, operator docs and architecture/protocol docs synchronized", "deeper_surfaces_phase4_rollout": "completed — phase 4 rollout and operator readiness plan documented: canary rollout plan by feature flag/surface, explicit rollback playbook, operator docs and architecture/protocol docs synchronized",
"post_phase_test_fixes": "completed — fixed 4 test failures introduced by phases 1-3: iOS/Android push listNodes (missing publishHeartbeat before platform-filtered query), server.test agent.send (run_state events now precede done; added sendAndWaitForDone helper), httpBody 413 (req.destroy() closed socket before response could be sent; replaced with Connection: close header on 413 responses)", "post_phase_test_fixes": "completed — fixed 4 test failures introduced by phases 1-3: iOS/Android push listNodes (missing publishHeartbeat before platform-filtered query), server.test agent.send (run_state events now precede done; added sendAndWaitForDone helper), httpBody 413 (req.destroy() closed socket before response could be sent; replaced with Connection: close header on 413 responses)",
"personal_assistant_productization_plan": "completed — roadmap phases delivered: companion app-surface packaging/distribution outputs (including reference starters + CI), voice reliability, browser workflow reliability, and onboarding first-success funnel.", "personal_assistant_productization_plan": "completed — roadmap phases delivered: companion app-surface packaging/distribution outputs (including runnable/reference starters + iOS/Android runtime shell skeletons + CI), voice reliability, browser workflow reliability, and onboarding first-success funnel.",
"subagents_support": "completed — subagent phases 1-3 shipped with `subagent.spawn/send/list/cancel/delete/summary`, per-child queue mode (`followup|interrupt`), budgets (`max_turns`, `max_total_tokens`, `turn_timeout_ms`), tool-profile overrides, trace-linked audit events, `/subagents` inspection commands, and focused regression tests." "subagents_support": "completed — subagent phases 1-3 shipped with `subagent.spawn/send/list/cancel/delete/summary`, per-child queue mode (`followup|interrupt`), budgets (`max_turns`, `max_total_tokens`, `turn_timeout_ms`), tool-profile overrides, trace-linked audit events, `/subagents` inspection commands, and focused regression tests."
}, },
"soul_md_and_cron_create": { "soul_md_and_cron_create": {
+2
View File
@@ -41,6 +41,8 @@ This directory contains generated companion starter shells for:
- Android shell - Android shell
These are reference starters, not production binaries. Use them as a baseline for app packaging and distribution workflows. These are reference starters, not production binaries. Use them as a baseline for app packaging and distribution workflows.
iOS/Android starter directories include bootstrap plus runtime-skeleton files for register/status/location/push/handoff flows.
`; `;
} }
+9 -1
View File
@@ -32,18 +32,23 @@ describe('writeCompanionShellTemplate', () => {
}); });
const templateRaw = await readFile(`${outDir}/CompanionBootstrap.swift`, 'utf8'); const templateRaw = await readFile(`${outDir}/CompanionBootstrap.swift`, 'utf8');
const runtimeRaw = await readFile(`${outDir}/IOSCompanionRuntime.swift`, 'utf8');
const manifestRaw = await readFile(`${outDir}/companion.bootstrap.json`, 'utf8'); const manifestRaw = await readFile(`${outDir}/companion.bootstrap.json`, 'utf8');
const readmeRaw = await readFile(`${outDir}/README.md`, 'utf8'); const readmeRaw = await readFile(`${outDir}/README.md`, 'utf8');
expect(result.files).toEqual([ expect(result.files).toEqual([
`${outDir}/companion.bootstrap.json`, `${outDir}/companion.bootstrap.json`,
`${outDir}/CompanionBootstrap.swift`, `${outDir}/CompanionBootstrap.swift`,
`${outDir}/IOSCompanionRuntime.swift`,
`${outDir}/README.md`, `${outDir}/README.md`,
]); ]);
expect(templateRaw).toContain('struct CompanionBootstrap: Codable'); expect(templateRaw).toContain('struct CompanionBootstrap: Codable');
expect(templateRaw).toContain('node.push_token.set'); expect(runtimeRaw).toContain('final class IOSCompanionRuntime');
expect(runtimeRaw).toContain('"node.push_token.set"');
expect(runtimeRaw).toContain('"agent.send"');
expect(JSON.parse(manifestRaw)).toMatchObject({ node: { nodeId: 'test-node' } }); expect(JSON.parse(manifestRaw)).toMatchObject({ node: { nodeId: 'test-node' } });
expect(readmeRaw).toContain('Shell Template'); expect(readmeRaw).toContain('Shell Template');
expect(readmeRaw).toContain('IOSCompanionRuntime.swift');
await rm(tempDir, { recursive: true, force: true }); await rm(tempDir, { recursive: true, force: true });
}); });
@@ -65,8 +70,11 @@ describe('writeCompanionShellTemplate', () => {
}); });
const androidTemplate = await readFile(`${androidDir}/CompanionBootstrap.kt`, 'utf8'); const androidTemplate = await readFile(`${androidDir}/CompanionBootstrap.kt`, 'utf8');
const androidRuntime = await readFile(`${androidDir}/AndroidCompanionRuntime.kt`, 'utf8');
const macosTemplate = await readFile(`${macosDir}/MenuBarCompanion.swift`, 'utf8'); const macosTemplate = await readFile(`${macosDir}/MenuBarCompanion.swift`, 'utf8');
expect(androidTemplate).toContain('data class CompanionBootstrap'); expect(androidTemplate).toContain('data class CompanionBootstrap');
expect(androidRuntime).toContain('class AndroidCompanionRuntime');
expect(androidRuntime).toContain("\"node.register\"");
expect(macosTemplate).toContain('launchFlynnCompanion'); expect(macosTemplate).toContain('launchFlynnCompanion');
await rm(tempDir, { recursive: true, force: true }); await rm(tempDir, { recursive: true, force: true });
+350 -25
View File
@@ -15,7 +15,12 @@ export interface WriteCompanionShellTemplateResult {
files: string[]; files: string[];
} }
function swiftTemplate(manifest: CompanionBootstrapManifest): string { interface TemplateArtifact {
filename: string;
body: string;
}
function macosTemplate(manifest: CompanionBootstrapManifest): string {
return `import Foundation return `import Foundation
struct CompanionBootstrap: Codable { struct CompanionBootstrap: Codable {
@@ -57,7 +62,7 @@ func launchFlynnCompanion() throws {
`; `;
} }
function iosTemplate(manifest: CompanionBootstrapManifest): string { function iosBootstrapTemplate(manifest: CompanionBootstrapManifest): string {
return `import Foundation return `import Foundation
// Reference iOS bootstrap model for integrating with Flynn gateway runtime. // Reference iOS bootstrap model for integrating with Flynn gateway runtime.
@@ -93,7 +98,176 @@ struct Runtime: Codable {
`; `;
} }
function androidTemplate(manifest: CompanionBootstrapManifest): string { function iosRuntimeTemplate(manifest: CompanionBootstrapManifest): string {
return `import Foundation
enum CompanionRuntimeError: Error {
case invalidGatewayURL
case notConnected
case encodingFailure
}
private struct JsonRpcRequest<Params: Encodable>: Encodable {
let jsonrpc = "2.0"
let id: Int
let method: String
let params: Params
}
struct CompanionStatusPayload: Encodable {
let platform: String
let appVersion: String?
let statusText: String?
let batteryPct: Double?
}
struct CompanionLocationPayload: Encodable {
let latitude: Double
let longitude: Double
let source: String
}
private struct NodeRegisterParams: Encodable {
let nodeId: String
let role: String
let platform: String
let capabilities: [String]
}
private struct NodeStatusSetParams: Encodable {
let nodeId: String
let status: CompanionStatusPayload
}
private struct NodeLocationSetParams: Encodable {
let nodeId: String
let location: CompanionLocationPayload
}
private struct NodePushTokenSetParams: Encodable {
let nodeId: String
let provider: String
let token: String
let topic: String?
let environment: String?
}
private struct AgentSendParams: Encodable {
let message: String
let sessionId: String?
let awaitResponse: Bool
}
final class IOSCompanionRuntime {
private let bootstrap: CompanionBootstrap
private let session: URLSession
private var socket: URLSessionWebSocketTask?
private var nextRequestId = 1
private var heartbeatLoop: Task<Void, Never>?
init(bootstrap: CompanionBootstrap, session: URLSession = .shared) {
self.bootstrap = bootstrap
self.session = session
}
func connect() throws {
guard socket == nil else { return }
guard let url = URL(string: bootstrap.gateway.url) else {
throw CompanionRuntimeError.invalidGatewayURL
}
var request = URLRequest(url: url)
if let token = bootstrap.gateway.token, !token.isEmpty {
request.setValue("Bearer \\(token)", forHTTPHeaderField: "Authorization")
}
let task = session.webSocketTask(with: request)
task.resume()
socket = task
}
func disconnect() {
heartbeatLoop?.cancel()
heartbeatLoop = nil
socket?.cancel(with: .goingAway, reason: nil)
socket = nil
}
func registerNode() async throws {
try await sendRpc(
method: "node.register",
params: NodeRegisterParams(
nodeId: bootstrap.node.nodeId,
role: bootstrap.node.role,
platform: bootstrap.node.platform,
capabilities: bootstrap.node.capabilities
)
)
}
func publishStatus(_ status: CompanionStatusPayload) async throws {
try await sendRpc(
method: "node.status.set",
params: NodeStatusSetParams(nodeId: bootstrap.node.nodeId, status: status)
)
}
func publishLocation(_ location: CompanionLocationPayload) async throws {
try await sendRpc(
method: "node.location.set",
params: NodeLocationSetParams(nodeId: bootstrap.node.nodeId, location: location)
)
}
func registerPushToken(token: String, topic: String? = nil, environment: String? = nil) async throws {
try await sendRpc(
method: "node.push_token.set",
params: NodePushTokenSetParams(
nodeId: bootstrap.node.nodeId,
provider: "apns",
token: token,
topic: topic,
environment: environment
)
)
}
func sendHandoff(message: String, sessionId: String? = nil) async throws {
try await sendRpc(
method: "agent.send",
params: AgentSendParams(message: message, sessionId: sessionId, awaitResponse: true)
)
}
func startHeartbeatLoop(statusFactory: @escaping () -> CompanionStatusPayload) {
heartbeatLoop?.cancel()
let interval = UInt64(max(bootstrap.runtime.heartbeatSeconds, 1)) * 1_000_000_000
heartbeatLoop = Task { [weak self] in
while !Task.isCancelled {
guard let self else { return }
try? await self.publishStatus(statusFactory())
try? await Task.sleep(nanoseconds: interval)
}
}
}
private func sendRpc<Params: Encodable>(method: String, params: Params) async throws {
guard let socket else {
throw CompanionRuntimeError.notConnected
}
let request = JsonRpcRequest(id: nextRequestId, method: method, params: params)
nextRequestId += 1
let data = try JSONEncoder().encode(request)
guard let text = String(data: data, encoding: .utf8) else {
throw CompanionRuntimeError.encodingFailure
}
try await socket.send(.string(text))
}
}
// Generated for node: ${manifest.node.nodeId} (${manifest.node.platform})
`;
}
function androidBootstrapTemplate(manifest: CompanionBootstrapManifest): string {
return `package flynn.companion return `package flynn.companion
// Reference Android bootstrap model for integrating with Flynn gateway runtime. // Reference Android bootstrap model for integrating with Flynn gateway runtime.
@@ -129,41 +303,182 @@ data class Runtime(
`; `;
} }
function templateFilename(platform: CompanionShellTemplatePlatform): string { function androidRuntimeTemplate(manifest: CompanionBootstrapManifest): string {
if (platform === 'macos') { return `package flynn.companion
return 'MenuBarCompanion.swift';
} import java.util.concurrent.Executors
if (platform === 'ios') { import java.util.concurrent.ScheduledFuture
return 'CompanionBootstrap.swift'; import java.util.concurrent.TimeUnit
} import org.json.JSONArray
return 'CompanionBootstrap.kt'; import org.json.JSONObject
interface CompanionTransport {
fun connect(url: String, token: String?)
fun disconnect()
fun send(rawMessage: String)
} }
function templateBody( data class CompanionStatusPayload(
val platform: String,
val appVersion: String? = null,
val statusText: String? = null,
val batteryPct: Double? = null
)
data class CompanionLocationPayload(
val latitude: Double,
val longitude: Double,
val source: String = "device"
)
class AndroidCompanionRuntime(
private val bootstrap: CompanionBootstrap,
private val transport: CompanionTransport
) {
private var nextRequestId: Int = 1
private val scheduler = Executors.newSingleThreadScheduledExecutor()
private var heartbeatFuture: ScheduledFuture<*>? = null
fun connect() {
transport.connect(bootstrap.gateway.url, bootstrap.gateway.token)
}
fun disconnect() {
heartbeatFuture?.cancel(true)
heartbeatFuture = null
transport.disconnect()
}
fun registerNode() {
val params = JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("role", bootstrap.node.role)
.put("platform", bootstrap.node.platform)
.put("capabilities", JSONArray(bootstrap.node.capabilities))
sendRpc("node.register", params)
}
fun publishStatus(status: CompanionStatusPayload) {
val statusJson = JSONObject()
.put("platform", status.platform)
status.appVersion?.let { statusJson.put("appVersion", it) }
status.statusText?.let { statusJson.put("statusText", it) }
status.batteryPct?.let { statusJson.put("batteryPct", it) }
sendRpc(
"node.status.set",
JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("status", statusJson)
)
}
fun publishLocation(location: CompanionLocationPayload) {
sendRpc(
"node.location.set",
JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put(
"location",
JSONObject()
.put("latitude", location.latitude)
.put("longitude", location.longitude)
.put("source", location.source)
)
)
}
fun registerPushToken(token: String, topic: String? = null) {
val params = JSONObject()
.put("nodeId", bootstrap.node.nodeId)
.put("provider", "fcm")
.put("token", token)
topic?.let { params.put("topic", it) }
sendRpc("node.push_token.set", params)
}
fun sendHandoff(message: String, sessionId: String? = null) {
val params = JSONObject()
.put("message", message)
.put("awaitResponse", true)
sessionId?.let { params.put("sessionId", it) }
sendRpc("agent.send", params)
}
fun startHeartbeatLoop(statusFactory: () -> CompanionStatusPayload) {
heartbeatFuture?.cancel(true)
val intervalSeconds = bootstrap.runtime.heartbeatSeconds.coerceAtLeast(1).toLong()
heartbeatFuture = scheduler.scheduleAtFixedRate(
{ publishStatus(statusFactory()) },
0L,
intervalSeconds,
TimeUnit.SECONDS
)
}
private fun sendRpc(method: String, params: JSONObject) {
val request = JSONObject()
.put("jsonrpc", "2.0")
.put("id", nextRequestId++)
.put("method", method)
.put("params", params)
transport.send(request.toString())
}
}
// Generated for node: ${manifest.node.nodeId} (${manifest.node.platform})
`;
}
function templateArtifacts(
platform: CompanionShellTemplatePlatform, platform: CompanionShellTemplatePlatform,
manifest: CompanionBootstrapManifest, manifest: CompanionBootstrapManifest,
): string { ): TemplateArtifact[] {
if (platform === 'macos') { if (platform === 'macos') {
return swiftTemplate(manifest); return [{ filename: 'MenuBarCompanion.swift', body: macosTemplate(manifest) }];
} }
if (platform === 'ios') { if (platform === 'ios') {
return iosTemplate(manifest); return [
{ filename: 'CompanionBootstrap.swift', body: iosBootstrapTemplate(manifest) },
{ filename: 'IOSCompanionRuntime.swift', body: iosRuntimeTemplate(manifest) },
];
} }
return androidTemplate(manifest); return [
{ filename: 'CompanionBootstrap.kt', body: androidBootstrapTemplate(manifest) },
{ filename: 'AndroidCompanionRuntime.kt', body: androidRuntimeTemplate(manifest) },
];
} }
function readmeBody(platform: CompanionShellTemplatePlatform): string { function platformArticle(platform: CompanionShellTemplatePlatform): 'a' | 'an' {
return `# Flynn Companion ${platform} Shell Template if (platform === 'ios' || platform === 'android') {
return 'an';
}
return 'a';
}
This directory contains a generated starter template for a ${platform} companion shell. function displayPlatformName(platform: CompanionShellTemplatePlatform): string {
if (platform === 'macos') {
return 'macOS';
}
if (platform === 'ios') {
return 'iOS';
}
return 'Android';
}
function readmeBody(platform: CompanionShellTemplatePlatform, templateFiles: string[]): string {
const platformName = displayPlatformName(platform);
const templateList = templateFiles.map((name) => `- \`${name}\``).join('\n');
return `# Flynn Companion ${platformName} Shell Template
This directory contains a generated starter template for ${platformArticle(platform)} ${platformName} companion shell.
Files: Files:
- \`companion.bootstrap.json\`: resolved Flynn companion bootstrap contract - \`companion.bootstrap.json\`: resolved Flynn companion bootstrap contract
- \`${templateFilename(platform)}\`: platform-native starter model/wrapper snippet ${templateList}
Notes: Notes:
- These templates are intentionally minimal and should be integrated into your app project. - These templates are intentionally minimal and should be integrated into your app project.
- Runtime transport should use Flynn gateway JSON-RPC node methods (\`node.register\`, \`node.status.set\`, \`node.location.set\`, \`node.push_token.set\`). - Runtime transport should use Flynn gateway JSON-RPC node methods (\`node.register\`, \`node.status.set\`, \`node.location.set\`, \`node.push_token.set\`, \`agent.send\`).
`; `;
} }
@@ -172,16 +487,26 @@ export async function writeCompanionShellTemplate(
): Promise<WriteCompanionShellTemplateResult> { ): Promise<WriteCompanionShellTemplateResult> {
await mkdir(input.outputDir, { recursive: true }); await mkdir(input.outputDir, { recursive: true });
const manifestPath = `${input.outputDir}/companion.bootstrap.json`; const manifestPath = `${input.outputDir}/companion.bootstrap.json`;
const templatePath = `${input.outputDir}/${templateFilename(input.platform)}`;
const readmePath = `${input.outputDir}/README.md`; const readmePath = `${input.outputDir}/README.md`;
const artifacts = templateArtifacts(input.platform, input.manifest);
const artifactPaths = artifacts.map((artifact) => `${input.outputDir}/${artifact.filename}`);
await writeFile(manifestPath, `${JSON.stringify(input.manifest, null, 2)}\n`, 'utf8'); await writeFile(manifestPath, `${JSON.stringify(input.manifest, null, 2)}\n`, 'utf8');
await writeFile(templatePath, templateBody(input.platform, input.manifest), 'utf8'); for (let i = 0; i < artifacts.length; i += 1) {
await writeFile(readmePath, readmeBody(input.platform), 'utf8'); await writeFile(artifactPaths[i], artifacts[i].body, 'utf8');
}
await writeFile(
readmePath,
readmeBody(
input.platform,
artifacts.map((artifact) => artifact.filename),
),
'utf8',
);
return { return {
outputDir: input.outputDir, outputDir: input.outputDir,
platform: input.platform, platform: input.platform,
files: [manifestPath, templatePath, readmePath], files: [manifestPath, ...artifactPaths, readmePath],
}; };
} }