Add web terminal, git credentials, OpenSCAD, review loops, automations
- Web terminal (PTY over WebSocket, xterm.js, sandboxed, reattach with replay) - Per-user git credentials and commit identity (credential store in the user's home) - OpenSCAD tool (STL/3MF + preview renders, bounding box) and cad skill; 3D model card - Craft phase inferred from tool activity when the agent does not report it - Configurable review loop per mode (rounds, reviewer model, criteria) - Delete config and Markdown agents from the agent editor - Automations: cron-scheduled agents from Markdown job files, schedule tool, page - change_directory tool (only on the user's request) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a0d0e707f2
commit
df1924b0a1
41 files changed
+2809
-34
No files matched your search
@@ -97,6 +97,22 @@ impl Store {
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS queue_session ON queue(session_id, created_at);
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_run INTEGER,
|
||||
last_session_id TEXT,
|
||||
last_status TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
user_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, key)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS push_subs (
|
||||
endpoint TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
@@ -881,6 +897,106 @@ impl Store {
|
||||
// --- audit log & backup ----------------------------------------------------------
|
||||
|
||||
impl Store {
|
||||
// --- scheduled jobs -------------------------------------------------------
|
||||
|
||||
fn job_row(r: &rusqlite::Row) -> rusqlite::Result<crate::jobs::Job> {
|
||||
Ok(crate::jobs::Job {
|
||||
id: r.get(0)?,
|
||||
owner_id: r.get(1)?,
|
||||
path: r.get(2)?,
|
||||
enabled: r.get::<_, i64>(3)? != 0,
|
||||
last_run: r.get(4)?,
|
||||
last_session_id: r.get(5)?,
|
||||
last_status: r.get(6)?,
|
||||
created_at: r.get(7)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn jobs(&self, owner: Option<&str>) -> Result<Vec<crate::jobs::Job>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at FROM jobs
|
||||
WHERE ?1 IS NULL OR owner_id = ?1 ORDER BY created_at",
|
||||
)?;
|
||||
let rows = stmt.query_map([owner], Self::job_row)?;
|
||||
Ok(rows.collect::<std::result::Result<_, _>>()?)
|
||||
}
|
||||
|
||||
pub fn job(&self, id: &str) -> Result<Option<crate::jobs::Job>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
Ok(conn
|
||||
.query_row("SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at FROM jobs WHERE id = ?1", [id], Self::job_row)
|
||||
.optional()?)
|
||||
}
|
||||
|
||||
/// Register a job file (or return the existing registration of that file).
|
||||
pub fn job_register(&self, owner: &str, path: &str) -> Result<crate::jobs::Job> {
|
||||
{
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO jobs (id, owner_id, path, enabled, created_at) VALUES (?1, ?2, ?3, 1, ?4)
|
||||
ON CONFLICT(path) DO UPDATE SET enabled = 1",
|
||||
rusqlite::params![crate::types::new_id(), owner, path, now_ms()],
|
||||
)?;
|
||||
}
|
||||
let conn = self.conn.lock().unwrap();
|
||||
Ok(conn.query_row(
|
||||
"SELECT id, owner_id, path, enabled, last_run, last_session_id, last_status, created_at FROM jobs WHERE path = ?1",
|
||||
[path],
|
||||
Self::job_row,
|
||||
)?)
|
||||
}
|
||||
|
||||
pub fn job_started(&self, id: &str, session_id: &str, ts: i64) -> Result<()> {
|
||||
self.conn.lock().unwrap().execute(
|
||||
"UPDATE jobs SET last_run = ?2, last_session_id = ?3, last_status = 'running' WHERE id = ?1",
|
||||
rusqlite::params![id, ts, session_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn job_finished(&self, id: &str, session_id: Option<&str>, status: &str) -> Result<()> {
|
||||
self.conn.lock().unwrap().execute(
|
||||
"UPDATE jobs SET last_status = ?2, last_session_id = COALESCE(?3, last_session_id) WHERE id = ?1",
|
||||
rusqlite::params![id, status, session_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn job_touch(&self, id: &str, ts: i64) -> Result<()> {
|
||||
self.conn.lock().unwrap().execute("UPDATE jobs SET last_run = ?2 WHERE id = ?1", rusqlite::params![id, ts])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn job_set_enabled(&self, id: &str, enabled: bool) -> Result<()> {
|
||||
self.conn.lock().unwrap().execute("UPDATE jobs SET enabled = ?2 WHERE id = ?1", rusqlite::params![id, enabled as i64])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn job_delete(&self, id: &str) -> Result<()> {
|
||||
self.conn.lock().unwrap().execute("DELETE FROM jobs WHERE id = ?1", [id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- per-user settings (JSON) ------------------------------------------
|
||||
|
||||
pub fn user_setting<T: serde::de::DeserializeOwned + Default>(&self, user_id: &str, key: &str) -> Result<T> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let v: Option<String> = conn
|
||||
.query_row("SELECT value FROM user_settings WHERE user_id = ?1 AND key = ?2", [user_id, key], |r| r.get(0))
|
||||
.optional()?;
|
||||
Ok(v.and_then(|v| serde_json::from_str(&v).ok()).unwrap_or_default())
|
||||
}
|
||||
|
||||
pub fn set_user_setting<T: serde::Serialize>(&self, user_id: &str, key: &str, value: &T) -> Result<()> {
|
||||
self.conn.lock().unwrap().execute(
|
||||
"INSERT INTO user_settings (user_id, key, value) VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT(user_id, key) DO UPDATE SET value = ?3",
|
||||
rusqlite::params![user_id, key, serde_json::to_string(value)?],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// --- web push ---------------------------------------------------------
|
||||
|
||||
pub fn push_add(&self, user_id: &str, endpoint: &str, p256dh: &str, auth: &str) -> Result<()> {
|
||||
|
||||
Reference in new issue
Block a user