Merge remote-tracking branch 'origin/main' into jackamadeo/fix-auto-updating

This commit is contained in:
Jack Amadeo
2025-10-16 16:23:05 -04:00
23 changed files with 372 additions and 761 deletions
+10 -10
View File
@@ -192,10 +192,10 @@ jobs:
source_job_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
unsigned_url="s3://block-goose-artifacts-bucket-production/unsigned/goose-${GITHUB_SHA}-${{ github.run_id }}-intel.zip"
zip -q -u -r out/goose-darwin-x64/goose_intel_mac.zip entitlements.plist
zip -q -u -r out/Goose-darwin-x64/Goose_intel_mac.zip entitlements.plist
# upload unsigned goose to transfer bucket so it can be passed to lambda
aws s3 cp --quiet out/goose-darwin-x64/goose_intel_mac.zip "${unsigned_url}"
aws s3 cp --quiet out/Goose-darwin-x64/Goose_intel_mac.zip "${unsigned_url}"
# begin signing
echo "🚀 launching signing process"
@@ -247,7 +247,7 @@ jobs:
# download the signed app from S3
echo "⬇️ downloading signed app"
aws s3 cp --quiet "${signed_url}" out/goose-darwin-x64/goose_intel_mac.zip
aws s3 cp --quiet "${signed_url}" out/Goose-darwin-x64/Goose_intel_mac.zip
working-directory: ui/desktop
- name: Final cleanup before artifact upload
@@ -261,26 +261,26 @@ jobs:
- name: Upload Desktop artifact
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # pin@v4
with:
name: goose-darwin-x64
path: ui/desktop/out/goose-darwin-x64/goose_intel_mac.zip
name: Goose-darwin-x64
path: ui/desktop/out/Goose-darwin-x64/Goose_intel_mac.zip
- name: Quick launch test (macOS)
if: ${{ inputs.quick_test }}
run: |
# Ensure no quarantine attributes (if needed)
xattr -cr "ui/desktop/out/goose-darwin-x64/goose.app"
echo "Opening goose.app..."
open -g "ui/desktop/out/goose-darwin-x64/goose.app"
xattr -cr "ui/desktop/out/Goose-darwin-x64/Goose.app"
echo "Opening Goose.app..."
open -g "ui/desktop/out/Goose-darwin-x64/Goose.app"
# Give the app a few seconds to start and write logs
sleep 5
# Check if it's running
if pgrep -f "goose.app/Contents/MacOS/goose" > /dev/null; then
if pgrep -f "Goose.app/Contents/MacOS/Goose" > /dev/null; then
echo "App appears to be running."
else
echo "App did not stay open. Possible crash or startup error."
exit 1
fi
# Kill the app to clean up
pkill -f "goose.app/Contents/MacOS/goose"
pkill -f "Goose.app/Contents/MacOS/Goose"
+3 -3
View File
@@ -180,21 +180,21 @@ jobs:
- name: Upload .deb package
uses: actions/upload-artifact@v4
with:
name: goose-linux-x64-deb
name: Goose-linux-x64-deb
path: ui/desktop/out/make/deb/x64/*.deb
if-no-files-found: error
- name: Upload .rpm package
uses: actions/upload-artifact@v4
with:
name: goose-linux-x64-rpm
name: Goose-linux-x64-rpm
path: ui/desktop/out/make/rpm/x64/*.rpm
if-no-files-found: error
- name: Upload combined Linux packages
uses: actions/upload-artifact@v4
with:
name: goose-linux-x64
name: Goose-linux-x64
path: |
ui/desktop/out/make/deb/x64/*.deb
ui/desktop/out/make/rpm/x64/*.rpm
+20 -20
View File
@@ -284,12 +284,12 @@ jobs:
- name: Copy exe/dll to final out folder and prepare flat distribution
run: |
cd ui/desktop
mkdir -p ./out/goose-win32-x64/resources/bin
rsync -av src/bin/ out/goose-win32-x64/resources/bin/
mkdir -p ./out/Goose-win32-x64/resources/bin
rsync -av src/bin/ out/Goose-win32-x64/resources/bin/
# Create flat distribution structure
mkdir -p ./dist-windows
cp -r ./out/goose-win32-x64/* ./dist-windows/
cp -r ./out/Goose-win32-x64/* ./dist-windows/
# Verify the final structure
echo "📋 Final flat distribution structure:"
@@ -318,8 +318,8 @@ jobs:
wget -q https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar -O jsign.jar
echo "05ca18d4ab7b8c2183289b5378d32860f0ea0f3bdab1f1b8cae5894fb225fa8a jsign.jar" | sha256sum -c
# Sign the main Electron executable (goose.exe)
echo "🔐 Signing main Electron executable: goose.exe"
# Sign the main Electron executable (Goose.exe)
echo "🔐 Signing main Electron executable: Goose.exe"
cd ui/desktop/dist-windows/
java -jar ${GITHUB_WORKSPACE}/jsign.jar \
@@ -329,12 +329,12 @@ jobs:
--alias windows-codesign \
--certfile "${GITHUB_WORKSPACE}/block-codesign-cert.pem" \
--tsaurl "http://timestamp.digicert.com" \
--name "goose" \
--name "Goose" \
--url "https://github.com/block/goose" \
"goose.exe"
"Goose.exe"
osslsigncode verify goose.exe
echo "✅ Main executable goose.exe signed successfully"
osslsigncode verify Goose.exe
echo "✅ Main executable Goose.exe signed successfully"
# Sign the backend executable (goosed.exe)
echo "🔐 Signing backend executable: goosed.exe"
@@ -347,7 +347,7 @@ jobs:
--alias windows-codesign \
--certfile "${GITHUB_WORKSPACE}/block-codesign-cert.pem" \
--tsaurl "http://timestamp.digicert.com" \
--name "goose Backend" \
--name "Goose Backend" \
--url "https://github.com/block/goose" \
"goosed.exe"
@@ -357,8 +357,8 @@ jobs:
# Show final file status
echo "📋 Final signed files:"
cd ../../
ls -la goose.exe
sha256sum goose.exe
ls -la Goose.exe
sha256sum Goose.exe
ls -la resources/bin/goosed.exe
sha256sum resources/bin/goosed.exe
@@ -371,8 +371,8 @@ jobs:
run: |
echo "📋 Verifying both signed executables in final distribution:"
echo "Main executable:"
ls -la ui/desktop/dist-windows/goose.exe
osslsigncode verify ui/desktop/dist-windows/goose.exe
ls -la ui/desktop/dist-windows/Goose.exe
osslsigncode verify ui/desktop/dist-windows/Goose.exe
echo "✅ Main executable signature verification passed"
echo "Backend executable:"
@@ -387,18 +387,18 @@ jobs:
echo "📦 Creating Windows zip package..."
# Create a zip file from the dist-windows directory
zip -r "goose-win32-x64.zip" dist-windows/
zip -r "Goose-win32-x64.zip" dist-windows/
echo "✅ Windows zip package created:"
ls -la goose-win32-x64.zip
ls -la Goose-win32-x64.zip
# Also create the zip in the expected output structure for consistency
mkdir -p out/goose-win32-x64/
cp goose-win32-x64.zip out/goose-win32-x64/
mkdir -p out/Goose-win32-x64/
cp Goose-win32-x64.zip out/Goose-win32-x64/
# 11) Upload the final Windows build
- name: Upload Windows build artifacts
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # pin@v4
with:
name: goose-win32-x64
path: ui/desktop/out/goose-win32-x64/goose-win32-x64.zip
name: Goose-win32-x64
path: ui/desktop/out/Goose-win32-x64/Goose-win32-x64.zip
+10 -10
View File
@@ -226,10 +226,10 @@ jobs:
source_job_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
unsigned_url="s3://block-goose-artifacts-bucket-production/unsigned/goose-${GITHUB_SHA}-${{ github.run_id }}-arm64.zip"
zip -q -u -r out/goose-darwin-arm64/goose.zip entitlements.plist
zip -q -u -r out/Goose-darwin-arm64/Goose.zip entitlements.plist
# upload unsigned goose to transfer bucket so it can be passed to lambda
aws s3 cp --quiet out/goose-darwin-arm64/goose.zip "${unsigned_url}"
aws s3 cp --quiet out/Goose-darwin-arm64/Goose.zip "${unsigned_url}"
# begin signing
echo "🚀 launching signing process"
@@ -281,7 +281,7 @@ jobs:
# download the signed app from S3
echo "⬇️ downloading signed app"
aws s3 cp --quiet "${signed_url}" out/goose-darwin-arm64/goose.zip
aws s3 cp --quiet "${signed_url}" out/Goose-darwin-arm64/Goose.zip
working-directory: ui/desktop
- name: Final cleanup before artifact upload
@@ -296,26 +296,26 @@ jobs:
id: upload-app-bundle
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # pin@v4
with:
name: goose-darwin-arm64
path: ui/desktop/out/goose-darwin-arm64/goose.zip
name: Goose-darwin-arm64
path: ui/desktop/out/Goose-darwin-arm64/Goose.zip
- name: Quick launch test (macOS)
if: ${{ inputs.quick_test }}
run: |
# Ensure no quarantine attributes (if needed)
xattr -cr "ui/desktop/out/goose-darwin-arm64/goose.app"
echo "Opening goose.app..."
open -g "ui/desktop/out/goose-darwin-arm64/goose.app"
xattr -cr "ui/desktop/out/Goose-darwin-arm64/Goose.app"
echo "Opening Goose.app..."
open -g "ui/desktop/out/Goose-darwin-arm64/Goose.app"
# Give the app a few seconds to start and write logs
sleep 5
# Check if it's running
if pgrep -f "goose.app/Contents/MacOS/goose" > /dev/null; then
if pgrep -f "Goose.app/Contents/MacOS/Goose" > /dev/null; then
echo "App appears to be running."
else
echo "App did not stay open. Possible crash or startup error."
exit 1
fi
# Kill the app to clean up
pkill -f "goose.app/Contents/MacOS/goose"
pkill -f "Goose.app/Contents/MacOS/Goose"
+1 -1
View File
@@ -116,7 +116,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: |
goose-*.tar.bz2
goose*.zip
Goose*.zip
*.deb
*.rpm
download_cli.sh
+1 -1
View File
@@ -118,7 +118,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: |
goose-*.tar.bz2
goose*.zip
Goose*.zip
*.deb
*.rpm
download_cli.sh
@@ -81,7 +81,7 @@ jobs:
- name: Download Intel artifact
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # pin@v4
with:
name: goose-darwin-x64
name: Goose-darwin-x64
path: intel-dist
- name: Comment on PR with Intel download link
@@ -91,7 +91,7 @@ jobs:
body: |
### macOS Intel Desktop App (x64)
[💻 Download macOS Desktop App (Intel x64, signed)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-darwin-x64.zip)
[💻 Download macOS Desktop App (Intel x64, signed)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/Goose-darwin-x64.zip)
**Instructions:**
After downloading, unzip the file and drag the goose.app to your Applications folder. The app is signed and notarized for macOS.
@@ -82,7 +82,7 @@ jobs:
- name: Download Windows artifact
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # pin@v4
with:
name: desktop-windows-dist
name: Goose-win32-x64
path: windows-dist
- name: Comment on PR with Windows download link
@@ -92,9 +92,9 @@ jobs:
body: |
### Windows Desktop App
[🪟 Download Windows Desktop App (x64, signed)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/desktop-windows-dist.zip)
[🪟 Download Windows Desktop App (x64, signed)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/Goose-win32-x64.zip)
**Instructions:**
After downloading, unzip the file and run goose.exe. The app is signed for Windows.
After downloading, unzip the file and run Goose.exe. The app is signed for Windows.
This link is provided by nightly.link and will work even if you're not logged into GitHub.
+2 -2
View File
@@ -123,7 +123,7 @@ jobs:
- name: Download ARM64 artifact
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # pin@v4
with:
name: goose-darwin-arm64
name: Goose-darwin-arm64
path: arm64-dist
- name: Comment on PR with ARM64 download link
@@ -133,7 +133,7 @@ jobs:
body: |
### macOS ARM64 Desktop App (Apple Silicon)
[📱 Download macOS Desktop App (arm64, unsigned)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-darwin-arm64.zip)
[📱 Download macOS Desktop App (arm64, unsigned)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/Goose-darwin-arm64.zip)
**Instructions:**
After downloading, unzip the file and drag the goose.app to a location you prefer. The app is unsigned, so to run it run `xattr -r -d com.apple.quarantine '/path/to/goose.app'` and then open the app
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
body: |
### macOS ARM64 Desktop App (Apple Silicon)
[📱 Download macOS Desktop App (arm64, unsigned)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-darwin-arm64.zip)
[📱 Download macOS Desktop App (arm64, unsigned)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/Goose-darwin-arm64.zip)
**Instructions:**
After downloading, unzip the file and drag the goose.app to a location you prefer. The app is unsigned, so to run it run `xattr -r -d com.apple.quarantine '/path/to/goose.app'` and then open the app
+2 -2
View File
@@ -102,7 +102,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: |
goose-*.tar.bz2
goose*.zip
Goose*.zip
*.deb
*.rpm
download_cli.sh
@@ -120,7 +120,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: |
goose-*.tar.bz2
goose*.zip
Goose*.zip
*.deb
*.rpm
download_cli.sh
@@ -1,352 +0,0 @@
use crate::agents::extension::PlatformExtensionContext;
use crate::agents::mcp_client::{Error, McpClientTrait};
use crate::session::SessionManager;
use anyhow::Result;
use async_trait::async_trait;
use indoc::indoc;
use rmcp::model::{
CallToolResult, Content, GetPromptResult, Implementation, InitializeResult, JsonObject,
ListPromptsResult, ListResourcesResult, ListToolsResult, ProtocolVersion, ReadResourceResult,
ServerCapabilities, ServerNotification, Tool, ToolAnnotations, ToolsCapability,
};
use schemars::{schema_for, JsonSchema};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
pub static EXTENSION_NAME: &str = "chat_recall";
/// Parameters for the chat_recall tool
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct ChatRecallParams {
/// Search keywords. Use multiple related terms/synonyms (e.g., 'database postgres sql'). Mutually exclusive with session_id.
#[serde(skip_serializing_if = "Option::is_none")]
query: Option<String>,
/// Session ID to load. Returns first/last 3 messages. Mutually exclusive with query.
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
/// Max results (default: 10, max: 50). Search mode only.
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<i64>,
/// ISO 8601 date (e.g., '2025-10-01T00:00:00Z'). Search mode only.
#[serde(skip_serializing_if = "Option::is_none")]
after_date: Option<String>,
/// ISO 8601 date (e.g., '2025-10-15T23:59:59Z'). Search mode only.
#[serde(skip_serializing_if = "Option::is_none")]
before_date: Option<String>,
}
pub struct ChatRecallClient {
info: InitializeResult,
context: PlatformExtensionContext,
}
impl ChatRecallClient {
pub fn new(context: PlatformExtensionContext) -> Result<Self> {
let info = InitializeResult {
protocol_version: ProtocolVersion::V_2025_03_26,
capabilities: ServerCapabilities {
tools: Some(ToolsCapability {
list_changed: Some(false),
}),
resources: None,
prompts: None,
completions: None,
experimental: None,
logging: None,
},
server_info: Implementation {
name: EXTENSION_NAME.to_string(),
title: Some("Chat Recall".to_string()),
version: "1.0.0".to_string(),
icons: None,
website_url: None,
},
instructions: Some(indoc! {r#"
Chat Recall
Search past conversations and load session summaries when the user expects some memory or context.
Two modes:
- Search mode: Use query with keywords/synonyms to find relevant messages
- Load mode: Use session_id to get first and last messages of a specific session
"#}.to_string()),
};
Ok(Self { info, context })
}
#[allow(clippy::too_many_lines)]
async fn handle_chat_recall(
&self,
arguments: Option<JsonObject>,
) -> Result<Vec<Content>, String> {
let arguments = arguments.ok_or("Missing arguments")?;
let session_id = arguments
.get("session_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(sid) = session_id {
// LOAD MODE: Get session summary (first and last few messages)
match SessionManager::get_session(&sid, true).await {
Ok(loaded_session) => {
let conversation = loaded_session.conversation.as_ref();
if conversation.is_none() {
return Ok(vec![Content::text(format!(
"Session {} has no conversation.",
sid
))]);
}
let msgs = conversation.unwrap().messages();
let total = msgs.len();
if total == 0 {
return Ok(vec![Content::text(format!(
"Session {} has no messages.",
sid
))]);
}
let mut output = format!(
"Session: {} (ID: {})\nWorking Dir: {}\nTotal Messages: {}\n\n",
loaded_session.description,
sid,
loaded_session.working_dir.display(),
total
);
// Show first 3 messages
let first_count = std::cmp::min(3, total);
output.push_str("--- First Few Messages ---\n\n");
for (idx, msg) in msgs.iter().take(first_count).enumerate() {
output.push_str(&format!("{}. [{:?}] ", idx + 1, msg.role));
for content in &msg.content {
if let Some(text) = content.as_text() {
output.push_str(text);
output.push('\n');
}
}
output.push('\n');
}
// Show last 3 messages (if different from first)
if total > first_count {
output.push_str("--- Last Few Messages ---\n\n");
let last_count = std::cmp::min(3, total);
let skip_count = total.saturating_sub(last_count);
for (idx, msg) in msgs.iter().skip(skip_count).enumerate() {
output.push_str(&format!(
"{}. [{:?}] ",
skip_count + idx + 1,
msg.role
));
for content in &msg.content {
if let Some(text) = content.as_text() {
output.push_str(text);
output.push('\n');
}
}
output.push('\n');
}
}
Ok(vec![Content::text(output)])
}
Err(e) => Err(format!("Failed to load session: {}", e)),
}
} else {
// SEARCH MODE: Search across all sessions
let query = arguments
.get("query")
.and_then(|v| v.as_str())
.ok_or("Missing required parameter: query or session_id")?
.to_string();
let limit = arguments
.get("limit")
.and_then(|v| v.as_i64())
.map(|l| l as usize)
.unwrap_or(10)
.min(50);
let after_date = arguments
.get("after_date")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc));
let before_date = arguments
.get("before_date")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc));
// Exclude current session from results to avoid self-referential loops
let exclude_session_id = self.context.session_id.clone();
match SessionManager::search_chat_history(
&query,
Some(limit),
after_date,
before_date,
exclude_session_id,
)
.await
{
Ok(results) => {
let formatted_results = if results.total_matches == 0 {
format!("No results found for query: '{}'", query)
} else {
let mut output = format!(
"Found {} matching message(s) across {} session(s) for query: '{}'\n\n",
results.total_matches,
results.results.len(),
query
);
for (idx, result) in results.results.iter().enumerate() {
output.push_str(&format!(
"{}. Session: {} (ID: {})\n Working Dir: {}\n Last Activity: {}\n Showing {} of {} total message(s) in session:\n\n",
idx + 1,
result.session_description,
result.session_id,
result.session_working_dir,
result.last_activity.format("%Y-%m-%d"),
result.messages.len(),
result.total_messages_in_session
));
for (msg_idx, message) in result.messages.iter().enumerate() {
output.push_str(&format!(
" {}.{} [{}]\n {}\n\n",
idx + 1,
msg_idx + 1,
message.role,
message
.content
.lines()
.map(|line| format!(" {}", line))
.collect::<Vec<_>>()
.join("\n")
));
}
}
output
};
Ok(vec![Content::text(formatted_results)])
}
Err(e) => Err(format!("Chat recall failed: {}", e)),
}
}
}
fn get_tools() -> Vec<Tool> {
// Generate JSON schema from the ChatRecallParams struct
let schema = schema_for!(ChatRecallParams);
let schema_value =
serde_json::to_value(schema).expect("Failed to serialize ChatRecallParams schema");
let input_schema = schema_value
.as_object()
.expect("Schema should be an object")
.clone();
vec![Tool::new(
"chat_recall".to_string(),
indoc! {r#"
Search past chat or load session summaries. Use when it is clear user expects some memory or context.
search mode (query): Use multiple keywords/synonyms. Returns messages grouped by session, ordered by recency. Supports date filters.
load mode (session_id): Returns first/last 3 messages of a session.
"#}
.to_string(),
input_schema,
)
.annotate(ToolAnnotations {
title: Some("Recall past conversations".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
idempotent_hint: Some(true),
open_world_hint: Some(false),
})]
}
}
#[async_trait]
impl McpClientTrait for ChatRecallClient {
async fn list_resources(
&self,
_next_cursor: Option<String>,
_cancellation_token: CancellationToken,
) -> Result<ListResourcesResult, Error> {
Err(Error::TransportClosed)
}
async fn read_resource(
&self,
_uri: &str,
_cancellation_token: CancellationToken,
) -> Result<ReadResourceResult, Error> {
Err(Error::TransportClosed)
}
async fn list_tools(
&self,
_next_cursor: Option<String>,
_cancellation_token: CancellationToken,
) -> Result<ListToolsResult, Error> {
Ok(ListToolsResult {
tools: Self::get_tools(),
next_cursor: None,
})
}
async fn call_tool(
&self,
name: &str,
arguments: Option<JsonObject>,
_cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> {
let content = match name {
"chat_recall" => self.handle_chat_recall(arguments).await,
_ => Err(format!("Unknown tool: {}", name)),
};
match content {
Ok(content) => Ok(CallToolResult::success(content)),
Err(error) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
error
))])),
}
}
async fn list_prompts(
&self,
_next_cursor: Option<String>,
_cancellation_token: CancellationToken,
) -> Result<ListPromptsResult, Error> {
Err(Error::TransportClosed)
}
async fn get_prompt(
&self,
_name: &str,
_arguments: Value,
_cancellation_token: CancellationToken,
) -> Result<GetPromptResult, Error> {
Err(Error::TransportClosed)
}
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification> {
mpsc::channel(1).1
}
fn get_info(&self) -> Option<&InitializeResult> {
Some(&self.info)
}
}
-14
View File
@@ -1,4 +1,3 @@
use crate::agents::chat_recall_extension;
use crate::agents::todo_extension;
use std::collections::HashMap;
@@ -50,19 +49,6 @@ pub static PLATFORM_EXTENSIONS: Lazy<HashMap<&'static str, PlatformExtensionDef>
},
);
map.insert(
chat_recall_extension::EXTENSION_NAME,
PlatformExtensionDef {
name: chat_recall_extension::EXTENSION_NAME,
description:
"Enable chat recall to search past conversations and load session summaries",
default_enabled: true,
client_factory: |ctx| {
Box::new(chat_recall_extension::ChatRecallClient::new(ctx).unwrap())
},
},
);
map
});
-1
View File
@@ -1,5 +1,4 @@
mod agent;
pub(crate) mod chat_recall_extension;
pub mod extension;
pub mod extension_malware_check;
pub mod extension_manager;
+2 -2
View File
@@ -116,11 +116,11 @@ pub fn manage_schedule_tool() -> Tool {
PLATFORM_MANAGE_SCHEDULE_TOOL_NAME.to_string(),
indoc! {r#"
Manage scheduled recipe execution for this goose instance.
Actions:
- "list": List all scheduled jobs
- "create": Create a new scheduled job from a recipe file
- "run_now": Execute a scheduled job immediately
- "run_now": Execute a scheduled job immediately
- "pause": Pause a scheduled job
- "unpause": Resume a paused job
- "delete": Remove a scheduled job
@@ -1,286 +0,0 @@
use crate::conversation::message::MessageContent;
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize)]
pub struct ChatRecallResult {
pub session_id: String,
pub session_description: String,
pub session_working_dir: String,
pub last_activity: DateTime<Utc>,
pub total_messages_in_session: usize,
pub messages: Vec<ChatRecallMessage>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChatRecallMessage {
pub role: String,
pub content: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Serialize)]
pub struct ChatRecallResults {
pub results: Vec<ChatRecallResult>,
pub total_matches: usize,
}
type SqlQueryRow = (
String,
String,
String,
DateTime<Utc>,
String,
String,
DateTime<Utc>,
);
type SessionMessageGroup = (
String,
String,
DateTime<Utc>,
Vec<(String, String, DateTime<Utc>)>,
);
pub struct ChatHistorySearch<'a> {
pool: &'a Pool<Sqlite>,
query: &'a str,
limit: usize,
after_date: Option<DateTime<Utc>>,
before_date: Option<DateTime<Utc>>,
exclude_session_id: Option<String>,
}
impl<'a> ChatHistorySearch<'a> {
pub fn new(
pool: &'a Pool<Sqlite>,
query: &'a str,
limit: Option<usize>,
after_date: Option<DateTime<Utc>>,
before_date: Option<DateTime<Utc>>,
exclude_session_id: Option<String>,
) -> Self {
Self {
pool,
query,
limit: limit.unwrap_or(10),
after_date,
before_date,
exclude_session_id,
}
}
pub async fn execute(self) -> Result<ChatRecallResults> {
let keywords = self.parse_keywords();
if keywords.is_empty() {
return Ok(ChatRecallResults {
results: vec![],
total_matches: 0,
});
}
let rows = self.fetch_rows(&keywords).await?;
let session_messages = self.process_rows(rows);
let session_totals = self.get_session_totals(&session_messages).await?;
let results = self.convert_to_results(session_messages, session_totals);
Ok(results)
}
async fn fetch_rows(&self, keywords: &[String]) -> Result<Vec<SqlQueryRow>> {
let sql = self.build_sql(keywords);
let mut query_builder = sqlx::query_as::<_, SqlQueryRow>(&sql);
for keyword in keywords {
query_builder = query_builder.bind(keyword);
}
if let Some(exclude_id) = &self.exclude_session_id {
query_builder = query_builder.bind(exclude_id);
}
if let Some(after) = self.after_date {
query_builder = query_builder.bind(after);
}
if let Some(before) = self.before_date {
query_builder = query_builder.bind(before);
}
query_builder = query_builder.bind(self.limit as i64);
Ok(query_builder.fetch_all(self.pool).await?)
}
fn parse_keywords(&self) -> Vec<String> {
self.query
.split_whitespace()
.map(|word| format!("%{}%", word.to_lowercase()))
.collect()
}
fn build_sql(&self, keywords: &[String]) -> String {
let mut sql = String::from(
r#"
SELECT
s.id as session_id,
s.description as session_description,
s.working_dir as session_working_dir,
s.created_at as session_created_at,
m.role,
m.content_json,
m.timestamp
FROM messages m
INNER JOIN sessions s ON m.session_id = s.id
WHERE EXISTS (
SELECT 1 FROM json_each(m.content_json)
WHERE json_extract(value, '$.type') = 'text'
AND (
"#,
);
for (i, _) in keywords.iter().enumerate() {
if i > 0 {
sql.push_str(" OR ");
}
sql.push_str("LOWER(json_extract(value, '$.text')) LIKE ?");
}
sql.push_str(
r#"
)
)
"#,
);
if self.exclude_session_id.is_some() {
sql.push_str(" AND s.id != ?");
}
if self.after_date.is_some() {
sql.push_str(" AND m.timestamp >= ?");
}
if self.before_date.is_some() {
sql.push_str(" AND m.timestamp <= ?");
}
sql.push_str(" ORDER BY m.timestamp DESC LIMIT ?");
sql
}
fn process_rows(&self, rows: Vec<SqlQueryRow>) -> HashMap<String, SessionMessageGroup> {
let mut session_messages: HashMap<String, SessionMessageGroup> = HashMap::new();
for (
session_id,
session_description,
session_working_dir,
session_created_at,
role,
content_json,
timestamp,
) in rows
{
if let Ok(content_vec) = serde_json::from_str::<Vec<MessageContent>>(&content_json) {
let text_parts = Self::extract_text_content(content_vec);
if !text_parts.is_empty() {
let entry = session_messages.entry(session_id.clone()).or_insert((
session_description.clone(),
session_working_dir.clone(),
session_created_at,
Vec::new(),
));
entry
.3
.push((role.clone(), text_parts.join("\n"), timestamp));
}
}
}
session_messages
}
fn extract_text_content(content_vec: Vec<MessageContent>) -> Vec<String> {
content_vec
.into_iter()
.filter_map(|content| match content {
MessageContent::Text(ref tc) => Some(tc.text.clone()),
MessageContent::ToolRequest(ref tr) => {
Some(format!("[Tool: {}]", tr.to_readable_string()))
}
MessageContent::ToolResponse(_) => Some("[Tool Response]".to_string()),
MessageContent::Thinking(ref t) => Some(format!("[Thinking: {}]", t.thinking)),
_ => None,
})
.collect()
}
async fn get_session_totals(
&self,
session_messages: &HashMap<String, SessionMessageGroup>,
) -> Result<HashMap<String, usize>> {
let mut session_totals: HashMap<String, usize> = HashMap::new();
for session_id in session_messages.keys() {
let count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM messages WHERE session_id = ?")
.bind(session_id)
.fetch_one(self.pool)
.await
.unwrap_or(0);
session_totals.insert(session_id.clone(), count as usize);
}
Ok(session_totals)
}
fn convert_to_results(
&self,
session_messages: HashMap<String, SessionMessageGroup>,
session_totals: HashMap<String, usize>,
) -> ChatRecallResults {
let mut results: Vec<ChatRecallResult> = session_messages
.into_iter()
.map(
|(session_id, (description, working_dir, _created_at, messages))| {
let message_vec: Vec<ChatRecallMessage> = messages
.into_iter()
.map(|(role, content, timestamp)| ChatRecallMessage {
role,
content,
timestamp,
})
.collect();
let last_activity = message_vec
.iter()
.map(|m| m.timestamp)
.max()
.unwrap_or_else(chrono::Utc::now);
let total_messages_in_session =
session_totals.get(&session_id).copied().unwrap_or(0);
ChatRecallResult {
session_id,
session_description: description,
session_working_dir: working_dir,
last_activity,
total_messages_in_session,
messages: message_vec,
}
},
)
.collect();
results.sort_by(|a, b| b.last_activity.cmp(&a.last_activity));
let total_matches = results.iter().map(|r| r.messages.len()).sum();
ChatRecallResults {
results,
total_matches,
}
}
}
-1
View File
@@ -1,4 +1,3 @@
mod chat_history_search;
pub mod extension_data;
mod legacy;
pub mod session_manager;
@@ -69,8 +69,6 @@ pub struct SessionInsights {
total_tokens: i64,
}
pub type SessionId = String;
impl SessionUpdateBuilder {
fn new(session_id: String) -> Self {
Self {
@@ -243,19 +241,6 @@ impl SessionManager {
Ok(())
}
}
pub async fn search_chat_history(
query: &str,
limit: Option<usize>,
after_date: Option<DateTime<Utc>>,
before_date: Option<DateTime<Utc>>,
exclude_session_id: Option<String>,
) -> Result<crate::session::chat_history_search::ChatRecallResults> {
Self::instance()
.await?
.search_chat_history(query, limit, after_date, before_date, exclude_session_id)
.await
}
}
pub struct SessionStorage {
@@ -980,28 +965,6 @@ impl SessionStorage {
self.get_session(&session.id, true).await
}
async fn search_chat_history(
&self,
query: &str,
limit: Option<usize>,
after_date: Option<DateTime<Utc>>,
before_date: Option<DateTime<Utc>>,
exclude_session_id: Option<String>,
) -> Result<crate::session::chat_history_search::ChatRecallResults> {
use crate::session::chat_history_search::ChatHistorySearch;
ChatHistorySearch::new(
&self.pool,
query,
limit,
after_date,
before_date,
exclude_session_id,
)
.execute()
.await
}
}
#[cfg(test)]
@@ -8,13 +8,13 @@ const DesktopInstallButtons = () => {
<div className="pill-button">
<Link
className="button button--primary button--lg"
to="https://github.com/block/goose/releases/download/stable/goose.zip"
to="https://github.com/block/goose/releases/download/stable/Goose.zip"
>
<IconDownload /> macOS Silicon
</Link>
<Link
className="button button--primary button--lg"
to="https://github.com/block/goose/releases/download/stable/goose_intel_mac.zip"
to="https://github.com/block/goose/releases/download/stable/Goose_intel_mac.zip"
>
<IconDownload /> macOS Intel
</Link>
@@ -8,7 +8,7 @@ const WindowsDesktopInstallButtons = () => {
<div className="pill-button">
<Link
className="button button--primary button--lg"
to="https://github.com/block/goose/releases/download/stable/goose-win32-x64.zip"
to="https://github.com/block/goose/releases/download/stable/Goose-win32-x64.zip"
>
<IconDownload /> Windows
</Link>
@@ -0,0 +1,302 @@
version: 1.0.0
title: Code Documentation Generator
description: Automatically generates comprehensive documentation for codebases by analyzing source files, extracting APIs, and creating formatted documentation with examples and best practices
author:
contact: ARYPROGRAMMER
activities:
- Scan and analyze source code files in the project
- Extract functions, classes, methods, and their signatures
- Generate documentation with descriptions and usage examples
- Create API reference documentation with type information
- Build interactive documentation with cross-references
- Store documentation patterns in repository files for consistency
prompt: |
You are a Code Documentation Generator that creates comprehensive, well-structured documentation for codebases.
Your goal is to analyze source code, extract relevant information, and generate clear, helpful documentation.
Key capabilities:
- Analyze code structure and extract documentable elements
- Generate consistent documentation following best practices
- Create usage examples and code snippets
- Build cross-referenced documentation with links
- Store project-specific documentation styles in repository files
- Research documentation standards for the detected language
IMPORTANT: Always detect the programming language first and apply language-specific documentation standards.
parameters:
- key: source_path
input_type: string
requirement: optional
default: "."
description: "Path to the source code directory or specific file to document"
- key: doc_format
input_type: string
requirement: optional
default: "html"
description: "Documentation format: 'markdown', 'html', 'restructuredtext', 'javadoc', 'jsdoc'"
- key: include_examples
input_type: string
requirement: optional
default: "true"
description: "Whether to generate usage examples for documented items (true/false)"
- key: output_location
input_type: string
requirement: optional
default: "./docs"
description: "Directory where documentation files will be saved"
- key: doc_style
input_type: string
requirement: optional
default: "comprehensive"
description: "Documentation style: 'minimal' (brief descriptions), 'standard' (descriptions + params), 'comprehensive' (full with examples and cross-refs)"
- key: language_hint
input_type: string
requirement: optional
default: ""
description: "Optional: specify programming language if auto-detection is insufficient (e.g., 'python', 'javascript', 'rust', 'go')"
extensions:
- type: builtin
name: developer
display_name: Developer
timeout: 600
bundled: true
description: For file operations, code analysis, and documentation generation
- type: builtin
name: browser
display_name: Browser
timeout: 300
bundled: true
description: For researching documentation standards and best practices
settings:
temperature: 0.3
instructions: |
Generate comprehensive documentation for the codebase with the following configuration:
- Source Path: {{ source_path }}
- Documentation Format: {{ doc_format }}
- Include Examples: {{ include_examples }}
- Output Location: {{ output_location }}
- Documentation Style: {{ doc_style }}
{% if language_hint %}
- Language Hint: {{ language_hint }}
{% endif %}
Follow this workflow:
## Phase 1: Project Analysis and Setup
1. **Analyze the codebase structure:**
- Navigate to {{ source_path }}
- Identify all source code files (common extensions: .py, .js, .ts, .rs, .go, .java, .cpp, .rb, etc.)
- {% if language_hint %}Focus on {{ language_hint }} files{% else %}Detect the primary programming language(s){% endif %}
- Map the project structure (modules, packages, components)
2. **Check for existing documentation:**
- Look for existing doc files, READMEs, or inline documentation
- Check if there are documentation standards already in use
- Check for `.goosehints`, `AGENTS.md`, or `DOCS_GUIDELINES.md` files with documentation preferences
- If documentation already exists, analyze it to maintain consistency in style and format
- Preserve existing documentation structure while filling in gaps for undocumented items
- Extract inline docstrings/comments to use as foundation for generated documentation
3. **Research documentation standards:**
- Use the browser to look up the standard documentation format for the detected language
- For Python: Look for PEP 257 docstring conventions and Sphinx documentation
- For JavaScript/TypeScript: Look for JSDoc standards
- For Rust: Look for rustdoc conventions
- For Go: Look for godoc conventions
- Check if `.goosehints` or `AGENTS.md` already exists and use those guidelines if present
- If no local guidelines exist, prepare to create them based on detected language standards
## Phase 2: Code Analysis and Extraction
4. **Extract documentable elements:**
For each source file, identify and extract:
- **Functions/Methods:**
* Name and signature
* Parameters with types (if available)
* Return type
* Existing comments or docstrings
* Purpose based on implementation analysis
* Cyclomatic complexity level (simple: 1-5 branches, moderate: 6-10, complex: 11+)
- **Classes/Structs/Types:**
* Name and inheritance/implementation
* Constructor/initialization
* Properties/fields
* Methods
* Usage patterns
- **Constants and Variables:**
* Public constants
* Configuration variables
* Exported values
- **Modules/Packages:**
* Module purpose (inferred from: exported functions, file name, directory structure, and existing comments)
* Exported interfaces
* Dependencies
* Note: If purpose cannot be confidently determined, use placeholder like "TODO: Add module description" and flag for manual review
5. **Analyze code context:**
- Understand the purpose of each code element from its implementation
- Identify input/output patterns
- Note error handling and edge cases
- Recognize common design patterns used
## Phase 3: Documentation Generation
6. **Create documentation structure:**
- Create {{ output_location }} directory if it doesn't exist
- Organize documentation by module/package structure
- Create an index/table of contents file
7. **Generate documentation for each element:**
{% if doc_style == "minimal" %}
For minimal style, include:
- Brief one-line description
- Function/method signature
- Parameter list
{% elif doc_style == "standard" %}
For standard style, include:
- Clear description of purpose
- Full parameter documentation with types and descriptions
- Return value documentation
- Basic usage information
{% else %}
For comprehensive style, include:
- Detailed description of purpose and behavior
- Complete parameter documentation with types, descriptions, and constraints
- Return value documentation with possible values
- Exceptions/errors that may be raised
{% if include_examples == "true" %}
- Usage examples demonstrating common scenarios
- Code snippets showing best practices
{% endif %}
- Links to related functions/classes
- Notes on performance, thread-safety, or other important considerations
{% endif %}
8. **Format documentation:**
{% if doc_format == "markdown" %}
- Use Markdown formatting with proper headers (##, ###)
- Create code blocks with language-specific syntax highlighting
- Use tables for parameter lists
- Add hyperlinks using [text](#anchor) for internal links and [text](file.md#section) for cross-file references
- Create anchor links using header IDs (e.g., ## Function Name creates #function-name anchor)
{% elif doc_format == "html" %}
- Generate valid HTML5 documentation
- Include CSS styling for readability
- Add navigation links using <a href="..."> tags
- Create searchable index
{% elif doc_format == "restructuredtext" %}
- Use reStructuredText formatting
- Proper directive usage for code blocks
- Cross-reference using :ref:`label` and :doc:`filename`
- Use :py:func:`module.function` for Python cross-references
{% elif doc_format == "javadoc" or doc_format == "jsdoc" %}
- Generate language-specific doc comments
- Use @param, @return, @throws tags
- Include @example tags for usage
- Use @see and @link for cross-references
{% endif %}
{% if include_examples == "true" %}
9. **Generate usage examples:**
For each documented function/class:
- Generate synthetic but realistic usage examples that demonstrate the API
- Show input data and expected output based on the function's signature and implementation
- Demonstrate error handling where applicable
- Include edge cases where relevant
- Mark examples clearly as "Example Usage" or "Code Example"
- Ensure examples use the actual function signatures from the codebase
- If you find actual usage in test files or other parts of the codebase, you may reference or adapt those patterns
- Keep examples simple and focused on demonstrating the specific functionality
{% endif %}
## Phase 4: Cross-Referencing and Index
10. **Build cross-references:**
{% if doc_format == "markdown" %}
- Link related functions and classes using [FunctionName](#functionname) format
- Create "See Also" sections with links to related documentation
- Build dependency graphs showing relationships
- Add links from examples back to detailed docs using relative paths
{% elif doc_format == "html" %}
- Link related functions and classes using <a href="#anchor"> tags
- Create "See Also" sections
- Build dependency graphs showing relationships
- Add links from examples back to detailed docs
{% elif doc_format == "restructuredtext" %}
- Use :ref:`label` for internal references
- Use :doc:`filename` for cross-file references
- Create "See Also" sections with proper reST directives
{% else %}
- Use format-appropriate linking mechanisms
- Create "See Also" sections
- Build relationship documentation
{% endif %}
11. **Generate documentation index:**
- Create a master index file (index.md, index.html, etc.)
- Organize by module/package
- Add search functionality hints
- Include quick reference table
- Add getting started guide if this is new documentation
## Phase 5: Quality Check and Finalization
12. **Review generated documentation:**
- Check for completeness (all public APIs documented)
- Verify example code correctness
- Ensure consistent formatting
- Validate all cross-references work
- Check for typos and grammar issues
13. **Save documentation patterns:**
- Create or update `.goosehints` file in {{ source_path }} with documentation preferences
- Store documentation style, format preferences, and project-specific conventions
- Include example patterns that worked well
- Consider updating `AGENTS.md` with documentation standards for team reference
- Optionally create `DOCS_GUIDELINES.md` with detailed documentation style guide
14. **Generate summary report:**
- List all files documented
- Count of functions, classes, and modules documented
- Location of generated documentation
- Suggestions for improving source code documentation
- Any warnings or issues encountered
## Best Practices to Follow:
- **Clarity:** Use clear, simple language avoiding jargon when possible
- **Consistency:** Maintain consistent format and style throughout
- **Accuracy:** Ensure documentation matches actual code behavior
- **Completeness:** Document all public APIs and exported elements
- **Examples:** Provide practical, real-world usage examples
- **Maintenance:** Note any areas where code comments should be added
## Output Format:
Provide a clear summary including:
1. Number of files analyzed
2. Number of elements documented (functions, classes, etc.)
3. Location of generated documentation files
4. Quick preview of the documentation structure
5. Suggestions for improving inline code documentation
6. Confirmation that documentation guidelines have been saved to `.goosehints` or `AGENTS.md`
**Note**: Documentation preferences and patterns are stored in repository files (`.goosehints`, `AGENTS.md`, or `DOCS_GUIDELINES.md`) rather than memory to avoid cluttering the LLM context on unrelated tasks.
+4 -4
View File
@@ -65,8 +65,8 @@ module.exports = {
{
name: '@electron-forge/maker-deb',
config: {
name: 'goose',
bin: 'goose',
name: 'Goose',
bin: 'Goose',
maintainer: 'Block, Inc.',
homepage: 'https://block.github.io/goose/',
categories: ['Development'],
@@ -79,8 +79,8 @@ module.exports = {
{
name: '@electron-forge/maker-rpm',
config: {
name: 'goose',
bin: 'goose',
name: 'Goose',
bin: 'Goose',
maintainer: 'Block, Inc.',
homepage: 'https://block.github.io/goose/',
categories: ['Development'],
+6 -6
View File
@@ -1,8 +1,8 @@
{
"name": "goose-app",
"productName": "goose",
"productName": "Goose",
"version": "1.9.0",
"description": "goose App",
"description": "Goose App",
"engines": {
"node": "^22.17.1"
},
@@ -16,11 +16,11 @@
"start:test-error": "GOOSE_TEST_ERROR=true electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"bundle:default": "node scripts/prepare-platform-binaries.js && npm run make && (cd out/goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent goose.app goose.zip) || echo 'out/goose-darwin-arm64 not found; either the binary is not built or you are not on macOS'",
"bundle:alpha": "ALPHA=true node scripts/prepare-platform-binaries.js && ALPHA=true npm run make && (cd out/goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent goose.app goose_alpha.zip) || echo 'out/goose-darwin-arm64 not found; either the binary is not built or you are not on macOS'",
"bundle:default": "node scripts/prepare-platform-binaries.js && npm run make && (cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip) || echo 'out/Goose-darwin-arm64 not found; either the binary is not built or you are not on macOS'",
"bundle:alpha": "ALPHA=true node scripts/prepare-platform-binaries.js && ALPHA=true npm run make && (cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose_alpha.zip) || echo 'out/Goose-darwin-arm64 not found; either the binary is not built or you are not on macOS'",
"bundle:windows": "node scripts/build-main.js && ELECTRON_PLATFORM=win32 node scripts/prepare-platform-binaries.js && npm run make -- --platform=win32 --arch=x64",
"bundle:intel": "node scripts/prepare-platform-binaries.js && npm run make -- --arch=x64 && cd out/goose-darwin-x64 && ditto -c -k --sequesterRsrc --keepParent goose.app goose_intel_mac.zip",
"debug": "echo 'run --remote-debugging-port=8315' && lldb out/goose-darwin-arm64/goose.app",
"bundle:intel": "node scripts/prepare-platform-binaries.js && npm run make -- --arch=x64 && cd out/Goose-darwin-x64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose_intel_mac.zip",
"debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app",
"test-e2e": "npm run generate-api && playwright test",
"test-e2e:dev": "npm run generate-api && playwright test --reporter=list --retries=0 --max-failures=1",
"test-e2e:ui": "npm run generate-api && playwright test --ui",