groove-dev 0.27.139 → 0.27.141

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.
Files changed (75) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +225 -16
  5. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  6. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  7. package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  9. package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
  20. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  22. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  23. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
  24. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  25. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
  27. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
  30. package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
  31. package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
  32. package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
  33. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  34. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/integrations-registry.json +12 -44
  38. package/packages/daemon/package.json +1 -1
  39. package/packages/daemon/src/api.js +225 -16
  40. package/packages/daemon/src/index.js +2 -0
  41. package/packages/daemon/src/integrations.js +10 -0
  42. package/packages/daemon/src/introducer.js +17 -1
  43. package/packages/daemon/src/journalist.js +169 -0
  44. package/packages/daemon/src/keeper.js +277 -0
  45. package/packages/daemon/src/model-lab.js +11 -0
  46. package/packages/daemon/src/process.js +76 -0
  47. package/packages/daemon/src/validate.js +9 -0
  48. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  49. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  50. package/packages/gui/dist/index.html +2 -2
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/app.jsx +4 -0
  53. package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
  55. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  56. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  57. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  58. package/packages/gui/src/components/editor/file-tree.jsx +2 -86
  59. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  60. package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
  61. package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  63. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  64. package/packages/gui/src/stores/groove.js +302 -20
  65. package/packages/gui/src/views/agents.jsx +118 -60
  66. package/packages/gui/src/views/editor.jsx +67 -219
  67. package/packages/gui/src/views/memory.jsx +460 -0
  68. package/packages/gui/src/views/model-lab.jsx +1 -6
  69. package/packages/gui/src/views/models.jsx +658 -565
  70. package/plan_files/keeper-manual.md +295 -0
  71. package/plan_files/keeper-memory-system.md +223 -0
  72. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
  73. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
  74. package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
  75. package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
@@ -0,0 +1,277 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ // Keeper — lightweight tagged memory system.
3
+ // Flat file-backed storage: one tag = one markdown file.
4
+ // Hierarchical namespacing via "/" in tag names (groove/memory-system → groove/memory-system.md).
5
+
6
+ import { resolve, dirname } from 'path';
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
8
+
9
+ export const KEEPER_COMMANDS = {
10
+ save: { syntax: '[save] #tag', description: 'Save the current message as a tagged memory' },
11
+ append: { syntax: '[append] #tag', description: 'Add content to an existing memory without overwriting' },
12
+ update: { syntax: '[update] #tag', description: 'Open the memory editor to edit in place' },
13
+ delete: { syntax: '[delete] #tag', description: 'Delete a tagged memory' },
14
+ read: { syntax: '[read] #tag1 #tag2 ...', description: 'Send memory content to the agent — agent reads it, chat stays clean' },
15
+ view: { syntax: '[view] #tag', description: 'Open a memory in the viewer' },
16
+ doc: { syntax: '[doc] #tag', description: 'AI reads the full conversation and generates a robust document' },
17
+ link: { syntax: '[link] #tag path/to/doc', description: 'Link a memory tag to a NORTHSTAR or external document' },
18
+ instruct: { syntax: '[instruct]', description: 'Show all Keeper commands and usage instructions' },
19
+ };
20
+
21
+ export class Keeper {
22
+ constructor(grooveDir) {
23
+ this.dir = resolve(grooveDir, 'keeper');
24
+ this.indexPath = resolve(this.dir, 'index.json');
25
+ mkdirSync(this.dir, { recursive: true });
26
+ this._index = this._loadIndex();
27
+ }
28
+
29
+ _loadIndex() {
30
+ try {
31
+ if (existsSync(this.indexPath)) {
32
+ return JSON.parse(readFileSync(this.indexPath, 'utf8'));
33
+ }
34
+ } catch { /* corrupted — rebuild */ }
35
+ return {};
36
+ }
37
+
38
+ _saveIndex() {
39
+ writeFileSync(this.indexPath, JSON.stringify(this._index, null, 2));
40
+ }
41
+
42
+ _tagToPath(tag) {
43
+ const safe = tag.replace(/[^a-zA-Z0-9/_-]/g, '');
44
+ if (!safe) throw new Error('Invalid tag name');
45
+ return resolve(this.dir, `${safe}.md`);
46
+ }
47
+
48
+ _ensureParentDir(filePath) {
49
+ const dir = dirname(filePath);
50
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
51
+ }
52
+
53
+ _normalize(tag) {
54
+ return tag.replace(/^#/, '').trim().toLowerCase();
55
+ }
56
+
57
+ // ── CRUD ──────────────────────────────────────────────────
58
+
59
+ save(tag, content) {
60
+ if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
61
+ if (content === undefined || content === null) throw new Error('Content is required');
62
+ const normalized = this._normalize(tag);
63
+ if (!normalized) throw new Error('Tag is required');
64
+ const filePath = this._tagToPath(normalized);
65
+ this._ensureParentDir(filePath);
66
+ writeFileSync(filePath, String(content));
67
+ this._index[normalized] = {
68
+ tag: normalized,
69
+ type: this._index[normalized]?.type || 'manual',
70
+ links: this._index[normalized]?.links || [],
71
+ createdAt: this._index[normalized]?.createdAt || new Date().toISOString(),
72
+ updatedAt: new Date().toISOString(),
73
+ size: String(content).length,
74
+ };
75
+ this._saveIndex();
76
+ return { tag: normalized, ...this._index[normalized] };
77
+ }
78
+
79
+ append(tag, content) {
80
+ if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
81
+ if (!content) throw new Error('Content is required');
82
+ const normalized = this._normalize(tag);
83
+ if (!normalized) throw new Error('Tag is required');
84
+ const filePath = this._tagToPath(normalized);
85
+ if (!existsSync(filePath)) {
86
+ return this.save(normalized, content);
87
+ }
88
+ const existing = readFileSync(filePath, 'utf8');
89
+ const updated = existing + '\n\n---\n\n' + String(content);
90
+ writeFileSync(filePath, updated);
91
+ this._index[normalized] = {
92
+ ...this._index[normalized],
93
+ updatedAt: new Date().toISOString(),
94
+ size: updated.length,
95
+ };
96
+ this._saveIndex();
97
+ return { tag: normalized, ...this._index[normalized] };
98
+ }
99
+
100
+ get(tag) {
101
+ const normalized = this._normalize(tag);
102
+ if (!normalized) throw new Error('Tag is required');
103
+ const filePath = this._tagToPath(normalized);
104
+ if (!existsSync(filePath)) return null;
105
+ const content = readFileSync(filePath, 'utf8');
106
+ const meta = this._index[normalized] || {};
107
+ return { tag: normalized, content, ...meta };
108
+ }
109
+
110
+ pull(tags) {
111
+ const items = [];
112
+ for (const tag of tags) {
113
+ const normalized = this._normalize(tag);
114
+ if (!normalized) continue;
115
+ const item = this.get(normalized);
116
+ if (item) items.push(item);
117
+ const children = this.children(normalized);
118
+ for (const child of children) {
119
+ const childItem = this.get(child.tag);
120
+ if (childItem && !items.some(i => i.tag === childItem.tag)) {
121
+ items.push(childItem);
122
+ }
123
+ }
124
+ }
125
+ if (items.length === 0) return null;
126
+ return items.map(i => `## #${i.tag}\n\n${i.content}`).join('\n\n---\n\n');
127
+ }
128
+
129
+ update(tag, content) {
130
+ const normalized = this._normalize(tag);
131
+ if (!normalized) throw new Error('Tag is required');
132
+ if (content === undefined || content === null) throw new Error('Content is required');
133
+ const filePath = this._tagToPath(normalized);
134
+ if (!existsSync(filePath)) throw new Error(`Memory #${normalized} does not exist`);
135
+ writeFileSync(filePath, String(content));
136
+ this._index[normalized] = {
137
+ ...this._index[normalized],
138
+ updatedAt: new Date().toISOString(),
139
+ size: String(content).length,
140
+ };
141
+ this._saveIndex();
142
+ return { tag: normalized, ...this._index[normalized] };
143
+ }
144
+
145
+ delete(tag) {
146
+ const normalized = this._normalize(tag);
147
+ if (!normalized) throw new Error('Tag is required');
148
+ const filePath = this._tagToPath(normalized);
149
+ if (!existsSync(filePath)) return false;
150
+ unlinkSync(filePath);
151
+ delete this._index[normalized];
152
+ this._saveIndex();
153
+ return true;
154
+ }
155
+
156
+ // ── Doc (AI-generated) ───────────────────────────────────
157
+
158
+ saveDoc(tag, content) {
159
+ const normalized = this._normalize(tag);
160
+ if (!normalized) throw new Error('Tag is required');
161
+ const result = this.save(normalized, content);
162
+ this._index[normalized].type = 'doc';
163
+ this._saveIndex();
164
+ return { tag: normalized, ...this._index[normalized] };
165
+ }
166
+
167
+ // ── Links ─────────────────────────────────────────────────
168
+
169
+ link(tag, docPath) {
170
+ const normalized = this._normalize(tag);
171
+ if (!normalized) throw new Error('Tag is required');
172
+ if (!docPath) throw new Error('Document path is required');
173
+ if (!this._index[normalized]) {
174
+ this.save(normalized, `Linked to: ${docPath}`);
175
+ }
176
+ const links = this._index[normalized].links || [];
177
+ if (!links.includes(docPath)) {
178
+ links.push(docPath);
179
+ }
180
+ this._index[normalized].links = links;
181
+ this._saveIndex();
182
+ return { tag: normalized, ...this._index[normalized] };
183
+ }
184
+
185
+ unlink(tag, docPath) {
186
+ const normalized = this._normalize(tag);
187
+ if (!normalized) throw new Error('Tag is required');
188
+ if (!this._index[normalized]) return false;
189
+ this._index[normalized].links = (this._index[normalized].links || []).filter(l => l !== docPath);
190
+ this._saveIndex();
191
+ return true;
192
+ }
193
+
194
+ // ── Query ─────────────────────────────────────────────────
195
+
196
+ list() {
197
+ return Object.values(this._index).sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
198
+ }
199
+
200
+ children(parentTag) {
201
+ const normalized = this._normalize(parentTag);
202
+ const prefix = normalized + '/';
203
+ return Object.values(this._index)
204
+ .filter(entry => entry.tag.startsWith(prefix))
205
+ .sort((a, b) => a.tag.localeCompare(b.tag));
206
+ }
207
+
208
+ tree() {
209
+ const entries = this.list();
210
+ const roots = [];
211
+ const childMap = {};
212
+
213
+ for (const entry of entries) {
214
+ const parts = entry.tag.split('/');
215
+ if (parts.length === 1) {
216
+ roots.push({ ...entry, children: [] });
217
+ } else {
218
+ const parent = parts.slice(0, -1).join('/');
219
+ if (!childMap[parent]) childMap[parent] = [];
220
+ childMap[parent].push(entry);
221
+ }
222
+ }
223
+
224
+ for (const root of roots) {
225
+ root.children = childMap[root.tag] || [];
226
+ }
227
+
228
+ for (const [parent, children] of Object.entries(childMap)) {
229
+ if (!roots.some(r => r.tag === parent) && !parent.includes('/')) {
230
+ roots.push({ tag: parent, virtual: true, children });
231
+ }
232
+ }
233
+
234
+ return roots.sort((a, b) => a.tag.localeCompare(b.tag));
235
+ }
236
+
237
+ search(query) {
238
+ if (!query) return this.list();
239
+ const q = query.toLowerCase();
240
+ const results = [];
241
+ for (const entry of Object.values(this._index)) {
242
+ if (entry.tag.includes(q)) {
243
+ results.push({ ...entry, matchType: 'tag' });
244
+ continue;
245
+ }
246
+ const item = this.get(entry.tag);
247
+ if (item && item.content.toLowerCase().includes(q)) {
248
+ results.push({ ...entry, matchType: 'content' });
249
+ }
250
+ }
251
+ return results.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
252
+ }
253
+
254
+ // ── Command parser ────────────────────────────────────────
255
+
256
+ static parseCommand(text) {
257
+ const cmdMatch = text.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]|\b(save|append|update|delete|view|doc|link|read)\b(?=\s+#[\w/.-])/i);
258
+ if (!cmdMatch) return null;
259
+ const command = (cmdMatch[1] || cmdMatch[2]).toLowerCase();
260
+ const rest = text.slice(cmdMatch.index + cmdMatch[0].length).trim();
261
+
262
+ if (command === 'instruct') {
263
+ return { command, tags: [], extra: null };
264
+ }
265
+
266
+ if (command === 'link') {
267
+ const linkMatch = rest.match(/^((?:#[\w/.-]+\s*)+)\s+(.+)$/);
268
+ if (!linkMatch) return null;
269
+ const tags = linkMatch[1].match(/#[\w/.-]+/g).map(t => t.replace(/^#/, ''));
270
+ return { command, tags, extra: linkMatch[2].trim() };
271
+ }
272
+
273
+ const tags = (rest.match(/#[\w/.-]+/g) || []).map(t => t.replace(/^#/, ''));
274
+ if (tags.length === 0 && command !== 'instruct') return null;
275
+ return { command, tags, extra: null };
276
+ }
277
+ }
@@ -95,6 +95,17 @@ export class ModelLab {
95
95
  removeRuntime(id) {
96
96
  const rt = this.runtimes.get(id);
97
97
  if (!rt) return null;
98
+
99
+ // Stop the llama-server process if this is a local model runtime
100
+ if (rt._localModelId) {
101
+ const mm = this.daemon.modelManager;
102
+ const ls = this.daemon.llamaServer;
103
+ if (mm && ls) {
104
+ const modelPath = mm.getModelPath(rt._localModelId);
105
+ if (modelPath) ls.stopServer(modelPath).catch(() => {});
106
+ }
107
+ }
108
+
98
109
  this.runtimes.delete(id);
99
110
  this._saveRuntimes();
100
111
  this.daemon.broadcast({ type: 'lab:runtime:removed', data: { id } });
@@ -1976,6 +1976,10 @@ For normal file edits within your scope, proceed without review.
1976
1976
  * Resume a completed agent's session with a new message.
1977
1977
  * Uses --resume SESSION_ID for zero cold-start continuation.
1978
1978
  * Falls back to full spawn if no session ID available.
1979
+ *
1980
+ * If the agent has been idle for longer than IDLE_CONTEXT_THRESHOLD,
1981
+ * spawns fresh with the full conversation thread instead of --resume.
1982
+ * This avoids degraded context from internal compaction.
1979
1983
  */
1980
1984
  async resume(agentId, message) {
1981
1985
  const { registry, locks } = this.daemon;
@@ -1996,6 +2000,23 @@ For normal file edits within your scope, proceed without review.
1996
2000
  return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
1997
2001
  }
1998
2002
 
2003
+ // Context-aware idle resume: if the agent has been idle long enough for
2004
+ // internal compaction to degrade context, spawn fresh with the full
2005
+ // conversation thread instead of resuming the compacted session.
2006
+ const IDLE_CONTEXT_THRESHOLD_MS = 5 * 60_000; // 5 minutes (matches prompt cache TTL)
2007
+ const idleMs = agent.lastActivity
2008
+ ? Date.now() - new Date(agent.lastActivity).getTime()
2009
+ : Infinity;
2010
+
2011
+ if (idleMs > IDLE_CONTEXT_THRESHOLD_MS && this.daemon.journalist) {
2012
+ const resumePrompt = this.daemon.journalist.buildConversationResumePrompt(agent, message);
2013
+ if (resumePrompt) {
2014
+ console.log(`[Groove] Agent ${agent.name} idle ${Math.round(idleMs / 60000)}min — using conversation-thread resume`);
2015
+ // Use rotation machinery but with our richer prompt instead of handoff brief
2016
+ return this._conversationResume(agentId, agent, resumePrompt);
2017
+ }
2018
+ }
2019
+
1999
2020
  const provider = getProvider(agent.provider || 'claude-code');
2000
2021
  if (!provider?.buildResumeCommand) {
2001
2022
  return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
@@ -2125,6 +2146,61 @@ For normal file edits within your scope, proceed without review.
2125
2146
  return newAgent;
2126
2147
  }
2127
2148
 
2149
+ /**
2150
+ * Conversation-thread resume: spawns a fresh agent with the full
2151
+ * user↔assistant conversation as context instead of using --resume
2152
+ * on a potentially compacted session. Used when idle > threshold.
2153
+ */
2154
+ async _conversationResume(agentId, agent, resumePrompt) {
2155
+ const { registry, locks } = this.daemon;
2156
+ const config = { ...agent };
2157
+
2158
+ if (this.handles.has(agentId)) {
2159
+ await this.kill(agentId);
2160
+ }
2161
+ registry.remove(agentId);
2162
+ locks.release(agentId);
2163
+
2164
+ const newAgent = await this.spawn({
2165
+ role: config.role,
2166
+ scope: config.scope,
2167
+ provider: config.provider,
2168
+ model: config.model,
2169
+ prompt: resumePrompt,
2170
+ permission: config.permission || 'full',
2171
+ workingDir: config.workingDir,
2172
+ name: config.name,
2173
+ teamId: config.teamId,
2174
+ isRotation: true,
2175
+ });
2176
+
2177
+ // Carry cumulative token count for tracking
2178
+ if (config.tokensUsed > 0) {
2179
+ registry.update(newAgent.id, { tokensUsed: config.tokensUsed });
2180
+ }
2181
+
2182
+ if (this.daemon.timeline) {
2183
+ this.daemon.timeline.recordEvent('conversation_resume', {
2184
+ agentId: newAgent.id,
2185
+ oldAgentId: agentId,
2186
+ agentName: newAgent.name,
2187
+ role: config.role,
2188
+ idleMinutes: Math.round((Date.now() - new Date(config.lastActivity).getTime()) / 60000),
2189
+ });
2190
+ }
2191
+
2192
+ this.daemon.broadcast({
2193
+ type: 'rotation:complete',
2194
+ agentId: newAgent.id,
2195
+ agentName: newAgent.name,
2196
+ oldAgentId: agentId,
2197
+ reason: 'conversation_resume',
2198
+ tokensSaved: 0,
2199
+ });
2200
+
2201
+ return newAgent;
2202
+ }
2203
+
2128
2204
  async _resumeAgentLoop(agentId, agent, message, provider) {
2129
2205
  const { registry, locks } = this.daemon;
2130
2206
  const config = { ...agent };
@@ -112,6 +112,12 @@ export function validateAgentConfig(config) {
112
112
  if (!isNaN(v) && v >= 0 && v <= 100) verbosity = Math.round(v);
113
113
  }
114
114
 
115
+ const validEffort = ['min', 'low', 'default', 'high', 'max'];
116
+ const effort = validEffort.includes(config.effort) ? config.effort : undefined;
117
+
118
+ const validRouting = ['fixed', 'auto', 'auto-floor'];
119
+ const routingMode = validRouting.includes(config.routingMode) ? config.routingMode : undefined;
120
+
115
121
  // Return sanitized config (only known fields)
116
122
  return {
117
123
  role: config.role,
@@ -131,7 +137,10 @@ export function validateAgentConfig(config) {
131
137
  reasoningEffort: numericReasoningEffort ?? reasoningEffort,
132
138
  temperature,
133
139
  verbosity,
140
+ effort,
141
+ routingMode,
134
142
  labPresetId: (typeof config.labPresetId === 'string' && config.labPresetId.length <= 64) ? config.labPresetId : undefined,
143
+ keeperTags: Array.isArray(config.keeperTags) ? config.keeperTags.filter(t => typeof t === 'string').slice(0, 20) : undefined,
135
144
  };
136
145
  }
137
146