tide-commander 1.65.0 → 1.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{BossLogsModal-U4mcLIP3.js → BossLogsModal-D6nnEtFf.js} +1 -1
- package/dist/assets/{BossSpawnModal-CiFvgaQ1.js → BossSpawnModal-DmPw2xym.js} +1 -1
- package/dist/assets/{ControlsModal-Bz4EQ0ii.js → ControlsModal-OEZddGmt.js} +1 -1
- package/dist/assets/{DockerLogsModal-CSMh4uRZ.js → DockerLogsModal-CzpJKdxd.js} +1 -1
- package/dist/assets/{EmbeddedEditor-ecjZKKkD.js → EmbeddedEditor-Dh9neVWW.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-pFwSGVxa.js → GmailOAuthSetup-FtYHQDDw.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-CPvUA9JE.js → GoogleOAuthSetup-reYSDcFV.js} +1 -1
- package/dist/assets/{IframeModal-DcTPyjLy.js → IframeModal-DbW9WCx6.js} +1 -1
- package/dist/assets/{IntegrationsPanel-DHdp8HO-.js → IntegrationsPanel-DooKtzpO.js} +2 -2
- package/dist/assets/{LogViewerModal-QWGgGmiP.js → LogViewerModal-BamqTQzc.js} +1 -1
- package/dist/assets/{MonitoringModal-C8cfJlg5.js → MonitoringModal-DiOT_xpJ.js} +1 -1
- package/dist/assets/{PM2LogsModal-uUu_bTeC.js → PM2LogsModal-C4hIfwGk.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-C-Zxn6n6.js → RestoreArchivedAreaModal-CQ3NdSvn.js} +1 -1
- package/dist/assets/{Scene2DCanvas-0uRu2G4V.js → Scene2DCanvas-BfcwE6aA.js} +1 -1
- package/dist/assets/{SceneManager-xyitf6BI.js → SceneManager-D409BuL6.js} +1 -1
- package/dist/assets/{SkillsPanel-Bgcx1OI3.js → SkillsPanel-B0V-BFfO.js} +1 -1
- package/dist/assets/{SpawnModal-CS3BMuY9.js → SpawnModal-gPT83rW4.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DNWrbv9p.js → SubordinateAssignmentModal-Dvvqv0yZ.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-D-pLDqne.js → TriggerManagerPanel-BmDBe8xx.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-Cu7zjFdE.js → WorkflowEditorPanel-PS4W8Q3F.js} +1 -1
- package/dist/assets/{index-cGfzMto2.js → index-BSdgxlrR.js} +1 -1
- package/dist/assets/{index-2BVW4z0_.js → index-BkpkUL3C.js} +1 -1
- package/dist/assets/{index-CZl-6UH7.js → index-C7cIg4BE.js} +1 -1
- package/dist/assets/{index-DEfCPZTr.js → index-CJYuOBJD.js} +3 -3
- package/dist/assets/{index-Yy2LrlI7.js → index-D9NmRir8.js} +2 -2
- package/dist/assets/{index-Dd0cfrn9.js → index-DvbZsLxj.js} +1 -1
- package/dist/assets/index-baPDjRvq.js +1 -0
- package/dist/assets/{index-DM70jqOd.js → index-cCrsvvfk.js} +1 -1
- package/dist/assets/main-B3L4mgQ4.css +1 -0
- package/dist/assets/{main-BNhrjJHj.js → main-BzSUj-VM.js} +5 -5
- package/dist/assets/{web-BnX_BsjB.js → web-DffxvD3t.js} +1 -1
- package/dist/assets/{web-CyBgxhK2.js → web-y3e1lmyW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/data/builtin-skills/backup-restore.js +242 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/index.js +4 -0
- package/dist/src/packages/server/routes/agents.js +27 -0
- package/dist/src/packages/server/services/backup-service.js +148 -0
- package/package.json +2 -1
- package/scripts/backup-data.sh +116 -0
- package/scripts/krunner/install-krunner-integration.sh +53 -0
- package/scripts/krunner/org.riven.tide.krunner.service +3 -0
- package/scripts/krunner/plasma-runner-tide-commander.desktop +16 -0
- package/scripts/krunner/tide-krunner-runner.js +335 -0
- package/scripts/recover-agents.ts +623 -0
- package/dist/assets/index-CWyxpmuL.js +0 -1
- package/dist/assets/main-4iQEaD98.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{bI as a}from"./main-
|
|
1
|
+
import{bI as a}from"./main-BzSUj-VM.js";import{ImpactStyle as i,NotificationType as r}from"./index-D9NmRir8.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{bI as s}from"./main-
|
|
1
|
+
import{bI as s}from"./main-BzSUj-VM.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-BzSUj-VM.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-B3L4mgQ4.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
export const backupRestore = {
|
|
2
|
+
slug: 'backup-restore',
|
|
3
|
+
name: 'Backup & Restore',
|
|
4
|
+
description: 'Use this skill to list, inspect, or restore Tide Commander data backups. Covers reviewing available backup snapshots, comparing them, and restoring specific config files or the full data directory from a backup.',
|
|
5
|
+
allowedTools: ['Bash(ls:*)', 'Bash(tar:*)', 'Bash(cp:*)', 'Bash(mv:*)', 'Bash(mkdir:*)', 'Bash(sqlite3:*)', 'Bash(sha256sum:*)', 'Bash(diff:*)', 'Bash(date:*)', 'Bash(stat:*)', 'Bash(bash:*)', 'Bash(curl:*)', 'Read', 'Grep', 'Glob'],
|
|
6
|
+
content: `# Backup & Restore
|
|
7
|
+
|
|
8
|
+
Tide Commander automatically backs up its data directory every hour using an in-process scheduler. This skill documents how backups work, where they live, and how to review or restore them.
|
|
9
|
+
|
|
10
|
+
## How Backups Work
|
|
11
|
+
|
|
12
|
+
- The scheduler runs inside the Commander node process (no cron or external deps).
|
|
13
|
+
- It calls \`scripts/backup-data.sh\` which stages all top-level JSON configs and safely copies the SQLite database using \`sqlite3 .backup\`.
|
|
14
|
+
- A **content signature** (sha256, 16 chars) is computed from the staged files. If a backup with the same signature already exists, the run is skipped (no duplicate backups for unchanged data).
|
|
15
|
+
- Backups are compressed tarballs named \`backup-<YYYYMMDDThhmmss>-<signature>.tar.gz\`.
|
|
16
|
+
|
|
17
|
+
## Rotation Policy
|
|
18
|
+
|
|
19
|
+
- The **8 newest** hourly backups are always kept.
|
|
20
|
+
- Plus the **most recent backup from each of the 2 most recent prior days** that have backups.
|
|
21
|
+
- Up to ~10 backups total.
|
|
22
|
+
|
|
23
|
+
## Key Paths
|
|
24
|
+
|
|
25
|
+
| Item | Path |
|
|
26
|
+
|------|------|
|
|
27
|
+
| Live data directory | \`~/.local/share/tide-commander/\` |
|
|
28
|
+
| Backup directory | \`~/.local/share/tide-commander-backups/\` |
|
|
29
|
+
| Backup script | \`scripts/backup-data.sh\` (relative to project root) |
|
|
30
|
+
| Backup log | \`~/.local/share/tide-commander-backups/backup.log\` |
|
|
31
|
+
| Scheduler settings | \`~/.local/share/tide-commander/backup-settings.json\` |
|
|
32
|
+
|
|
33
|
+
## What Is Backed Up
|
|
34
|
+
|
|
35
|
+
All top-level files in the data directory:
|
|
36
|
+
|
|
37
|
+
| File | Contents |
|
|
38
|
+
|------|----------|
|
|
39
|
+
| \`agents.json\` | Agent configurations, positions, session IDs |
|
|
40
|
+
| \`skills.json\` | Skill registry and assignments |
|
|
41
|
+
| \`triggers.json\` | Workflow trigger definitions |
|
|
42
|
+
| \`workflow-definitions.json\` | Workflow state machines |
|
|
43
|
+
| \`buildings.json\` | Database building configs |
|
|
44
|
+
| \`secrets.json\` | Encrypted API keys (AES-256-GCM) |
|
|
45
|
+
| \`custom-agent-classes.json\` | Custom agent class definitions |
|
|
46
|
+
| \`delegation-history.json\` | Boss agent delegation history |
|
|
47
|
+
| \`areas.json\` | Area/zone layouts |
|
|
48
|
+
| \`slack-config.json\` | Slack integration config |
|
|
49
|
+
| \`calendar-config.json\` | Google Calendar config |
|
|
50
|
+
| \`drive-config.json\` | Google Drive config |
|
|
51
|
+
| \`system-prompt.json\` | Global system prompt |
|
|
52
|
+
| \`session-history.json\` | Session history metadata |
|
|
53
|
+
| \`running-processes.json\` | Active process records |
|
|
54
|
+
| \`events.db\` | SQLite event store (trigger events, audit log, workflow instances, etc.) |
|
|
55
|
+
| \`*.json.bak\` | Atomic-write backup copies of each JSON file |
|
|
56
|
+
|
|
57
|
+
Subdirectories like \`query-history/\`, \`generated/\`, \`snapshots/\`, and \`templates/\` are **not** included (considered derivable).
|
|
58
|
+
|
|
59
|
+
## Common Tasks
|
|
60
|
+
|
|
61
|
+
### Check Backup Scheduler Status
|
|
62
|
+
|
|
63
|
+
\`\`\`bash
|
|
64
|
+
curl -s -H "X-Auth-Token: YOUR_TOKEN" http://localhost:5174/api/agents/system-settings/backup | python3 -m json.tool
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
Returns: \`enabled\`, \`running\`, \`scriptPath\`, \`scriptExists\`, \`backupDir\`, \`lastRunAt\`, \`lastRunOk\`, \`lastRunError\`.
|
|
68
|
+
|
|
69
|
+
### Enable / Disable Backups
|
|
70
|
+
|
|
71
|
+
\`\`\`bash
|
|
72
|
+
# Enable
|
|
73
|
+
curl -s -X POST -H "Content-Type: application/json" -H "X-Auth-Token: YOUR_TOKEN" http://localhost:5174/api/agents/system-settings/backup -d '{"enabled":true}'
|
|
74
|
+
|
|
75
|
+
# Disable
|
|
76
|
+
curl -s -X POST -H "Content-Type: application/json" -H "X-Auth-Token: YOUR_TOKEN" http://localhost:5174/api/agents/system-settings/backup -d '{"enabled":false}'
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
### Run a Manual Backup Now
|
|
80
|
+
|
|
81
|
+
\`\`\`bash
|
|
82
|
+
bash scripts/backup-data.sh
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
Or from the project root if running a compiled install, the script is at the same relative path.
|
|
86
|
+
|
|
87
|
+
### List Available Backups
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
ls -lhtr ~/.local/share/tide-commander-backups/backup-*.tar.gz
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
Filenames sort chronologically. The signature suffix lets you see which backups have identical content.
|
|
94
|
+
|
|
95
|
+
### Inspect a Backup (List Contents)
|
|
96
|
+
|
|
97
|
+
\`\`\`bash
|
|
98
|
+
tar -tzf ~/.local/share/tide-commander-backups/backup-<TIMESTAMP>-<SIG>.tar.gz
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
### Extract a Backup to a Temp Directory for Review
|
|
102
|
+
|
|
103
|
+
\`\`\`bash
|
|
104
|
+
REVIEW_DIR=$(mktemp -d)
|
|
105
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-<TIMESTAMP>-<SIG>.tar.gz -C "$REVIEW_DIR"
|
|
106
|
+
ls -la "$REVIEW_DIR"
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
Then inspect individual files:
|
|
110
|
+
\`\`\`bash
|
|
111
|
+
# View agents config from the backup
|
|
112
|
+
cat "$REVIEW_DIR/agents.json" | python3 -m json.tool | head -60
|
|
113
|
+
|
|
114
|
+
# Compare a file between backup and live
|
|
115
|
+
diff <(python3 -m json.tool "$REVIEW_DIR/agents.json") <(python3 -m json.tool ~/.local/share/tide-commander/agents.json)
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
### Compare Two Backups
|
|
119
|
+
|
|
120
|
+
\`\`\`bash
|
|
121
|
+
DIR_A=$(mktemp -d)
|
|
122
|
+
DIR_B=$(mktemp -d)
|
|
123
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-OLDER.tar.gz -C "$DIR_A"
|
|
124
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-NEWER.tar.gz -C "$DIR_B"
|
|
125
|
+
diff -rq "$DIR_A" "$DIR_B"
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
### Query the SQLite Database from a Backup
|
|
129
|
+
|
|
130
|
+
\`\`\`bash
|
|
131
|
+
REVIEW_DIR=$(mktemp -d)
|
|
132
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-<TIMESTAMP>-<SIG>.tar.gz -C "$REVIEW_DIR"
|
|
133
|
+
sqlite3 "$REVIEW_DIR/events.db" ".tables"
|
|
134
|
+
sqlite3 "$REVIEW_DIR/events.db" "SELECT COUNT(*) FROM workflow_instances;"
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
## Restore Procedures
|
|
138
|
+
|
|
139
|
+
### IMPORTANT: Safety Rules
|
|
140
|
+
|
|
141
|
+
1. **Always stop Commander before restoring** to avoid write conflicts.
|
|
142
|
+
2. **Back up the current live state first** before overwriting anything.
|
|
143
|
+
3. **Prefer selective restore** (single files) over full restore when possible.
|
|
144
|
+
4. **Never restore running-processes.json** — it tracks ephemeral PIDs that won't be valid after restart.
|
|
145
|
+
|
|
146
|
+
### Restore a Single JSON Config File
|
|
147
|
+
|
|
148
|
+
This is the safest approach. Use when one specific config got corrupted or lost.
|
|
149
|
+
|
|
150
|
+
\`\`\`bash
|
|
151
|
+
# 1. Pick the backup to restore from
|
|
152
|
+
ls -lhtr ~/.local/share/tide-commander-backups/backup-*.tar.gz
|
|
153
|
+
|
|
154
|
+
# 2. Extract just the file you need
|
|
155
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-<TIMESTAMP>-<SIG>.tar.gz -C /tmp ./agents.json
|
|
156
|
+
|
|
157
|
+
# 3. Back up the current live file
|
|
158
|
+
cp ~/.local/share/tide-commander/agents.json ~/.local/share/tide-commander/agents.json.pre-restore
|
|
159
|
+
|
|
160
|
+
# 4. Copy the restored file into place
|
|
161
|
+
cp /tmp/agents.json ~/.local/share/tide-commander/agents.json
|
|
162
|
+
|
|
163
|
+
# 5. Restart Commander to pick up the change
|
|
164
|
+
\`\`\`
|
|
165
|
+
|
|
166
|
+
Replace \`agents.json\` with whichever file you need to restore.
|
|
167
|
+
|
|
168
|
+
### Restore the Full Data Directory
|
|
169
|
+
|
|
170
|
+
Use when the entire data directory is corrupted or missing.
|
|
171
|
+
|
|
172
|
+
\`\`\`bash
|
|
173
|
+
# 1. Stop Commander first
|
|
174
|
+
|
|
175
|
+
# 2. Back up whatever is currently there (even if broken)
|
|
176
|
+
mv ~/.local/share/tide-commander ~/.local/share/tide-commander-broken-$(date +%Y%m%d)
|
|
177
|
+
|
|
178
|
+
# 3. Create fresh data directory
|
|
179
|
+
mkdir -p ~/.local/share/tide-commander
|
|
180
|
+
|
|
181
|
+
# 4. Pick the backup to restore from
|
|
182
|
+
ls -lhtr ~/.local/share/tide-commander-backups/backup-*.tar.gz
|
|
183
|
+
|
|
184
|
+
# 5. Extract the chosen backup into the data directory
|
|
185
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-<TIMESTAMP>-<SIG>.tar.gz -C ~/.local/share/tide-commander
|
|
186
|
+
|
|
187
|
+
# 6. Remove running-processes.json (stale PIDs)
|
|
188
|
+
rm -f ~/.local/share/tide-commander/running-processes.json
|
|
189
|
+
rm -f ~/.local/share/tide-commander/running-processes.json.bak
|
|
190
|
+
|
|
191
|
+
# 7. Remove .bak files (they'll be recreated on first write)
|
|
192
|
+
rm -f ~/.local/share/tide-commander/*.json.bak
|
|
193
|
+
|
|
194
|
+
# 8. Start Commander
|
|
195
|
+
\`\`\`
|
|
196
|
+
|
|
197
|
+
### Restore Only the SQLite Event Database
|
|
198
|
+
|
|
199
|
+
\`\`\`bash
|
|
200
|
+
# 1. Extract the database from the backup
|
|
201
|
+
tar -xzf ~/.local/share/tide-commander-backups/backup-<TIMESTAMP>-<SIG>.tar.gz -C /tmp ./events.db
|
|
202
|
+
|
|
203
|
+
# 2. Back up the current one
|
|
204
|
+
cp ~/.local/share/tide-commander/events.db ~/.local/share/tide-commander/events.db.pre-restore
|
|
205
|
+
|
|
206
|
+
# 3. Replace it (Commander should be stopped or at minimum idle)
|
|
207
|
+
cp /tmp/events.db ~/.local/share/tide-commander/events.db
|
|
208
|
+
|
|
209
|
+
# 4. Remove WAL files so SQLite starts fresh
|
|
210
|
+
rm -f ~/.local/share/tide-commander/events.db-wal
|
|
211
|
+
rm -f ~/.local/share/tide-commander/events.db-shm
|
|
212
|
+
\`\`\`
|
|
213
|
+
|
|
214
|
+
## Troubleshooting
|
|
215
|
+
|
|
216
|
+
### Backups Not Being Created
|
|
217
|
+
|
|
218
|
+
1. Check scheduler status via the API (see above).
|
|
219
|
+
2. Check if the script exists: \`ls -la scripts/backup-data.sh\`
|
|
220
|
+
3. Check the backup log: \`tail -20 ~/.local/share/tide-commander-backups/backup.log\`
|
|
221
|
+
4. Run the script manually to see errors: \`bash scripts/backup-data.sh\`
|
|
222
|
+
5. Ensure \`sqlite3\` is installed: \`which sqlite3\`
|
|
223
|
+
|
|
224
|
+
### All Backups Have the Same Signature
|
|
225
|
+
|
|
226
|
+
This means data hasn't changed between runs. This is expected behavior — the dedup is working correctly. Backups are only created when content actually changes.
|
|
227
|
+
|
|
228
|
+
### Backup Directory Growing Too Large
|
|
229
|
+
|
|
230
|
+
Check total size:
|
|
231
|
+
\`\`\`bash
|
|
232
|
+
du -sh ~/.local/share/tide-commander-backups/
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
The rotation policy (8 hourly + 2 prior days) should keep it bounded. If backups are unusually large, the SQLite database may have grown. Consider running \`VACUUM\` on the live database:
|
|
236
|
+
\`\`\`bash
|
|
237
|
+
sqlite3 ~/.local/share/tide-commander/events.db "VACUUM;"
|
|
238
|
+
\`\`\`
|
|
239
|
+
|
|
240
|
+
Then the next backup will pick up the smaller database.
|
|
241
|
+
`,
|
|
242
|
+
};
|
|
@@ -21,6 +21,7 @@ import { bossInstructions } from './boss-instructions.js';
|
|
|
21
21
|
import { workflowDesigner } from './workflow-designer.js';
|
|
22
22
|
import { triggerDesigner } from './trigger-designer.js';
|
|
23
23
|
import { workflowBuilder } from './workflow-builder.js';
|
|
24
|
+
import { backupRestore } from './backup-restore.js';
|
|
24
25
|
/**
|
|
25
26
|
* All built-in skills that ship with Tide Commander
|
|
26
27
|
*/
|
|
@@ -41,6 +42,7 @@ export const BUILTIN_SKILLS = [
|
|
|
41
42
|
workflowDesigner,
|
|
42
43
|
triggerDesigner,
|
|
43
44
|
workflowBuilder,
|
|
45
|
+
backupRestore,
|
|
44
46
|
];
|
|
45
47
|
/**
|
|
46
48
|
* Get the ID for a built-in skill based on its slug
|
|
@@ -15,6 +15,7 @@ import * as eventQueries from './data/event-queries.js';
|
|
|
15
15
|
import { logger, closeFileLogging, getLogFilePath, createLogger } from './utils/logger.js';
|
|
16
16
|
import { setupTerminalWsProxy } from './services/terminal-proxy.js';
|
|
17
17
|
import { initIntegrations, shutdownIntegrations, getIntegrationTriggerHandlers } from './integrations/integration-registry.js';
|
|
18
|
+
import { initBackupService, shutdownBackupService } from './services/backup-service.js';
|
|
18
19
|
// Configuration
|
|
19
20
|
const PORT = process.env.PORT || 6200;
|
|
20
21
|
const HOST = process.env.HOST || (process.env.LISTEN_ALL_INTERFACES ? '::' : '127.0.0.1');
|
|
@@ -138,6 +139,8 @@ async function main() {
|
|
|
138
139
|
for (const handler of getIntegrationTriggerHandlers()) {
|
|
139
140
|
triggerService.registerHandler(handler);
|
|
140
141
|
}
|
|
142
|
+
// Start hourly backup scheduler (reads persisted enabled/disabled setting)
|
|
143
|
+
initBackupService();
|
|
141
144
|
logger.server.log(`Data directory: ${getDataDir()}`);
|
|
142
145
|
logger.server.log(`Log file: ${getLogFilePath()}`);
|
|
143
146
|
// Create Express app and HTTP server
|
|
@@ -199,6 +202,7 @@ async function main() {
|
|
|
199
202
|
}, FORCE_SHUTDOWN_TIMEOUT_MS);
|
|
200
203
|
forceShutdownTimer.unref();
|
|
201
204
|
try {
|
|
205
|
+
shutdownBackupService();
|
|
202
206
|
triggerService.shutdown();
|
|
203
207
|
workflowService.shutdown();
|
|
204
208
|
await shutdownIntegrations();
|
|
@@ -17,6 +17,7 @@ import { buildCustomAgentConfig } from '../websocket/handlers/command-handler.js
|
|
|
17
17
|
import { clearDelegation, getBossForSubordinate } from '../websocket/handlers/boss-response-handler.js';
|
|
18
18
|
import { OpencodeBackend } from '../opencode/backend.js';
|
|
19
19
|
import { getSystemPrompt, setSystemPrompt, clearSystemPrompt, isEchoPromptEnabled, setEchoPromptEnabled, getCodexBinaryPath, setCodexBinaryPath, isTmuxModeEnabled, setTmuxModeEnabled } from '../services/system-prompt-service.js';
|
|
20
|
+
import { getBackupStatus, setBackupEnabled } from '../services/backup-service.js';
|
|
20
21
|
const log = createLogger('Routes');
|
|
21
22
|
const router = Router();
|
|
22
23
|
// Store for broadcasting via WebSocket
|
|
@@ -1050,4 +1051,30 @@ router.post('/system-settings/tmux-mode', (req, res) => {
|
|
|
1050
1051
|
res.status(500).json({ error: err.message });
|
|
1051
1052
|
}
|
|
1052
1053
|
});
|
|
1054
|
+
// GET /api/system-settings/backup - Get hourly backup scheduler status
|
|
1055
|
+
router.get('/system-settings/backup', (_req, res) => {
|
|
1056
|
+
try {
|
|
1057
|
+
res.json(getBackupStatus());
|
|
1058
|
+
}
|
|
1059
|
+
catch (err) {
|
|
1060
|
+
log.error(' Failed to get backup status:', err);
|
|
1061
|
+
res.status(500).json({ error: err.message });
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
// POST /api/system-settings/backup - Enable or disable the hourly backup scheduler
|
|
1065
|
+
router.post('/system-settings/backup', (req, res) => {
|
|
1066
|
+
try {
|
|
1067
|
+
const { enabled } = req.body;
|
|
1068
|
+
if (typeof enabled !== 'boolean') {
|
|
1069
|
+
res.status(400).json({ error: 'enabled must be a boolean' });
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const status = setBackupEnabled(enabled);
|
|
1073
|
+
res.json({ success: true, ...status });
|
|
1074
|
+
}
|
|
1075
|
+
catch (err) {
|
|
1076
|
+
log.error(' Failed to set backup enabled:', err);
|
|
1077
|
+
res.status(500).json({ error: err.message });
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1053
1080
|
export default router;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Service
|
|
3
|
+
*
|
|
4
|
+
* In-process hourly backup scheduler. Runs inside the Commander node process
|
|
5
|
+
* (no cron, no external deps). When Commander is down the data files can't
|
|
6
|
+
* change, so there's no downside to coupling the backup lifecycle to the
|
|
7
|
+
* server process.
|
|
8
|
+
*
|
|
9
|
+
* The actual backup work is done by scripts/backup-data.sh (shipped with the
|
|
10
|
+
* package). This service calls it via child_process.execFile on a setInterval.
|
|
11
|
+
*/
|
|
12
|
+
import { execFile } from 'child_process';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { createLogger } from '../utils/logger.js';
|
|
18
|
+
const log = createLogger('BackupService');
|
|
19
|
+
const INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
20
|
+
// ── Paths ────────────────────────────────────────────────────────────────────
|
|
21
|
+
const DATA_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tide-commander');
|
|
22
|
+
const SETTINGS_FILE = path.join(DATA_DIR, 'backup-settings.json');
|
|
23
|
+
const BACKUP_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tide-commander-backups');
|
|
24
|
+
function findProjectRoot() {
|
|
25
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
while (dir !== '/' && dir !== '') {
|
|
27
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
const parent = path.dirname(dir);
|
|
31
|
+
if (parent === dir)
|
|
32
|
+
break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return process.cwd();
|
|
36
|
+
}
|
|
37
|
+
const PROJECT_ROOT = findProjectRoot();
|
|
38
|
+
export function getBackupScriptPath() {
|
|
39
|
+
return path.join(PROJECT_ROOT, 'scripts', 'backup-data.sh');
|
|
40
|
+
}
|
|
41
|
+
function readSettings() {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8'));
|
|
45
|
+
return { enabled: !!data.enabled };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
log.error(`Failed to read backup settings: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
// Default: enabled — backups are on for everyone out of the box.
|
|
52
|
+
return { enabled: true };
|
|
53
|
+
}
|
|
54
|
+
function writeSettings(settings) {
|
|
55
|
+
if (!fs.existsSync(DATA_DIR)) {
|
|
56
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
// ── In-process scheduler ─────────────────────────────────────────────────────
|
|
61
|
+
let timer = null;
|
|
62
|
+
let lastRunAt = null;
|
|
63
|
+
let lastRunOk = null;
|
|
64
|
+
let lastRunError = null;
|
|
65
|
+
function runBackupScript() {
|
|
66
|
+
const scriptPath = getBackupScriptPath();
|
|
67
|
+
if (!fs.existsSync(scriptPath)) {
|
|
68
|
+
log.error(`Backup script not found at ${scriptPath} — skipping`);
|
|
69
|
+
lastRunAt = new Date().toISOString();
|
|
70
|
+
lastRunOk = false;
|
|
71
|
+
lastRunError = 'Script not found';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
log.log('Starting hourly backup…');
|
|
75
|
+
execFile('bash', [scriptPath], { timeout: 120_000 }, (err, stdout, stderr) => {
|
|
76
|
+
lastRunAt = new Date().toISOString();
|
|
77
|
+
if (err) {
|
|
78
|
+
lastRunOk = false;
|
|
79
|
+
lastRunError = stderr?.trim() || err.message;
|
|
80
|
+
log.error(`Backup failed: ${lastRunError}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
lastRunOk = true;
|
|
84
|
+
lastRunError = null;
|
|
85
|
+
const msg = stdout?.trim();
|
|
86
|
+
if (msg)
|
|
87
|
+
log.log(msg);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function startScheduler() {
|
|
92
|
+
if (timer)
|
|
93
|
+
return; // already running
|
|
94
|
+
timer = setInterval(runBackupScript, INTERVAL_MS);
|
|
95
|
+
timer.unref(); // don't hold the process open
|
|
96
|
+
log.log('Backup scheduler started (every 1 h)');
|
|
97
|
+
// Run immediately on first start so the first backup doesn't wait an hour.
|
|
98
|
+
runBackupScript();
|
|
99
|
+
}
|
|
100
|
+
function stopScheduler() {
|
|
101
|
+
if (!timer)
|
|
102
|
+
return;
|
|
103
|
+
clearInterval(timer);
|
|
104
|
+
timer = null;
|
|
105
|
+
log.log('Backup scheduler stopped');
|
|
106
|
+
}
|
|
107
|
+
export function getBackupStatus() {
|
|
108
|
+
const settings = readSettings();
|
|
109
|
+
return {
|
|
110
|
+
enabled: settings.enabled,
|
|
111
|
+
running: timer !== null,
|
|
112
|
+
scriptPath: getBackupScriptPath(),
|
|
113
|
+
scriptExists: fs.existsSync(getBackupScriptPath()),
|
|
114
|
+
backupDir: BACKUP_DIR,
|
|
115
|
+
lastRunAt,
|
|
116
|
+
lastRunOk,
|
|
117
|
+
lastRunError,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function setBackupEnabled(enabled) {
|
|
121
|
+
writeSettings({ enabled });
|
|
122
|
+
if (enabled) {
|
|
123
|
+
startScheduler();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
stopScheduler();
|
|
127
|
+
}
|
|
128
|
+
return getBackupStatus();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Called once at server boot. Reads persisted setting and starts the
|
|
132
|
+
* scheduler if backups are enabled.
|
|
133
|
+
*/
|
|
134
|
+
export function initBackupService() {
|
|
135
|
+
const { enabled } = readSettings();
|
|
136
|
+
if (enabled) {
|
|
137
|
+
startScheduler();
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
log.log('Backups disabled — scheduler not started');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Called on graceful shutdown to clear the interval timer cleanly.
|
|
145
|
+
*/
|
|
146
|
+
export function shutdownBackupService() {
|
|
147
|
+
stopScheduler();
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tide-commander",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.66.0",
|
|
4
4
|
"description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
|
+
"scripts",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tide Commander data backup script.
|
|
3
|
+
#
|
|
4
|
+
# - Backs up ~/.local/share/tide-commander (SQLite + JSON configs).
|
|
5
|
+
# - Uses sqlite3 .backup to avoid corruption from concurrent writes.
|
|
6
|
+
# - Names backups with a content signature so unchanged state does not
|
|
7
|
+
# create a new copy.
|
|
8
|
+
# - Rotation: keeps the 8 newest hourly backups, plus the most recent
|
|
9
|
+
# backup from each of the 2 most recent prior days.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
DATA_DIR="${TIDE_COMMANDER_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/tide-commander}"
|
|
14
|
+
BACKUP_DIR="${TIDE_COMMANDER_BACKUP_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/tide-commander-backups}"
|
|
15
|
+
HOURLY_KEEP=8
|
|
16
|
+
PRIOR_DAYS_KEEP=2
|
|
17
|
+
|
|
18
|
+
log() { printf '[tide-backup %s] %s\n' "$(date -Iseconds)" "$*"; }
|
|
19
|
+
|
|
20
|
+
if [[ ! -d "$DATA_DIR" ]]; then
|
|
21
|
+
log "data dir not found: $DATA_DIR (nothing to back up)"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
mkdir -p "$BACKUP_DIR"
|
|
26
|
+
|
|
27
|
+
STAGE_DIR="$(mktemp -d)"
|
|
28
|
+
trap 'rm -rf "$STAGE_DIR"' EXIT
|
|
29
|
+
|
|
30
|
+
# Stage JSON / text config files (top-level only; subdirs like query-history
|
|
31
|
+
# and generated are considered derivable and skipped to keep backups small).
|
|
32
|
+
shopt -s nullglob
|
|
33
|
+
for f in "$DATA_DIR"/*.json "$DATA_DIR"/*.json.bak; do
|
|
34
|
+
[[ -f "$f" ]] && cp -p "$f" "$STAGE_DIR/"
|
|
35
|
+
done
|
|
36
|
+
shopt -u nullglob
|
|
37
|
+
|
|
38
|
+
# Safe SQLite backup. Requires sqlite3 on PATH; if missing, fall back to a
|
|
39
|
+
# plain copy of the main db file.
|
|
40
|
+
if [[ -f "$DATA_DIR/events.db" ]]; then
|
|
41
|
+
if command -v sqlite3 >/dev/null 2>&1; then
|
|
42
|
+
sqlite3 "$DATA_DIR/events.db" ".backup '$STAGE_DIR/events.db'"
|
|
43
|
+
else
|
|
44
|
+
log "sqlite3 not found, using plain copy (may be inconsistent if written during copy)"
|
|
45
|
+
cp -p "$DATA_DIR/events.db" "$STAGE_DIR/events.db"
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Compute signature: deterministic hash of staged file contents + names.
|
|
50
|
+
# (Mtimes intentionally excluded so identical content skips new backup.)
|
|
51
|
+
SIGNATURE=$(
|
|
52
|
+
cd "$STAGE_DIR" && \
|
|
53
|
+
find . -type f -print0 | LC_ALL=C sort -z | \
|
|
54
|
+
xargs -0 sha256sum | sha256sum | cut -c1-16
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Skip if a backup with this signature already exists.
|
|
58
|
+
if compgen -G "$BACKUP_DIR/backup-*-$SIGNATURE.tar.gz" >/dev/null; then
|
|
59
|
+
log "no changes since last backup (sig=$SIGNATURE), skipping"
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
TS="$(date +%Y%m%dT%H%M%S)"
|
|
64
|
+
OUT="$BACKUP_DIR/backup-${TS}-${SIGNATURE}.tar.gz"
|
|
65
|
+
tar -C "$STAGE_DIR" -czf "$OUT" .
|
|
66
|
+
log "created $OUT"
|
|
67
|
+
|
|
68
|
+
# Rotation: files are named backup-YYYYMMDDThhmmss-<sig>.tar.gz, which sorts
|
|
69
|
+
# correctly lexicographically (newest last when sorted ascending).
|
|
70
|
+
rotate() {
|
|
71
|
+
local today
|
|
72
|
+
today="$(date +%Y%m%d)"
|
|
73
|
+
|
|
74
|
+
mapfile -t files < <(ls -1 "$BACKUP_DIR"/backup-*.tar.gz 2>/dev/null | sort -r) || return 0
|
|
75
|
+
[[ ${#files[@]} -eq 0 ]] && return 0
|
|
76
|
+
|
|
77
|
+
declare -A keep=()
|
|
78
|
+
|
|
79
|
+
# Keep newest $HOURLY_KEEP regardless of day.
|
|
80
|
+
local i=0
|
|
81
|
+
for f in "${files[@]}"; do
|
|
82
|
+
if (( i < HOURLY_KEEP )); then
|
|
83
|
+
keep["$f"]=1
|
|
84
|
+
i=$((i + 1))
|
|
85
|
+
else
|
|
86
|
+
break
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
# Keep the newest backup from each of the most recent prior days.
|
|
91
|
+
declare -A seen_days=()
|
|
92
|
+
local prior=0
|
|
93
|
+
for f in "${files[@]}"; do
|
|
94
|
+
local name day
|
|
95
|
+
name="$(basename "$f")"
|
|
96
|
+
# Strip "backup-" (7 chars), take the next 8 (YYYYMMDD).
|
|
97
|
+
day="${name:7:8}"
|
|
98
|
+
[[ "$day" == "$today" ]] && continue
|
|
99
|
+
[[ -n "${seen_days[$day]:-}" ]] && continue
|
|
100
|
+
seen_days["$day"]=1
|
|
101
|
+
keep["$f"]=1
|
|
102
|
+
prior=$((prior + 1))
|
|
103
|
+
(( prior >= PRIOR_DAYS_KEEP )) && break
|
|
104
|
+
done
|
|
105
|
+
|
|
106
|
+
for f in "${files[@]}"; do
|
|
107
|
+
if [[ -z "${keep[$f]:-}" ]]; then
|
|
108
|
+
rm -f "$f"
|
|
109
|
+
log "rotated out $(basename "$f")"
|
|
110
|
+
fi
|
|
111
|
+
done
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
rotate
|
|
115
|
+
|
|
116
|
+
log "done (kept $(ls -1 "$BACKUP_DIR"/backup-*.tar.gz 2>/dev/null | wc -l) backup(s))"
|