@pellux/goodvibes-agent 0.1.2 → 0.1.3
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 +6 -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/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 +8 -4
- 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/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
|
};
|
|
@@ -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;
|