@pellux/goodvibes-agent 0.1.107 → 0.1.108

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 (79) hide show
  1. package/CHANGELOG.md +38 -13
  2. package/README.md +27 -11
  3. package/docs/README.md +5 -5
  4. package/docs/{runtime-connection.md → connected-services.md} +8 -8
  5. package/docs/getting-started.md +28 -12
  6. package/docs/release-and-publishing.md +4 -4
  7. package/package.json +2 -12
  8. package/src/agent/reminder-schedule-format.ts +75 -0
  9. package/src/agent/reminder-schedule.ts +494 -0
  10. package/src/agent/routine-schedule-format.ts +7 -7
  11. package/src/agent/routine-schedule-promotion.ts +1 -1
  12. package/src/agent/routine-schedule-receipts.ts +1 -1
  13. package/src/agent/skill-discovery.ts +2 -0
  14. package/src/cli/agent-knowledge-args.ts +93 -0
  15. package/src/cli/agent-knowledge-command.ts +200 -369
  16. package/src/cli/agent-knowledge-format.ts +58 -7
  17. package/src/cli/agent-knowledge-methods.ts +84 -0
  18. package/src/cli/agent-knowledge-runtime.ts +240 -0
  19. package/src/cli/completion.ts +0 -2
  20. package/src/cli/config-overrides.ts +2 -2
  21. package/src/cli/help.ts +34 -15
  22. package/src/cli/management-commands.ts +2 -2
  23. package/src/cli/management.ts +8 -8
  24. package/src/cli/package-verification.ts +7 -3
  25. package/src/cli/parser.ts +10 -4
  26. package/src/cli/service-posture.ts +6 -6
  27. package/src/cli/status.ts +32 -32
  28. package/src/input/agent-workspace-activation.ts +24 -13
  29. package/src/input/agent-workspace-basic-command-editors.ts +448 -0
  30. package/src/input/agent-workspace-categories.ts +42 -34
  31. package/src/input/agent-workspace-channels.ts +3 -3
  32. package/src/input/agent-workspace-command-editor.ts +65 -0
  33. package/src/input/agent-workspace-editors.ts +17 -2
  34. package/src/input/agent-workspace-knowledge-query-editor.ts +74 -0
  35. package/src/input/agent-workspace-knowledge-url-editor.ts +95 -0
  36. package/src/input/agent-workspace-reminder-schedule-editor.ts +125 -0
  37. package/src/input/agent-workspace-routine-schedule-editor.ts +127 -0
  38. package/src/input/agent-workspace-setup.ts +2 -2
  39. package/src/input/agent-workspace-types.ts +20 -2
  40. package/src/input/agent-workspace-voice-media.ts +5 -5
  41. package/src/input/agent-workspace.ts +39 -2
  42. package/src/input/commands/agent-runtime-profile-runtime.ts +1 -1
  43. package/src/input/commands/agent-skills-runtime.ts +94 -2
  44. package/src/input/commands/brief-runtime.ts +126 -0
  45. package/src/input/commands/channels-runtime.ts +47 -0
  46. package/src/input/commands/health-runtime.ts +10 -10
  47. package/src/input/commands/knowledge.ts +176 -1
  48. package/src/input/commands/planning-runtime.ts +1 -1
  49. package/src/input/commands/platform-access-runtime.ts +10 -10
  50. package/src/input/commands/policy-dispatch.ts +1 -1
  51. package/src/input/commands/schedule-runtime.ts +42 -5
  52. package/src/input/commands/security-runtime.ts +1 -1
  53. package/src/input/commands/session-content.ts +1 -1
  54. package/src/input/commands/session-workflow.ts +1 -1
  55. package/src/input/commands/shell-core.ts +4 -2
  56. package/src/input/commands/tasks-runtime.ts +3 -3
  57. package/src/input/commands.ts +4 -0
  58. package/src/input/handler-onboarding.ts +4 -4
  59. package/src/input/handler.ts +3 -2
  60. package/src/input/onboarding/onboarding-wizard-helpers.ts +1 -1
  61. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +13 -13
  62. package/src/input/onboarding/onboarding-wizard-steps.ts +8 -8
  63. package/src/input/settings-modal-agent-policy.ts +1 -1
  64. package/src/input/slash-command-parser.ts +60 -0
  65. package/src/panels/builtin/agent.ts +1 -1
  66. package/src/panels/builtin/operations.ts +2 -2
  67. package/src/panels/provider-account-snapshot.ts +1 -1
  68. package/src/panels/provider-health-domains.ts +6 -6
  69. package/src/panels/subscription-panel.ts +1 -1
  70. package/src/panels/tasks-panel.ts +3 -3
  71. package/src/renderer/agent-workspace.ts +43 -30
  72. package/src/renderer/help-overlay.ts +1 -1
  73. package/src/renderer/settings-modal.ts +13 -13
  74. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  75. package/src/runtime/bootstrap.ts +8 -8
  76. package/src/runtime/index.ts +2 -2
  77. package/src/runtime/onboarding/derivation.ts +6 -6
  78. package/src/shell/service-settings-sync.ts +1 -1
  79. package/src/version.ts +1 -1
@@ -0,0 +1,125 @@
1
+ import type { AgentWorkspaceActionResult, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
2
+ import { isAffirmative } from './agent-workspace-editors.ts';
3
+ import { quoteSlashCommandArg } from './slash-command-parser.ts';
4
+
5
+ type AgentWorkspaceFieldReader = (fieldId: string) => string;
6
+ type ReminderScheduleKind = 'cron' | 'every' | 'at';
7
+
8
+ export type AgentReminderScheduleEditorSubmission =
9
+ | {
10
+ readonly kind: 'editor';
11
+ readonly editor: AgentWorkspaceLocalEditor;
12
+ readonly status: string;
13
+ readonly actionResult?: AgentWorkspaceActionResult;
14
+ }
15
+ | {
16
+ readonly kind: 'dispatch';
17
+ readonly command: string;
18
+ readonly status: string;
19
+ readonly actionResult: AgentWorkspaceActionResult;
20
+ };
21
+
22
+ export function createReminderScheduleEditor(): AgentWorkspaceLocalEditor {
23
+ return {
24
+ kind: 'reminder-schedule',
25
+ mode: 'create',
26
+ title: 'Create Reminder',
27
+ selectedFieldIndex: 0,
28
+ message: 'Create one connected reminder schedule. Type yes on the final field to confirm.',
29
+ fields: [
30
+ { id: 'message', label: 'Reminder', value: '', required: true, multiline: true, hint: 'What should the Agent remind you about? Ctrl-J inserts a new line.' },
31
+ { id: 'scheduleKind', label: 'Schedule type', value: 'at', required: true, multiline: false, hint: 'at, every, or cron.' },
32
+ { id: 'scheduleValue', label: 'Schedule value', value: '', required: true, multiline: false, hint: 'Examples: 2026-06-01T09:00:00-05:00, 7d, or 0 9 * * *.' },
33
+ { id: 'timezone', label: 'Timezone', value: '', required: false, multiline: false, hint: 'Optional IANA timezone for cron schedules, for example America/Chicago.' },
34
+ { id: 'scheduleName', label: 'Schedule name', value: '', required: false, multiline: false, hint: 'Optional display name for the connected reminder.' },
35
+ { id: 'deliveryChannel', label: 'Delivery channel', value: '', required: false, multiline: false, hint: 'Optional channel target, for example slack or slack:ops-alerts:Ops.' },
36
+ { id: 'disabled', label: 'Create disabled', value: 'no', required: false, multiline: false, hint: 'yes/no. Default no.' },
37
+ { id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to run /schedule remind with --yes.' },
38
+ ],
39
+ };
40
+ }
41
+
42
+ export function buildAgentReminderScheduleEditorSubmission(
43
+ editor: AgentWorkspaceLocalEditor,
44
+ readField: AgentWorkspaceFieldReader,
45
+ commandDispatchAvailable: boolean,
46
+ ): AgentReminderScheduleEditorSubmission {
47
+ const confirm = readField('confirm');
48
+ if (!isAffirmative(confirm)) {
49
+ return {
50
+ kind: 'editor',
51
+ editor: { ...editor, message: 'Type yes to confirm reminder schedule creation.' },
52
+ status: 'Reminder schedule creation not confirmed.',
53
+ };
54
+ }
55
+
56
+ const scheduleKind = readScheduleKind(readField('scheduleKind'));
57
+ if (!scheduleKind) {
58
+ const detail = 'Schedule type must be at, every, or cron.';
59
+ return {
60
+ kind: 'editor',
61
+ editor: { ...editor, message: detail },
62
+ status: detail,
63
+ actionResult: {
64
+ kind: 'error',
65
+ title: 'Reminder schedule type invalid',
66
+ detail,
67
+ },
68
+ };
69
+ }
70
+
71
+ if (!commandDispatchAvailable) {
72
+ return {
73
+ kind: 'editor',
74
+ editor: { ...editor, message: 'Command dispatch is unavailable; cannot create a reminder from this workspace.' },
75
+ status: 'Command dispatch unavailable.',
76
+ actionResult: {
77
+ kind: 'error',
78
+ title: 'Command dispatch unavailable',
79
+ detail: 'The reminder schedule command cannot be opened from this runtime.',
80
+ },
81
+ };
82
+ }
83
+
84
+ const parts = [
85
+ '/schedule',
86
+ 'remind',
87
+ '--message',
88
+ quoteSlashCommandArg(readField('message')),
89
+ `--${scheduleKind}`,
90
+ quoteSlashCommandArg(readField('scheduleValue')),
91
+ ];
92
+ const timezone = readField('timezone');
93
+ if (timezone.length > 0) parts.push('--timezone', quoteSlashCommandArg(timezone));
94
+ const scheduleName = readField('scheduleName');
95
+ if (scheduleName.length > 0) parts.push('--name', quoteSlashCommandArg(scheduleName));
96
+ const deliveryChannel = readField('deliveryChannel');
97
+ if (deliveryChannel.length > 0) parts.push('--delivery-channel', quoteSlashCommandArg(deliveryChannel));
98
+ if (isOptionalAffirmative(readField('disabled'))) parts.push('--disabled');
99
+ parts.push('--yes');
100
+ const command = parts.join(' ');
101
+
102
+ return {
103
+ kind: 'dispatch',
104
+ command,
105
+ status: 'Opening reminder schedule creation.',
106
+ actionResult: {
107
+ kind: 'dispatched',
108
+ title: 'Opening reminder schedule creation',
109
+ detail: 'The workspace handed a confirmed reminder schedule command to the shell-owned command router.',
110
+ command,
111
+ safety: 'safe',
112
+ },
113
+ };
114
+ }
115
+
116
+ function readScheduleKind(value: string): ReminderScheduleKind | null {
117
+ const normalized = value.trim().toLowerCase();
118
+ if (normalized === 'cron' || normalized === 'every' || normalized === 'at') return normalized;
119
+ return null;
120
+ }
121
+
122
+ function isOptionalAffirmative(value: string): boolean {
123
+ const normalized = value.trim().toLowerCase();
124
+ return normalized === 'yes' || normalized === 'y' || normalized === 'true' || normalized === 'enabled' || normalized === 'on';
125
+ }
@@ -0,0 +1,127 @@
1
+ import type { AgentWorkspaceActionResult, AgentWorkspaceLocalEditor, AgentWorkspaceLocalLibraryItem } from './agent-workspace-types.ts';
2
+ import { isAffirmative } from './agent-workspace-editors.ts';
3
+ import { quoteSlashCommandArg } from './slash-command-parser.ts';
4
+
5
+ type AgentWorkspaceFieldReader = (fieldId: string) => string;
6
+ type RoutineScheduleKind = 'cron' | 'every' | 'at';
7
+
8
+ export type AgentRoutineScheduleEditorSubmission =
9
+ | {
10
+ readonly kind: 'editor';
11
+ readonly editor: AgentWorkspaceLocalEditor;
12
+ readonly status: string;
13
+ readonly actionResult?: AgentWorkspaceActionResult;
14
+ }
15
+ | {
16
+ readonly kind: 'dispatch';
17
+ readonly command: string;
18
+ readonly status: string;
19
+ readonly actionResult: AgentWorkspaceActionResult;
20
+ };
21
+
22
+ export function createRoutineScheduleEditor(
23
+ selectedRoutine: AgentWorkspaceLocalLibraryItem | null,
24
+ ): AgentWorkspaceLocalEditor {
25
+ const selected = selectedRoutine ? `${selectedRoutine.id} (${selectedRoutine.name})` : 'No routine selected';
26
+ return {
27
+ kind: 'routine-schedule',
28
+ mode: 'create',
29
+ title: 'Promote Routine to Schedule',
30
+ selectedFieldIndex: 0,
31
+ message: `Create one connected schedule from a reviewed local routine. Selected: ${selected}. Type yes on the final field to confirm.`,
32
+ fields: [
33
+ { id: 'routineId', label: 'Routine id', value: selectedRoutine?.id ?? '', required: true, multiline: false, hint: 'Local Agent routine id to promote.' },
34
+ { id: 'scheduleKind', label: 'Schedule type', value: 'cron', required: true, multiline: false, hint: 'cron, every, or at.' },
35
+ { id: 'scheduleValue', label: 'Schedule value', value: '', required: true, multiline: false, hint: 'Examples: 0 9 * * *, 7d, or 2026-06-01T09:00:00-05:00.' },
36
+ { id: 'timezone', label: 'Timezone', value: '', required: false, multiline: false, hint: 'Optional IANA timezone, for example America/Chicago.' },
37
+ { id: 'scheduleName', label: 'Schedule name', value: selectedRoutine?.name ?? '', required: false, multiline: false, hint: 'Optional display name for the connected schedule.' },
38
+ { id: 'deliveryChannel', label: 'Delivery channel', value: '', required: false, multiline: false, hint: 'Optional channel target, for example slack or slack:ops-alerts:Ops.' },
39
+ { id: 'disabled', label: 'Create disabled', value: 'no', required: false, multiline: false, hint: 'yes/no. Default no.' },
40
+ { id: 'confirm', label: 'Confirm', value: '', required: true, multiline: false, hint: 'Type yes to run /schedule promote-routine with --yes.' },
41
+ ],
42
+ };
43
+ }
44
+
45
+ export function buildAgentRoutineScheduleEditorSubmission(
46
+ editor: AgentWorkspaceLocalEditor,
47
+ readField: AgentWorkspaceFieldReader,
48
+ commandDispatchAvailable: boolean,
49
+ ): AgentRoutineScheduleEditorSubmission {
50
+ const confirm = readField('confirm');
51
+ if (!isAffirmative(confirm)) {
52
+ return {
53
+ kind: 'editor',
54
+ editor: { ...editor, message: 'Type yes to confirm routine schedule promotion.' },
55
+ status: 'Routine schedule promotion not confirmed.',
56
+ };
57
+ }
58
+
59
+ const scheduleKind = readScheduleKind(readField('scheduleKind'));
60
+ if (!scheduleKind) {
61
+ const detail = 'Schedule type must be cron, every, or at.';
62
+ return {
63
+ kind: 'editor',
64
+ editor: { ...editor, message: detail },
65
+ status: detail,
66
+ actionResult: {
67
+ kind: 'error',
68
+ title: 'Routine schedule type invalid',
69
+ detail,
70
+ },
71
+ };
72
+ }
73
+
74
+ if (!commandDispatchAvailable) {
75
+ return {
76
+ kind: 'editor',
77
+ editor: { ...editor, message: 'Command dispatch is unavailable; cannot promote a routine from this workspace.' },
78
+ status: 'Command dispatch unavailable.',
79
+ actionResult: {
80
+ kind: 'error',
81
+ title: 'Command dispatch unavailable',
82
+ detail: 'The routine schedule promotion command cannot be opened from this runtime.',
83
+ },
84
+ };
85
+ }
86
+
87
+ const parts = [
88
+ '/schedule',
89
+ 'promote-routine',
90
+ quoteSlashCommandArg(readField('routineId')),
91
+ `--${scheduleKind}`,
92
+ quoteSlashCommandArg(readField('scheduleValue')),
93
+ ];
94
+ const timezone = readField('timezone');
95
+ if (timezone.length > 0) parts.push('--timezone', quoteSlashCommandArg(timezone));
96
+ const scheduleName = readField('scheduleName');
97
+ if (scheduleName.length > 0) parts.push('--name', quoteSlashCommandArg(scheduleName));
98
+ const deliveryChannel = readField('deliveryChannel');
99
+ if (deliveryChannel.length > 0) parts.push('--delivery-channel', quoteSlashCommandArg(deliveryChannel));
100
+ if (isOptionalAffirmative(readField('disabled'))) parts.push('--disabled');
101
+ parts.push('--yes');
102
+ const command = parts.join(' ');
103
+
104
+ return {
105
+ kind: 'dispatch',
106
+ command,
107
+ status: 'Opening routine schedule promotion.',
108
+ actionResult: {
109
+ kind: 'dispatched',
110
+ title: 'Opening routine schedule promotion',
111
+ detail: 'The workspace handed a confirmed routine schedule promotion command to the shell-owned command router.',
112
+ command,
113
+ safety: 'safe',
114
+ },
115
+ };
116
+ }
117
+
118
+ function readScheduleKind(value: string): RoutineScheduleKind | null {
119
+ const normalized = value.trim().toLowerCase();
120
+ if (normalized === 'cron' || normalized === 'every' || normalized === 'at') return normalized;
121
+ return null;
122
+ }
123
+
124
+ function isOptionalAffirmative(value: string): boolean {
125
+ const normalized = value.trim().toLowerCase();
126
+ return normalized === 'yes' || normalized === 'y' || normalized === 'true' || normalized === 'enabled' || normalized === 'on';
127
+ }
@@ -39,9 +39,9 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
39
39
  return [
40
40
  {
41
41
  id: 'runtime',
42
- label: 'GoodVibes runtime',
42
+ label: 'Connected services',
43
43
  status: 'ready',
44
- detail: `Agent will connect to ${input.runtimeBaseUrl}; runtime ownership stays outside this product.`,
44
+ detail: `Agent will connect to ${input.runtimeBaseUrl}; service ownership stays outside this product.`,
45
45
  command: '/health',
46
46
  },
47
47
  {
@@ -10,6 +10,24 @@ export type AgentWorkspaceActionKind = 'command' | 'guidance' | 'workspace' | 'e
10
10
 
11
11
  export type AgentWorkspaceLocalEditorKind = 'memory' | 'persona' | 'skill' | 'routine' | 'profile';
12
12
 
13
+ export type AgentWorkspaceEditorKind =
14
+ | AgentWorkspaceLocalEditorKind
15
+ | 'knowledge-url'
16
+ | 'knowledge-file'
17
+ | 'knowledge-bookmarks'
18
+ | 'knowledge-browser-history'
19
+ | 'knowledge-connector-ingest'
20
+ | 'knowledge-search'
21
+ | 'knowledge-ask'
22
+ | 'tts-prompt'
23
+ | 'image-input'
24
+ | 'skill-bundle'
25
+ | 'skill-discovery-import'
26
+ | 'profile-template-export'
27
+ | 'profile-template-import'
28
+ | 'routine-schedule'
29
+ | 'reminder-schedule';
30
+
13
31
  export type AgentWorkspaceLocalOperation =
14
32
  | 'memory-edit'
15
33
  | 'memory-review'
@@ -42,7 +60,7 @@ export interface AgentWorkspaceEditorField {
42
60
  }
43
61
 
44
62
  export interface AgentWorkspaceLocalEditor {
45
- readonly kind: AgentWorkspaceLocalEditorKind;
63
+ readonly kind: AgentWorkspaceEditorKind;
46
64
  readonly mode: 'create' | 'update' | 'delete';
47
65
  readonly recordId?: string;
48
66
  readonly title: string;
@@ -57,7 +75,7 @@ export interface AgentWorkspaceAction {
57
75
  readonly detail: string;
58
76
  readonly command?: string;
59
77
  readonly targetCategoryId?: string;
60
- readonly editorKind?: AgentWorkspaceLocalEditorKind;
78
+ readonly editorKind?: AgentWorkspaceEditorKind;
61
79
  readonly localKind?: AgentWorkspaceLocalEditorKind;
62
80
  readonly selectionDelta?: number;
63
81
  readonly localOperation?: AgentWorkspaceLocalOperation;
@@ -123,8 +123,8 @@ function buildProviderStatus(
123
123
  const nextStep = setupState === 'ready'
124
124
  ? `${label} is ready for explicit Agent voice/media use.`
125
125
  : setupState === 'needs-secret'
126
- ? `Configure one of ${spec.secretKeyOptions.join('|')} in the owning runtime environment.`
127
- : `${label} is registered; confirm any provider-specific setup in the owning runtime.`;
126
+ ? `Configure one of ${spec.secretKeyOptions.join('|')} in the owning GoodVibes host environment.`
127
+ : `${label} is registered; confirm any provider-specific setup in the owning GoodVibes host.`;
128
128
  return {
129
129
  id: provider.id,
130
130
  label,
@@ -150,7 +150,7 @@ function missingSelectedProviderStatus(selectedProviderId: string): AgentWorkspa
150
150
  secretKeyOptions: [],
151
151
  configuredSecretKeys: [],
152
152
  missingSecretKeyOptions: [],
153
- nextStep: `Selected TTS provider ${selectedProviderId} is not registered in this runtime.`,
153
+ nextStep: `Selected TTS provider ${selectedProviderId} is not registered in connected GoodVibes services.`,
154
154
  };
155
155
  }
156
156
 
@@ -168,9 +168,9 @@ function browserToolState(context: CommandContext): AgentWorkspaceVoiceMediaRead
168
168
  }
169
169
 
170
170
  function browserToolNextStep(state: AgentWorkspaceVoiceMediaReadiness['browserToolState']): string {
171
- if (state === 'disabled') return 'Inspect MCP browser/automation tools; enable browser access in the owning runtime only when needed.';
171
+ if (state === 'disabled') return 'Inspect MCP browser/automation tools; enable browser access in the owning GoodVibes host only when needed.';
172
172
  if (state === 'local-only') return 'Browser tooling is local-only; keep external exposure off unless explicitly configured.';
173
- return 'Public browser URL is configured; use explicit user action and runtime policy before browser-side effects.';
173
+ return 'Public browser URL is configured; use explicit user action and Agent policy before browser-side effects.';
174
174
  }
175
175
 
176
176
  export function buildAgentWorkspaceVoiceMediaReadiness(options: {
@@ -8,6 +8,8 @@ import { createAgentRuntimeProfile, type AgentRuntimeProfileInfo } from '../agen
8
8
  import { AgentSkillRegistry } from '../agent/skill-registry.ts';
9
9
  import { activateAgentWorkspaceSelection } from './agent-workspace-activation.ts';
10
10
  import { AGENT_WORKSPACE_CATEGORIES } from './agent-workspace-categories.ts';
11
+ import { buildAgentWorkspaceCommandEditorSubmission, isAgentWorkspaceCommandEditorKind } from './agent-workspace-command-editor.ts';
12
+ import { quoteSlashCommandArg } from './slash-command-parser.ts';
11
13
  import { createDeleteEditor, createMemoryUpdateEditor, createPersonaUpdateEditor, createRoutineUpdateEditor, createSkillUpdateEditor, editorCategoryId, isAffirmative, splitList } from './agent-workspace-editors.ts';
12
14
  import { deleteAgentWorkspaceMemoryEditor, submitAgentWorkspaceMemoryEditor } from './agent-workspace-memory-editor.ts';
13
15
  import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
@@ -379,6 +381,19 @@ export class AgentWorkspace {
379
381
  safety: 'safe',
380
382
  };
381
383
  } else if (operation === 'routine-start') {
384
+ if (this.hasCommandDispatch()) {
385
+ const command = `/routines start ${quoteSlashCommandArg(selected.id)}`;
386
+ this.dispatchWorkspaceCommand(command);
387
+ this.status = `Opening routine: ${selected.name}.`;
388
+ this.lastActionResult = {
389
+ kind: 'dispatched',
390
+ title: `Opening routine ${selected.name}`,
391
+ detail: `${selected.name} will print its workflow steps in the main conversation. No hidden job was created.`,
392
+ command,
393
+ safety: 'safe',
394
+ };
395
+ return;
396
+ }
382
397
  AgentRoutineRegistry.fromShellPaths(shellPaths).markStarted(selected.id);
383
398
  this.finishLocalOperation('routine', `Started routine ${selected.name}`, `${selected.name} was marked started for this main-conversation workflow. No hidden job was created.`);
384
399
  } else if (operation === 'routine-enable') {
@@ -472,6 +487,11 @@ export class AgentWorkspace {
472
487
  this.status = `${missing.label} is required.`;
473
488
  return;
474
489
  }
490
+ if (isAgentWorkspaceCommandEditorKind(editor.kind)) {
491
+ this.submitCommandEditor(editor);
492
+ requestRender?.();
493
+ return;
494
+ }
475
495
  if (editor.kind === 'memory') {
476
496
  if (editor.mode === 'delete') {
477
497
  try {
@@ -608,6 +628,21 @@ export class AgentWorkspace {
608
628
  }
609
629
  }
610
630
 
631
+ private submitCommandEditor(editor: AgentWorkspaceLocalEditor): void {
632
+ const result = buildAgentWorkspaceCommandEditorSubmission(editor, (fieldId) => this.editorField(fieldId), this.hasCommandDispatch());
633
+ if (result.kind === 'editor') {
634
+ this.localEditor = result.editor;
635
+ this.status = result.status;
636
+ if (result.actionResult) this.lastActionResult = result.actionResult;
637
+ return;
638
+ }
639
+
640
+ this.localEditor = null;
641
+ this.status = result.status;
642
+ this.lastActionResult = result.actionResult;
643
+ this.dispatchWorkspaceCommand(result.command);
644
+ }
645
+
611
646
  private submitLocalDeleteEditor(shellPaths: ShellPathService, editor: AgentWorkspaceLocalEditor): void {
612
647
  const expectedId = editor.recordId ?? '';
613
648
  const confirmedId = this.editorField('confirm');
@@ -629,9 +664,11 @@ export class AgentWorkspace {
629
664
  } else if (editor.kind === 'skill') {
630
665
  const removed = AgentSkillRegistry.fromShellPaths(shellPaths).deleteSkill(expectedId);
631
666
  this.finishLocalDelete(editor.kind, removed.id, removed.name);
632
- } else {
667
+ } else if (editor.kind === 'routine') {
633
668
  const removed = AgentRoutineRegistry.fromShellPaths(shellPaths).deleteRoutine(expectedId);
634
669
  this.finishLocalDelete(editor.kind, removed.id, removed.name);
670
+ } else {
671
+ throw new Error(`Unsupported delete editor kind: ${editor.kind}`);
635
672
  }
636
673
  }
637
674
 
@@ -647,7 +684,7 @@ export class AgentWorkspace {
647
684
  this.status = 'Deletion not confirmed.';
648
685
  return;
649
686
  }
650
- this.finishLocalDelete(editor.kind, removed.id, removed.name);
687
+ this.finishLocalDelete('memory', removed.id, removed.name);
651
688
  }
652
689
 
653
690
  private async submitMemoryEditor(editor: AgentWorkspaceLocalEditor): Promise<void> {
@@ -83,7 +83,7 @@ function renderGuide(homeDirectory: string): string {
83
83
  '5. Create an Agent profile from the imported starter:',
84
84
  ' /agent-profile create <name> --template <imported-id> --yes',
85
85
  '',
86
- 'This writes only Agent-local starter/profile state. It does not mutate the runtime host, default wiki, or non-Agent knowledge segments.',
86
+ 'This writes only Agent-local starter/profile state. It does not mutate connected GoodVibes services, default wiki, or non-Agent knowledge segments.',
87
87
  ].join('\n');
88
88
  }
89
89
 
@@ -1,4 +1,5 @@
1
1
  import { AgentSkillRegistry, type AgentSkillBundleRecord, type AgentSkillRecord } from '../../agent/skill-registry.ts';
2
+ import { discoverSkills, type SkillRecord } from '../../agent/skill-discovery.ts';
2
3
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
3
4
  import { requireShellPaths } from './runtime-services.ts';
4
5
 
@@ -73,6 +74,89 @@ function renderList(title: string, registry: AgentSkillRegistry, skills: readonl
73
74
  ].join('\n');
74
75
  }
75
76
 
77
+ function summarizeDiscoveredSkill(skill: SkillRecord): string {
78
+ const description = skill.description ? ` - ${skill.description}` : '';
79
+ const dependencies = skill.dependencies.length > 0 ? ` deps=${skill.dependencies.join(',')}` : '';
80
+ const includes = skill.includes.length > 0 ? ` includes=${skill.includes.join(',')}` : '';
81
+ return ` ${skill.name} ${skill.origin}${description}${dependencies}${includes}\n path: ${skill.path}`;
82
+ }
83
+
84
+ function renderDiscoveredSkills(skills: readonly SkillRecord[]): string {
85
+ if (skills.length === 0) {
86
+ return [
87
+ 'Discovered Agent skill files',
88
+ ' No SKILL.md or .md skill files found in project/global Agent skill folders.',
89
+ ' Search roots: .goodvibes/skills, .goodvibes/agent/skills, ~/.goodvibes/skills, ~/.goodvibes/agent/skills',
90
+ ].join('\n');
91
+ }
92
+ return [
93
+ `Discovered Agent skill files (${skills.length})`,
94
+ ...skills.map(summarizeDiscoveredSkill),
95
+ '',
96
+ 'Import one with: /agent-skills import-discovered <name> --yes',
97
+ ].join('\n');
98
+ }
99
+
100
+ function discoveredSkillLookupValues(skill: SkillRecord): readonly string[] {
101
+ const slug = skill.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
102
+ const basename = skill.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
103
+ return [
104
+ skill.name,
105
+ slug,
106
+ skill.path,
107
+ basename,
108
+ ].map((value) => value.trim().toLowerCase()).filter(Boolean);
109
+ }
110
+
111
+ function findDiscoveredSkill(skills: readonly SkillRecord[], idOrName: string): SkillRecord | null {
112
+ const lookup = idOrName.trim().toLowerCase();
113
+ if (!lookup) return null;
114
+ return skills.find((skill) => discoveredSkillLookupValues(skill).includes(lookup)) ?? null;
115
+ }
116
+
117
+ function frontmatterList(skill: SkillRecord, key: string): readonly string[] {
118
+ const value = skill.frontmatter[key];
119
+ if (!value) return [];
120
+ return splitList(value);
121
+ }
122
+
123
+ async function importDiscoveredSkill(args: readonly string[], ctx: CommandContext, skillRegistry: AgentSkillRegistry): Promise<void> {
124
+ const parsed = parseSkillArgs(args);
125
+ const name = parsed.rest.join(' ').trim();
126
+ if (!name) {
127
+ ctx.print('Usage: /agent-skills import-discovered <name> [--enabled] --yes');
128
+ return;
129
+ }
130
+ const discovered = findDiscoveredSkill(await discoverSkills(requireShellPaths(ctx)), name);
131
+ if (!discovered) {
132
+ ctx.print(`Unknown discovered Agent skill: ${name}\nRun /agent-skills discover to inspect available skill files.`);
133
+ return;
134
+ }
135
+ if (!parsed.yes) {
136
+ ctx.print([
137
+ 'Agent skill import preview',
138
+ ` name: ${discovered.name}`,
139
+ ` origin: ${discovered.origin}`,
140
+ ` path: ${discovered.path}`,
141
+ ` description: ${discovered.description || '(none)'}`,
142
+ ` procedure characters: ${discovered.body.length}`,
143
+ ' next: rerun with --yes to import into the Agent-local skill registry',
144
+ ].join('\n'));
145
+ return;
146
+ }
147
+ const skill = skillRegistry.create({
148
+ name: discovered.name,
149
+ description: discovered.description || `Imported skill from ${discovered.origin} skill file.`,
150
+ procedure: discovered.body,
151
+ triggers: frontmatterList(discovered, 'triggers'),
152
+ tags: frontmatterList(discovered, 'tags'),
153
+ enabled: parsed.flags.get('enabled') === 'true',
154
+ source: 'imported',
155
+ provenance: `discovered:${discovered.origin}:${discovered.path}`,
156
+ });
157
+ ctx.print(`Imported Agent skill ${skill.id}: ${skill.name}`);
158
+ }
159
+
76
160
  function renderBundleList(title: string, registry: AgentSkillRegistry, bundles: readonly AgentSkillBundleRecord[]): string {
77
161
  const snapshot = registry.snapshot();
78
162
  if (bundles.length === 0) {
@@ -248,6 +332,14 @@ export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx:
248
332
  runBundleCommand(args.slice(1), ctx, skillRegistry);
249
333
  return;
250
334
  }
335
+ if (sub === 'discover' || sub === 'discovered') {
336
+ ctx.print(renderDiscoveredSkills(await discoverSkills(requireShellPaths(ctx))));
337
+ return;
338
+ }
339
+ if (sub === 'import-discovered' || sub === 'import-skill') {
340
+ await importDiscoveredSkill(args.slice(1), ctx, skillRegistry);
341
+ return;
342
+ }
251
343
  if (sub === 'list' || sub === 'open') {
252
344
  ctx.print(renderList('Agent Skills', skillRegistry, skillRegistry.list()));
253
345
  return;
@@ -351,7 +443,7 @@ export async function runAgentSkillsRuntimeCommand(args: readonly string[], ctx:
351
443
  ctx.print(`Deleted Agent skill ${removed.id}: ${removed.name}`);
352
444
  return;
353
445
  }
354
- ctx.print('Usage: /agent-skills [list|enabled|search|show|create|update|enable|disable|review|stale|delete|bundle]');
446
+ ctx.print('Usage: /agent-skills [list|enabled|discover|import-discovered|search|show|create|update|enable|disable|review|stale|delete|bundle]');
355
447
  } catch (error) {
356
448
  printError(ctx, error);
357
449
  }
@@ -362,7 +454,7 @@ export function registerAgentSkillsRuntimeCommands(registry: CommandRegistry): v
362
454
  name: 'agent-skills',
363
455
  aliases: ['askills', 'local-skills', 'skills', 'skill'],
364
456
  description: 'Manage local GoodVibes Agent skills',
365
- usage: '[list|enabled|search <query>|show <id>|create --name <name> --description <summary> --procedure <steps>|update <id> [--name ...] [--description ...] [--procedure ...]|enable <id>|disable <id>|review <id>|stale <id> <reason...>|delete <id> --yes|bundle ...]',
457
+ usage: '[list|enabled|discover|import-discovered <name> --yes|search <query>|show <id>|create --name <name> --description <summary> --procedure <steps>|update <id> [--name ...] [--description ...] [--procedure ...]|enable <id>|disable <id>|review <id>|stale <id> <reason...>|delete <id> --yes|bundle ...]',
366
458
  handler: runAgentSkillsRuntimeCommand,
367
459
  });
368
460
  }