A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: resolved packaging pi issues + refactor

- pi is bun binary so has JIT so need to explicitly tell apple
to allow JIT via entitlments, else we can dynamic memory allocation error
while executing Pi

+153 -85
+9
entitleme.plist
··· 1 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" 2 + "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>com.apple.security.cs.allow-jit</key> 6 + <true/> 7 + </dict> 8 + </plist> 9 +
+25
pkg/build.sh
··· 43 43 --strict \ 44 44 "${PKG_CLI_BIN_PATH}/tiles" 45 45 46 + 47 + echo "Embedding Pi" 48 + # Copying pi artifacts into extracted pi folder 49 + cp pi-darwin-arm64.tar.gz "${PKG_LIBS_PATH}" 50 + 51 + tar -xvf "${PKG_LIBS_PATH}/pi-darwin-arm64.tar.gz" -C "${PKG_LIBS_PATH}" 52 + 53 + rm "${PKG_LIBS_PATH}/pi-darwin-arm64.tar.gz" 54 + 55 + # removing unnecessary files 56 + # rm -rf "${DIST_DIR}/tmp/pi/examples" 57 + 58 + # Signing the pi binary 59 + 60 + echo "Signing Pi binary..." 61 + 62 + codesign --force \ 63 + --sign "$DEVELOPER_ID_APPLICATION" \ 64 + --options runtime \ 65 + --timestamp \ 66 + --entitlements entitleme.plist \ 67 + --strict \ 68 + "${PKG_LIBS_PATH}/pi/pi" 69 + 70 + 46 71 # Build venvstack and move to /usr/local/share/tiles 47 72 # 48 73 # flushing this folder, else the final zip will have previous app-server zips too (#84)
+1
pkg/bundle_network_installer.sh
··· 14 14 # signing 15 15 productsign \ 16 16 --sign "$DEVELOPER_ID_INSTALLER" \ 17 + --entitlements entitleme.plist \ 17 18 pkg/tiles-dist-unsigned.pkg \ 18 19 pkg/tiles.pkg 19 20
+30 -38
scripts/bundler.sh
··· 16 16 OS=$(uname -s | tr '[:upper:]' '[:lower:]') 17 17 ARCH=$(uname -m) 18 18 19 - # Final tar.gz name 19 + # Name for final tar.gz 20 + 20 21 OUT_NAME="${BINARY_NAME}-v${VERSION}-${ARCH}-${OS}" 21 22 22 23 echo "🚀 Building ${BINARY_NAME} (${TARGET} mode)..." ··· 24 25 cargo build -p tiles --${TARGET} 25 26 26 27 27 - # Destination where the release build is generated 28 + # Destination where the release binary is generated 28 29 CLI_BIN_PATH="target/${TARGET}/${BINARY_NAME}" 29 30 30 31 chmod +x "${CLI_BIN_PATH}" 31 32 32 - # echo "Signing the Tiles binary..." 33 - 34 - # # Signing the tiles binary 35 - # codesign --force \ 36 - # --sign "$DEVELOPER_ID_APPLICATION"\ 37 - # --options runtime \ 38 - # --timestamp \ 39 - # --strict \ 40 - # "${CLI_BIN_PATH}" 41 - 42 - # # echo "Notarizing Tiles binary..." 33 + echo "Signing the Tiles binary..." 43 34 44 - # # notarizing the tiles binary 45 - # xcrun notarytool submit --force "${CLI_BIN_PATH}" \ 46 - # --keychain-profile "tiles-notary-profile" \ 47 - # --wait 35 + # Signing the tiles binary 36 + codesign --force \ 37 + --sign "$DEVELOPER_ID_APPLICATION"\ 38 + --options runtime \ 39 + --timestamp \ 40 + --strict \ 41 + "${CLI_BIN_PATH}" 48 42 49 43 50 44 mkdir -p "${DIST_DIR}/tmp" ··· 60 54 rm "${DIST_DIR}/tmp/pi-darwin-arm64.tar.gz" 61 55 62 56 # removing unnecessary files 63 - rm -rf "${DIST_DIR}/tmp/pi/examples" 57 + # rm -rf "${DIST_DIR}/tmp/pi/examples" 64 58 65 59 # Signing the pi binary 66 - # codesign --force \ 67 - # --sign "$DEVELOPER_ID_APPLICATION"\ 68 - # --options runtime \ 69 - # --timestamp \ 70 - # --strict \ 71 - # "${DIST_DIR}/tmp/pi/pi" 60 + 61 + echo "Signing Pi binary..." 72 62 73 - # echo "Notarizing Pibinary..." 63 + codesign --force \ 64 + --sign "$DEVELOPER_ID_APPLICATION" \ 65 + --options runtime \ 66 + --timestamp \ 67 + --entitlements entitleme.plist \ 68 + --strict \ 69 + "${DIST_DIR}/tmp/pi/pi" 74 70 75 - # # notarizing the pi binary 76 - # xcrun notarytool submit --force "${DIST_DIR}/tmp/pi/pi" \ 77 - # --keychain-profile "tiles-notary-profile" \ 78 - # --wait 79 71 80 72 # flushing this folder, else the final zip will have previous app-server zips too (#84) 81 - # rm -rf "${SERVER_DIR}/stack_export_prod" 73 + rm -rf "${SERVER_DIR}/stack_export_prod" 82 74 83 - # echo "🔒 Locking the venvstack...." 75 + echo "🔒 Locking the venvstack...." 84 76 85 - # venvstacks lock server/stack/venvstacks.toml 77 + venvstacks lock server/stack/venvstacks.toml 86 78 87 - # echo "🛠️ Building the venvstack...." 79 + echo "🛠️ Building the venvstack...." 88 80 89 - # venvstacks build server/stack/venvstacks.toml 81 + venvstacks build server/stack/venvstacks.toml 90 82 91 - # echo "📦 Publishing the venvstack...." 83 + echo "📦 Publishing the venvstack...." 92 84 93 - # venvstacks publish --tag-outputs --output-dir ../stack_export_prod server/stack/venvstacks.toml 85 + venvstacks publish --tag-outputs --output-dir ../stack_export_prod server/stack/venvstacks.toml 94 86 95 87 cp -r "${SERVER_DIR}" "${DIST_DIR}/tmp/" 96 88 97 89 rm -rf "${DIST_DIR}/tmp/server/__pycache__" 98 - rm -rf "${DIST_DIR}/tmp/server/mem_agent/__pycache__" 99 - rm -rf "${DIST_DIR}/tmp/server/backend/__pycache__" 90 + # rm -rf "${DIST_DIR}/tmp/server/mem_agent/__pycache__" 91 + # rm -rf "${DIST_DIR}/tmp/server/backend/__pycache__" 100 92 rm -rf "${DIST_DIR}/tmp/server/.venv" 101 93 rm -rf "${DIST_DIR}/tmp/server/stack" 102 94
+2 -4
scripts/install.sh
··· 14 14 PI_DIR="/usr/local/share/tiles/pi" 15 15 16 16 TMPDIR="$(mktemp -d)" 17 + 17 18 OS=$(uname -s | tr '[:upper:]' '[:lower:]') 18 19 ARCH=$(uname -m) 19 20 ··· 57 58 58 59 mkdir -p "${PI_DIR}" 59 60 60 - cp -r "${TMPDIR}/pi-darwin-arm64.tar.gz" "${PI_DIR}/" 61 + cp -r "${TMPDIR}/pi"/* "${PI_DIR}/" 61 62 62 - tar -xvf "${PI_DIR}/pi-darwin-arm64.tar.gz" 63 - 64 - rm "${PI_DIR}/pi-darwin-arm64.tar.gz" 65 63 66 64 log "📦 Installing Python server to ${SERVER_DIR}..." 67 65 rm -rf "${SERVER_DIR}"
-1
scripts/tar.exclude
··· 5 5 .env 6 6 .git 7 7 .DS_Store 8 -
+21 -5
server/api.py
··· 34 34 35 35 @app.get("/ping") 36 36 async def ping(): 37 - return {"message": "Badda-Bing Badda-Bang"} 37 + return {"message": "Welcome to the jungle"} 38 38 39 39 40 40 @app.post("/start") ··· 78 78 raise HTTPException(status_code=500, detail=str(e)) 79 79 80 80 81 - @app.exception_handler(RequestValidationError) 82 - async def validation_exception_handler(request: Request, exc: RequestValidationError): 81 + @app.exception_handler(HTTPException) 82 + async def validation_exception_handler(request: Request, exc: HTTPException): 83 83 return JSONResponse( 84 - status_code=422, 85 - content={"detail": exc.errors()}, 84 + status_code=exc.status_code, 85 + content={"detail": exc.detail}, 86 86 ) 87 87 88 88 89 + @app.middleware("http") 90 + async def catch_all(request, call_next): 91 + try: 92 + return await call_next(request) 93 + except Exception as e: 94 + print("UNCAUGHT:", repr(e)) 95 + raise 96 + 97 + 89 98 @app.post("/v1/responses") 90 99 async def create_chat_response(request: ResponsesRequest): 91 100 """ 92 101 Create a response with openResponses format 93 102 """ 103 + 104 + try: 105 + ResponsesRequest.model_validate(request) 106 + except Exception as e: 107 + print(e) 108 + 109 + print(f"REQUEST => {request}") 94 110 95 111 if request.stream: 96 112 return StreamingResponse(
+8 -4
server/backend/mlx.py
··· 29 29 30 30 from ..cache_utils import get_model_path 31 31 from ..schemas import ( 32 + CAssistantMessageItemParam, 33 + CDeveloperMessageItemParam, 34 + CSystemMessageItemParam, 35 + CUserMessageItemParam, 32 36 ChatCompletionRequest, 33 37 ChatMessage, 34 38 GenerationMetrics, ··· 721 725 for item in convos: 722 726 print(f"ITEM {item}") 723 727 match item: 724 - case UserMessageItemParam(): 728 + case CUserMessageItemParam(): 725 729 content = "" 726 730 if isinstance(item.content, list): 727 731 content = item.content[0].text ··· 730 734 convo_list.append( 731 735 Message.from_role_and_content(Role.USER, content) # pyright: ignore 732 736 ) 733 - case DeveloperMessageItemParam(): 737 + case CDeveloperMessageItemParam(): 734 738 convo_list.append( 735 739 Message.from_role_and_content( 736 740 Role.DEVELOPER, ··· 739 743 ), # pyright: ignore ) 740 744 ) 741 745 ) 742 - case AssistantMessageItemParam(): 746 + case CAssistantMessageItemParam(): 743 747 convo_list.append( 744 748 Message.from_role_and_content( 745 749 Role.ASSISTANT, item.content.root 746 750 ) # pyright: ignore 747 751 ) 748 - case SystemMessageItemParam(): 752 + case CSystemMessageItemParam(): 749 753 convo_list.append( 750 754 Message.from_role_and_content(Role.SYSTEM, item.content.root) 751 755 )
+1 -1
server/pyproject.toml
··· 21 21 exclude = ["backend", "backend.*"] 22 22 23 23 [tool.uv] 24 - exclude-newer="10 days" 24 + exclude-newer="2026-04-05T09:43:13.300458+00:00"
+31 -7
server/schemas.py
··· 1 1 from dataclasses import dataclass 2 2 from enum import Enum, auto 3 - from typing import Any, Dict, List, Union, override 3 + from typing import Any, Dict, List, Literal, Union, override 4 4 5 5 from openresponses_types import ReasoningParam, TruncationEnum 6 6 from openresponses_types.types import ( ··· 88 88 model: str 89 89 90 90 91 + class CUserMessageItemParam(UserMessageItemParam): 92 + type: Literal["message"] = "message" 93 + 94 + 95 + class CSystemMessageItemParam(SystemMessageItemParam): 96 + type: Literal["message"] = "message" 97 + 98 + 99 + class CDeveloperMessageItemParam(DeveloperMessageItemParam): 100 + type: Literal["message"] = "message" 101 + 102 + 103 + class CAssistantMessageItemParam(AssistantMessageItemParam): 104 + type: Literal["message"] = "message" 105 + 106 + 107 + class CFunctionCallItemParam(FunctionCallItemParam): 108 + type: Literal["function_call"] = "function_call" 109 + 110 + 111 + class CFunctionCallOutputItemParam(FunctionCallOutputItemParam): 112 + type: Literal["function_call_output"] = "function_call_output" 113 + 114 + 91 115 class ResponsesRequest(BaseModel): 92 116 model: str = "mlx-community/gpt-oss-20b-MXFP4-Q4" 93 117 input: ( ··· 95 119 | list[ 96 120 ItemReferenceParam 97 121 | ReasoningItemParam 98 - | UserMessageItemParam 99 - | SystemMessageItemParam 100 - | DeveloperMessageItemParam 101 - | AssistantMessageItemParam 102 - | FunctionCallItemParam 103 - | FunctionCallOutputItemParam 122 + | CUserMessageItemParam 123 + | CSystemMessageItemParam 124 + | CDeveloperMessageItemParam 125 + | CAssistantMessageItemParam 126 + | CFunctionCallItemParam 127 + | CFunctionCallOutputItemParam 104 128 ] 105 129 ) 106 130 reasoning: ReasoningParam = ReasoningParam(
+1 -1
server/stack/requirements/app-server/pylock.app-server.meta.json
··· 1 1 { 2 - "lock_input_hash": "sha256:d7a2f2a68301eb65d01fc121715ef8b96a6af48a315d580340d976fde6e1dc47", 2 + "lock_input_hash": "sha256:3c4cafa1e5147c3de588c39652d8ba0046be8d1bea5ae8a547a7bb8b1bb9e734", 3 3 "lock_version": 1, 4 4 "locked_at": "2026-04-05T09:43:13.300458+00:00", 5 5 "other_inputs_hash": "sha256:63b3c2cfe2ec414938e81dace7aac779c7b902bae681618cd8827e9f16880985",
+1
server/stack/requirements/app-server/requirements-app-server.in
··· 6 6 black==25.9.0 7 7 openai-harmony==0.0.8 8 8 openresponses-types 9 + httpx==0.28.1
+4 -1
server/stack/venvstacks.toml
··· 27 27 "mlx-lm==0.31.0", 28 28 "black==25.9.0", 29 29 "openai-harmony==0.0.8", 30 - "openresponses-types" 30 + "openresponses-types", 31 + "httpx==0.28.1" 31 32 ] 32 33 33 34 [tool.uv] 35 + exclude-newer="2026-04-05T09:43:13.300458+00:00" 36 + 34 37 # Only resolve for the relevant target platforms 35 38 environments = [ 36 39 "sys_platform == 'darwin' and platform_machine == 'arm64'",
+2 -5
tiles/src/core/chats.rs
··· 154 154 155 155 let session_id: String = session_id_db.unwrap_or("".to_owned()); 156 156 157 - if session_id.len() > 0 && !session_map.contains_key(&session_id) { 157 + if !session_id.is_empty() && !session_map.contains_key(&session_id) { 158 158 // lets fetch the session details 159 159 match fetch_session(conn, &session_id) { 160 160 Ok(session) => { ··· 187 187 188 188 let sessions: Vec<Session> = session_map.into_values().collect(); 189 189 190 - Ok(DeltaChat { 191 - chats: chats, 192 - sessions: sessions, 193 - }) 190 + Ok(DeltaChat { chats, sessions }) 194 191 } 195 192 196 193 pub fn apply_delta(chat_conn: &mut Connection, delta_chats: DeltaChat) -> Result<()> {
+16 -17
tiles/src/runtime/mlx.rs
··· 1 - use crate::core::accounts::{User, get_current_user}; 1 + use crate::core::accounts::get_current_user; 2 2 use crate::core::chats::{Message, create_session, save_chat}; 3 3 use crate::core::storage::db::Dbconn; 4 4 use crate::runtime::RunArgs; ··· 37 37 total_latency_s: f64, 38 38 } 39 39 40 + #[allow(dead_code)] 40 41 impl BenchmarkMetrics { 41 42 fn update(&mut self, metrics: BenchmarkMetrics) -> &Self { 42 43 if self.ttft_ms == 0.0 { ··· 323 324 async fn start_repl( 324 325 mlx_runtime: &MLXRuntime, 325 326 modelfile: &Modelfile, 326 - run_args: &RunArgs, 327 + _run_args: &RunArgs, 327 328 db_conn: &Dbconn, 328 329 ) -> Result<()> { 329 330 let modelname = modelfile ··· 337 338 let config = Config::builder().auto_add_history(true).build(); 338 339 let mut editor = Editor::<TilesHinter, DefaultHistory>::with_config(config).unwrap(); 339 340 editor.set_helper(Some(TilesHinter)); 340 - let mut g_reply: String = "".to_owned(); 341 - let mut prev_response_id: String = String::from(""); 341 + // let mut g_reply: String = "".to_owned(); 342 + // let mut prev_response_id: String = String::from(""); 342 343 343 - let mut conversations: Vec<Message> = vec![]; 344 + // let mut conversations: Vec<Message> = vec![]; 344 345 345 346 let mut pi_process = start_pi_rpc(&modelname)?; 346 347 let mut session_id = String::new(); ··· 361 362 let state: GetStateData = 362 363 serde_json::from_value(msg.data.expect("get state parsing failed"))?; 363 364 session_id = state.session_id; 364 - info!("Current session: {}", session_id); 365 365 } 366 366 367 367 loop { ··· 432 432 } 433 433 } 434 434 435 - let mut remaining_count = run_args.relay_count; 436 - let mut python_code: String = "".to_owned(); 437 - let mut bench_metrics: BenchmarkMetrics = BenchmarkMetrics { 438 - ttft_ms: 0.0, 439 - total_tokens: 0, 440 - tokens_per_second: 0.0, 441 - total_latency_s: 0.0, 442 - }; 443 - let mut is_agent_streaming: bool = false; 435 + // let mut bench_metrics: BenchmarkMetrics = BenchmarkMetrics { 436 + // ttft_ms: 0.0, 437 + // total_tokens: 0, 438 + // tokens_per_second: 0.0, 439 + // total_latency_s: 0.0, 440 + // }; 444 441 let reader = BufReader::new(&mut stdout); 445 442 let mut session_turn_count = 0; 446 443 let mut last_chat_id: String = "".to_owned(); ··· 472 469 // first time 473 470 if session_turn_count == 1 { 474 471 info!("Created session {}", session_id); 475 - create_session(&db_conn.chat, &session_id, "dummy", &current_user.user_id)?; 472 + create_session(&db_conn.chat, &session_id, &input, &current_user.user_id)?; 476 473 } 477 474 let parent_chat_id = if session_turn_count == 1 { 478 475 None ··· 513 510 if response_msg.success { 514 511 match response_msg.command { 515 512 CommandType::Unknown => { 516 - println!("{}", line); 517 513 continue; 518 514 } 519 515 cmd => process_command(cmd, response_msg.data)?, ··· 819 815 // } 820 816 // } 821 817 818 + #[allow(dead_code)] 822 819 fn extract_python(content: &str) -> String { 823 820 if content.contains("<python>") && content.contains("</python>") { 824 821 let list_a = content.split("<python>").collect::<Vec<&str>>(); ··· 853 850 } 854 851 } 855 852 853 + //TODO: Deprecated if not needed 854 + #[allow(dead_code)] 856 855 fn create_chat_input(input: &str, prompt: &str, conversations: &[Message]) -> Vec<Message> { 857 856 let dev_msg = Message { 858 857 r#type: "message".to_owned(),
+1 -1
tiles/src/utils/config.rs
··· 443 443 444 444 #[allow(dead_code)] 445 445 fn try_update_pi_provider_model(config: &str, model_name: &str) -> Result<String> { 446 - let mut pi_model_config: PiModelConfig = serde_json::from_str(&config)?; 446 + let mut pi_model_config: PiModelConfig = serde_json::from_str(config)?; 447 447 let mut tiles_provider_config: PiProviderConfig = pi_model_config 448 448 .providers 449 449 .get("tiles")