tide-commander 1.90.0 → 1.91.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-CbmJrD24.js → BossLogsModal-XsTxfWM8.js} +1 -1
- package/dist/assets/{BossSpawnModal-DU0qQ0rG.js → BossSpawnModal-DqQMPxHu.js} +1 -1
- package/dist/assets/{ControlsModal-D1g7JwQt.js → ControlsModal-5mzDDdS5.js} +1 -1
- package/dist/assets/{DockerLogsModal-CkyvuwA2.js → DockerLogsModal-2eHlxyKa.js} +1 -1
- package/dist/assets/{EmbeddedEditor-DbEqhrGZ.js → EmbeddedEditor-Bi9Ysd99.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-w1td375-.js → GmailOAuthSetup-5u85N8Br.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-Bh7zsyVT.js → GoogleOAuthSetup-OxT_QwZL.js} +1 -1
- package/dist/assets/{IframeModal-BVjYTjHT.js → IframeModal-Bn1kdP1S.js} +1 -1
- package/dist/assets/{IntegrationsPanel-C8C4RTuV.js → IntegrationsPanel-BehHkKJu.js} +2 -2
- package/dist/assets/{LogViewerModal-Hl2pWr16.js → LogViewerModal-JuUpWFPL.js} +1 -1
- package/dist/assets/{MonitoringModal-DyjG8VYN.js → MonitoringModal-CLk3uqDa.js} +1 -1
- package/dist/assets/{PM2LogsModal-Cxo0h_TY.js → PM2LogsModal-C_NpOsos.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-B5d3KfEX.js → RestoreArchivedAreaModal-Cbcg2Fm8.js} +1 -1
- package/dist/assets/{Scene2DCanvas-Bz4d5QPH.js → Scene2DCanvas-4C-jHERv.js} +1 -1
- package/dist/assets/{SceneManager-Dio2xh20.js → SceneManager-BoRV8xt3.js} +1 -1
- package/dist/assets/{SkillsPanel-BF_nctIH.js → SkillsPanel-Bwk3UEY_.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-BhzqLsTN.js → SlackMultiInstanceSetup-t-g3hdbr.js} +1 -1
- package/dist/assets/{SpawnModal-o42cyCtw.js → SpawnModal-BOXkPtaJ.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-BN1fhaoP.js → SubordinateAssignmentModal-CLHq5a9b.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-DYYfw4yH.js → TriggerManagerPanel-DuWagsLi.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-W-O_xV8J.js → WorkflowEditorPanel-Bevs1fpc.js} +1 -1
- package/dist/assets/{index-Ce3gEeTm.js → index-B4JdUiAe.js} +2 -2
- package/dist/assets/{index-BREmErtZ.js → index-CJuTMFz9.js} +1 -1
- package/dist/assets/{index-DZ9nmuXW.js → index-CdKOXIM2.js} +1 -1
- package/dist/assets/{index-CA6Z2DnN.js → index-CiXA-Zp-.js} +1 -1
- package/dist/assets/{index-DIp_vmTu.js → index-DBt10C9K.js} +1 -1
- package/dist/assets/{index-Dd46f0FP.js → index-Dd063aRs.js} +1 -1
- package/dist/assets/{index-BiFafZOt.js → index-DxHwQ6CI.js} +3 -3
- package/dist/assets/{index-CpjE7hlE.js → index-H8kj1tuO.js} +1 -1
- package/dist/assets/{index-Bms941O_.js → index-vFrHpR5s.js} +1 -1
- package/dist/assets/{main-zTbYBQt_.js → main-5eyR3isL.js} +93 -93
- package/dist/assets/{main-BwtifKg3.css → main-CrGeO0Sc.css} +1 -1
- package/dist/assets/{web-BUClwxRF.js → web-Cx_ySRHK.js} +1 -1
- package/dist/assets/{web-EQqNdAMY.js → web-DGO1VHbi.js} +1 -1
- package/dist/assets/{web-BovL85Jz.js → web-DMjkVCWy.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/claude/backend.js +11 -0
- package/dist/src/packages/server/data/builtin-skills/agent-memory.js +126 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/routes/agents.js +182 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as s}from"./main-
|
|
1
|
+
import{ck as s}from"./main-5eyR3isL.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};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ck as a}from"./main-
|
|
1
|
+
import{ck as a}from"./main-5eyR3isL.js";import{ImpactStyle as i,NotificationType as r}from"./index-B4JdUiAe.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{ck as t}from"./main-
|
|
1
|
+
import{ck as t}from"./main-5eyR3isL.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
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-5eyR3isL.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-CrGeO0Sc.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -9,6 +9,7 @@ import { createLogger, sanitizeUnicode } from '../utils/index.js';
|
|
|
9
9
|
import { TIDE_COMMANDER_APPENDED_PROMPT } from '../prompts/tide-commander.js';
|
|
10
10
|
import { getSystemPrompt, isEchoPromptEnabled } from '../services/system-prompt-service.js';
|
|
11
11
|
import { loadAreas } from '../data/index.js';
|
|
12
|
+
import { getAgent } from '../services/agent-service.js';
|
|
12
13
|
const log = createLogger('Backend');
|
|
13
14
|
// Track tool_use_id to tool_name mapping for matching tool_result events
|
|
14
15
|
// This is a module-level map that persists across parseEvent calls
|
|
@@ -46,6 +47,16 @@ export function buildAppendedProjectInstructions(config) {
|
|
|
46
47
|
sections.push(`## Area-Level Prompt (${agentArea.name})`, areaPrompt);
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
// Per-agent persistent memory — the agent's own notes/lessons accumulated
|
|
51
|
+
// over time. Injected between the global system prompt and class instructions
|
|
52
|
+
// so the agent's self-curated context is visible before class-level rules.
|
|
53
|
+
if (config.agentId) {
|
|
54
|
+
const agent = getAgent(config.agentId);
|
|
55
|
+
const agentMemory = agent?.memory?.trim();
|
|
56
|
+
if (agentMemory) {
|
|
57
|
+
sections.push('## Agent Memory (Your Notes To Yourself)', 'The following are notes you have saved to your own persistent memory across conversations — past lessons, user preferences, project context, and references you have chosen to retain. Use them as authoritative context but verify before acting on stale-sounding facts. Update them via the `agent-memory` skill when you learn something worth keeping.', agentMemory);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
49
60
|
const customPrompt = config.customAgent?.definition?.prompt?.trim();
|
|
50
61
|
if (customPrompt) {
|
|
51
62
|
sections.push('## Agent Class Instructions', 'The following instructions are mandatory unless the user explicitly overrides them.', customPrompt);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const agentMemory = {
|
|
2
|
+
slug: 'agent-memory',
|
|
3
|
+
name: 'Agent Memory',
|
|
4
|
+
description: 'Save important notes, lessons, preferences, and context to your persistent agent memory. Use proactively whenever you learn something worth remembering across conversations.',
|
|
5
|
+
allowedTools: ['Bash(curl:*)'],
|
|
6
|
+
assignedAgentClasses: ['*'],
|
|
7
|
+
content: `# Agent Memory (Your Own Persistent Notes)
|
|
8
|
+
|
|
9
|
+
You have a **per-agent persistent memory string** that is automatically injected into your system prompt on every turn under the section \`## Agent Memory (Your Notes To Yourself)\`. It survives across conversations, restarts, and context resets. It is yours — only you (and the user via UI) edit it.
|
|
10
|
+
|
|
11
|
+
Use it to remember things that would otherwise be lost the next time your context is cleared.
|
|
12
|
+
|
|
13
|
+
## Endpoints
|
|
14
|
+
|
|
15
|
+
All endpoints are authenticated with the shared \`X-Auth-Token: abcd\` header (same as every other Tide Commander API).
|
|
16
|
+
|
|
17
|
+
\`\`\`bash
|
|
18
|
+
# Read your current memory
|
|
19
|
+
curl -s -H "X-Auth-Token: abcd" \\
|
|
20
|
+
http://localhost:5174/api/agents/YOUR_AGENT_ID/memory
|
|
21
|
+
|
|
22
|
+
# Replace your memory (FULL REPLACE — see read-modify-write below)
|
|
23
|
+
curl -s -X PATCH -H "X-Auth-Token: abcd" \\
|
|
24
|
+
http://localhost:5174/api/agents/YOUR_AGENT_ID/memory \\
|
|
25
|
+
-H "Content-Type: application/json" \\
|
|
26
|
+
-d @- <<'EOF'
|
|
27
|
+
{"memory": "## Project context\\n- foo\\n- bar\\n"}
|
|
28
|
+
EOF
|
|
29
|
+
|
|
30
|
+
# Clear your memory completely
|
|
31
|
+
curl -s -X DELETE -H "X-Auth-Token: abcd" \\
|
|
32
|
+
http://localhost:5174/api/agents/YOUR_AGENT_ID/memory
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
Substitute \`YOUR_AGENT_ID\` with the real agent ID from your identity block.
|
|
36
|
+
|
|
37
|
+
## When to write to memory
|
|
38
|
+
|
|
39
|
+
Save things that will still be useful **next conversation**, especially when they're not obvious from reading the code:
|
|
40
|
+
|
|
41
|
+
- **User preferences:** "User prefers terse responses with no trailing summaries"
|
|
42
|
+
- **Project facts:** "Backend uses Bun, not Node — \`npm run build\` is wrong, use \`bun run build\`"
|
|
43
|
+
- **Lessons from corrections:** "User got annoyed when I created new files for one-off scripts — prefer inline edits"
|
|
44
|
+
- **Useful debugging recipes:** "When tsc fails with TS2307 on .js extension, the file is in /src not /dist; build first"
|
|
45
|
+
- **External system references:** "Bugs tracked in Linear project 'INGEST'; deploy logs at grafana.internal/d/deploys"
|
|
46
|
+
- **Architectural decisions:** "Auth middleware was rewritten in v1.85 for compliance — do NOT re-introduce the old session-token pattern"
|
|
47
|
+
|
|
48
|
+
## When NOT to write to memory
|
|
49
|
+
|
|
50
|
+
- Code patterns / structure / file paths — re-derive with Grep
|
|
51
|
+
- Git history or recent diffs — \`git log\` is authoritative
|
|
52
|
+
- Things already in CLAUDE.md
|
|
53
|
+
- Temporary in-conversation state (current task, current todo list)
|
|
54
|
+
- Debug output, build errors, scratch logs
|
|
55
|
+
|
|
56
|
+
If removing a memory entry wouldn't confuse future-you, don't write it.
|
|
57
|
+
|
|
58
|
+
## CRITICAL: PATCH is a full replace — use read-modify-write
|
|
59
|
+
|
|
60
|
+
The PATCH endpoint overwrites your entire memory. You must:
|
|
61
|
+
|
|
62
|
+
1. **GET** the current memory first
|
|
63
|
+
2. **Merge** your new note into the existing content (don't blow it away)
|
|
64
|
+
3. **PATCH** the merged result back
|
|
65
|
+
|
|
66
|
+
Example flow:
|
|
67
|
+
|
|
68
|
+
\`\`\`bash
|
|
69
|
+
# 1. Read current memory
|
|
70
|
+
current=$(curl -s -H "X-Auth-Token: abcd" \\
|
|
71
|
+
http://localhost:5174/api/agents/YOUR_AGENT_ID/memory | \\
|
|
72
|
+
jq -r '.memory')
|
|
73
|
+
|
|
74
|
+
# 2. (You merge \$current with your new note in your head, then write it.)
|
|
75
|
+
|
|
76
|
+
# 3. Write merged result back
|
|
77
|
+
curl -s -X PATCH -H "X-Auth-Token: abcd" \\
|
|
78
|
+
http://localhost:5174/api/agents/YOUR_AGENT_ID/memory \\
|
|
79
|
+
-H "Content-Type: application/json" \\
|
|
80
|
+
-d @- <<'EOF'
|
|
81
|
+
{"memory": "<the merged content here>"}
|
|
82
|
+
EOF
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
If your memory is empty (first time), skip step 1 and just PATCH the new content.
|
|
86
|
+
|
|
87
|
+
## Suggested format — keep memory organized
|
|
88
|
+
|
|
89
|
+
Use a small set of markdown sections so the file stays scannable as it grows. A good starting structure:
|
|
90
|
+
|
|
91
|
+
\`\`\`markdown
|
|
92
|
+
## Project context
|
|
93
|
+
- The backend uses Bun, not Node.
|
|
94
|
+
- Frontend lives in src/packages/client.
|
|
95
|
+
|
|
96
|
+
## User preferences
|
|
97
|
+
- Terse responses, no trailing summaries.
|
|
98
|
+
- Always full project-relative paths.
|
|
99
|
+
|
|
100
|
+
## Lessons learned
|
|
101
|
+
- Don't run npm install — use bun install.
|
|
102
|
+
- The pre-commit hook will reject Co-Authored-By trailers.
|
|
103
|
+
|
|
104
|
+
## External references
|
|
105
|
+
- Linear project "INGEST" for pipeline bugs.
|
|
106
|
+
- Grafana board grafana.internal/d/api-latency for oncall.
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
Add, rewrite, or prune sections as your understanding evolves. Outdated entries are worse than no entries — delete them when they no longer apply.
|
|
110
|
+
|
|
111
|
+
## Size limit
|
|
112
|
+
|
|
113
|
+
Keep total memory under **~10KB**. Memory is injected into every system prompt for this agent, so bloat = wasted context window on every turn. If you're approaching that, prune the least useful entries before adding new ones.
|
|
114
|
+
|
|
115
|
+
## When to use this skill proactively
|
|
116
|
+
|
|
117
|
+
You do **not** need the user to ask. If you observe:
|
|
118
|
+
|
|
119
|
+
- The user **corrects** you ("no, use X instead") — save the correction.
|
|
120
|
+
- The user **confirms** a non-obvious approach ("yes that was right") — save the validated approach.
|
|
121
|
+
- You discover a **project-specific fact** the codebase doesn't make obvious — save it.
|
|
122
|
+
- You learn an **external resource** location — save the pointer.
|
|
123
|
+
|
|
124
|
+
Read your memory at the start of any task where remembered context would change your approach — but trust **current code** over memory if the two conflict (memory can go stale; update or remove stale entries when you spot them).
|
|
125
|
+
`,
|
|
126
|
+
};
|
|
@@ -17,6 +17,7 @@ import { exploreDatabase } from './explore-database.js';
|
|
|
17
17
|
import { releasePipeline } from './release-pipeline.js';
|
|
18
18
|
import { taskLabel } from './task-label.js';
|
|
19
19
|
import { agentTracking } from './agent-tracking.js';
|
|
20
|
+
import { agentMemory } from './agent-memory.js';
|
|
20
21
|
import { reportTaskToBoss } from './report-task-to-boss.js';
|
|
21
22
|
import { bossInstructions } from './boss-instructions.js';
|
|
22
23
|
import { workflowDesigner } from './workflow-designer.js';
|
|
@@ -39,6 +40,7 @@ export const BUILTIN_SKILLS = [
|
|
|
39
40
|
releasePipeline,
|
|
40
41
|
taskLabel,
|
|
41
42
|
agentTracking,
|
|
43
|
+
agentMemory,
|
|
42
44
|
reportTaskToBoss,
|
|
43
45
|
bossInstructions,
|
|
44
46
|
workflowDesigner,
|
|
@@ -7,7 +7,7 @@ import { spawn } from 'child_process';
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as os from 'os';
|
|
10
|
-
import { agentService, runtimeService, bossMessageService } from '../services/index.js';
|
|
10
|
+
import { agentService, runtimeService, bossMessageService, skillService } from '../services/index.js';
|
|
11
11
|
import { getClaudeProjectDir, loadAreas, saveAreas } from '../data/index.js';
|
|
12
12
|
import { loadSession } from '../claude/session-loader.js';
|
|
13
13
|
import { getAllCustomClasses } from '../services/custom-class-service.js';
|
|
@@ -581,6 +581,148 @@ router.post('/bulk/move-area', async (req, res) => {
|
|
|
581
581
|
res.status(500).json({ error: err.message });
|
|
582
582
|
}
|
|
583
583
|
});
|
|
584
|
+
// Normalize body { skillId?: string; skillIds?: string[] } into a deduped array.
|
|
585
|
+
// Accepts the legacy single-skill form so older callers keep working.
|
|
586
|
+
function normalizeSkillIds(body) {
|
|
587
|
+
const list = [];
|
|
588
|
+
if (Array.isArray(body.skillIds)) {
|
|
589
|
+
for (const id of body.skillIds) {
|
|
590
|
+
if (typeof id === 'string' && id)
|
|
591
|
+
list.push(id);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (typeof body.skillId === 'string' && body.skillId)
|
|
595
|
+
list.push(body.skillId);
|
|
596
|
+
const deduped = Array.from(new Set(list));
|
|
597
|
+
return deduped.length > 0 ? deduped : null;
|
|
598
|
+
}
|
|
599
|
+
// POST /api/agents/bulk/skills/add - Assign one or more skills to multiple agents (idempotent)
|
|
600
|
+
router.post('/bulk/skills/add', (req, res) => {
|
|
601
|
+
try {
|
|
602
|
+
const { agentIds } = req.body;
|
|
603
|
+
const skillIds = normalizeSkillIds(req.body ?? {});
|
|
604
|
+
if (!Array.isArray(agentIds) || agentIds.length === 0) {
|
|
605
|
+
res.status(400).json({ error: 'agentIds must be a non-empty array of strings' });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (!skillIds) {
|
|
609
|
+
res.status(400).json({ error: 'skillIds (or legacy skillId) is required' });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
for (const sid of skillIds) {
|
|
613
|
+
if (!skillService.getSkill(sid)) {
|
|
614
|
+
res.status(404).json({ error: `Skill not found: ${sid}` });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const results = [];
|
|
619
|
+
for (const sid of skillIds) {
|
|
620
|
+
const updated = [];
|
|
621
|
+
const alreadyHad = [];
|
|
622
|
+
const failed = [];
|
|
623
|
+
const initialSkill = skillService.getSkill(sid);
|
|
624
|
+
for (const agentId of agentIds) {
|
|
625
|
+
try {
|
|
626
|
+
const agent = agentService.getAgent(agentId);
|
|
627
|
+
if (!agent) {
|
|
628
|
+
failed.push(agentId);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
// Re-fetch on each iteration: assignSkillToAgent replaces the skill object
|
|
632
|
+
// in the Map, so a captured outer reference goes stale.
|
|
633
|
+
const current = skillService.getSkill(sid);
|
|
634
|
+
const alreadyAssigned = current?.assignedAgentIds.includes(agentId) ?? false;
|
|
635
|
+
const result = skillService.assignSkillToAgent(sid, agentId);
|
|
636
|
+
if (!result) {
|
|
637
|
+
failed.push(agentId);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (alreadyAssigned)
|
|
641
|
+
alreadyHad.push(agentId);
|
|
642
|
+
else
|
|
643
|
+
updated.push(agentId);
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
log.error(` Bulk add-skill failed for agent ${agentId} / skill ${sid}:`, err);
|
|
647
|
+
failed.push(agentId);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
log.log(`Bulk add-skill ${initialSkill.name}: ${updated.length} added, ${alreadyHad.length} already had, ${failed.length} failed`);
|
|
651
|
+
results.push({ skillId: sid, skillName: initialSkill.name, updated, alreadyHad, failed });
|
|
652
|
+
}
|
|
653
|
+
// Legacy flat shape (kept for single-skill callers) + new shape.
|
|
654
|
+
const legacy = results.length === 1
|
|
655
|
+
? { skillId: results[0].skillId, updated: results[0].updated, alreadyHad: results[0].alreadyHad, failed: results[0].failed }
|
|
656
|
+
: {};
|
|
657
|
+
res.json({ skillIds, results, ...legacy });
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
log.error(' Bulk add-skill failed:', err);
|
|
661
|
+
res.status(500).json({ error: err.message });
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
// POST /api/agents/bulk/skills/remove - Unassign one or more skills from multiple agents (idempotent)
|
|
665
|
+
router.post('/bulk/skills/remove', (req, res) => {
|
|
666
|
+
try {
|
|
667
|
+
const { agentIds } = req.body;
|
|
668
|
+
const skillIds = normalizeSkillIds(req.body ?? {});
|
|
669
|
+
if (!Array.isArray(agentIds) || agentIds.length === 0) {
|
|
670
|
+
res.status(400).json({ error: 'agentIds must be a non-empty array of strings' });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (!skillIds) {
|
|
674
|
+
res.status(400).json({ error: 'skillIds (or legacy skillId) is required' });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
for (const sid of skillIds) {
|
|
678
|
+
if (!skillService.getSkill(sid)) {
|
|
679
|
+
res.status(404).json({ error: `Skill not found: ${sid}` });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const results = [];
|
|
684
|
+
for (const sid of skillIds) {
|
|
685
|
+
const updated = [];
|
|
686
|
+
const didNotHave = [];
|
|
687
|
+
const failed = [];
|
|
688
|
+
const initialSkill = skillService.getSkill(sid);
|
|
689
|
+
for (const agentId of agentIds) {
|
|
690
|
+
try {
|
|
691
|
+
const agent = agentService.getAgent(agentId);
|
|
692
|
+
if (!agent) {
|
|
693
|
+
failed.push(agentId);
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const current = skillService.getSkill(sid);
|
|
697
|
+
const wasAssigned = current?.assignedAgentIds.includes(agentId) ?? false;
|
|
698
|
+
const result = skillService.unassignSkillFromAgent(sid, agentId);
|
|
699
|
+
if (!result) {
|
|
700
|
+
failed.push(agentId);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (wasAssigned)
|
|
704
|
+
updated.push(agentId);
|
|
705
|
+
else
|
|
706
|
+
didNotHave.push(agentId);
|
|
707
|
+
}
|
|
708
|
+
catch (err) {
|
|
709
|
+
log.error(` Bulk remove-skill failed for agent ${agentId} / skill ${sid}:`, err);
|
|
710
|
+
failed.push(agentId);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
log.log(`Bulk remove-skill ${initialSkill.name}: ${updated.length} removed, ${didNotHave.length} didn't have, ${failed.length} failed`);
|
|
714
|
+
results.push({ skillId: sid, skillName: initialSkill.name, updated, didNotHave, failed });
|
|
715
|
+
}
|
|
716
|
+
const legacy = results.length === 1
|
|
717
|
+
? { skillId: results[0].skillId, updated: results[0].updated, didNotHave: results[0].didNotHave, failed: results[0].failed }
|
|
718
|
+
: {};
|
|
719
|
+
res.json({ skillIds, results, ...legacy });
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
log.error(' Bulk remove-skill failed:', err);
|
|
723
|
+
res.status(500).json({ error: err.message });
|
|
724
|
+
}
|
|
725
|
+
});
|
|
584
726
|
// GET /api/agents/bulk/filters - Return available filter values
|
|
585
727
|
router.get('/bulk/filters', (_req, res) => {
|
|
586
728
|
try {
|
|
@@ -685,6 +827,45 @@ router.patch('/:id', (req, res) => {
|
|
|
685
827
|
ok: true,
|
|
686
828
|
});
|
|
687
829
|
});
|
|
830
|
+
// ============================================================================
|
|
831
|
+
// Agent Memory Routes — per-agent persistent notes injected into system prompt
|
|
832
|
+
// ============================================================================
|
|
833
|
+
// GET /api/agents/:id/memory - Read the agent's current memory string
|
|
834
|
+
router.get('/:id/memory', (req, res) => {
|
|
835
|
+
const agent = agentService.getAgent(req.params.id);
|
|
836
|
+
if (!agent) {
|
|
837
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const memory = typeof agent.memory === 'string' ? agent.memory : '';
|
|
841
|
+
res.json({ memory, length: memory.length });
|
|
842
|
+
});
|
|
843
|
+
// PATCH /api/agents/:id/memory - Replace the agent's memory (full replace).
|
|
844
|
+
// Body: { memory: string }
|
|
845
|
+
router.patch('/:id/memory', (req, res) => {
|
|
846
|
+
const { memory } = req.body;
|
|
847
|
+
if (typeof memory !== 'string') {
|
|
848
|
+
res.status(400).json({ error: 'memory must be a string' });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const updated = agentService.updateAgent(req.params.id, { memory }, false);
|
|
852
|
+
if (!updated) {
|
|
853
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
log.log(`Agent ${updated.name} (${updated.id}): memory updated (${memory.length} chars)`);
|
|
857
|
+
res.json({ ok: true, id: updated.id, length: memory.length });
|
|
858
|
+
});
|
|
859
|
+
// DELETE /api/agents/:id/memory - Clear the agent's memory
|
|
860
|
+
router.delete('/:id/memory', (req, res) => {
|
|
861
|
+
const updated = agentService.updateAgent(req.params.id, { memory: '' }, false);
|
|
862
|
+
if (!updated) {
|
|
863
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
log.log(`Agent ${updated.name} (${updated.id}): memory cleared`);
|
|
867
|
+
res.json({ ok: true, id: updated.id });
|
|
868
|
+
});
|
|
688
869
|
// DELETE /api/agents/:id - Delete agent
|
|
689
870
|
router.delete('/:id', (req, res) => {
|
|
690
871
|
const deleted = agentService.deleteAgent(req.params.id);
|