@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,309 @@
1
+ import { discoverSkills, type SkillRecord } from '../agent/skill-discovery.ts';
2
+ import { formatAgentRecordOrigin, formatAgentRecordReviewState } from '../agent/record-labels.ts';
3
+ import {
4
+ buildAgentSkillRequirements,
5
+ evaluateAgentSkillReadiness,
6
+ formatAgentSkillRequirement,
7
+ type AgentSkillRecord,
8
+ } from '../agent/skill-registry.ts';
9
+ import type { CliCommandOutput } from './types.ts';
10
+ import type { CliCommandRuntime } from './management.ts';
11
+ import { handleSkillBundleCommand, renderBundleList } from './skill-bundle-command.ts';
12
+ import {
13
+ csvOption,
14
+ errorOutput,
15
+ failure,
16
+ hasFlag,
17
+ optionValue,
18
+ parseOptions,
19
+ requiredOption,
20
+ shellPaths,
21
+ skillRegistry,
22
+ success,
23
+ type ParsedOptions,
24
+ } from './local-library-command-shared.ts';
25
+
26
+ function summarizeSkill(skill: AgentSkillRecord): string {
27
+ const enabled = skill.enabled ? 'enabled' : 'disabled';
28
+ const tags = skill.tags.length > 0 ? ` tags ${skill.tags.join(', ')}` : '';
29
+ const readiness = evaluateAgentSkillReadiness(skill);
30
+ const ready = readiness.ready ? 'ready' : `needs ${readiness.missing.map(formatAgentSkillRequirement).join(',')}`;
31
+ return ` ${skill.id} ${enabled} ${formatAgentRecordReviewState(skill.reviewState)} ${ready} ${skill.name} - ${skill.description}${tags}`;
32
+ }
33
+
34
+ function renderSkillList(title: string, path: string, skills: readonly AgentSkillRecord[], emptyMessage?: string): string {
35
+ if (skills.length === 0) {
36
+ return [
37
+ title,
38
+ emptyMessage ? '' : ' No local Agent skills yet.',
39
+ ` ${emptyMessage ?? 'No Agent-local skills yet.'}`,
40
+ emptyMessage ? '' : ' Create one with: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>',
41
+ ].filter(Boolean).join('\n');
42
+ }
43
+ return [
44
+ `${title} (${skills.length})`,
45
+ ` store ${path}`,
46
+ ...skills.map(summarizeSkill),
47
+ ].join('\n');
48
+ }
49
+
50
+ function summarizeDiscoveredSkill(skill: SkillRecord): string {
51
+ const description = skill.description ? ` - ${skill.description}` : '';
52
+ const dependencies = skill.dependencies.length > 0 ? ` dependencies ${skill.dependencies.join(', ')}` : '';
53
+ const includes = skill.includes.length > 0 ? ` includes ${skill.includes.join(', ')}` : '';
54
+ return [
55
+ ` ${skill.name} ${skill.origin}${description}${dependencies}${includes}`,
56
+ ` path ${skill.path}`,
57
+ ].join('\n');
58
+ }
59
+
60
+ function renderDiscoveredSkillList(skills: readonly SkillRecord[]): string {
61
+ if (skills.length === 0) {
62
+ return [
63
+ 'Discovered Agent skill files',
64
+ ' No SKILL.md or .md skill files found in Agent skill folders.',
65
+ ' Search roots: .goodvibes/skills, .goodvibes/agent/skills, ~/.goodvibes/skills, ~/.goodvibes/agent/skills',
66
+ ].join('\n');
67
+ }
68
+ return [
69
+ `Discovered Agent skill files (${skills.length})`,
70
+ ...skills.map(summarizeDiscoveredSkill),
71
+ '',
72
+ 'Import one with: goodvibes-agent skills import-discovered <name> --yes',
73
+ ].join('\n');
74
+ }
75
+
76
+ function discoveredSkillLookupValues(skill: SkillRecord): readonly string[] {
77
+ const slug = skill.name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
78
+ const basename = skill.path.split(/[\\/]/).pop()?.replace(/\.md$/i, '') ?? '';
79
+ return [skill.name, slug, skill.path, basename]
80
+ .map((value) => value.trim().toLowerCase())
81
+ .filter(Boolean);
82
+ }
83
+
84
+ function findDiscoveredSkill(skills: readonly SkillRecord[], idOrName: string): SkillRecord | null {
85
+ const lookup = idOrName.trim().toLowerCase();
86
+ if (!lookup) return null;
87
+ return skills.find((skill) => discoveredSkillLookupValues(skill).includes(lookup)) ?? null;
88
+ }
89
+
90
+ function discoveredFrontmatterList(skill: SkillRecord, key: string): readonly string[] {
91
+ const value = skill.frontmatter[key];
92
+ if (!value) return [];
93
+ return value.split(',').map((entry) => entry.trim()).filter(Boolean);
94
+ }
95
+
96
+ function discoveredFrontmatterAnyList(skill: SkillRecord, keys: readonly string[]): readonly string[] {
97
+ for (const key of keys) {
98
+ const values = discoveredFrontmatterList(skill, key);
99
+ if (values.length > 0) return values;
100
+ }
101
+ return [];
102
+ }
103
+
104
+ function renderSkill(skill: AgentSkillRecord): string {
105
+ const readiness = evaluateAgentSkillReadiness(skill);
106
+ return [
107
+ `Skill ${skill.name}`,
108
+ ` id ${skill.id}`,
109
+ ` enabled: ${skill.enabled ? 'yes' : 'no'}`,
110
+ ` readiness: ${readiness.ready ? 'ready' : 'needs setup'}`,
111
+ ` requirements: ${skill.requirements.map(formatAgentSkillRequirement).join(', ') || '(none)'}`,
112
+ readiness.missing.length > 0 ? ` missing: ${readiness.missing.map(formatAgentSkillRequirement).join(', ')}` : '',
113
+ ` review: ${formatAgentRecordReviewState(skill.reviewState)}`,
114
+ ` origin: ${formatAgentRecordOrigin(skill.source, skill.provenance)}`,
115
+ ` tags: ${skill.tags.join(', ') || '(none)'}`,
116
+ ` triggers: ${skill.triggers.join(', ') || '(manual)'}`,
117
+ ` created: ${skill.createdAt}`,
118
+ ` updated: ${skill.updatedAt}`,
119
+ skill.staleReason ? ` stale reason: ${skill.staleReason}` : '',
120
+ '',
121
+ skill.description,
122
+ '',
123
+ skill.procedure,
124
+ ].filter((line): line is string => Boolean(line)).join('\n');
125
+ }
126
+
127
+ function usageSkills(): string {
128
+ return 'Usage: goodvibes-agent skills [list|enabled|active|attention|discover|import-discovered <name> --yes|search <query>|show <id>|create [--requires-env A,B] [--requires-command gh,jq]|update <id>|enable <id>|disable <id>|review <id>|stale <id> <reason>|delete <id> --yes|bundle ...]';
129
+ }
130
+
131
+ function skillPayloadFromOptions(options: ParsedOptions): {
132
+ readonly name: string;
133
+ readonly description: string;
134
+ readonly procedure: string;
135
+ readonly tags: readonly string[] | undefined;
136
+ readonly triggers: readonly string[] | undefined;
137
+ readonly requirements: ReturnType<typeof buildAgentSkillRequirements>;
138
+ readonly enabled: boolean | undefined;
139
+ readonly provenance: string;
140
+ } {
141
+ const usage = 'Usage: goodvibes-agent skills create --name <name> --description <summary> --procedure <steps>';
142
+ return {
143
+ name: requiredOption(options, 'name', usage),
144
+ description: requiredOption(options, 'description', usage),
145
+ procedure: requiredOption(options, 'procedure', usage),
146
+ tags: csvOption(options, 'tags'),
147
+ triggers: csvOption(options, 'triggers'),
148
+ requirements: buildAgentSkillRequirements({
149
+ env: csvOption(options, 'requires-env'),
150
+ commands: csvOption(options, 'requires-command') ?? csvOption(options, 'requires-commands'),
151
+ }),
152
+ enabled: hasFlag(options, 'enabled') ? true : undefined,
153
+ provenance: optionValue(options, 'provenance') ?? 'Command',
154
+ };
155
+ }
156
+
157
+ export async function handleSkillsCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
158
+ try {
159
+ const [sub = 'list', ...rest] = runtime.cli.commandArgs;
160
+ const normalized = sub.toLowerCase();
161
+ if (normalized === 'bundle' || normalized === 'bundles') return handleSkillBundleCommand(runtime, rest);
162
+ const registry = skillRegistry(runtime);
163
+ const snapshot = registry.snapshot();
164
+ if (normalized === 'list' || normalized === 'ls') {
165
+ return success(runtime, 'agent.skills.list', { path: snapshot.path, skills: snapshot.skills, enabledCount: snapshot.enabledSkills.length }, renderSkillList('Agent skills', snapshot.path, snapshot.skills));
166
+ }
167
+ if (normalized === 'enabled') {
168
+ return success(runtime, 'agent.skills.enabled', { path: snapshot.path, skills: snapshot.enabledSkills }, renderSkillList('Enabled Agent skills', snapshot.path, snapshot.enabledSkills));
169
+ }
170
+ if (normalized === 'active') {
171
+ return success(runtime, 'agent.skills.active', { path: snapshot.path, skills: snapshot.activeSkills, bundles: snapshot.enabledBundles }, [
172
+ renderSkillList('Active Agent skills', snapshot.path, snapshot.activeSkills),
173
+ snapshot.enabledBundles.length > 0 ? renderBundleList('Enabled Agent skill bundles', snapshot.path, snapshot.enabledBundles, snapshot.skills) : '',
174
+ ].filter(Boolean).join('\n\n'));
175
+ }
176
+ if (normalized === 'attention' || normalized === 'needs-setup') {
177
+ const skills = snapshot.skills.filter((skill) => !evaluateAgentSkillReadiness(skill).ready);
178
+ return success(runtime, 'agent.skills.attention', { path: snapshot.path, skills }, renderSkillList('Agent skills needing setup', snapshot.path, skills, 'No Agent-local skills need setup.'));
179
+ }
180
+ if (normalized === 'discover') {
181
+ const discovered = await discoverSkills(shellPaths(runtime));
182
+ return success(runtime, 'agent.skills.discover', { skills: discovered }, renderDiscoveredSkillList(discovered));
183
+ }
184
+ if (normalized === 'import-discovered' || normalized === 'import-skill') {
185
+ const options = parseOptions(rest);
186
+ const name = options.positionals.join(' ').trim();
187
+ if (!name) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills import-discovered <name> [--enabled] --yes', 2);
188
+ const discovered = findDiscoveredSkill(await discoverSkills(shellPaths(runtime)), name);
189
+ if (!discovered) {
190
+ return failure(runtime, 'skill_discovery_not_found', `Unknown discovered Agent skill ${name}\nRun goodvibes-agent skills discover to inspect available skill files.`, 1);
191
+ }
192
+ if (!hasFlag(options, 'yes')) {
193
+ return success(runtime, 'agent.skills.import_discovered.preview', { skill: discovered }, [
194
+ 'Agent skill import preview',
195
+ ` name ${discovered.name}`,
196
+ ` origin ${discovered.origin}`,
197
+ ` path ${discovered.path}`,
198
+ ` description ${discovered.description || '(none)'}`,
199
+ ` procedure characters ${discovered.body.length}`,
200
+ ' next rerun with --yes to import into the Agent-local skill registry',
201
+ ].join('\n'));
202
+ }
203
+ const skill = registry.create({
204
+ name: discovered.name,
205
+ description: discovered.description || `Imported skill from ${discovered.origin} skill file.`,
206
+ procedure: discovered.body,
207
+ tags: discoveredFrontmatterList(discovered, 'tags'),
208
+ triggers: discoveredFrontmatterList(discovered, 'triggers'),
209
+ requirements: buildAgentSkillRequirements({
210
+ env: discoveredFrontmatterAnyList(discovered, ['requiresEnv', 'requires-env', 'requires_env']),
211
+ commands: discoveredFrontmatterAnyList(discovered, ['requiresCommands', 'requires-commands', 'requires_commands', 'commands']),
212
+ }),
213
+ enabled: hasFlag(options, 'enabled'),
214
+ source: 'imported',
215
+ provenance: `discovered:${discovered.origin}:${discovered.path}`,
216
+ });
217
+ return success(runtime, 'agent.skills.import_discovered', skill, [
218
+ `Imported Agent skill ${skill.id}: ${skill.name}${skill.enabled ? ' (enabled)' : ''}`,
219
+ ` name ${skill.name}`,
220
+ ` enabled ${skill.enabled ? 'yes' : 'no'}`,
221
+ ].join('\n'));
222
+ }
223
+ if (normalized === 'search' || normalized === 'find') {
224
+ const query = rest.join(' ').trim();
225
+ const results = registry.search(query);
226
+ return success(runtime, 'agent.skills.search', { query, results }, renderSkillList(`Agent skills matching "${query}"`, snapshot.path, results));
227
+ }
228
+ if (normalized === 'show' || normalized === 'get') {
229
+ const id = rest[0];
230
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills show <id>', 2);
231
+ const skill = registry.get(id);
232
+ if (!skill) return failure(runtime, 'skill_not_found', `Unknown Agent skill ${id}`, 1);
233
+ return success(runtime, 'agent.skills.show', skill, renderSkill(skill));
234
+ }
235
+ if (normalized === 'create') {
236
+ const skill = registry.create(skillPayloadFromOptions(parseOptions(rest)));
237
+ return success(runtime, 'agent.skills.create', skill, [
238
+ 'Agent skill created',
239
+ ` id ${skill.id}`,
240
+ skill.enabled ? ' (enabled)' : '',
241
+ ` enabled ${skill.enabled ? 'yes' : 'no'}`,
242
+ ].filter(Boolean).join('\n'));
243
+ }
244
+ if (normalized === 'update') {
245
+ const id = rest[0];
246
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills update <id> [--name ...] [--description ...] [--procedure ...]', 2);
247
+ const options = parseOptions(rest.slice(1));
248
+ const skill = registry.update(id, {
249
+ name: optionValue(options, 'name'),
250
+ description: optionValue(options, 'description'),
251
+ procedure: optionValue(options, 'procedure'),
252
+ tags: csvOption(options, 'tags'),
253
+ triggers: csvOption(options, 'triggers'),
254
+ requirements: options.values.has('requires-env') || options.values.has('requires-command') || options.values.has('requires-commands')
255
+ ? buildAgentSkillRequirements({
256
+ env: csvOption(options, 'requires-env'),
257
+ commands: csvOption(options, 'requires-command') ?? csvOption(options, 'requires-commands'),
258
+ })
259
+ : undefined,
260
+ provenance: optionValue(options, 'provenance'),
261
+ });
262
+ return success(runtime, 'agent.skills.update', skill, [
263
+ 'Agent skill updated',
264
+ ` id ${skill.id}`,
265
+ ].join('\n'));
266
+ }
267
+ if (normalized === 'enable' || normalized === 'disable') {
268
+ const id = rest[0];
269
+ if (!id) return failure(runtime, 'invalid_skill_command', `Usage: goodvibes-agent skills ${normalized} <id>`, 2);
270
+ const skill = registry.setEnabled(id, normalized === 'enable');
271
+ return success(runtime, `agent.skills.${normalized}`, skill, [
272
+ `Agent skill ${normalized === 'enable' ? 'enabled' : 'disabled'}`,
273
+ ` id ${skill.id}`,
274
+ ].join('\n'));
275
+ }
276
+ if (normalized === 'review') {
277
+ const id = rest[0];
278
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills review <id>', 2);
279
+ const skill = registry.markReviewed(id);
280
+ return success(runtime, 'agent.skills.review', skill, [
281
+ 'Agent skill reviewed',
282
+ ` id ${skill.id}`,
283
+ ].join('\n'));
284
+ }
285
+ if (normalized === 'stale') {
286
+ const id = rest[0];
287
+ if (!id || rest.length < 2) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills stale <id> <reason>', 2);
288
+ const skill = registry.markStale(id, rest.slice(1).join(' '));
289
+ return success(runtime, 'agent.skills.stale', skill, [
290
+ 'Agent skill marked stale',
291
+ ` id ${skill.id}`,
292
+ ].join('\n'));
293
+ }
294
+ if (normalized === 'delete' || normalized === 'remove' || normalized === 'rm') {
295
+ const options = parseOptions(rest);
296
+ const id = options.positionals[0];
297
+ if (!id) return failure(runtime, 'invalid_skill_command', 'Usage: goodvibes-agent skills delete <id> --yes', 2);
298
+ if (!hasFlag(options, 'yes')) return failure(runtime, 'confirmation_required', `Refusing to delete Agent skill ${id} without --yes.`, 2);
299
+ const skill = registry.deleteSkill(id);
300
+ return success(runtime, 'agent.skills.delete', skill, [
301
+ `Agent skill deleted: ${id}`,
302
+ ` id ${skill.id}`,
303
+ ].join('\n'));
304
+ }
305
+ return failure(runtime, 'invalid_skill_command', usageSkills(), 2);
306
+ } catch (error) {
307
+ return errorOutput(runtime, error, 'agent.skills.error');
308
+ }
309
+ }
package/src/cli/status.ts CHANGED
@@ -7,6 +7,7 @@ import type { CliExternalRuntimeSnapshot } from './external-runtime.ts';
7
7
  import { getProviderIdFromModel } from '../config/provider-model.ts';
8
8
  import { formatAgentRecordSource } from '../agent/record-labels.ts';
9
9
  import { formatAgentKnowledgeFailureKind } from './agent-knowledge-format.ts';
10
+ import { computeApprovalPosture, type ApprovalPosture } from '../permissions/approval-posture.ts';
10
11
 
11
12
  export interface CliStatusOptions {
12
13
  readonly configManager: Pick<ConfigManager, 'get'>;
@@ -51,6 +52,8 @@ export interface CliStatusSnapshot {
51
52
  readonly auth: {
52
53
  readonly permissionMode: unknown;
53
54
  readonly permissionLabel: string;
55
+ readonly autoApprove: boolean;
56
+ readonly bypassesPrompts: boolean;
54
57
  readonly secretPolicy: unknown;
55
58
  readonly secretPolicyLabel: string;
56
59
  readonly runtimeAuthSignal: CliAuthStatus | null;
@@ -79,11 +82,27 @@ function yesNo(value: unknown): string {
79
82
  return value === true ? 'yes' : 'no';
80
83
  }
81
84
 
82
- function permissionModeLabel(mode: unknown): string {
83
- if (mode === 'prompt') return 'Ask before powerful actions';
84
- if (mode === 'allow-all') return 'Allow everything';
85
- if (mode === 'custom') return 'Custom rules';
86
- return String(mode ?? 'unknown');
85
+ /** permissions.tools.* keys mirrors PermissionsToolConfig in the SDK config schema. */
86
+ const PERMISSION_TOOL_KEYS = [
87
+ 'read', 'write', 'edit', 'exec', 'find', 'fetch', 'analyze',
88
+ 'inspect', 'agent', 'state', 'workflow', 'registry', 'delegate', 'mcp',
89
+ ] as const;
90
+
91
+ /**
92
+ * Reads the effective approval posture the SAME way the permission gate
93
+ * decides it (behavior.autoApprove first, then permissions.mode), so the
94
+ * displayed label can never disagree with what actually happens when a tool
95
+ * runs. Do not compute a posture label from permissions.mode alone here —
96
+ * that was the original bug (this surface ignored behavior.autoApprove).
97
+ */
98
+ function readApprovalPosture(config: Pick<ConfigManager, 'get'>): ApprovalPosture {
99
+ const customTools: Record<string, unknown> = {};
100
+ for (const key of PERMISSION_TOOL_KEYS) customTools[key] = config.get(`permissions.tools.${key}`);
101
+ return computeApprovalPosture({
102
+ autoApprove: config.get('behavior.autoApprove') === true,
103
+ mode: config.get('permissions.mode'),
104
+ customTools,
105
+ });
87
106
  }
88
107
 
89
108
  function secretPolicyLabel(policy: unknown): string {
@@ -97,6 +116,7 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
97
116
  const config = options.configManager;
98
117
  const permissionMode = config.get('permissions.mode');
99
118
  const secretPolicy = config.get('storage.secretPolicy');
119
+ const posture = readApprovalPosture(config);
100
120
  const marker = options.onboardingMarkers?.effective;
101
121
 
102
122
  const findings: CliDoctorFinding[] = [];
@@ -176,6 +196,18 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
176
196
  });
177
197
  }
178
198
 
199
+ if (posture.autoApprove) {
200
+ findings.push({
201
+ id: 'auto-approve-enabled',
202
+ area: 'security',
203
+ severity: 'risk',
204
+ summary: 'Auto-approve is on: tool calls never prompt.',
205
+ cause: 'behavior.autoApprove is true.',
206
+ impact: 'Every tool call — including powerful write, edit, network, and execution actions — runs without a Human-in-the-Loop (HITL) approval prompt, regardless of permissions.mode or any custom per-tool rule.',
207
+ action: 'Disable behavior.autoApprove unless this is an intentionally trusted environment.',
208
+ });
209
+ }
210
+
179
211
  if (permissionMode === 'allow-all') {
180
212
  findings.push({
181
213
  id: 'allow-all-permissions',
@@ -210,6 +242,7 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
210
242
  const webBinding = resolveRuntimeEndpointBinding(config, 'web');
211
243
  const marker = options.onboardingMarkers?.effective;
212
244
  const findings = buildCliDoctorFindings(options);
245
+ const posture = readApprovalPosture(config);
213
246
  return {
214
247
  title: options.doctor ? 'GoodVibes Agent doctor' : 'GoodVibes Agent status',
215
248
  workingDirectory: options.workingDirectory,
@@ -221,7 +254,9 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
221
254
  },
222
255
  auth: {
223
256
  permissionMode: config.get('permissions.mode'),
224
- permissionLabel: permissionModeLabel(config.get('permissions.mode')),
257
+ permissionLabel: posture.label,
258
+ autoApprove: posture.autoApprove,
259
+ bypassesPrompts: posture.bypassesPrompts,
225
260
  secretPolicy: config.get('storage.secretPolicy'),
226
261
  secretPolicyLabel: secretPolicyLabel(config.get('storage.secretPolicy')),
227
262
  runtimeAuthSignal: options.auth ?? null,
@@ -267,7 +302,8 @@ export function renderCliStatus(options: CliStatusOptions): string {
267
302
  ` reasoning ${String(config.get('provider.reasoningEffort'))}`,
268
303
  '',
269
304
  'Auth',
270
- ` permissions: ${permissionModeLabel(config.get('permissions.mode'))} (${String(config.get('permissions.mode'))})`,
305
+ ` permissions: ${snapshot.auth.permissionLabel} (${String(config.get('permissions.mode'))})`,
306
+ ` autoApprove: ${yesNo(snapshot.auth.autoApprove)} (behavior.autoApprove)`,
271
307
  ` secretPolicy: ${secretPolicyLabel(config.get('storage.secretPolicy'))} (${String(config.get('storage.secretPolicy'))})`,
272
308
  options.auth
273
309
  ? ` local auth store ${options.auth.userStorePresent ? 'present' : 'missing'} (${options.auth.userStorePath})`
@@ -2,6 +2,8 @@ import type { CommandContext, CommandRegistry } from '../input/command-registry.
2
2
  import type { InputHandler } from '../input/handler.ts';
3
3
  import { readOnboardingCompletionMarker } from '../runtime/onboarding/index.ts';
4
4
  import type { GoodVibesCliParseResult } from './types.ts';
5
+ import { checkRecoveryFile, readLastSessionPointer } from '@/runtime/index.ts';
6
+ import { resolveResumableSession, surfaceResumeRelaunchNotice } from './resume-relaunch-notice.ts';
5
7
 
6
8
  export type InteractiveTerminalCheckInput = {
7
9
  readonly binary: string;
@@ -95,7 +97,7 @@ export function applyInitialTuiCliState(options: {
95
97
  readonly input: InputHandler;
96
98
  readonly commandRegistry: CommandRegistry;
97
99
  readonly commandContext: CommandContext;
98
- readonly shellPaths: Parameters<typeof readOnboardingCompletionMarker>[0];
100
+ readonly shellPaths: Parameters<typeof readOnboardingCompletionMarker>[0] & { readonly homeDirectory: string };
99
101
  readonly render: () => void;
100
102
  }): void {
101
103
  const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
@@ -110,6 +112,27 @@ export function applyInitialTuiCliState(options: {
110
112
  }
111
113
  } else if (!onboardingCompletionMarker.payload) {
112
114
  input.openAgentWorkspace(commandContext, 'setup', 'ONBOARDING');
115
+ } else {
116
+ // Normal relaunch: onboarding is done and the user didn't ask for
117
+ // onboarding or an explicit `sessions resume`. Surface an honest,
118
+ // non-blocking resume affordance instead of silently starting fresh
119
+ // (the W4-A4 dogfood finding) — never auto-resume, declining is
120
+ // frictionless (just start typing).
121
+ surfaceResumeRelaunchNotice({
122
+ getLastSessionPointer: () => readLastSessionPointer({
123
+ workingDirectory: shellPaths.workingDirectory,
124
+ homeDirectory: shellPaths.homeDirectory,
125
+ }),
126
+ isRecoveryPending: () => Boolean(checkRecoveryFile({
127
+ workingDirectory: shellPaths.workingDirectory,
128
+ homeDirectory: shellPaths.homeDirectory,
129
+ })),
130
+ findSession: (sessionId) => {
131
+ const sessionManager = commandContext.session?.sessionManager;
132
+ return sessionManager ? resolveResumableSession(sessionManager, sessionId) : null;
133
+ },
134
+ print: (text) => commandContext.print(text),
135
+ });
113
136
  }
114
137
 
115
138
  if (seededPrompt) {
@@ -12,7 +12,6 @@ const EXTERNAL_HOST_SETTING_PREFIXES = [
12
12
  ] as const;
13
13
 
14
14
  const EXTERNAL_HOST_SETTING_KEYS = new Set<string>([
15
- 'danger.daemon',
16
15
  'danger.httpListener',
17
16
  ]);
18
17
 
@@ -5,6 +5,10 @@ import type { SecretScope, SecretStorageMedium } from './secrets.ts';
5
5
  export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
6
6
  // email section (app-layer extension, key is string-cast for ConfigKey compatibility)
7
7
  'email.passwordRef' as unknown as ConfigKey,
8
+ // calendar OAuth advanced overrides — only the client SECRET is secret-backed; the
9
+ // client id is not a secret (RFC 8252) and stays a plain config value.
10
+ 'calendar.google.clientSecretRef' as unknown as ConfigKey,
11
+ 'calendar.microsoft.clientSecretRef' as unknown as ConfigKey,
8
12
  'surfaces.slack.signingSecret',
9
13
  'surfaces.slack.botToken',
10
14
  'surfaces.slack.appToken',
@@ -11,7 +11,13 @@ import { LAYOUT } from '../renderer/layout.ts';
11
11
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
12
12
  import { renderConversationCollapsedFragment, renderConversationEventLine } from '../renderer/conversation-surface.ts';
13
13
  import { GLYPHS } from '../renderer/ui-primitives.ts';
14
+ import { activeTheme } from '../renderer/theme.ts';
14
15
  import type { BlockMeta, ConversationMessageSnapshot } from './conversation';
16
+
17
+ // Transcript tokens are read live per render (const T = activeTheme() at the top
18
+ // of each render function that styles content) so a dark→light repaint
19
+ // re-resolves with no module reload. Dark values are byte-identical to the
20
+ // agent's prior static reads.
15
21
  import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
16
22
  import { extractUserDisplayText } from '@pellux/goodvibes-sdk/platform/core';
17
23
 
@@ -39,9 +45,10 @@ export function renderConversationUserMessage(
39
45
  message: Extract<Message, { role: 'user' }>,
40
46
  width: number,
41
47
  ): void {
48
+ const T = activeTheme();
42
49
  const displayText = extractUserDisplayText(message.content);
43
50
  if (message.cancelled) {
44
- context.history.addLines(UIFactory.createMessageBar(width, displayText, '#3a1a1a', '196', ' x ', true));
51
+ context.history.addLines(UIFactory.createMessageBar(width, displayText, T.errorBarBg, '196', ' x ', true));
45
52
  return;
46
53
  }
47
54
  context.history.addLines(UIFactory.createMessageBar(width, displayText));
@@ -55,22 +62,23 @@ export function renderConversationAssistantMessage(
55
62
  collapseThreshold: number,
56
63
  msgIdx: number,
57
64
  ): void {
65
+ const T = activeTheme();
58
66
  const assistantHeaderDetails = [];
59
67
  if (message.model) {
60
- assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg: '#94a3b8', dim: true });
68
+ assistantHeaderDetails.push({ text: ` ${message.model}${message.provider ? ` (${message.provider})` : ''} `, fg: T.modelNameDim, dim: true });
61
69
  }
62
70
  if (message.toolCalls && message.toolCalls.length > 0) {
63
- assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} tools:${message.toolCalls.length} `, fg: '#38bdf8' });
71
+ assistantHeaderDetails.push({ text: ` ${GLYPHS.status.pending} tools:${message.toolCalls.length} `, fg: T.toolAccent });
64
72
  }
65
73
  if (message.reasoningContent || message.reasoningSummary) {
66
- assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg: '#a855f7', dim: true });
74
+ assistantHeaderDetails.push({ text: ` ${GLYPHS.status.active} reasoning `, fg: T.reasoningAccent, dim: true });
67
75
  }
68
76
  if (assistantHeaderDetails.length > 0) {
69
77
  context.history.addLine(renderConversationEventLine(width, {
70
78
  marker: GLYPHS.status.active,
71
- markerFg: '#22d3ee',
79
+ markerFg: T.assistantHeader,
72
80
  label: 'assistant',
73
- labelFg: '#22d3ee',
81
+ labelFg: T.assistantHeader,
74
82
  detailFg: '244',
75
83
  }, assistantHeaderDetails));
76
84
  }
@@ -198,6 +206,7 @@ export function renderConversationToolMessage(
198
206
  width: number,
199
207
  msgIdx: number,
200
208
  ): void {
209
+ const T = activeTheme();
201
210
  const collapseKey = `msg_${msgIdx}`;
202
211
  const blockIdx = context.blockRegistry.length;
203
212
  const startLine = context.history.getLineCount();
@@ -221,13 +230,13 @@ export function renderConversationToolMessage(
221
230
 
222
231
  context.history.addLine(renderConversationEventLine(width, {
223
232
  marker: blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active,
224
- markerFg: blockType === 'diff' ? '#f59e0b' : '#38bdf8',
233
+ markerFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
225
234
  label: blockType === 'diff' ? 'diff' : 'tool result',
226
- labelFg: blockType === 'diff' ? '#f59e0b' : '#38bdf8',
235
+ labelFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
227
236
  detailFg: '244',
228
237
  }, [
229
238
  ...(message.toolName
230
- ? [{ text: ` ${friendlyToolLabel(message.toolName)} `, fg: '#e2e8f0' as const }]
239
+ ? [{ text: ` ${friendlyToolLabel(message.toolName)} `, fg: T.toolNameFg }]
231
240
  : [{ text: ` ${summarizeCallId(message.callId || 'standalone')} `, fg: '244' as const, dim: true }]),
232
241
  { text: ` ${isCollapsed ? GLYPHS.navigation.collapsed : GLYPHS.navigation.expanded} ${lineCount} line${lineCount === 1 ? '' : 's'} `, fg: '244', dim: true },
233
242
  ]));
@@ -242,9 +251,9 @@ export function renderConversationToolMessage(
242
251
  : preview;
243
252
  const rendered = renderConversationCollapsedFragment(collapsedText, width, {
244
253
  prefix: blockType === 'diff' ? ` ${GLYPHS.status.dualPane} ` : ` ${GLYPHS.navigation.collapsed} `,
245
- prefixFg: blockType === 'diff' ? '#f59e0b' : '#38bdf8',
254
+ prefixFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
246
255
  text: '244',
247
- bodyBg: '#1a1a1a',
256
+ bodyBg: T.collapsedBodyBg,
248
257
  dim: true,
249
258
  });
250
259
  context.history.addLines(rendered);
@@ -0,0 +1,41 @@
1
+ /**
2
+ * focus-tracker — tracks whether the terminal window currently has OS-level
3
+ * input focus, using the terminal focus-reporting protocol (DECSET ?1004h,
4
+ * enabled at terminal setup in main.ts). The SDK's InputTokenizer already
5
+ * parses the resulting `\x1b[I` (focus-in) / `\x1b[O` (focus-out) sequences
6
+ * into `{ type: 'focus', action }` tokens (platform/core/tokenizer.ts); this
7
+ * module is the single place those tokens land (consumed in
8
+ * src/input/handler-feed.ts's feedInputTokens()).
9
+ *
10
+ * Honest degradation: a terminal that does not implement focus reporting
11
+ * (or a multiplexer that swallows the sequences) never sends focus tokens.
12
+ * In that case `isFocused()` stays `null` forever — "unknown", never a
13
+ * guessed `true`/`false`. Callers that gate a user-facing behavior on focus
14
+ * (e.g. the unfocused-alert notifiers in this directory) must treat `null`
15
+ * the same as "not focused" per the fallback rule: alerts fire when the
16
+ * terminal is definitely unfocused OR when focus state was never observed.
17
+ */
18
+ export class FocusTracker {
19
+ private focused: boolean | null = null;
20
+
21
+ /** True/false once at least one focus token has arrived; null if none ever has. */
22
+ isFocused(): boolean | null {
23
+ return this.focused;
24
+ }
25
+
26
+ /** Called from the input pipeline on every focus token. */
27
+ setFocused(value: boolean): void {
28
+ this.focused = value;
29
+ }
30
+
31
+ /**
32
+ * The alert-gating rule shared by every unfocused-alert notifier: fire
33
+ * when the terminal is known to be unfocused, or when focus was never
34
+ * observed (honest fallback — never silently suppress on an unknown
35
+ * terminal). Only `isFocused() === true` (a real, observed focus-in with
36
+ * no observed focus-out since) suppresses an alert.
37
+ */
38
+ shouldAlertWhenUnfocused(): boolean {
39
+ return this.focused !== true;
40
+ }
41
+ }