@pellux/goodvibes-agent 1.5.5 → 1.5.6

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/main.js +94130 -79335
  4. package/docs/tools-and-commands.md +2 -1
  5. package/package.json +6 -3
  6. package/src/agent/autonomy-schedule-format.ts +1 -1
  7. package/src/agent/autonomy-schedule.ts +3 -3
  8. package/src/agent/calendar/calendar-oauth-service.ts +324 -0
  9. package/src/agent/calendar-subscription-registry.ts +497 -0
  10. package/src/agent/email/email-service.ts +57 -0
  11. package/src/agent/email/smtp-client.ts +30 -10
  12. package/src/agent/memory-prompt.ts +143 -12
  13. package/src/agent/operator-actions.ts +26 -22
  14. package/src/agent/prompt-context-receipts.ts +45 -18
  15. package/src/agent/reminder-schedule-format.ts +1 -1
  16. package/src/agent/reminder-schedule.ts +3 -3
  17. package/src/agent/routine-schedule-format.ts +2 -2
  18. package/src/agent/routine-schedule-promotion.ts +5 -5
  19. package/src/agent/routine-schedule-receipts.ts +2 -2
  20. package/src/agent/schedule-edit-format.ts +1 -1
  21. package/src/agent/schedule-edit.ts +5 -5
  22. package/src/agent/session-registration.ts +281 -0
  23. package/src/agent/vibe-file.ts +50 -0
  24. package/src/cli/local-library-command-shared.ts +146 -0
  25. package/src/cli/management.ts +2 -1
  26. package/src/cli/personas-command.ts +252 -0
  27. package/src/cli/resume-relaunch-notice.ts +151 -0
  28. package/src/cli/routines-command.ts +25 -2
  29. package/src/cli/service-posture.ts +3 -2
  30. package/src/cli/skill-bundle-command.ts +175 -0
  31. package/src/cli/skills-command.ts +309 -0
  32. package/src/cli/status.ts +43 -7
  33. package/src/cli/tui-startup.ts +24 -1
  34. package/src/config/agent-settings-policy.ts +0 -1
  35. package/src/config/secret-config.ts +4 -0
  36. package/src/core/conversation-rendering.ts +20 -11
  37. package/src/core/focus-tracker.ts +41 -0
  38. package/src/core/system-message-noise.ts +108 -0
  39. package/src/core/system-message-router.ts +54 -1
  40. package/src/core/thinking-overlay.ts +83 -0
  41. package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
  42. package/src/input/agent-workspace-access-command-editors.ts +108 -128
  43. package/src/input/agent-workspace-activation.ts +15 -0
  44. package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
  45. package/src/input/agent-workspace-basic-command-editors.ts +433 -564
  46. package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
  47. package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
  48. package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
  49. package/src/input/agent-workspace-categories.ts +7 -2
  50. package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
  51. package/src/input/agent-workspace-channel-command-editors.ts +42 -46
  52. package/src/input/agent-workspace-command-editor-engine.ts +133 -0
  53. package/src/input/agent-workspace-command-editor.ts +4 -0
  54. package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
  55. package/src/input/agent-workspace-email-connect-editor.ts +162 -0
  56. package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
  57. package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
  58. package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
  59. package/src/input/agent-workspace-library-command-editors.ts +16 -2
  60. package/src/input/agent-workspace-live-counters.ts +55 -0
  61. package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
  62. package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
  63. package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
  64. package/src/input/agent-workspace-media-command-editors.ts +10 -8
  65. package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
  66. package/src/input/agent-workspace-memory-command-editors.ts +116 -141
  67. package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
  68. package/src/input/agent-workspace-operations-command-editors.ts +150 -162
  69. package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
  70. package/src/input/agent-workspace-provider-command-editors.ts +58 -68
  71. package/src/input/agent-workspace-search.ts +8 -1
  72. package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
  73. package/src/input/agent-workspace-session-command-editors.ts +160 -195
  74. package/src/input/agent-workspace-settings.ts +40 -1
  75. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
  76. package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
  77. package/src/input/agent-workspace-snapshot-builders.ts +431 -0
  78. package/src/input/agent-workspace-snapshot-config.ts +43 -0
  79. package/src/input/agent-workspace-snapshot.ts +198 -432
  80. package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
  81. package/src/input/agent-workspace-task-command-editors.ts +27 -28
  82. package/src/input/agent-workspace-types.ts +35 -1
  83. package/src/input/agent-workspace.ts +5 -14
  84. package/src/input/command-registry.ts +23 -1
  85. package/src/input/commands/calendar-connect-runtime.ts +226 -0
  86. package/src/input/commands/calendar-runtime.ts +110 -7
  87. package/src/input/commands/calendar-subscription-runtime.ts +225 -0
  88. package/src/input/commands/email-runtime.ts +100 -40
  89. package/src/input/commands/knowledge.ts +1 -1
  90. package/src/input/commands/network-scan-runtime.ts +75 -0
  91. package/src/input/commands/operator-actions-runtime.ts +9 -6
  92. package/src/input/commands/personas-runtime.ts +1 -1
  93. package/src/input/commands/schedule-runtime.ts +4 -4
  94. package/src/input/commands/session-content.ts +13 -1
  95. package/src/input/commands/session-workflow.ts +15 -20
  96. package/src/input/commands/session.ts +3 -1
  97. package/src/input/commands/shell-core.ts +20 -6
  98. package/src/input/commands.ts +2 -2
  99. package/src/input/delete-key-policy.ts +46 -0
  100. package/src/input/feed-context-factory.ts +10 -0
  101. package/src/input/handler-feed.ts +87 -0
  102. package/src/input/handler-modal-routes.ts +6 -1
  103. package/src/input/handler.ts +5 -0
  104. package/src/input/panel-paste-flood-guard.ts +94 -0
  105. package/src/input/settings-modal-types.ts +5 -3
  106. package/src/input/settings-modal.ts +21 -0
  107. package/src/main.ts +36 -36
  108. package/src/permissions/approval-posture.ts +141 -0
  109. package/src/renderer/agent-workspace-context-lines.ts +10 -2
  110. package/src/renderer/compositor.ts +27 -4
  111. package/src/renderer/diff.ts +61 -18
  112. package/src/renderer/fullscreen-primitives.ts +37 -18
  113. package/src/renderer/markdown.ts +20 -10
  114. package/src/renderer/modal-factory.ts +25 -15
  115. package/src/renderer/overlay-box.ts +23 -12
  116. package/src/renderer/process-indicator.ts +8 -3
  117. package/src/renderer/prompt-content-width.ts +16 -0
  118. package/src/renderer/settings-modal-helpers.ts +2 -0
  119. package/src/renderer/settings-modal.ts +2 -0
  120. package/src/renderer/startup-theme-probe.ts +35 -0
  121. package/src/renderer/status-glyphs.ts +11 -15
  122. package/src/renderer/system-message.ts +17 -2
  123. package/src/renderer/term-caps.ts +318 -0
  124. package/src/renderer/terminal-bg-probe.ts +373 -0
  125. package/src/renderer/terminal-escapes.ts +24 -0
  126. package/src/renderer/theme-mode-config.ts +87 -0
  127. package/src/renderer/theme.ts +241 -0
  128. package/src/renderer/thinking.ts +12 -3
  129. package/src/renderer/tool-call.ts +7 -3
  130. package/src/renderer/ui-factory.ts +92 -36
  131. package/src/renderer/ui-primitives.ts +33 -93
  132. package/src/runtime/bootstrap-core.ts +15 -0
  133. package/src/runtime/bootstrap-hook-bridge.ts +6 -0
  134. package/src/runtime/bootstrap-shell.ts +2 -0
  135. package/src/runtime/bootstrap.ts +68 -5
  136. package/src/runtime/calendar-boot-refresh.ts +105 -0
  137. package/src/runtime/lan-scan-consent.ts +253 -0
  138. package/src/runtime/services.ts +127 -2
  139. package/src/runtime/session-spine-rest-transport.ts +160 -0
  140. package/src/runtime/terminal-output-guard.ts +6 -1
  141. package/src/runtime/ui-services.ts +3 -0
  142. package/src/shell/agent-workspace-fullscreen.ts +5 -0
  143. package/src/shell/terminal-focus-mode.ts +120 -0
  144. package/src/shell/ui-openers.ts +13 -4
  145. package/src/tools/agent-harness-autonomy-intake.ts +5 -5
  146. package/src/tools/agent-harness-autonomy-queue.ts +1 -1
  147. package/src/tools/agent-harness-background-processes.ts +2 -1
  148. package/src/tools/agent-harness-metadata.ts +16 -8
  149. package/src/tools/agent-harness-operator-methods.ts +44 -8
  150. package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
  151. package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
  152. package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
  153. package/src/tools/agent-harness-personal-ops-types.ts +6 -0
  154. package/src/tools/agent-harness-prompt-context.ts +26 -12
  155. package/src/tools/agent-harness-workspace-actions.ts +4 -0
  156. package/src/tools/agent-local-registry-args.ts +117 -0
  157. package/src/tools/agent-local-registry-memory.ts +227 -0
  158. package/src/tools/agent-local-registry-tool.ts +19 -237
  159. package/src/tools/agent-operator-briefing-tool.ts +2 -2
  160. package/src/tools/agent-operator-method-tool.ts +13 -0
  161. package/src/tools/agent-policy-explanation.ts +39 -4
  162. package/src/tools/agent-schedule-tool.ts +5 -5
  163. package/src/utils/terminal-width.ts +98 -1
  164. package/src/version.ts +1 -1
  165. package/src/cli/local-library-command.ts +0 -825
@@ -0,0 +1,252 @@
1
+ import { discoverPersonas, type DiscoveredPersonaRecord } from '../agent/persona-discovery.ts';
2
+ import type { AgentPersonaRecord } from '../agent/persona-registry.ts';
3
+ import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
4
+ import type { CliCommandOutput } from './types.ts';
5
+ import type { CliCommandRuntime } from './management.ts';
6
+ import {
7
+ csvOption,
8
+ errorOutput,
9
+ failure,
10
+ hasFlag,
11
+ optionValue,
12
+ parseOptions,
13
+ personaRegistry,
14
+ requiredOption,
15
+ shellPaths,
16
+ success,
17
+ } from './local-library-command-shared.ts';
18
+
19
+ function summarizePersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
20
+ const active = persona.id === activePersonaId ? 'active' : 'available';
21
+ const tags = persona.tags.length > 0 ? ` tags ${persona.tags.join(', ')}` : '';
22
+ return ` ${persona.id} ${active} ${formatAgentRecordReviewState(persona.reviewState)} ${persona.name} - ${persona.description}${tags}`;
23
+ }
24
+
25
+ function renderPersonaList(title: string, path: string, personas: readonly AgentPersonaRecord[], activePersonaId: string | null): string {
26
+ if (personas.length === 0) {
27
+ return [
28
+ title,
29
+ ' No local Agent personas yet. Create one with: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>',
30
+ ].join('\n');
31
+ }
32
+ return [
33
+ `${title} (${personas.length})`,
34
+ ` store ${path}`,
35
+ ...personas.map((persona) => summarizePersona(persona, activePersonaId)),
36
+ ].join('\n');
37
+ }
38
+
39
+ function renderPersona(persona: AgentPersonaRecord, activePersonaId: string | null): string {
40
+ return [
41
+ `Persona ${persona.name}`,
42
+ ` id ${persona.id}`,
43
+ ` active: ${persona.id === activePersonaId ? 'yes' : 'no'}`,
44
+ ` review: ${formatAgentRecordReviewState(persona.reviewState)}`,
45
+ ` origin: ${formatAgentRecordOrigin(persona.source, persona.provenance)}`,
46
+ ` tags: ${persona.tags.join(', ') || '(none)'}`,
47
+ ` triggers: ${persona.triggers.join(', ') || '(manual)'}`,
48
+ ` created: ${persona.createdAt}`,
49
+ ` updated: ${persona.updatedAt}`,
50
+ persona.staleReason ? ` stale reason: ${persona.staleReason}` : '',
51
+ '',
52
+ persona.description,
53
+ '',
54
+ persona.body,
55
+ ].filter((line): line is string => Boolean(line)).join('\n');
56
+ }
57
+
58
+ function summarizeDiscoveredPersona(persona: DiscoveredPersonaRecord): string {
59
+ const description = persona.description ? ` - ${persona.description}` : '';
60
+ return [
61
+ ` ${persona.name} ${persona.origin}${description}`,
62
+ ` path ${persona.path}`,
63
+ ].join('\n');
64
+ }
65
+
66
+ function renderDiscoveredPersonaList(personas: readonly DiscoveredPersonaRecord[]): string {
67
+ if (personas.length === 0) {
68
+ return [
69
+ 'Discovered Agent persona files',
70
+ ' No persona markdown files found in Agent persona folders.',
71
+ ' Search roots: .goodvibes/personas, .goodvibes/agent/personas, ~/.goodvibes/personas, ~/.goodvibes/agent/personas',
72
+ ].join('\n');
73
+ }
74
+ return [
75
+ `Discovered Agent persona files (${personas.length})`,
76
+ ...personas.map(summarizeDiscoveredPersona),
77
+ '',
78
+ 'Import one with: goodvibes-agent personas import-discovered <name> --yes',
79
+ ].join('\n');
80
+ }
81
+
82
+ function discoveredPersonaLookupValues(persona: DiscoveredPersonaRecord): readonly string[] {
83
+ const slug = persona.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
84
+ const basename = persona.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
85
+ return [persona.name, slug, persona.path, basename]
86
+ .map((value) => value.trim().toLowerCase())
87
+ .filter(Boolean);
88
+ }
89
+
90
+ function findDiscoveredPersona(personas: readonly DiscoveredPersonaRecord[], idOrName: string): DiscoveredPersonaRecord | null {
91
+ const lookup = idOrName.trim().toLowerCase();
92
+ if (!lookup) return null;
93
+ return personas.find((persona) => discoveredPersonaLookupValues(persona).includes(lookup)) ?? null;
94
+ }
95
+
96
+ function discoveredPersonaFrontmatterList(persona: DiscoveredPersonaRecord, key: string): readonly string[] {
97
+ const value = persona.frontmatter[key];
98
+ if (!value) return [];
99
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
100
+ }
101
+
102
+ function usagePersonas(): string {
103
+ return 'Usage: goodvibes-agent personas [list|active|discover|import-discovered <name> --yes|search <query>|show <id>|create|update <id>|use <id>|clear|review <id>|stale <id> <reason>|delete <id> --yes]';
104
+ }
105
+
106
+ export async function handlePersonasCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
107
+ try {
108
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
109
+ const normalized = sub.toLowerCase();
110
+ const registry = personaRegistry(runtime);
111
+ const snapshot = registry.snapshot();
112
+ if (normalized === 'list' || normalized === 'ls') {
113
+ return success(runtime, 'agent.personas.list', snapshot, renderPersonaList('Agent personas', snapshot.path, snapshot.personas, snapshot.activePersonaId));
114
+ }
115
+ if (normalized === 'active') {
116
+ const active = snapshot.activePersona;
117
+ if (!active) return failure(runtime, 'agent.personas.active_missing', 'No active Agent persona.', 1);
118
+ return success(runtime, 'agent.personas.active', active, renderPersona(active, active.id));
119
+ }
120
+ if (normalized === 'discover') {
121
+ const discovered = await discoverPersonas(shellPaths(runtime));
122
+ return success(runtime, 'agent.personas.discover', { personas: discovered }, renderDiscoveredPersonaList(discovered));
123
+ }
124
+ if (normalized === 'import-discovered' || normalized === 'import-persona') {
125
+ const options = parseOptions(rest);
126
+ const name = options.positionals.join(' ').trim();
127
+ if (!name) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas import-discovered <name> [--use] --yes', 2);
128
+ const discovered = findDiscoveredPersona(await discoverPersonas(shellPaths(runtime)), name);
129
+ if (!discovered) {
130
+ return failure(runtime, 'persona_discovery_not_found', `Unknown discovered Agent persona ${name}\nRun goodvibes-agent personas discover to inspect available persona files.`, 1);
131
+ }
132
+ if (!hasFlag(options, 'yes')) {
133
+ return success(runtime, 'agent.personas.import_discovered.preview', { persona: discovered }, [
134
+ 'Agent persona import preview',
135
+ ` name ${discovered.name}`,
136
+ ` origin ${discovered.origin}`,
137
+ ` path ${discovered.path}`,
138
+ ` description ${discovered.description || '(none)'}`,
139
+ ` body characters ${discovered.body.length}`,
140
+ ' next rerun with --yes to import into the Agent-local persona registry',
141
+ ].join('\n'));
142
+ }
143
+ const persona = registry.create({
144
+ name: discovered.name,
145
+ description: discovered.description || `Imported persona from ${discovered.origin} markdown file.`,
146
+ body: discovered.body,
147
+ tags: discoveredPersonaFrontmatterList(discovered, 'tags'),
148
+ triggers: discoveredPersonaFrontmatterList(discovered, 'triggers'),
149
+ source: 'imported',
150
+ provenance: `discovered:${discovered.origin}:${discovered.path}`,
151
+ });
152
+ if (hasFlag(options, 'use')) registry.setActive(persona.id);
153
+ return success(runtime, 'agent.personas.import_discovered', persona, [
154
+ `Imported Agent persona ${persona.id}: ${persona.name}${hasFlag(options, 'use') ? ' (active)' : ''}`,
155
+ ` name ${persona.name}`,
156
+ ` active ${hasFlag(options, 'use') ? 'yes' : 'no'}`,
157
+ ].join('\n'));
158
+ }
159
+ if (normalized === 'search' || normalized === 'find') {
160
+ const query = rest.join(' ').trim();
161
+ const results = registry.search(query);
162
+ return success(runtime, 'agent.personas.search', { query, results }, renderPersonaList(`Agent personas matching "${query}"`, snapshot.path, results, snapshot.activePersonaId));
163
+ }
164
+ if (normalized === 'show' || normalized === 'get') {
165
+ const id = rest[0];
166
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas show <id>', 2);
167
+ const persona = registry.get(id);
168
+ if (!persona) return failure(runtime, 'persona_not_found', `Unknown Agent persona ${id}`, 1);
169
+ return success(runtime, 'agent.personas.show', persona, renderPersona(persona, snapshot.activePersonaId));
170
+ }
171
+ if (normalized === 'create') {
172
+ const options = parseOptions(rest);
173
+ const persona = registry.create({
174
+ name: requiredOption(options, 'name', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
175
+ description: requiredOption(options, 'description', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
176
+ body: requiredOption(options, 'body', 'Usage: goodvibes-agent personas create --name <name> --description <summary> --body <instructions>'),
177
+ tags: csvOption(options, 'tags'),
178
+ triggers: csvOption(options, 'triggers'),
179
+ provenance: optionValue(options, 'provenance') ?? 'Command',
180
+ });
181
+ if (hasFlag(options, 'use')) registry.setActive(persona.id);
182
+ return success(runtime, 'agent.personas.create', persona, [
183
+ 'Agent persona created',
184
+ ` id ${persona.id}`,
185
+ hasFlag(options, 'use') ? ' (active)' : '',
186
+ ` active ${hasFlag(options, 'use') ? 'yes' : 'no'}`,
187
+ ].filter(Boolean).join('\n'));
188
+ }
189
+ if (normalized === 'update') {
190
+ const id = rest[0];
191
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas update <id> [--name ...] [--description ...] [--body ...]', 2);
192
+ const options = parseOptions(rest.slice(1));
193
+ const persona = registry.update(id, {
194
+ name: optionValue(options, 'name'),
195
+ description: optionValue(options, 'description'),
196
+ body: optionValue(options, 'body'),
197
+ tags: csvOption(options, 'tags'),
198
+ triggers: csvOption(options, 'triggers'),
199
+ provenance: optionValue(options, 'provenance'),
200
+ });
201
+ return success(runtime, 'agent.personas.update', persona, [
202
+ 'Agent persona updated',
203
+ ` id ${persona.id}`,
204
+ ].join('\n'));
205
+ }
206
+ if (normalized === 'use') {
207
+ const id = rest[0];
208
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas use <id>', 2);
209
+ const persona = registry.setActive(id);
210
+ return success(runtime, 'agent.personas.use', persona, [
211
+ 'Active Agent persona',
212
+ ` id ${persona.id}`,
213
+ ].join('\n'));
214
+ }
215
+ if (normalized === 'clear') {
216
+ registry.clearActive();
217
+ return success(runtime, 'agent.personas.clear', { activePersonaId: null }, 'Active Agent persona cleared.');
218
+ }
219
+ if (normalized === 'review') {
220
+ const id = rest[0];
221
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas review <id>', 2);
222
+ const persona = registry.markReviewed(id);
223
+ return success(runtime, 'agent.personas.review', persona, [
224
+ 'Agent persona reviewed',
225
+ ` id ${persona.id}`,
226
+ ].join('\n'));
227
+ }
228
+ if (normalized === 'stale') {
229
+ const id = rest[0];
230
+ if (!id || rest.length < 2) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas stale <id> <reason>', 2);
231
+ const persona = registry.markStale(id, rest.slice(1).join(' '));
232
+ return success(runtime, 'agent.personas.stale', persona, [
233
+ 'Agent persona marked stale',
234
+ ` id ${persona.id}`,
235
+ ].join('\n'));
236
+ }
237
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
238
+ const options = parseOptions(rest);
239
+ const id = options.positionals[0];
240
+ if (!id) return failure(runtime, 'invalid_persona_command', 'Usage: goodvibes-agent personas delete <id> --yes', 2);
241
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent persona ${id} without --yes.`, 2);
242
+ const persona = registry.deletePersona(id);
243
+ return success(runtime, 'agent.personas.delete', persona, [
244
+ `Agent persona deleted: ${id}`,
245
+ ` id ${persona.id}`,
246
+ ].join('\n'));
247
+ }
248
+ return failure(runtime, 'invalid_persona_command', usagePersonas(), 2);
249
+ } catch (error) {
250
+ return errorOutput(runtime, error, 'agent.personas.error');
251
+ }
252
+ }
@@ -0,0 +1,151 @@
1
+ import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
2
+
3
+ /**
4
+ * RESUME-ON-RELAUNCH (W4-A4).
5
+ *
6
+ * The dogfood finding: relaunching the agent after a normal (non-crash) exit
7
+ * silently starts fresh with no offer or notice about the prior session — the
8
+ * only way back was knowing the exact `goodvibes-agent sessions resume <id>`
9
+ * command, or digging into `/health continuity`. This module builds an
10
+ * honest, one-line boot notice that surfaces the resume affordance without
11
+ * ever auto-resuming or blocking startup.
12
+ *
13
+ * Honesty rules (never claim restorability the app can't deliver):
14
+ * - no last-session pointer on disk -> no notice (clean first run)
15
+ * - a pointer exists but the session it names is missing/unreadable (stale
16
+ * or dangling) -> say so plainly and point at `/session list`, never a
17
+ * resume action that would dead-end
18
+ * - a pointer exists and the session resolves -> an actionable one-liner
19
+ * naming the EXISTING `/session resume <id>` path (this is surfacing, not
20
+ * new resume logic)
21
+ * - never auto-resume: the user always chooses, and declining is
22
+ * frictionless (just start typing — the saved session stays put)
23
+ */
24
+
25
+ /** Minimal session facts needed to render an honest resume notice. */
26
+ export interface ResumableSessionSummary {
27
+ readonly sessionId: string;
28
+ readonly title: string;
29
+ readonly timestamp: number;
30
+ readonly messageCount: number;
31
+ }
32
+
33
+ export interface ResumeRelaunchNoticeInput {
34
+ /** The session id recorded in the last-session pointer file, or null when no pointer exists. */
35
+ readonly pointerSessionId: string | null;
36
+ /**
37
+ * The resolved session the pointer refers to, or null when the pointer
38
+ * exists but the session it names could not be found/read (stale or
39
+ * dangling pointer).
40
+ */
41
+ readonly session: ResumableSessionSummary | null;
42
+ readonly now?: number;
43
+ }
44
+
45
+ /**
46
+ * Formats an elapsed duration the way a human would describe "how long ago".
47
+ *
48
+ * F4 fix: this used Math.round at every tier, which rounds UP near a
49
+ * boundary — a session from 45 minutes ago (2,700,000ms) rounded to "1h ago"
50
+ * (round(45/60) = 1), which reads as roughly twice as stale as it really is.
51
+ * Every tier below now floors instead, so a duration is never described as
52
+ * further in the past than it actually is: 45m through 59m59s all read as
53
+ * "Nm ago", and the "1h" label only appears once a full hour has actually
54
+ * elapsed. Minutes are floored to at least 1 once past the 45s "moments"
55
+ * cutoff, so a duration just past that cutoff never reads as "0m ago".
56
+ */
57
+ export function formatRelaunchAge(elapsedMs: number): string {
58
+ const elapsed = Math.max(0, elapsedMs);
59
+ const seconds = Math.floor(elapsed / 1000);
60
+ if (seconds < 45) return 'moments';
61
+ const minutes = Math.max(1, Math.floor(seconds / 60));
62
+ if (minutes < 60) return `${minutes}m`;
63
+ const hours = Math.floor(minutes / 60);
64
+ if (hours < 36) return `${hours}h`;
65
+ const days = Math.floor(hours / 24);
66
+ return `${days}d`;
67
+ }
68
+
69
+ /**
70
+ * Builds the boot-time resume notice text, or null when nothing should be
71
+ * shown (no pointer at all — a clean first run, or the caller already routed
72
+ * an explicit resume/onboarding command elsewhere).
73
+ */
74
+ export function buildResumeRelaunchNotice(input: ResumeRelaunchNoticeInput): string | null {
75
+ if (!input.pointerSessionId) return null;
76
+
77
+ if (!input.session) {
78
+ return [
79
+ '[Resume] Your last session pointer is unavailable (the saved session may have been moved, renamed, or deleted).',
80
+ ' Starting fresh. Run /session list to see any sessions that are still on disk.',
81
+ ].join('\n');
82
+ }
83
+
84
+ // F5: a 0-message session has nothing to resume into — offering a resume
85
+ // action for it is a dead-end same as a stale pointer, but less honest,
86
+ // since the session itself does resolve. Stay silent, exactly like the
87
+ // no-pointer (clean first run) case above, rather than dangle a resume
88
+ // command in front of an empty conversation.
89
+ if (input.session.messageCount <= 0) return null;
90
+
91
+ const { session } = input;
92
+ const age = formatRelaunchAge((input.now ?? Date.now()) - session.timestamp);
93
+ const label = session.title.trim() || session.sessionId;
94
+ const messageWord = session.messageCount === 1 ? 'message' : 'messages';
95
+
96
+ return [
97
+ `[Resume] Your last session is available: "${label}" — ${age} ago, ${session.messageCount} ${messageWord}.`,
98
+ ` Resume it: /session resume ${session.sessionId}`,
99
+ ' Or just start typing to begin a new session — the saved one stays put.',
100
+ ].join('\n');
101
+ }
102
+
103
+ /**
104
+ * Resolves the pointer session id to its display facts via the existing
105
+ * SessionManager, honestly returning null when the session is missing or
106
+ * unreadable rather than throwing.
107
+ */
108
+ export function resolveResumableSession(
109
+ sessionManager: Pick<SessionManager, 'list'>,
110
+ pointerSessionId: string,
111
+ ): ResumableSessionSummary | null {
112
+ const found = sessionManager.list().find((session) => session.name === pointerSessionId);
113
+ if (!found) return null;
114
+ return {
115
+ sessionId: found.name,
116
+ title: found.title,
117
+ timestamp: found.timestamp,
118
+ messageCount: found.messageCount,
119
+ };
120
+ }
121
+
122
+ export interface SurfaceResumeRelaunchNoticeDeps {
123
+ readonly getLastSessionPointer: () => string | null;
124
+ /** True when a crash-recovery prompt is already showing (that flow owns its own restore path and takes priority). */
125
+ readonly isRecoveryPending: () => boolean;
126
+ readonly findSession: (sessionId: string) => ResumableSessionSummary | null;
127
+ readonly print: (text: string) => void;
128
+ readonly now?: () => number;
129
+ }
130
+
131
+ /**
132
+ * Decides whether to surface the resume notice and prints it. Never throws,
133
+ * never blocks startup, and never auto-resumes — the user always chooses by
134
+ * running the printed command or by just typing to start fresh.
135
+ */
136
+ export function surfaceResumeRelaunchNotice(deps: SurfaceResumeRelaunchNoticeDeps): void {
137
+ try {
138
+ if (deps.isRecoveryPending()) return;
139
+ const pointerSessionId = deps.getLastSessionPointer();
140
+ if (!pointerSessionId) return;
141
+ const session = deps.findSession(pointerSessionId);
142
+ const notice = buildResumeRelaunchNotice({
143
+ pointerSessionId,
144
+ session,
145
+ now: deps.now ? deps.now() : Date.now(),
146
+ });
147
+ if (notice) deps.print(notice);
148
+ } catch {
149
+ // Never block startup on a resume-notice failure.
150
+ }
151
+ }
@@ -290,7 +290,7 @@ async function handleRoutinePromotion(runtime: CliCommandRuntime, args: readonly
290
290
  if (!parsed.yes) {
291
291
  const value: RoutinesCommandSuccess<typeof preview> = {
292
292
  ok: true,
293
- kind: 'schedules.create.preview',
293
+ kind: 'automation.schedules.create.preview',
294
294
  data: preview,
295
295
  };
296
296
  return {
@@ -496,6 +496,29 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
496
496
  exitCode: 0,
497
497
  };
498
498
  }
499
+ if (normalized === 'delete' || normalized === 'remove') {
500
+ const options = parseRoutineOptions(rest);
501
+ const id = options.positionals[0];
502
+ if (!id) return { output: 'Usage: goodvibes-agent routines delete <id> --yes', exitCode: 2 };
503
+ if (!options.yes) {
504
+ return { output: `Refusing to delete Agent routine ${id} without --yes.`, exitCode: 2 };
505
+ }
506
+ try {
507
+ const routine = registry.deleteRoutine(id);
508
+ const value: RoutinesCommandSuccess<AgentRoutineRecord> = { ok: true, kind: 'agent.routines.delete', data: routine };
509
+ return {
510
+ output: jsonOrText(runtime, value, `Deleted Agent routine ${routine.id}: ${routine.name}`),
511
+ exitCode: 0,
512
+ };
513
+ } catch (error) {
514
+ return commandFailure(
515
+ runtime,
516
+ 'routine_not_found',
517
+ error instanceof Error ? error.message : String(error),
518
+ 1,
519
+ );
520
+ }
521
+ }
499
522
  if (normalized === 'receipts' || normalized === 'history') {
500
523
  const snapshot = routineReceiptStore(runtime).snapshot();
501
524
  const value: RoutinesCommandSuccess<typeof snapshot> = {
@@ -544,7 +567,7 @@ export async function handleRoutinesCommand(runtime: CliCommandRuntime): Promise
544
567
  return handleRoutinePromotion(runtime, rest);
545
568
  }
546
569
  return {
547
- output: `Usage: goodvibes-agent routines [list|enabled|attention|discover|import-discovered <name> --yes|create --name <name> --description <summary> --steps <steps>|show <id>|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-channel <channel>|--delivery-route <route>|--delivery-webhook <url>] --yes]`,
570
+ output: `Usage: goodvibes-agent routines [list|enabled|attention|discover|import-discovered <name> --yes|create --name <name> --description <summary> --steps <steps>|show <id>|delete <id> --yes|receipts|reconcile|receipt <id>|promote <id> (--cron <expr>|--every <interval>|--at <iso-time>) [--delivery-channel <channel>|--delivery-route <route>|--delivery-webhook <url>] --yes]`,
548
571
  exitCode: 2,
549
572
  };
550
573
  }
@@ -1,6 +1,7 @@
1
1
  import { closeSync, existsSync, openSync, readSync, statSync } from 'node:fs';
2
2
  import net from 'node:net';
3
3
  import { isAbsolute, join } from 'node:path';
4
+ import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
4
5
  import type { ConfigKey, ConfigManager } from '../config/index.ts';
5
6
  import { resolveRuntimeEndpointBinding } from './endpoints.ts';
6
7
  import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
@@ -178,7 +179,7 @@ export async function buildCliServicePosture(
178
179
  enabled: runtime.configManager.get('service.enabled') === true,
179
180
  autostart: runtime.configManager.get('service.autostart') === true,
180
181
  restartOnFailure: runtime.configManager.get('service.restartOnFailure') === true,
181
- daemonEnabled: runtime.configManager.get('danger.daemon') === true,
182
+ daemonEnabled: resolveDaemonEnabled(runtime.configManager),
182
183
  };
183
184
  const serverBackedEnabled = config.daemonEnabled || endpoints.some((endpoint) => endpoint.enabled);
184
185
  const issues: string[] = [];
@@ -228,7 +229,7 @@ export function formatCliServicePosture(posture: CliServicePosture, json = false
228
229
  ' Agent starts connected host: no',
229
230
  ` external host config present: ${yesNo(posture.config.enabled)}`,
230
231
  ' external host lifecycle config: ignored by Agent',
231
- ` legacy host switch present: ${yesNo(posture.config.daemonEnabled)}`,
232
+ ` daemon considered enabled: ${yesNo(posture.config.daemonEnabled)}`,
232
233
  ` log: ${posture.log.path ?? 'n/a'} (${posture.log.exists ? 'present' : 'missing'})`,
233
234
  ...(posture.log.readError ? [` log read error: ${posture.log.readError}`] : []),
234
235
  '',
@@ -0,0 +1,175 @@
1
+ import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
2
+ import {
3
+ evaluateAgentSkillBundleReadiness,
4
+ formatAgentSkillRequirement,
5
+ type AgentSkillBundleRecord,
6
+ type AgentSkillRecord,
7
+ } from '../agent/skill-registry.ts';
8
+ import type { CliCommandOutput } from './types.ts';
9
+ import type { CliCommandRuntime } from './management.ts';
10
+ import {
11
+ csvOption,
12
+ errorOutput,
13
+ failure,
14
+ hasFlag,
15
+ optionValue,
16
+ parseOptions,
17
+ requiredOption,
18
+ skillRegistry,
19
+ success,
20
+ } from './local-library-command-shared.ts';
21
+
22
+ function summarizeBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkillRecord[]): string {
23
+ const enabled = bundle.enabled ? 'enabled' : 'disabled';
24
+ const readiness = evaluateAgentSkillBundleReadiness(bundle, skills);
25
+ const missing = [
26
+ ...readiness.missingRequirements.map(formatAgentSkillRequirement),
27
+ ...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
28
+ ];
29
+ const ready = readiness.ready ? 'ready' : `needs ${missing.join(',')}`;
30
+ return ` ${bundle.id} ${enabled} ${formatAgentRecordReviewState(bundle.reviewState)} ${ready} ${bundle.name} - ${bundle.description} skills ${bundle.skillIds.join(',')}`;
31
+ }
32
+
33
+ export function renderBundleList(title: string, path: string, bundles: readonly AgentSkillBundleRecord[], skills: readonly AgentSkillRecord[], emptyMessage?: string): string {
34
+ if (bundles.length === 0) {
35
+ return [
36
+ title,
37
+ ` ${emptyMessage ?? 'No Agent-local skill bundles yet.'}`,
38
+ emptyMessage ? '' : ' Create one with: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>',
39
+ ].filter(Boolean).join('\n');
40
+ }
41
+ return [
42
+ `${title} (${bundles.length})`,
43
+ ` store ${path}`,
44
+ ...bundles.map((bundle) => summarizeBundle(bundle, skills)),
45
+ ].join('\n');
46
+ }
47
+
48
+ function renderBundle(bundle: AgentSkillBundleRecord, skills: readonly AgentSkillRecord[]): string {
49
+ const readiness = evaluateAgentSkillBundleReadiness(bundle, skills);
50
+ const missing = [
51
+ ...readiness.missingRequirements.map(formatAgentSkillRequirement),
52
+ ...readiness.missingSkillIds.map((skillId) => `skill:${skillId}`),
53
+ ];
54
+ return [
55
+ `Skill bundle ${bundle.name}`,
56
+ ` id ${bundle.id}`,
57
+ ` enabled: ${bundle.enabled ? 'yes' : 'no'}`,
58
+ ` readiness: ${readiness.ready ? 'ready' : 'needs setup'}`,
59
+ missing.length > 0 ? ` missing: ${missing.join(', ')}` : '',
60
+ ` review: ${formatAgentRecordReviewState(bundle.reviewState)}`,
61
+ ` origin: ${formatAgentRecordOrigin(bundle.source, bundle.provenance)}`,
62
+ ` skills: ${bundle.skillIds.join(', ')}`,
63
+ ` created: ${bundle.createdAt}`,
64
+ ` updated: ${bundle.updatedAt}`,
65
+ bundle.staleReason ? ` stale reason: ${bundle.staleReason}` : '',
66
+ '',
67
+ bundle.description,
68
+ ].filter((line): line is string => Boolean(line)).join('\n');
69
+ }
70
+
71
+ function usageBundles(): string {
72
+ return 'Usage: goodvibes-agent skills bundle [list|enabled|attention|search <query>|show <id>|create|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes]';
73
+ }
74
+
75
+ export async function handleSkillBundleCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
76
+ try {
77
+ const [sub = 'list', ...rest] = args;
78
+ const normalized = sub.toLowerCase();
79
+ const registry = skillRegistry(runtime);
80
+ const snapshot = registry.snapshot();
81
+ if (normalized === 'list' || normalized === 'ls') {
82
+ return success(runtime, 'agent.skills.bundles.list', { path: snapshot.path, bundles: snapshot.bundles }, renderBundleList('Agent skill bundles', snapshot.path, snapshot.bundles, snapshot.skills));
83
+ }
84
+ if (normalized === 'enabled') {
85
+ return success(runtime, 'agent.skills.bundles.enabled', { path: snapshot.path, bundles: snapshot.enabledBundles }, renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles, snapshot.skills));
86
+ }
87
+ if (normalized === 'attention' || normalized === 'needs-setup') {
88
+ const bundles = snapshot.bundles.filter((bundle) => !evaluateAgentSkillBundleReadiness(bundle, snapshot.skills).ready);
89
+ return success(runtime, 'agent.skills.bundles.attention', { path: snapshot.path, bundles }, renderBundleList('Agent skill bundles needing setup', snapshot.path, bundles, snapshot.skills, 'No Agent-local skill bundles need setup.'));
90
+ }
91
+ if (normalized === 'search' || normalized === 'find') {
92
+ const query = rest.join(' ').trim();
93
+ const results = registry.searchBundles(query);
94
+ return success(runtime, 'agent.skills.bundles.search', { query, results }, renderBundleList(`Agent skill bundles matching "${query}"`, snapshot.path, results, snapshot.skills));
95
+ }
96
+ if (normalized === 'show' || normalized === 'get') {
97
+ const id = rest[0];
98
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle show <id>', 2);
99
+ const bundle = registry.getBundle(id);
100
+ if (!bundle) return failure(runtime, 'skill_bundle_not_found', `Unknown Agent skill bundle ${id}`, 1);
101
+ return success(runtime, 'agent.skills.bundles.show', bundle, renderBundle(bundle, snapshot.skills));
102
+ }
103
+ if (normalized === 'create') {
104
+ const options = parseOptions(rest);
105
+ const usage = 'Usage: goodvibes-agent skills bundle create --name <name> --description <summary> --skills <id,id>';
106
+ const bundle = registry.createBundle({
107
+ name: requiredOption(options, 'name', usage),
108
+ description: requiredOption(options, 'description', usage),
109
+ skillIds: requiredOption(options, 'skills', usage).split(',').map((entry) => entry.trim()).filter(Boolean),
110
+ enabled: hasFlag(options, 'enabled'),
111
+ provenance: optionValue(options, 'provenance') ?? 'Command',
112
+ });
113
+ return success(runtime, 'agent.skills.bundles.create', bundle, [
114
+ 'Agent skill bundle created',
115
+ ` id ${bundle.id}`,
116
+ ].join('\n'));
117
+ }
118
+ if (normalized === 'update') {
119
+ const id = rest[0];
120
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle update <id> [--name ...] [--description ...] [--skills id,id]', 2);
121
+ const options = parseOptions(rest.slice(1));
122
+ const bundle = registry.updateBundle(id, {
123
+ name: optionValue(options, 'name'),
124
+ description: optionValue(options, 'description'),
125
+ skillIds: csvOption(options, 'skills'),
126
+ provenance: optionValue(options, 'provenance'),
127
+ });
128
+ return success(runtime, 'agent.skills.bundles.update', bundle, [
129
+ 'Agent skill bundle updated',
130
+ ` id ${bundle.id}`,
131
+ ].join('\n'));
132
+ }
133
+ if (normalized === 'enable' || normalized === 'disable') {
134
+ const id = rest[0];
135
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', `Usage: goodvibes-agent skills bundle ${normalized} <id>`, 2);
136
+ const bundle = registry.setBundleEnabled(id, normalized === 'enable');
137
+ return success(runtime, `agent.skills.bundles.${normalized}`, bundle, [
138
+ `Agent skill bundle ${normalized === 'enable' ? 'enabled' : 'disabled'}`,
139
+ ` id ${bundle.id}`,
140
+ ].join('\n'));
141
+ }
142
+ if (normalized === 'review') {
143
+ const id = rest[0];
144
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle review <id>', 2);
145
+ const bundle = registry.markBundleReviewed(id);
146
+ return success(runtime, 'agent.skills.bundles.review', bundle, [
147
+ 'Agent skill bundle reviewed',
148
+ ` id ${bundle.id}`,
149
+ ].join('\n'));
150
+ }
151
+ if (normalized === 'stale') {
152
+ const id = rest[0];
153
+ if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle stale <id> <reason>', 2);
154
+ const bundle = registry.markBundleStale(id, rest.slice(1).join(' '));
155
+ return success(runtime, 'agent.skills.bundles.stale', bundle, [
156
+ 'Agent skill bundle marked stale',
157
+ ` id ${bundle.id}`,
158
+ ].join('\n'));
159
+ }
160
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
161
+ const options = parseOptions(rest);
162
+ const id = options.positionals[0];
163
+ if (!id) return failure(runtime, 'invalid_skill_bundle_command', 'Usage: goodvibes-agent skills bundle delete <id> --yes', 2);
164
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill bundle ${id} without --yes.`, 2);
165
+ const bundle = registry.deleteBundle(id);
166
+ return success(runtime, 'agent.skills.bundles.delete', bundle, [
167
+ 'Agent skill bundle deleted',
168
+ ` id ${bundle.id}`,
169
+ ].join('\n'));
170
+ }
171
+ return failure(runtime, 'invalid_skill_bundle_command', usageBundles(), 2);
172
+ } catch (error) {
173
+ return errorOutput(runtime, error, 'agent.skills.bundles.error');
174
+ }
175
+ }