@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +12 -1
  4. package/docs/README.md +2 -0
  5. package/docs/getting-started.md +19 -1
  6. package/docs/release-and-publishing.md +3 -1
  7. package/package.json +10 -1
  8. package/src/agent/persona-registry.ts +379 -0
  9. package/src/agent/skill-registry.ts +360 -0
  10. package/src/audio/spoken-turn-model-routing.ts +2 -1
  11. package/src/cli/agent-knowledge-command.ts +46 -10
  12. package/src/cli/management-commands.ts +3 -1
  13. package/src/config/surface.ts +1 -0
  14. package/src/input/agent-workspace.ts +32 -2
  15. package/src/input/command-registry.ts +4 -1
  16. package/src/input/commands/agent-skills-runtime.ts +216 -0
  17. package/src/input/commands/knowledge.ts +18 -18
  18. package/src/input/commands/personas-runtime.ts +219 -0
  19. package/src/input/commands/skills-runtime.ts +7 -2
  20. package/src/input/commands.ts +4 -0
  21. package/src/input/panel-integration-actions.ts +0 -52
  22. package/src/main.ts +2 -1
  23. package/src/panels/builtin/session.ts +4 -3
  24. package/src/panels/index.ts +0 -5
  25. package/src/panels/orchestration-panel.ts +4 -5
  26. package/src/panels/qr-panel.ts +3 -2
  27. package/src/panels/tasks-panel.ts +4 -4
  28. package/src/renderer/agent-workspace.ts +2 -0
  29. package/src/runtime/bootstrap-command-context.ts +3 -0
  30. package/src/runtime/bootstrap-command-parts.ts +6 -2
  31. package/src/runtime/bootstrap-core.ts +9 -5
  32. package/src/runtime/bootstrap-shell.ts +3 -1
  33. package/src/runtime/bootstrap.ts +10 -2
  34. package/src/runtime/cloudflare-control-plane.ts +2 -1
  35. package/src/runtime/services.ts +3 -3
  36. package/src/version.ts +1 -1
  37. package/src/daemon/cli.ts +0 -55
  38. package/src/daemon/safe-serve.ts +0 -61
  39. package/src/panels/diff-panel.ts +0 -520
  40. package/src/panels/file-explorer-panel.ts +0 -584
  41. package/src/panels/file-preview-panel.ts +0 -434
  42. package/src/panels/git-panel.ts +0 -638
  43. package/src/panels/sandbox-panel.ts +0 -283
  44. package/src/panels/symbol-outline-panel.ts +0 -486
  45. package/src/panels/worktree-panel.ts +0 -182
  46. 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: 'tui',
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
- route: string,
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: route, route, data };
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, 'knowledge.status', async (connection) => (
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, 'knowledge.ask', async (connection) => (
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, 'knowledge.search', async (connection) => (
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, 'knowledge.ingest.url', async (connection) => (
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, 'knowledge.status', async (routeConnection) => (
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, 'sessions.messages.create', async (connection) => {
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('tui', { daemonHomeDir });
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));
@@ -1 +1,2 @@
1
1
  export const GOODVIBES_AGENT_SURFACE_ROOT = 'agent';
2
+ export const GOODVIBES_AGENT_PAIRING_SURFACE = 'goodvibes-agent';
@@ -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: 'Open skills', detail: 'Inspect discovered skills and skill catalog state.', command: '/skills open', kind: 'command', safety: 'read-only' },
171
- { id: 'personas', label: 'Persona library', detail: 'Use local Agent personas to shape serial assistant behavior without spawning background agents.', kind: 'guidance', safety: 'safe' },
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;