@pellux/goodvibes-agent 0.1.2 → 0.1.4
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +12 -1
- package/docs/README.md +2 -0
- package/docs/getting-started.md +19 -1
- package/docs/release-and-publishing.md +3 -1
- package/package.json +10 -1
- package/src/agent/persona-registry.ts +379 -0
- package/src/agent/skill-registry.ts +360 -0
- package/src/audio/spoken-turn-model-routing.ts +2 -1
- package/src/cli/agent-knowledge-command.ts +46 -10
- package/src/cli/management-commands.ts +3 -1
- package/src/config/surface.ts +1 -0
- package/src/input/agent-workspace.ts +32 -2
- package/src/input/command-registry.ts +4 -1
- package/src/input/commands/agent-skills-runtime.ts +216 -0
- package/src/input/commands/knowledge.ts +18 -18
- package/src/input/commands/personas-runtime.ts +219 -0
- package/src/input/commands/skills-runtime.ts +7 -2
- package/src/input/commands.ts +4 -0
- package/src/input/panel-integration-actions.ts +0 -52
- package/src/main.ts +2 -1
- package/src/panels/builtin/session.ts +4 -3
- package/src/panels/index.ts +0 -5
- package/src/panels/orchestration-panel.ts +4 -5
- package/src/panels/qr-panel.ts +3 -2
- package/src/panels/tasks-panel.ts +4 -4
- package/src/renderer/agent-workspace.ts +2 -0
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +6 -2
- package/src/runtime/bootstrap-core.ts +9 -5
- package/src/runtime/bootstrap-shell.ts +3 -1
- package/src/runtime/bootstrap.ts +10 -2
- package/src/runtime/cloudflare-control-plane.ts +2 -1
- package/src/runtime/services.ts +3 -3
- package/src/version.ts +1 -1
- package/src/daemon/cli.ts +0 -55
- package/src/daemon/safe-serve.ts +0 -61
- package/src/panels/diff-panel.ts +0 -520
- package/src/panels/file-explorer-panel.ts +0 -584
- package/src/panels/file-preview-panel.ts +0 -434
- package/src/panels/git-panel.ts +0 -638
- package/src/panels/sandbox-panel.ts +0 -283
- package/src/panels/symbol-outline-panel.ts +0 -486
- package/src/panels/worktree-panel.ts +0 -182
- package/src/panels/wrfc-panel.ts +0 -609
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
|
+
|
|
7
|
+
export type AgentSkillSource = 'user' | 'agent' | 'imported' | 'system';
|
|
8
|
+
export type AgentSkillReviewState = 'fresh' | 'reviewed' | 'stale';
|
|
9
|
+
|
|
10
|
+
export interface AgentSkillRecord {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly procedure: string;
|
|
15
|
+
readonly triggers: readonly string[];
|
|
16
|
+
readonly tags: readonly string[];
|
|
17
|
+
readonly enabled: boolean;
|
|
18
|
+
readonly source: AgentSkillSource;
|
|
19
|
+
readonly provenance: string;
|
|
20
|
+
readonly reviewState: AgentSkillReviewState;
|
|
21
|
+
readonly staleReason?: string;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
readonly updatedAt: string;
|
|
24
|
+
readonly reviewedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentSkillCreateInput {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly description: string;
|
|
30
|
+
readonly procedure: string;
|
|
31
|
+
readonly triggers?: readonly string[];
|
|
32
|
+
readonly tags?: readonly string[];
|
|
33
|
+
readonly enabled?: boolean;
|
|
34
|
+
readonly source?: AgentSkillSource;
|
|
35
|
+
readonly provenance?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentSkillUpdateInput {
|
|
39
|
+
readonly name?: string;
|
|
40
|
+
readonly description?: string;
|
|
41
|
+
readonly procedure?: string;
|
|
42
|
+
readonly triggers?: readonly string[];
|
|
43
|
+
readonly tags?: readonly string[];
|
|
44
|
+
readonly provenance?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentSkillSnapshot {
|
|
48
|
+
readonly path: string;
|
|
49
|
+
readonly skills: readonly AgentSkillRecord[];
|
|
50
|
+
readonly enabledSkills: readonly AgentSkillRecord[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SkillStoreFile {
|
|
54
|
+
readonly version: 1;
|
|
55
|
+
readonly skills: readonly AgentSkillRecord[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STORE_VERSION = 1;
|
|
59
|
+
|
|
60
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readString(value: unknown, fallback = ''): string {
|
|
65
|
+
return typeof value === 'string' ? value : fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readStringArray(value: unknown): string[] {
|
|
69
|
+
if (!Array.isArray(value)) return [];
|
|
70
|
+
return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeName(name: string): string {
|
|
74
|
+
return name.trim().replace(/\s+/g, ' ');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeList(values: readonly string[] | undefined): string[] {
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
const result: string[] = [];
|
|
80
|
+
for (const value of values ?? []) {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (!trimmed) continue;
|
|
83
|
+
const key = trimmed.toLowerCase();
|
|
84
|
+
if (seen.has(key)) continue;
|
|
85
|
+
seen.add(key);
|
|
86
|
+
result.push(trimmed);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function slugify(value: string): string {
|
|
92
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
93
|
+
return slug || 'skill';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function nowIso(): string {
|
|
97
|
+
return new Date().toISOString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseSkill(value: unknown): AgentSkillRecord | null {
|
|
101
|
+
if (!isRecord(value)) return null;
|
|
102
|
+
const id = readString(value.id).trim();
|
|
103
|
+
const name = normalizeName(readString(value.name));
|
|
104
|
+
const description = readString(value.description).trim();
|
|
105
|
+
const procedure = readString(value.procedure).trim();
|
|
106
|
+
if (!id || !name || !description || !procedure) return null;
|
|
107
|
+
const reviewState = value.reviewState === 'reviewed' || value.reviewState === 'stale' ? value.reviewState : 'fresh';
|
|
108
|
+
const source = value.source === 'agent' || value.source === 'imported' || value.source === 'system' ? value.source : 'user';
|
|
109
|
+
const staleReason = readString(value.staleReason).trim();
|
|
110
|
+
const reviewedAt = readString(value.reviewedAt).trim();
|
|
111
|
+
return {
|
|
112
|
+
id,
|
|
113
|
+
name,
|
|
114
|
+
description,
|
|
115
|
+
procedure,
|
|
116
|
+
triggers: readStringArray(value.triggers),
|
|
117
|
+
tags: readStringArray(value.tags),
|
|
118
|
+
enabled: value.enabled === true,
|
|
119
|
+
source,
|
|
120
|
+
provenance: readString(value.provenance, source).trim() || source,
|
|
121
|
+
reviewState,
|
|
122
|
+
staleReason: staleReason || undefined,
|
|
123
|
+
createdAt: readString(value.createdAt, nowIso()),
|
|
124
|
+
updatedAt: readString(value.updatedAt, nowIso()),
|
|
125
|
+
reviewedAt: reviewedAt || undefined,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseStore(raw: string): SkillStoreFile {
|
|
130
|
+
const parsed: unknown = JSON.parse(raw);
|
|
131
|
+
if (!isRecord(parsed)) return { version: STORE_VERSION, skills: [] };
|
|
132
|
+
return {
|
|
133
|
+
version: STORE_VERSION,
|
|
134
|
+
skills: Array.isArray(parsed.skills)
|
|
135
|
+
? parsed.skills.map(parseSkill).filter((entry): entry is AgentSkillRecord => entry !== null)
|
|
136
|
+
: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatStore(store: SkillStoreFile): string {
|
|
141
|
+
return `${JSON.stringify(store, null, 2)}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function skillStorePath(shellPaths: ShellPathService): string {
|
|
145
|
+
return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'skills', 'skills.json');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class AgentSkillRegistry {
|
|
149
|
+
public constructor(private readonly storePath: string) {}
|
|
150
|
+
|
|
151
|
+
public static fromShellPaths(shellPaths: ShellPathService): AgentSkillRegistry {
|
|
152
|
+
return new AgentSkillRegistry(skillStorePath(shellPaths));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public snapshot(): AgentSkillSnapshot {
|
|
156
|
+
const store = this.readStore();
|
|
157
|
+
return {
|
|
158
|
+
path: this.storePath,
|
|
159
|
+
skills: [...store.skills],
|
|
160
|
+
enabledSkills: store.skills.filter((skill) => skill.enabled),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public list(): readonly AgentSkillRecord[] {
|
|
165
|
+
return this.snapshot().skills;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public search(query: string): readonly AgentSkillRecord[] {
|
|
169
|
+
const normalized = query.trim().toLowerCase();
|
|
170
|
+
if (!normalized) return this.list();
|
|
171
|
+
return this.list().filter((skill) => [
|
|
172
|
+
skill.id,
|
|
173
|
+
skill.name,
|
|
174
|
+
skill.description,
|
|
175
|
+
skill.procedure,
|
|
176
|
+
...skill.tags,
|
|
177
|
+
...skill.triggers,
|
|
178
|
+
].some((field) => field.toLowerCase().includes(normalized)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public get(idOrName: string): AgentSkillRecord | null {
|
|
182
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
183
|
+
if (!lookup) return null;
|
|
184
|
+
return this.list().find((skill) => skill.id.toLowerCase() === lookup || skill.name.toLowerCase() === lookup) ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public create(input: AgentSkillCreateInput): AgentSkillRecord {
|
|
188
|
+
const store = this.readStore();
|
|
189
|
+
const name = normalizeName(input.name);
|
|
190
|
+
const description = input.description.trim();
|
|
191
|
+
const procedure = input.procedure.trim();
|
|
192
|
+
this.validateRequired(name, description, procedure);
|
|
193
|
+
assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? [])]);
|
|
194
|
+
const duplicate = store.skills.find((skill) => skill.name.toLowerCase() === name.toLowerCase());
|
|
195
|
+
if (duplicate) throw new Error(`Skill already exists: ${duplicate.id}`);
|
|
196
|
+
const timestamp = nowIso();
|
|
197
|
+
const skill: AgentSkillRecord = {
|
|
198
|
+
id: this.nextId(name, store.skills),
|
|
199
|
+
name,
|
|
200
|
+
description,
|
|
201
|
+
procedure,
|
|
202
|
+
triggers: normalizeList(input.triggers),
|
|
203
|
+
tags: normalizeList(input.tags),
|
|
204
|
+
enabled: input.enabled === true,
|
|
205
|
+
source: input.source ?? 'user',
|
|
206
|
+
provenance: input.provenance?.trim() || input.source || 'user',
|
|
207
|
+
reviewState: 'fresh',
|
|
208
|
+
createdAt: timestamp,
|
|
209
|
+
updatedAt: timestamp,
|
|
210
|
+
};
|
|
211
|
+
this.writeStore({ ...store, skills: [...store.skills, skill] });
|
|
212
|
+
return skill;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public update(idOrName: string, input: AgentSkillUpdateInput): AgentSkillRecord {
|
|
216
|
+
const store = this.readStore();
|
|
217
|
+
const existing = this.findInStore(store, idOrName);
|
|
218
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
219
|
+
const name = input.name === undefined ? existing.name : normalizeName(input.name);
|
|
220
|
+
const description = input.description === undefined ? existing.description : input.description.trim();
|
|
221
|
+
const procedure = input.procedure === undefined ? existing.procedure : input.procedure.trim();
|
|
222
|
+
this.validateRequired(name, description, procedure);
|
|
223
|
+
assertNoSecretLikeText([name, description, procedure, ...(input.tags ?? []), ...(input.triggers ?? [])]);
|
|
224
|
+
const duplicate = store.skills.find((skill) => skill.id !== existing.id && skill.name.toLowerCase() === name.toLowerCase());
|
|
225
|
+
if (duplicate) throw new Error(`Skill already exists: ${duplicate.id}`);
|
|
226
|
+
const updated: AgentSkillRecord = {
|
|
227
|
+
...existing,
|
|
228
|
+
name,
|
|
229
|
+
description,
|
|
230
|
+
procedure,
|
|
231
|
+
triggers: input.triggers === undefined ? existing.triggers : normalizeList(input.triggers),
|
|
232
|
+
tags: input.tags === undefined ? existing.tags : normalizeList(input.tags),
|
|
233
|
+
provenance: input.provenance === undefined ? existing.provenance : input.provenance.trim() || existing.provenance,
|
|
234
|
+
reviewState: 'fresh',
|
|
235
|
+
staleReason: undefined,
|
|
236
|
+
reviewedAt: undefined,
|
|
237
|
+
updatedAt: nowIso(),
|
|
238
|
+
};
|
|
239
|
+
this.writeStore({
|
|
240
|
+
...store,
|
|
241
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
242
|
+
});
|
|
243
|
+
return updated;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public setEnabled(idOrName: string, enabled: boolean): AgentSkillRecord {
|
|
247
|
+
const store = this.readStore();
|
|
248
|
+
const existing = this.findInStore(store, idOrName);
|
|
249
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
250
|
+
const updated: AgentSkillRecord = { ...existing, enabled, updatedAt: nowIso() };
|
|
251
|
+
this.writeStore({
|
|
252
|
+
...store,
|
|
253
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
254
|
+
});
|
|
255
|
+
return updated;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public markReviewed(idOrName: string): AgentSkillRecord {
|
|
259
|
+
const store = this.readStore();
|
|
260
|
+
const existing = this.findInStore(store, idOrName);
|
|
261
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
262
|
+
const updated: AgentSkillRecord = {
|
|
263
|
+
...existing,
|
|
264
|
+
reviewState: 'reviewed',
|
|
265
|
+
staleReason: undefined,
|
|
266
|
+
reviewedAt: nowIso(),
|
|
267
|
+
updatedAt: nowIso(),
|
|
268
|
+
};
|
|
269
|
+
this.writeStore({
|
|
270
|
+
...store,
|
|
271
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
272
|
+
});
|
|
273
|
+
return updated;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
public markStale(idOrName: string, reason: string): AgentSkillRecord {
|
|
277
|
+
const store = this.readStore();
|
|
278
|
+
const existing = this.findInStore(store, idOrName);
|
|
279
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
280
|
+
const updated: AgentSkillRecord = {
|
|
281
|
+
...existing,
|
|
282
|
+
reviewState: 'stale',
|
|
283
|
+
staleReason: reason.trim() || 'Marked stale by user.',
|
|
284
|
+
updatedAt: nowIso(),
|
|
285
|
+
};
|
|
286
|
+
this.writeStore({
|
|
287
|
+
...store,
|
|
288
|
+
skills: store.skills.map((skill) => skill.id === existing.id ? updated : skill),
|
|
289
|
+
});
|
|
290
|
+
return updated;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
public deleteSkill(idOrName: string): AgentSkillRecord {
|
|
294
|
+
const store = this.readStore();
|
|
295
|
+
const existing = this.findInStore(store, idOrName);
|
|
296
|
+
if (!existing) throw new Error(`Unknown skill: ${idOrName}`);
|
|
297
|
+
this.writeStore({
|
|
298
|
+
...store,
|
|
299
|
+
skills: store.skills.filter((skill) => skill.id !== existing.id),
|
|
300
|
+
});
|
|
301
|
+
return existing;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private validateRequired(name: string, description: string, procedure: string): void {
|
|
305
|
+
if (!name) throw new Error('Skill name is required.');
|
|
306
|
+
if (!description) throw new Error('Skill description is required.');
|
|
307
|
+
if (!procedure) throw new Error('Skill procedure is required.');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private nextId(name: string, skills: readonly AgentSkillRecord[]): string {
|
|
311
|
+
const base = slugify(name);
|
|
312
|
+
const ids = new Set(skills.map((skill) => skill.id));
|
|
313
|
+
if (!ids.has(base)) return base;
|
|
314
|
+
for (let index = 2; index < 1000; index += 1) {
|
|
315
|
+
const candidate = `${base}-${index}`;
|
|
316
|
+
if (!ids.has(candidate)) return candidate;
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`Could not allocate skill id for ${name}.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private findInStore(store: SkillStoreFile, idOrName: string): AgentSkillRecord | null {
|
|
322
|
+
const lookup = idOrName.trim().toLowerCase();
|
|
323
|
+
if (!lookup) return null;
|
|
324
|
+
return store.skills.find((skill) => skill.id.toLowerCase() === lookup || skill.name.toLowerCase() === lookup) ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private readStore(): SkillStoreFile {
|
|
328
|
+
if (!existsSync(this.storePath)) return { version: STORE_VERSION, skills: [] };
|
|
329
|
+
try {
|
|
330
|
+
return parseStore(readFileSync(this.storePath, 'utf-8'));
|
|
331
|
+
} catch (error) {
|
|
332
|
+
throw new Error(`Could not read Agent skill store: ${error instanceof Error ? error.message : String(error)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private writeStore(store: SkillStoreFile): void {
|
|
337
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
338
|
+
const tmpPath = `${this.storePath}.tmp`;
|
|
339
|
+
writeFileSync(tmpPath, formatStore(store), 'utf-8');
|
|
340
|
+
renameSync(tmpPath, this.storePath);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function buildEnabledSkillsPrompt(shellPaths: ShellPathService): string | null {
|
|
345
|
+
const enabled = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot().enabledSkills;
|
|
346
|
+
if (enabled.length === 0) return null;
|
|
347
|
+
return [
|
|
348
|
+
'## Enabled GoodVibes Agent Skills',
|
|
349
|
+
'Use these local reusable procedures inside the same serial assistant conversation when they fit the user request.',
|
|
350
|
+
'',
|
|
351
|
+
...enabled.slice(0, 8).flatMap((skill) => [
|
|
352
|
+
`### ${skill.name}`,
|
|
353
|
+
`Description: ${skill.description}`,
|
|
354
|
+
`Review state: ${skill.reviewState}`,
|
|
355
|
+
`Triggers: ${skill.triggers.join(', ') || '(manual)'}`,
|
|
356
|
+
skill.procedure,
|
|
357
|
+
'',
|
|
358
|
+
]),
|
|
359
|
+
].join('\n').trim();
|
|
360
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
2
2
|
import type { ModelDefinition, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
3
3
|
import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
4
|
import type { Orchestrator, OrchestratorUserInputOptions } from '../core/orchestrator.ts';
|
|
5
|
+
import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
|
|
5
6
|
|
|
6
7
|
const SPOKEN_TURN_SOURCE = 'tts';
|
|
7
8
|
|
|
@@ -27,7 +28,7 @@ export function createSpokenTurnInputOptions(): OrchestratorUserInputOptions {
|
|
|
27
28
|
return {
|
|
28
29
|
origin: {
|
|
29
30
|
source: SPOKEN_TURN_SOURCE,
|
|
30
|
-
surface:
|
|
31
|
+
surface: GOODVIBES_AGENT_PAIRING_SURFACE,
|
|
31
32
|
metadata: { spokenOutput: true },
|
|
32
33
|
},
|
|
33
34
|
};
|
|
@@ -32,6 +32,42 @@ interface AgentKnowledgeSuccess<TData> {
|
|
|
32
32
|
|
|
33
33
|
type AgentKnowledgeResult<TData> = AgentKnowledgeSuccess<TData> | AgentKnowledgeFailure;
|
|
34
34
|
|
|
35
|
+
interface DaemonCallMethod {
|
|
36
|
+
readonly kind: string;
|
|
37
|
+
readonly route: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const AGENT_KNOWLEDGE_METHODS = {
|
|
41
|
+
status: {
|
|
42
|
+
kind: 'agentKnowledge.status',
|
|
43
|
+
route: '/api/goodvibes-agent/knowledge/status',
|
|
44
|
+
},
|
|
45
|
+
ask: {
|
|
46
|
+
kind: 'agentKnowledge.ask',
|
|
47
|
+
route: '/api/goodvibes-agent/knowledge/ask',
|
|
48
|
+
},
|
|
49
|
+
search: {
|
|
50
|
+
kind: 'agentKnowledge.search',
|
|
51
|
+
route: '/api/goodvibes-agent/knowledge/search',
|
|
52
|
+
},
|
|
53
|
+
ingestUrl: {
|
|
54
|
+
kind: 'agentKnowledge.ingest.url',
|
|
55
|
+
route: '/api/goodvibes-agent/knowledge/ingest/url',
|
|
56
|
+
},
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
const DELEGATION_METHOD = {
|
|
60
|
+
kind: 'sessions.messages.create',
|
|
61
|
+
route: 'sessions.messages.create',
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
interface DelegationResult {
|
|
65
|
+
readonly sessionId: string;
|
|
66
|
+
readonly message: unknown;
|
|
67
|
+
readonly task: string;
|
|
68
|
+
readonly wrfcRequested: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
function isRecord(value: unknown): value is JsonRecord {
|
|
36
72
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
37
73
|
}
|
|
@@ -323,7 +359,7 @@ function formatFailure(failure: AgentKnowledgeFailure, json: boolean): string {
|
|
|
323
359
|
|
|
324
360
|
async function runKnowledgeCall<TData>(
|
|
325
361
|
runtime: CliCommandRuntime,
|
|
326
|
-
|
|
362
|
+
method: DaemonCallMethod,
|
|
327
363
|
call: (connection: AgentDaemonConnection) => Promise<TData>,
|
|
328
364
|
): Promise<AgentKnowledgeResult<TData>> {
|
|
329
365
|
const connection = resolveDaemonConnection(runtime);
|
|
@@ -333,14 +369,14 @@ async function runKnowledgeCall<TData>(
|
|
|
333
369
|
kind: 'auth_required',
|
|
334
370
|
error: `No daemon operator token found at ${connection.tokenPath}`,
|
|
335
371
|
baseUrl: connection.baseUrl,
|
|
336
|
-
route,
|
|
372
|
+
route: method.route,
|
|
337
373
|
};
|
|
338
374
|
}
|
|
339
375
|
try {
|
|
340
376
|
const data = await call(connection);
|
|
341
|
-
return { ok: true, kind:
|
|
377
|
+
return { ok: true, kind: method.kind, route: method.route, data };
|
|
342
378
|
} catch (error) {
|
|
343
|
-
return classifyKnowledgeError(error, connection, route);
|
|
379
|
+
return classifyKnowledgeError(error, connection, method.route);
|
|
344
380
|
}
|
|
345
381
|
}
|
|
346
382
|
|
|
@@ -350,7 +386,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
350
386
|
const json = runtime.cli.flags.outputFormat === 'json';
|
|
351
387
|
|
|
352
388
|
if (normalized === 'status') {
|
|
353
|
-
const result = await runKnowledgeCall(runtime,
|
|
389
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.status, async (connection) => (
|
|
354
390
|
await createAgentSdk(connection).knowledge.status()
|
|
355
391
|
));
|
|
356
392
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -366,7 +402,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
366
402
|
const mode = readOptionValue(rest, '--mode');
|
|
367
403
|
const selectedMode = mode === 'concise' || mode === 'standard' || mode === 'detailed' ? mode : 'standard';
|
|
368
404
|
const limit = readPositiveInt(rest, '--limit', 8);
|
|
369
|
-
const result = await runKnowledgeCall(runtime,
|
|
405
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.ask, async (connection) => (
|
|
370
406
|
await createAgentSdk(connection).knowledge.ask({
|
|
371
407
|
query,
|
|
372
408
|
limit,
|
|
@@ -387,7 +423,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
387
423
|
const query = commandValues(rest).join(' ').trim();
|
|
388
424
|
if (!query) return { output: 'Usage: goodvibes-agent knowledge search <query> [--limit <n>]', exitCode: 2 };
|
|
389
425
|
const limit = readPositiveInt(rest, '--limit', 10);
|
|
390
|
-
const result = await runKnowledgeCall(runtime,
|
|
426
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.search, async (connection) => (
|
|
391
427
|
await createAgentSdk(connection).knowledge.search({ query, limit })
|
|
392
428
|
));
|
|
393
429
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -403,7 +439,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
403
439
|
if (!url) return { output: 'Usage: goodvibes-agent knowledge ingest-url <url> [--title <title>] [--tags a,b]', exitCode: 2 };
|
|
404
440
|
const title = readOptionValue(rest, '--title');
|
|
405
441
|
const tags = readStringList(rest, '--tags');
|
|
406
|
-
const result = await runKnowledgeCall(runtime,
|
|
442
|
+
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.ingestUrl, async (connection) => (
|
|
407
443
|
await createAgentSdk(connection).operator.invoke('knowledge.ingest.url', {
|
|
408
444
|
url,
|
|
409
445
|
title,
|
|
@@ -432,7 +468,7 @@ export async function handleCompatCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
432
468
|
const daemonRecord = isRecord(daemon.body) ? daemon.body : {};
|
|
433
469
|
const daemonVersion = readString(daemonRecord, 'version') ?? 'unknown';
|
|
434
470
|
const versionCompatible = daemon.ok && daemonVersion === metadata.sdkVersion;
|
|
435
|
-
const knowledgeRoute = await runKnowledgeCall(runtime,
|
|
471
|
+
const knowledgeRoute = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.status, async (routeConnection) => (
|
|
436
472
|
await createAgentSdk(routeConnection).knowledge.status()
|
|
437
473
|
));
|
|
438
474
|
const knowledgeRouteReady = knowledgeRoute.ok;
|
|
@@ -482,7 +518,7 @@ export async function handleDelegateCommand(runtime: CliCommandRuntime): Promise
|
|
|
482
518
|
exitCode: 2,
|
|
483
519
|
};
|
|
484
520
|
}
|
|
485
|
-
const result = await runKnowledgeCall(runtime,
|
|
521
|
+
const result = await runKnowledgeCall<DelegationResult>(runtime, DELEGATION_METHOD, async (connection) => {
|
|
486
522
|
const sdk = createAgentSdk(connection);
|
|
487
523
|
const created = await sdk.operator.invoke('sessions.create', {
|
|
488
524
|
title: `Agent delegation: ${task.slice(0, 72)}`,
|
|
@@ -11,6 +11,7 @@ import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
|
11
11
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
12
|
import type { CliCommandRuntime } from './management.ts';
|
|
13
13
|
import { extractAuthorizationCode, formatJsonOrText, hasCommandFlag, openBrowser, probeTcp, readAuthPaths, runNonInteractiveAgent, urlHostForBindHost, withRuntimeServices, yesNo } from './management.ts';
|
|
14
|
+
import { GOODVIBES_AGENT_PAIRING_SURFACE } from '../config/surface.ts';
|
|
14
15
|
|
|
15
16
|
export async function renderSubscriptions(runtime: CliCommandRuntime): Promise<string> {
|
|
16
17
|
return await withRuntimeServices(runtime, async (services) => {
|
|
@@ -370,13 +371,14 @@ export async function renderControlPlaneStatus(runtime: CliCommandRuntime): Prom
|
|
|
370
371
|
|
|
371
372
|
export async function renderPairing(runtime: CliCommandRuntime): Promise<string> {
|
|
372
373
|
const daemonHomeDir = join(runtime.homeDirectory, '.goodvibes', 'daemon');
|
|
373
|
-
const tokenRecord = getOrCreateCompanionToken(
|
|
374
|
+
const tokenRecord = getOrCreateCompanionToken(GOODVIBES_AGENT_PAIRING_SURFACE, { daemonHomeDir });
|
|
374
375
|
const binding = resolveRuntimeEndpointBinding(runtime.configManager, 'controlPlane');
|
|
375
376
|
const daemonUrl = `http://${urlHostForBindHost(binding.host)}:${binding.port}`;
|
|
376
377
|
const info = buildCompanionConnectionInfo({
|
|
377
378
|
daemonUrl,
|
|
378
379
|
token: tokenRecord.token,
|
|
379
380
|
username: 'admin',
|
|
381
|
+
surface: GOODVIBES_AGENT_PAIRING_SURFACE,
|
|
380
382
|
});
|
|
381
383
|
const payload = encodeConnectionPayload(info);
|
|
382
384
|
const qr = renderQrToString(generateQrMatrix(payload));
|
package/src/config/surface.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
2
2
|
import type { CommandContext } from './command-registry.ts';
|
|
3
|
+
import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
|
|
4
|
+
import { AgentSkillRegistry } from '../agent/skill-registry.ts';
|
|
3
5
|
|
|
4
6
|
export const AGENT_WORKSPACE_MODAL_NAME = 'agentWorkspace';
|
|
5
7
|
|
|
@@ -51,6 +53,10 @@ export interface AgentWorkspaceRuntimeSnapshot {
|
|
|
51
53
|
readonly daemonBaseUrl: string;
|
|
52
54
|
readonly daemonOwnership: 'external';
|
|
53
55
|
readonly sessionMemoryCount: number;
|
|
56
|
+
readonly localSkillCount: number;
|
|
57
|
+
readonly enabledSkillCount: number;
|
|
58
|
+
readonly localPersonaCount: number;
|
|
59
|
+
readonly activePersonaName: string;
|
|
54
60
|
readonly knowledgeRoute: '/api/goodvibes-agent/knowledge';
|
|
55
61
|
readonly knowledgeIsolation: 'agent-only';
|
|
56
62
|
readonly executionPolicy: 'serial-proactive';
|
|
@@ -98,6 +104,26 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
98
104
|
return 0;
|
|
99
105
|
}
|
|
100
106
|
})();
|
|
107
|
+
const personaSnapshot = (() => {
|
|
108
|
+
try {
|
|
109
|
+
const shellPaths = context.workspace?.shellPaths;
|
|
110
|
+
if (!shellPaths) return { count: 0, activeName: '(none)' };
|
|
111
|
+
const snapshot = AgentPersonaRegistry.fromShellPaths(shellPaths).snapshot();
|
|
112
|
+
return { count: snapshot.personas.length, activeName: snapshot.activePersona?.name ?? '(none)' };
|
|
113
|
+
} catch {
|
|
114
|
+
return { count: 0, activeName: '(unavailable)' };
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
const skillSnapshot = (() => {
|
|
118
|
+
try {
|
|
119
|
+
const shellPaths = context.workspace?.shellPaths;
|
|
120
|
+
if (!shellPaths) return { count: 0, enabled: 0 };
|
|
121
|
+
const snapshot = AgentSkillRegistry.fromShellPaths(shellPaths).snapshot();
|
|
122
|
+
return { count: snapshot.skills.length, enabled: snapshot.enabledSkills.length };
|
|
123
|
+
} catch {
|
|
124
|
+
return { count: 0, enabled: 0 };
|
|
125
|
+
}
|
|
126
|
+
})();
|
|
101
127
|
const warnings: string[] = [];
|
|
102
128
|
if (provider === 'unknown' || model === 'unknown') warnings.push('Provider/model unavailable in this runtime context.');
|
|
103
129
|
if (!context.executeCommand) warnings.push('Command dispatch is unavailable; workspace actions will show guidance only.');
|
|
@@ -112,6 +138,10 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
|
|
|
112
138
|
daemonBaseUrl: `http://${host}:${port}`,
|
|
113
139
|
daemonOwnership: 'external',
|
|
114
140
|
sessionMemoryCount,
|
|
141
|
+
localSkillCount: skillSnapshot.count,
|
|
142
|
+
enabledSkillCount: skillSnapshot.enabled,
|
|
143
|
+
localPersonaCount: personaSnapshot.count,
|
|
144
|
+
activePersonaName: personaSnapshot.activeName,
|
|
115
145
|
knowledgeRoute: '/api/goodvibes-agent/knowledge',
|
|
116
146
|
knowledgeIsolation: 'agent-only',
|
|
117
147
|
executionPolicy: 'serial-proactive',
|
|
@@ -167,8 +197,8 @@ export const AGENT_WORKSPACE_CATEGORIES: readonly AgentWorkspaceCategory[] = [
|
|
|
167
197
|
detail: 'Memory, skills, and personas stay Agent-local until stable shared daemon registry contracts exist. Secrets must not be stored as memory.',
|
|
168
198
|
actions: [
|
|
169
199
|
{ id: 'memory', label: 'Open memory', detail: 'Inspect local/session memory commands and surfaces.', command: '/memory', kind: 'command', safety: 'read-only' },
|
|
170
|
-
{ id: 'skills', label: '
|
|
171
|
-
{ id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', kind: '
|
|
200
|
+
{ id: 'skills', label: 'Local skill library', detail: 'Create, review, and enable local Agent reusable procedures.', command: '/agent-skills', kind: 'command', safety: 'safe' },
|
|
201
|
+
{ id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', command: '/personas', kind: 'command', safety: 'safe' },
|
|
172
202
|
],
|
|
173
203
|
},
|
|
174
204
|
{
|
|
@@ -180,7 +180,9 @@ export interface CommandExtensionRegistryServices {
|
|
|
180
180
|
|
|
181
181
|
export interface CommandExtensionServices
|
|
182
182
|
extends CommandExtensionRegistryServices,
|
|
183
|
-
CommandExtensionShellServices {
|
|
183
|
+
CommandExtensionShellServices {
|
|
184
|
+
readonly agentKnowledgeService?: import('@pellux/goodvibes-sdk/platform/knowledge').KnowledgeService;
|
|
185
|
+
}
|
|
184
186
|
|
|
185
187
|
/**
|
|
186
188
|
* CommandContext - Passed to every slash command handler so commands can
|
|
@@ -200,6 +202,7 @@ export interface CommandContext
|
|
|
200
202
|
readonly operator?: OperatorClient;
|
|
201
203
|
readonly peer?: PeerClient;
|
|
202
204
|
readonly providerApi?: ProviderApi;
|
|
205
|
+
readonly agentKnowledgeApi?: KnowledgeApi;
|
|
203
206
|
readonly knowledgeApi?: KnowledgeApi;
|
|
204
207
|
readonly hookApi?: HookApi;
|
|
205
208
|
readonly mcpApi?: McpApi;
|