@pellux/goodvibes-agent 0.1.117 → 1.0.0

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 (253) hide show
  1. package/CHANGELOG.md +13 -4
  2. package/README.md +35 -14
  3. package/bin/goodvibes-agent.ts +16 -2
  4. package/dist/package/main.js +176073 -170980
  5. package/docs/README.md +11 -5
  6. package/docs/channels-remote-and-api.md +50 -0
  7. package/docs/connected-host.md +3 -3
  8. package/docs/getting-started.md +29 -15
  9. package/docs/knowledge-artifacts-and-multimodal.md +91 -0
  10. package/docs/project-planning.md +79 -0
  11. package/docs/providers-and-routing.md +46 -0
  12. package/docs/release-and-publishing.md +44 -9
  13. package/docs/tools-and-commands.md +123 -0
  14. package/docs/voice-and-live-tts.md +51 -0
  15. package/package.json +2 -5
  16. package/src/agent/channel-delivery.ts +201 -0
  17. package/src/agent/media-generation.ts +159 -0
  18. package/src/agent/memory-prompt.ts +0 -1
  19. package/src/agent/note-registry.ts +329 -0
  20. package/src/agent/operator-actions.ts +343 -0
  21. package/src/agent/persona-registry.ts +15 -14
  22. package/src/agent/record-labels.ts +107 -0
  23. package/src/agent/reminder-schedule-format.ts +33 -24
  24. package/src/agent/reminder-schedule.ts +26 -25
  25. package/src/agent/routine-registry.ts +13 -12
  26. package/src/agent/routine-schedule-args.ts +2 -1
  27. package/src/agent/routine-schedule-format.ts +77 -53
  28. package/src/agent/routine-schedule-promotion.ts +34 -32
  29. package/src/agent/routine-schedule-receipts.ts +28 -26
  30. package/src/agent/runtime-profile-starters.ts +2 -2
  31. package/src/agent/runtime-profile.ts +18 -17
  32. package/src/agent/skill-registry.ts +25 -24
  33. package/src/cli/agent-knowledge-args.ts +5 -1
  34. package/src/cli/agent-knowledge-command.ts +39 -33
  35. package/src/cli/agent-knowledge-format.ts +80 -67
  36. package/src/cli/agent-knowledge-methods.ts +1 -1
  37. package/src/cli/agent-knowledge-runtime.ts +32 -26
  38. package/src/cli/bundle-command.ts +13 -8
  39. package/src/cli/config-overrides.ts +37 -36
  40. package/src/cli/external-runtime.ts +10 -4
  41. package/src/cli/help.ts +29 -11
  42. package/src/cli/local-library-command.ts +134 -68
  43. package/src/cli/management-commands.ts +98 -62
  44. package/src/cli/management.ts +52 -26
  45. package/src/cli/memory-command.ts +66 -32
  46. package/src/cli/parser.ts +37 -24
  47. package/src/cli/profiles-command.ts +52 -35
  48. package/src/cli/provider-auth-routes.ts +2 -1
  49. package/src/cli/routines-command.ts +44 -36
  50. package/src/cli/service-posture.ts +6 -6
  51. package/src/cli/status.ts +46 -121
  52. package/src/core/conversation-message-snapshot.ts +131 -0
  53. package/src/input/agent-workspace-activation.ts +33 -7
  54. package/src/input/agent-workspace-basic-command-editor-submission.ts +7 -3
  55. package/src/input/agent-workspace-basic-command-editors.ts +30 -10
  56. package/src/input/agent-workspace-categories.ts +276 -64
  57. package/src/input/agent-workspace-channel-command-editor-submission.ts +34 -0
  58. package/src/input/agent-workspace-channel-command-editors.ts +23 -5
  59. package/src/input/agent-workspace-channels.ts +35 -2
  60. package/src/input/agent-workspace-command-editor.ts +18 -2
  61. package/src/input/agent-workspace-config-reader.ts +16 -0
  62. package/src/input/agent-workspace-delegation-editor-submission.ts +1 -1
  63. package/src/input/agent-workspace-editors.ts +140 -2
  64. package/src/input/agent-workspace-knowledge-query-editor.ts +1 -1
  65. package/src/input/agent-workspace-learned-behavior.ts +2 -2
  66. package/src/input/agent-workspace-library-command-editor-submission.ts +1 -1
  67. package/src/input/agent-workspace-library-command-editors.ts +2 -2
  68. package/src/input/agent-workspace-local-operations.ts +218 -0
  69. package/src/input/agent-workspace-local-selection.ts +75 -0
  70. package/src/input/agent-workspace-media-command-editor-submission.ts +62 -0
  71. package/src/input/agent-workspace-media-command-editors.ts +27 -0
  72. package/src/input/agent-workspace-memory-command-editors.ts +1 -1
  73. package/src/input/agent-workspace-memory-editor.ts +2 -2
  74. package/src/input/agent-workspace-navigation.ts +38 -2
  75. package/src/input/agent-workspace-notify-editor-submission.ts +16 -2
  76. package/src/input/agent-workspace-operations-command-editor-submission.ts +63 -1
  77. package/src/input/agent-workspace-operations-command-editors.ts +80 -3
  78. package/src/input/agent-workspace-panel-route.ts +43 -0
  79. package/src/input/agent-workspace-reminder-schedule-editor.ts +1 -1
  80. package/src/input/agent-workspace-routine-schedule-editor.ts +2 -2
  81. package/src/input/agent-workspace-search.ts +169 -0
  82. package/src/input/agent-workspace-setup.ts +22 -11
  83. package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +6 -6
  84. package/src/input/agent-workspace-skill-bundle-command-editors.ts +10 -10
  85. package/src/input/agent-workspace-snapshot.ts +112 -13
  86. package/src/input/agent-workspace-task-command-editors.ts +4 -4
  87. package/src/input/agent-workspace-token.ts +18 -2
  88. package/src/input/agent-workspace-types.ts +92 -4
  89. package/src/input/agent-workspace-voice-media.ts +3 -6
  90. package/src/input/agent-workspace-web-research-editor.ts +104 -0
  91. package/src/input/agent-workspace.ts +136 -208
  92. package/src/input/command-registry.ts +4 -1
  93. package/src/input/commands/agent-runtime-profile-runtime.ts +45 -41
  94. package/src/input/commands/agent-skills-runtime.ts +83 -70
  95. package/src/input/commands/agent-workspace-runtime.ts +23 -7
  96. package/src/input/commands/brief-runtime.ts +25 -24
  97. package/src/input/commands/channels-runtime.ts +145 -31
  98. package/src/input/commands/delegation-runtime.ts +29 -23
  99. package/src/input/commands/experience-runtime.ts +6 -4
  100. package/src/input/commands/guidance-runtime.ts +4 -4
  101. package/src/input/commands/health-runtime.ts +140 -115
  102. package/src/input/commands/knowledge.ts +57 -56
  103. package/src/input/commands/local-provider-runtime.ts +36 -18
  104. package/src/input/commands/local-runtime.ts +138 -16
  105. package/src/input/commands/local-setup-review.ts +7 -7
  106. package/src/input/commands/mcp-runtime.ts +56 -35
  107. package/src/input/commands/notify-runtime.ts +38 -9
  108. package/src/input/commands/operator-actions-runtime.ts +138 -0
  109. package/src/input/commands/operator-runtime.ts +6 -17
  110. package/src/input/commands/personas-runtime.ts +45 -30
  111. package/src/input/commands/planning-runtime.ts +1 -1
  112. package/src/input/commands/platform-access-runtime.ts +29 -29
  113. package/src/input/commands/provider-accounts-runtime.ts +60 -39
  114. package/src/input/commands/qrcode-runtime.ts +45 -6
  115. package/src/input/commands/recall-bundle.ts +11 -11
  116. package/src/input/commands/recall-capture.ts +13 -11
  117. package/src/input/commands/recall-query.ts +29 -21
  118. package/src/input/commands/recall-review.ts +2 -1
  119. package/src/input/commands/routines-runtime.ts +59 -43
  120. package/src/input/commands/schedule-runtime.ts +33 -20
  121. package/src/input/commands/security-runtime.ts +4 -4
  122. package/src/input/commands/session-content.ts +80 -78
  123. package/src/input/commands/session-workflow.ts +132 -93
  124. package/src/input/commands/session.ts +3 -174
  125. package/src/input/commands/shell-core.ts +32 -17
  126. package/src/input/commands/subscription-runtime.ts +53 -179
  127. package/src/input/commands/tasks-runtime.ts +20 -20
  128. package/src/input/commands/work-plan-runtime.ts +33 -8
  129. package/src/input/commands.ts +2 -2
  130. package/src/input/feed-context-factory.ts +2 -1
  131. package/src/input/file-picker.ts +3 -2
  132. package/src/input/handler-command-route.ts +4 -7
  133. package/src/input/handler-content-actions.ts +89 -1
  134. package/src/input/handler-feed-routes.ts +19 -12
  135. package/src/input/handler-feed.ts +6 -3
  136. package/src/input/handler-interactions.ts +7 -5
  137. package/src/input/handler-modal-stack.ts +3 -3
  138. package/src/input/handler-onboarding.ts +24 -80
  139. package/src/input/handler-shortcuts.ts +15 -4
  140. package/src/input/handler.ts +47 -17
  141. package/src/input/input-history.ts +5 -6
  142. package/src/input/keybindings.ts +22 -11
  143. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -0
  144. package/src/input/onboarding/onboarding-wizard-constants.ts +1 -0
  145. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +147 -57
  146. package/src/input/onboarding/onboarding-wizard-steps.ts +46 -29
  147. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  148. package/src/input/onboarding/onboarding-wizard.ts +68 -0
  149. package/src/input/profile-picker-modal.ts +31 -12
  150. package/src/input/session-picker-modal.ts +21 -4
  151. package/src/input/settings-modal-behavior.ts +0 -3
  152. package/src/input/settings-modal-subscriptions.ts +3 -3
  153. package/src/input/settings-modal-types.ts +2 -13
  154. package/src/input/settings-modal.ts +6 -52
  155. package/src/input/submission-intent.ts +0 -1
  156. package/src/input/submission-router.ts +3 -3
  157. package/src/main.ts +18 -8
  158. package/src/panels/approval-panel.ts +8 -8
  159. package/src/panels/automation-control-panel.ts +2 -2
  160. package/src/panels/base-panel.ts +1 -1
  161. package/src/panels/builtin/agent.ts +1 -1
  162. package/src/panels/builtin/operations.ts +1 -10
  163. package/src/panels/builtin/session.ts +9 -9
  164. package/src/panels/builtin/shared.ts +2 -2
  165. package/src/panels/cost-tracker-panel.ts +1 -1
  166. package/src/panels/docs-panel.ts +5 -3
  167. package/src/panels/index.ts +0 -1
  168. package/src/panels/knowledge-panel.ts +6 -5
  169. package/src/panels/memory-panel.ts +7 -6
  170. package/src/panels/panel-list-panel.ts +36 -80
  171. package/src/panels/project-planning-panel.ts +18 -11
  172. package/src/panels/provider-account-snapshot.ts +51 -25
  173. package/src/panels/provider-accounts-panel.ts +33 -18
  174. package/src/panels/provider-health-domains.ts +45 -4
  175. package/src/panels/provider-health-panel.ts +5 -4
  176. package/src/panels/qr-panel.ts +1 -1
  177. package/src/panels/schedule-panel.ts +9 -17
  178. package/src/panels/security-panel.ts +8 -8
  179. package/src/panels/session-browser-panel.ts +10 -10
  180. package/src/panels/subscription-panel.ts +3 -3
  181. package/src/panels/system-messages-panel.ts +1 -1
  182. package/src/panels/tasks-panel.ts +20 -13
  183. package/src/panels/tool-inspector-panel.ts +19 -12
  184. package/src/panels/work-plan-panel.ts +5 -5
  185. package/src/planning/project-planning-coordinator.ts +1 -1
  186. package/src/provider-auth-route-display.ts +9 -0
  187. package/src/renderer/agent-workspace-style.ts +34 -0
  188. package/src/renderer/agent-workspace.ts +228 -52
  189. package/src/renderer/autocomplete-overlay.ts +25 -6
  190. package/src/renderer/bookmark-modal.ts +19 -4
  191. package/src/renderer/buffer.ts +4 -2
  192. package/src/renderer/context-inspector.ts +50 -13
  193. package/src/renderer/diff.ts +1 -1
  194. package/src/renderer/file-picker-overlay.ts +19 -6
  195. package/src/renderer/help-overlay.ts +106 -33
  196. package/src/renderer/history-search-overlay.ts +19 -4
  197. package/src/renderer/live-tail-modal.ts +27 -5
  198. package/src/renderer/mcp-workspace.ts +164 -50
  199. package/src/renderer/model-picker-overlay.ts +58 -2
  200. package/src/renderer/model-workspace.ts +104 -20
  201. package/src/renderer/process-modal.ts +27 -6
  202. package/src/renderer/profile-picker-modal.ts +20 -4
  203. package/src/renderer/search-overlay.ts +25 -5
  204. package/src/renderer/selection-modal-overlay.ts +46 -14
  205. package/src/renderer/session-picker-modal.ts +18 -1
  206. package/src/renderer/settings-modal-helpers.ts +2 -40
  207. package/src/renderer/settings-modal.ts +83 -50
  208. package/src/renderer/tool-call.ts +20 -11
  209. package/src/runtime/agent-runtime-events.ts +6 -6
  210. package/src/runtime/bootstrap-command-context.ts +7 -1
  211. package/src/runtime/bootstrap-command-parts.ts +9 -7
  212. package/src/runtime/bootstrap-core.ts +19 -1
  213. package/src/runtime/bootstrap-hook-bridge.ts +7 -18
  214. package/src/runtime/bootstrap-shell.ts +4 -4
  215. package/src/runtime/bootstrap.ts +31 -22
  216. package/src/runtime/connected-host-auth.ts +7 -2
  217. package/src/runtime/diagnostics/panels/index.ts +2 -2
  218. package/src/runtime/diagnostics/panels/policy.ts +7 -7
  219. package/src/runtime/index.ts +2 -1
  220. package/src/runtime/onboarding/apply-file-helpers.ts +66 -0
  221. package/src/runtime/onboarding/apply.ts +80 -79
  222. package/src/runtime/onboarding/derivation.ts +5 -8
  223. package/src/runtime/onboarding/snapshot.ts +0 -15
  224. package/src/runtime/onboarding/types.ts +8 -19
  225. package/src/runtime/onboarding/verify.ts +32 -10
  226. package/src/runtime/operator-token-cleanup.ts +1 -1
  227. package/src/runtime/services.ts +91 -22
  228. package/src/runtime/store/selectors/index.ts +3 -3
  229. package/src/runtime/store/state.ts +1 -4
  230. package/src/runtime/surface-feature-flags.ts +41 -6
  231. package/src/runtime/ui-read-models.ts +3 -4
  232. package/src/runtime/ui-services.ts +6 -6
  233. package/src/shell/blocking-input.ts +2 -1
  234. package/src/shell/ui-openers.ts +3 -13
  235. package/src/tools/agent-analysis-registry-policy.ts +0 -1
  236. package/src/tools/agent-channel-send-tool.ts +133 -0
  237. package/src/tools/agent-context-policy.ts +1 -1
  238. package/src/tools/agent-knowledge-ingest-tool.ts +405 -0
  239. package/src/tools/agent-knowledge-tool.ts +170 -0
  240. package/src/tools/agent-local-registry-tool.ts +269 -69
  241. package/src/tools/agent-media-generate-tool.ts +133 -0
  242. package/src/tools/agent-notify-tool.ts +143 -0
  243. package/src/tools/agent-operator-action-tool.ts +137 -0
  244. package/src/tools/agent-operator-briefing-tool.ts +217 -0
  245. package/src/tools/agent-reminder-schedule-tool.ts +237 -0
  246. package/src/tools/agent-tool-policy-guard.ts +8 -8
  247. package/src/tools/agent-work-plan-tool.ts +256 -0
  248. package/src/version.ts +1 -1
  249. package/src/input/commands/policy-dispatch.ts +0 -339
  250. package/src/input/commands/policy.ts +0 -13
  251. package/src/panels/panel-picker.ts +0 -105
  252. package/src/panels/policy-panel.ts +0 -308
  253. package/src/renderer/panel-picker-overlay.ts +0 -202
@@ -0,0 +1,329 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import type { ShellPathService } from '@/runtime/index.ts';
4
+ import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
5
+ import { assertNoSecretLikeText } from './persona-registry.ts';
6
+
7
+ export type AgentNoteSource = 'user' | 'agent' | 'imported' | 'system';
8
+ export type AgentNoteReviewState = 'fresh' | 'reviewed' | 'stale';
9
+
10
+ export interface AgentNoteRecord {
11
+ readonly id: string;
12
+ readonly title: string;
13
+ readonly body: string;
14
+ readonly tags: readonly string[];
15
+ readonly sourceUrl?: string;
16
+ readonly source: AgentNoteSource;
17
+ readonly provenance: string;
18
+ readonly reviewState: AgentNoteReviewState;
19
+ readonly staleReason?: string;
20
+ readonly createdAt: string;
21
+ readonly updatedAt: string;
22
+ readonly reviewedAt?: string;
23
+ }
24
+
25
+ export interface AgentNoteCreateInput {
26
+ readonly title: string;
27
+ readonly body: string;
28
+ readonly tags?: readonly string[];
29
+ readonly sourceUrl?: string;
30
+ readonly source?: AgentNoteSource;
31
+ readonly provenance?: string;
32
+ }
33
+
34
+ export interface AgentNoteUpdateInput {
35
+ readonly title?: string;
36
+ readonly body?: string;
37
+ readonly tags?: readonly string[];
38
+ readonly sourceUrl?: string;
39
+ readonly provenance?: string;
40
+ }
41
+
42
+ export interface AgentNoteSnapshot {
43
+ readonly path: string;
44
+ readonly notes: readonly AgentNoteRecord[];
45
+ readonly reviewQueue: readonly AgentNoteRecord[];
46
+ }
47
+
48
+ interface NoteStoreFile {
49
+ readonly version: 1;
50
+ readonly notes: readonly AgentNoteRecord[];
51
+ }
52
+
53
+ const STORE_VERSION = 1;
54
+
55
+ function isRecord(value: unknown): value is Record<string, unknown> {
56
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
57
+ }
58
+
59
+ function readString(value: unknown, fallback = ''): string {
60
+ return typeof value === 'string' ? value : fallback;
61
+ }
62
+
63
+ function readStringArray(value: unknown): string[] {
64
+ if (!Array.isArray(value)) return [];
65
+ return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
66
+ }
67
+
68
+ function normalizeTitle(title: string): string {
69
+ return title.trim().replace(/\s+/g, ' ');
70
+ }
71
+
72
+ function normalizeList(values: readonly string[] | undefined): string[] {
73
+ const seen = new Set<string>();
74
+ const result: string[] = [];
75
+ for (const value of values ?? []) {
76
+ const trimmed = value.trim();
77
+ if (!trimmed) continue;
78
+ const key = trimmed.toLowerCase();
79
+ if (seen.has(key)) continue;
80
+ seen.add(key);
81
+ result.push(trimmed);
82
+ }
83
+ return result;
84
+ }
85
+
86
+ function slugify(value: string): string {
87
+ const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
88
+ return slug || 'note';
89
+ }
90
+
91
+ function nowIso(): string {
92
+ return new Date().toISOString();
93
+ }
94
+
95
+ function assertNoNoteSecretLikeText(fields: readonly string[]): void {
96
+ try {
97
+ assertNoSecretLikeText(fields);
98
+ } catch {
99
+ throw new Error('Notes cannot store secret-looking values. Store a secret reference or remove the sensitive text.');
100
+ }
101
+ }
102
+
103
+ function parseNote(value: unknown): AgentNoteRecord | null {
104
+ if (!isRecord(value)) return null;
105
+ const id = readString(value.id).trim();
106
+ const title = normalizeTitle(readString(value.title));
107
+ const body = readString(value.body).trim();
108
+ if (!id || !title || !body) return null;
109
+ const reviewState = value.reviewState === 'reviewed' || value.reviewState === 'stale' ? value.reviewState : 'fresh';
110
+ const source = value.source === 'agent' || value.source === 'imported' || value.source === 'system' ? value.source : 'user';
111
+ const sourceUrl = readString(value.sourceUrl).trim();
112
+ const staleReason = readString(value.staleReason).trim();
113
+ const reviewedAt = readString(value.reviewedAt).trim();
114
+ return {
115
+ id,
116
+ title,
117
+ body,
118
+ tags: readStringArray(value.tags),
119
+ sourceUrl: sourceUrl || undefined,
120
+ source,
121
+ provenance: readString(value.provenance, source).trim() || source,
122
+ reviewState,
123
+ staleReason: staleReason || undefined,
124
+ createdAt: readString(value.createdAt, nowIso()),
125
+ updatedAt: readString(value.updatedAt, nowIso()),
126
+ reviewedAt: reviewedAt || undefined,
127
+ };
128
+ }
129
+
130
+ function parseStore(raw: string): NoteStoreFile {
131
+ const parsed: unknown = JSON.parse(raw);
132
+ if (!isRecord(parsed)) return { version: STORE_VERSION, notes: [] };
133
+ return {
134
+ version: STORE_VERSION,
135
+ notes: Array.isArray(parsed.notes)
136
+ ? parsed.notes.map(parseNote).filter((entry): entry is AgentNoteRecord => entry !== null)
137
+ : [],
138
+ };
139
+ }
140
+
141
+ function formatStore(store: NoteStoreFile): string {
142
+ return `${JSON.stringify(store, null, 2)}\n`;
143
+ }
144
+
145
+ type AgentLocalStorePaths = Pick<ShellPathService, 'resolveUserPath'>;
146
+
147
+ export function noteStorePath(shellPaths: AgentLocalStorePaths): string {
148
+ return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'notes', 'notes.json');
149
+ }
150
+
151
+ export class AgentNoteRegistry {
152
+ public constructor(private readonly storePath: string) {}
153
+
154
+ public static fromShellPaths(shellPaths: AgentLocalStorePaths): AgentNoteRegistry {
155
+ return new AgentNoteRegistry(noteStorePath(shellPaths));
156
+ }
157
+
158
+ public snapshot(): AgentNoteSnapshot {
159
+ const store = this.readStore();
160
+ const notes = [...store.notes].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
161
+ return {
162
+ path: this.storePath,
163
+ notes,
164
+ reviewQueue: notes.filter((note) => note.reviewState !== 'reviewed'),
165
+ };
166
+ }
167
+
168
+ public list(): readonly AgentNoteRecord[] {
169
+ return this.snapshot().notes;
170
+ }
171
+
172
+ public search(query: string): readonly AgentNoteRecord[] {
173
+ const normalized = query.trim().toLowerCase();
174
+ if (!normalized) return this.list();
175
+ return this.list().filter((note) => [
176
+ note.id,
177
+ note.title,
178
+ note.body,
179
+ note.sourceUrl ?? '',
180
+ ...note.tags,
181
+ ].some((field) => field.toLowerCase().includes(normalized)));
182
+ }
183
+
184
+ public get(idOrTitle: string): AgentNoteRecord | null {
185
+ const lookup = idOrTitle.trim().toLowerCase();
186
+ if (!lookup) return null;
187
+ return this.list().find((note) => note.id.toLowerCase() === lookup || note.title.toLowerCase() === lookup) ?? null;
188
+ }
189
+
190
+ public create(input: AgentNoteCreateInput): AgentNoteRecord {
191
+ const store = this.readStore();
192
+ const title = normalizeTitle(input.title);
193
+ const body = input.body.trim();
194
+ this.validateRequired(title, body);
195
+ const sourceUrl = input.sourceUrl?.trim() || undefined;
196
+ assertNoNoteSecretLikeText([title, body, sourceUrl ?? '', ...(input.tags ?? [])]);
197
+ const duplicate = store.notes.find((note) => note.title.toLowerCase() === title.toLowerCase());
198
+ if (duplicate) throw new Error(`Note already exists ${duplicate.id}`);
199
+ const timestamp = nowIso();
200
+ const note: AgentNoteRecord = {
201
+ id: this.nextId(title, store.notes),
202
+ title,
203
+ body,
204
+ tags: normalizeList(input.tags),
205
+ sourceUrl,
206
+ source: input.source ?? 'user',
207
+ provenance: input.provenance?.trim() || input.source || 'user',
208
+ reviewState: 'fresh',
209
+ createdAt: timestamp,
210
+ updatedAt: timestamp,
211
+ };
212
+ this.writeStore({ ...store, notes: [...store.notes, note] });
213
+ return note;
214
+ }
215
+
216
+ public update(idOrTitle: string, input: AgentNoteUpdateInput): AgentNoteRecord {
217
+ const store = this.readStore();
218
+ const existing = this.findInStore(store, idOrTitle);
219
+ if (!existing) throw new Error(`Unknown note ${idOrTitle}`);
220
+ const title = input.title === undefined ? existing.title : normalizeTitle(input.title);
221
+ const body = input.body === undefined ? existing.body : input.body.trim();
222
+ this.validateRequired(title, body);
223
+ const sourceUrl = input.sourceUrl === undefined ? existing.sourceUrl : input.sourceUrl.trim() || undefined;
224
+ assertNoNoteSecretLikeText([title, body, sourceUrl ?? '', ...(input.tags ?? [])]);
225
+ const duplicate = store.notes.find((note) => note.id !== existing.id && note.title.toLowerCase() === title.toLowerCase());
226
+ if (duplicate) throw new Error(`Note already exists ${duplicate.id}`);
227
+ const updated: AgentNoteRecord = {
228
+ ...existing,
229
+ title,
230
+ body,
231
+ tags: input.tags === undefined ? existing.tags : normalizeList(input.tags),
232
+ sourceUrl,
233
+ provenance: input.provenance === undefined ? existing.provenance : input.provenance.trim() || existing.provenance,
234
+ reviewState: 'fresh',
235
+ staleReason: undefined,
236
+ reviewedAt: undefined,
237
+ updatedAt: nowIso(),
238
+ };
239
+ this.writeStore({
240
+ ...store,
241
+ notes: store.notes.map((note) => note.id === existing.id ? updated : note),
242
+ });
243
+ return updated;
244
+ }
245
+
246
+ public markReviewed(idOrTitle: string): AgentNoteRecord {
247
+ const store = this.readStore();
248
+ const existing = this.findInStore(store, idOrTitle);
249
+ if (!existing) throw new Error(`Unknown note ${idOrTitle}`);
250
+ const updated: AgentNoteRecord = {
251
+ ...existing,
252
+ reviewState: 'reviewed',
253
+ staleReason: undefined,
254
+ reviewedAt: nowIso(),
255
+ updatedAt: nowIso(),
256
+ };
257
+ this.writeStore({
258
+ ...store,
259
+ notes: store.notes.map((note) => note.id === existing.id ? updated : note),
260
+ });
261
+ return updated;
262
+ }
263
+
264
+ public markStale(idOrTitle: string, reason: string): AgentNoteRecord {
265
+ const store = this.readStore();
266
+ const existing = this.findInStore(store, idOrTitle);
267
+ if (!existing) throw new Error(`Unknown note ${idOrTitle}`);
268
+ const updated: AgentNoteRecord = {
269
+ ...existing,
270
+ reviewState: 'stale',
271
+ staleReason: reason.trim() || 'Marked stale by user.',
272
+ updatedAt: nowIso(),
273
+ };
274
+ this.writeStore({
275
+ ...store,
276
+ notes: store.notes.map((note) => note.id === existing.id ? updated : note),
277
+ });
278
+ return updated;
279
+ }
280
+
281
+ public deleteNote(idOrTitle: string): AgentNoteRecord {
282
+ const store = this.readStore();
283
+ const existing = this.findInStore(store, idOrTitle);
284
+ if (!existing) throw new Error(`Unknown note ${idOrTitle}`);
285
+ this.writeStore({
286
+ ...store,
287
+ notes: store.notes.filter((note) => note.id !== existing.id),
288
+ });
289
+ return existing;
290
+ }
291
+
292
+ private validateRequired(title: string, body: string): void {
293
+ if (!title) throw new Error('Note title is required.');
294
+ if (!body) throw new Error('Note body is required.');
295
+ }
296
+
297
+ private nextId(title: string, notes: readonly AgentNoteRecord[]): string {
298
+ const base = slugify(title);
299
+ const ids = new Set(notes.map((note) => note.id));
300
+ if (!ids.has(base)) return base;
301
+ for (let index = 2; index < 1000; index += 1) {
302
+ const candidate = `${base}-${index}`;
303
+ if (!ids.has(candidate)) return candidate;
304
+ }
305
+ throw new Error(`Could not allocate note id for ${title}.`);
306
+ }
307
+
308
+ private findInStore(store: NoteStoreFile, idOrTitle: string): AgentNoteRecord | null {
309
+ const lookup = idOrTitle.trim().toLowerCase();
310
+ if (!lookup) return null;
311
+ return store.notes.find((note) => note.id.toLowerCase() === lookup || note.title.toLowerCase() === lookup) ?? null;
312
+ }
313
+
314
+ private readStore(): NoteStoreFile {
315
+ if (!existsSync(this.storePath)) return { version: STORE_VERSION, notes: [] };
316
+ try {
317
+ return parseStore(readFileSync(this.storePath, 'utf-8'));
318
+ } catch (error) {
319
+ throw new Error(`Could not read Agent note store ${error instanceof Error ? error.message : String(error)}`);
320
+ }
321
+ }
322
+
323
+ private writeStore(store: NoteStoreFile): void {
324
+ mkdirSync(dirname(this.storePath), { recursive: true });
325
+ const tmpPath = `${this.storePath}.tmp`;
326
+ writeFileSync(tmpPath, formatStore(store), 'utf-8');
327
+ renameSync(tmpPath, this.storePath);
328
+ }
329
+ }
@@ -0,0 +1,343 @@
1
+ import type { OperatorMethodInput } from '@pellux/goodvibes-sdk/contracts';
2
+
3
+ export type JsonRecord = Record<string, unknown>;
4
+
5
+ export type ApprovalActionId = 'approvals.approve' | 'approvals.deny' | 'approvals.cancel';
6
+ export type AutomationActionId =
7
+ | 'automation.jobs.run'
8
+ | 'automation.jobs.pause'
9
+ | 'automation.jobs.resume'
10
+ | 'automation.runs.cancel'
11
+ | 'automation.runs.retry'
12
+ | 'schedules.run';
13
+ export type OperatorActionId = ApprovalActionId | AutomationActionId;
14
+
15
+ type ApprovalActionInput =
16
+ | OperatorMethodInput<'approvals.approve'>
17
+ | OperatorMethodInput<'approvals.deny'>
18
+ | OperatorMethodInput<'approvals.cancel'>;
19
+
20
+ export interface OperatorActionArgs {
21
+ readonly action?: unknown;
22
+ readonly targetId?: unknown;
23
+ readonly approvalId?: unknown;
24
+ readonly jobId?: unknown;
25
+ readonly runId?: unknown;
26
+ readonly scheduleId?: unknown;
27
+ readonly note?: unknown;
28
+ readonly remember?: unknown;
29
+ }
30
+
31
+ export interface OperatorActionDescriptor {
32
+ readonly action: OperatorActionId;
33
+ readonly label: string;
34
+ readonly pathTemplate: string;
35
+ readonly targetField: 'approvalId' | 'jobId' | 'runId' | 'scheduleId';
36
+ }
37
+
38
+ export interface OperatorActionRequest {
39
+ readonly ok: true;
40
+ readonly descriptor: OperatorActionDescriptor;
41
+ readonly targetId: string;
42
+ readonly body: JsonRecord | null;
43
+ }
44
+
45
+ export interface OperatorActionSuccess {
46
+ readonly ok: true;
47
+ readonly methodId: OperatorActionId;
48
+ readonly path: string;
49
+ readonly body: unknown;
50
+ }
51
+
52
+ export interface OperatorActionFailure {
53
+ readonly ok: false;
54
+ readonly kind:
55
+ | 'auth_required'
56
+ | 'connected_host_unavailable'
57
+ | 'connected_host_route_unavailable'
58
+ | 'connected_host_error';
59
+ readonly error: string;
60
+ readonly methodId: OperatorActionId;
61
+ readonly path: string;
62
+ }
63
+
64
+ export type OperatorActionResult = OperatorActionSuccess | OperatorActionFailure;
65
+ export type OperatorActionParseResult = OperatorActionRequest | { readonly ok: false; readonly error: string };
66
+
67
+ export interface OperatorActionConnection {
68
+ readonly baseUrl: string;
69
+ readonly token: string | null;
70
+ readonly tokenPath?: string;
71
+ }
72
+
73
+ export const OPERATOR_ACTIONS: Record<OperatorActionId, OperatorActionDescriptor> = {
74
+ 'approvals.approve': {
75
+ action: 'approvals.approve',
76
+ label: 'approve approval',
77
+ pathTemplate: '/api/approvals/{approvalId}/approve',
78
+ targetField: 'approvalId',
79
+ },
80
+ 'approvals.deny': {
81
+ action: 'approvals.deny',
82
+ label: 'deny approval',
83
+ pathTemplate: '/api/approvals/{approvalId}/deny',
84
+ targetField: 'approvalId',
85
+ },
86
+ 'approvals.cancel': {
87
+ action: 'approvals.cancel',
88
+ label: 'cancel approval',
89
+ pathTemplate: '/api/approvals/{approvalId}/cancel',
90
+ targetField: 'approvalId',
91
+ },
92
+ 'automation.jobs.run': {
93
+ action: 'automation.jobs.run',
94
+ label: 'run automation job',
95
+ pathTemplate: '/api/automation/jobs/{jobId}/run',
96
+ targetField: 'jobId',
97
+ },
98
+ 'automation.jobs.pause': {
99
+ action: 'automation.jobs.pause',
100
+ label: 'pause automation job',
101
+ pathTemplate: '/api/automation/jobs/{jobId}/pause',
102
+ targetField: 'jobId',
103
+ },
104
+ 'automation.jobs.resume': {
105
+ action: 'automation.jobs.resume',
106
+ label: 'resume automation job',
107
+ pathTemplate: '/api/automation/jobs/{jobId}/resume',
108
+ targetField: 'jobId',
109
+ },
110
+ 'automation.runs.cancel': {
111
+ action: 'automation.runs.cancel',
112
+ label: 'cancel automation run',
113
+ pathTemplate: '/api/automation/runs/{runId}/cancel',
114
+ targetField: 'runId',
115
+ },
116
+ 'automation.runs.retry': {
117
+ action: 'automation.runs.retry',
118
+ label: 'retry automation run',
119
+ pathTemplate: '/api/automation/runs/{runId}/retry',
120
+ targetField: 'runId',
121
+ },
122
+ 'schedules.run': {
123
+ action: 'schedules.run',
124
+ label: 'run schedule',
125
+ pathTemplate: '/api/automation/schedules/{scheduleId}/run',
126
+ targetField: 'scheduleId',
127
+ },
128
+ };
129
+
130
+ export const OPERATOR_ACTION_IDS = Object.keys(OPERATOR_ACTIONS) as readonly OperatorActionId[];
131
+
132
+ export function isRecord(value: unknown): value is JsonRecord {
133
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
134
+ }
135
+
136
+ export function readOperatorActionString(value: unknown): string {
137
+ return typeof value === 'string' ? value.trim() : '';
138
+ }
139
+
140
+ export function readOperatorActionBoolean(value: unknown): boolean {
141
+ return value === true || value === 'true' || value === 'yes';
142
+ }
143
+
144
+ export function isOperatorActionId(value: unknown): value is OperatorActionId {
145
+ return typeof value === 'string' && value in OPERATOR_ACTIONS;
146
+ }
147
+
148
+ export function targetValue(args: OperatorActionArgs, descriptor: OperatorActionDescriptor): string {
149
+ const specific = args[descriptor.targetField];
150
+ return readOperatorActionString(specific) || readOperatorActionString(args.targetId);
151
+ }
152
+
153
+ export function renderOperatorActionPath(descriptor: OperatorActionDescriptor, targetId: string): string {
154
+ return descriptor.pathTemplate.replace(`{${descriptor.targetField}}`, encodeURIComponent(targetId));
155
+ }
156
+
157
+ function approvalBody(args: OperatorActionArgs): ApprovalActionInput {
158
+ const note = readOperatorActionString(args.note);
159
+ const body: ApprovalActionInput = {
160
+ approvalId: targetValue(args, OPERATOR_ACTIONS['approvals.approve']),
161
+ };
162
+ if (note) body.note = note;
163
+ if (args.remember !== undefined) body.remember = readOperatorActionBoolean(args.remember);
164
+ return body;
165
+ }
166
+
167
+ function requestBody(args: OperatorActionArgs, action: OperatorActionId): JsonRecord | null {
168
+ if (action !== 'approvals.approve' && action !== 'approvals.deny' && action !== 'approvals.cancel') return null;
169
+ const body = approvalBody(args);
170
+ const out: JsonRecord = {};
171
+ if (body.note) out.note = body.note;
172
+ if (body.remember !== undefined) out.remember = body.remember;
173
+ return Object.keys(out).length > 0 ? out : null;
174
+ }
175
+
176
+ export function buildOperatorActionRequest(rawArgs: unknown): OperatorActionParseResult {
177
+ if (!isRecord(rawArgs)) return { ok: false, error: 'Operator action arguments must be an object.' };
178
+ const args = rawArgs as OperatorActionArgs;
179
+ if (!isOperatorActionId(args.action)) {
180
+ return {
181
+ ok: false,
182
+ error: `action must be one of: ${OPERATOR_ACTION_IDS.join(', ')}`,
183
+ };
184
+ }
185
+ const descriptor = OPERATOR_ACTIONS[args.action];
186
+ const targetId = targetValue(args, descriptor);
187
+ if (!targetId) {
188
+ return {
189
+ ok: false,
190
+ error: `${descriptor.targetField} or targetId is required for ${descriptor.action}.`,
191
+ };
192
+ }
193
+ return {
194
+ ok: true,
195
+ descriptor,
196
+ targetId,
197
+ body: requestBody(args, descriptor.action),
198
+ };
199
+ }
200
+
201
+ async function readResponseBody(response: Response): Promise<unknown> {
202
+ const text = await response.text();
203
+ if (!text.trim()) return {};
204
+ try {
205
+ return JSON.parse(text) as unknown;
206
+ } catch {
207
+ return text;
208
+ }
209
+ }
210
+
211
+ function summarizeError(error: unknown): string {
212
+ return error instanceof Error ? error.message : String(error);
213
+ }
214
+
215
+ function classifyHttpFailure(
216
+ methodId: OperatorActionId,
217
+ path: string,
218
+ status: number,
219
+ body: unknown,
220
+ ): OperatorActionFailure {
221
+ const detail = isRecord(body) && typeof body.error === 'string' ? body.error : '';
222
+ return {
223
+ ok: false,
224
+ methodId,
225
+ path,
226
+ kind: status === 401 || status === 403
227
+ ? 'auth_required'
228
+ : status === 404
229
+ ? 'connected_host_route_unavailable'
230
+ : 'connected_host_error',
231
+ error: `HTTP ${status}${detail ? `: ${detail}` : ''}`,
232
+ };
233
+ }
234
+
235
+ export async function postOperatorAction(
236
+ connection: OperatorActionConnection,
237
+ request: OperatorActionRequest,
238
+ ): Promise<OperatorActionResult> {
239
+ const path = renderOperatorActionPath(request.descriptor, request.targetId);
240
+ const token = connection.token;
241
+ if (!token) {
242
+ return {
243
+ ok: false,
244
+ methodId: request.descriptor.action,
245
+ path,
246
+ kind: 'auth_required',
247
+ error: connection.tokenPath ? `no connected-host operator token found at ${connection.tokenPath}` : 'no connected-host operator token found',
248
+ };
249
+ }
250
+ const headers: HeadersInit = {
251
+ authorization: `Bearer ${token}`,
252
+ };
253
+ const init: RequestInit = {
254
+ method: 'POST',
255
+ headers,
256
+ };
257
+ if (request.body) {
258
+ headers['content-type'] = 'application/json';
259
+ init.body = JSON.stringify(request.body);
260
+ }
261
+ try {
262
+ const response = await fetch(`${connection.baseUrl}${path}`, init);
263
+ const body = await readResponseBody(response);
264
+ if (!response.ok) return classifyHttpFailure(request.descriptor.action, path, response.status, body);
265
+ return {
266
+ ok: true,
267
+ methodId: request.descriptor.action,
268
+ path,
269
+ body,
270
+ };
271
+ } catch (error) {
272
+ return {
273
+ ok: false,
274
+ methodId: request.descriptor.action,
275
+ path,
276
+ kind: 'connected_host_unavailable',
277
+ error: summarizeError(error),
278
+ };
279
+ }
280
+ }
281
+
282
+ export function statusFromOperatorActionBody(body: unknown): string {
283
+ if (!isRecord(body)) return 'ok';
284
+ const approval = body.approval;
285
+ if (isRecord(approval) && typeof approval.status === 'string') return approval.status;
286
+ const run = body.run;
287
+ if (isRecord(run) && typeof run.status === 'string') return run.status;
288
+ if (typeof body.status === 'string') return body.status;
289
+ if (typeof body.enabled === 'boolean') return body.enabled ? 'enabled' : 'disabled';
290
+ return 'ok';
291
+ }
292
+
293
+ function formatOperatorActionFailureKind(kind: OperatorActionFailure['kind']): string {
294
+ if (kind === 'auth_required') return 'authorization required';
295
+ if (kind === 'connected_host_unavailable') return 'connected host unavailable';
296
+ if (kind === 'connected_host_route_unavailable') return 'connected host route unavailable';
297
+ return 'connected host error';
298
+ }
299
+
300
+ export function formatOperatorActionSuccess(baseUrl: string, result: OperatorActionSuccess): string {
301
+ return [
302
+ 'Agent operator action completed',
303
+ ` method: ${result.methodId}`,
304
+ ` route: POST ${result.path}`,
305
+ ` connected host: ${baseUrl}`,
306
+ ` status: ${statusFromOperatorActionBody(result.body)}`,
307
+ ].join('\n');
308
+ }
309
+
310
+ export function formatOperatorActionFailure(result: OperatorActionFailure): string {
311
+ return [
312
+ `Agent operator action failed: ${result.kind} (${formatOperatorActionFailureKind(result.kind)})`,
313
+ ` method: ${result.methodId}`,
314
+ ` route: POST ${result.path}`,
315
+ ` error: ${result.error}`,
316
+ ].join('\n');
317
+ }
318
+
319
+ export function formatOperatorActionPreview(request: OperatorActionRequest, explicitUserRequest: string): string {
320
+ return [
321
+ 'Agent operator action preview',
322
+ ` method ${request.descriptor.action}`,
323
+ ` action ${request.descriptor.label}`,
324
+ ` target ${request.targetId}`,
325
+ ` route POST ${renderOperatorActionPath(request.descriptor, request.targetId)}`,
326
+ explicitUserRequest ? ` requested by ${explicitUserRequest}` : ' requested by (missing)',
327
+ '',
328
+ 'Confirmation required: type yes in the workspace form or pass --yes only when the user explicitly asked GoodVibes Agent to perform this exact operator action.',
329
+ ].join('\n');
330
+ }
331
+
332
+ export function formatOperatorActionToolPreview(request: OperatorActionRequest, explicitUserRequest: string): string {
333
+ return [
334
+ 'Agent operator action preview',
335
+ ` method ${request.descriptor.action}`,
336
+ ` action ${request.descriptor.label}`,
337
+ ` target ${request.targetId}`,
338
+ ` route POST ${renderOperatorActionPath(request.descriptor, request.targetId)}`,
339
+ explicitUserRequest ? ` requested by ${explicitUserRequest}` : ' requested by (missing)',
340
+ '',
341
+ 'Model tool confirmation required. Call this tool with confirm:true only when the user explicitly asked GoodVibes Agent to perform this exact operator action.',
342
+ ].join('\n');
343
+ }