@pellux/goodvibes-agent 0.1.107 → 0.1.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +38 -13
  2. package/README.md +27 -11
  3. package/docs/README.md +5 -5
  4. package/docs/{runtime-connection.md → connected-services.md} +8 -8
  5. package/docs/getting-started.md +28 -12
  6. package/docs/release-and-publishing.md +4 -4
  7. package/package.json +2 -12
  8. package/src/agent/reminder-schedule-format.ts +75 -0
  9. package/src/agent/reminder-schedule.ts +494 -0
  10. package/src/agent/routine-schedule-format.ts +7 -7
  11. package/src/agent/routine-schedule-promotion.ts +1 -1
  12. package/src/agent/routine-schedule-receipts.ts +1 -1
  13. package/src/agent/skill-discovery.ts +2 -0
  14. package/src/cli/agent-knowledge-args.ts +93 -0
  15. package/src/cli/agent-knowledge-command.ts +200 -369
  16. package/src/cli/agent-knowledge-format.ts +58 -7
  17. package/src/cli/agent-knowledge-methods.ts +84 -0
  18. package/src/cli/agent-knowledge-runtime.ts +240 -0
  19. package/src/cli/completion.ts +0 -2
  20. package/src/cli/config-overrides.ts +2 -2
  21. package/src/cli/help.ts +34 -15
  22. package/src/cli/management-commands.ts +2 -2
  23. package/src/cli/management.ts +8 -8
  24. package/src/cli/package-verification.ts +7 -3
  25. package/src/cli/parser.ts +10 -4
  26. package/src/cli/service-posture.ts +6 -6
  27. package/src/cli/status.ts +32 -32
  28. package/src/input/agent-workspace-activation.ts +24 -13
  29. package/src/input/agent-workspace-basic-command-editors.ts +448 -0
  30. package/src/input/agent-workspace-categories.ts +42 -34
  31. package/src/input/agent-workspace-channels.ts +3 -3
  32. package/src/input/agent-workspace-command-editor.ts +65 -0
  33. package/src/input/agent-workspace-editors.ts +17 -2
  34. package/src/input/agent-workspace-knowledge-query-editor.ts +74 -0
  35. package/src/input/agent-workspace-knowledge-url-editor.ts +95 -0
  36. package/src/input/agent-workspace-reminder-schedule-editor.ts +125 -0
  37. package/src/input/agent-workspace-routine-schedule-editor.ts +127 -0
  38. package/src/input/agent-workspace-setup.ts +2 -2
  39. package/src/input/agent-workspace-types.ts +20 -2
  40. package/src/input/agent-workspace-voice-media.ts +5 -5
  41. package/src/input/agent-workspace.ts +39 -2
  42. package/src/input/commands/agent-runtime-profile-runtime.ts +1 -1
  43. package/src/input/commands/agent-skills-runtime.ts +94 -2
  44. package/src/input/commands/brief-runtime.ts +126 -0
  45. package/src/input/commands/channels-runtime.ts +47 -0
  46. package/src/input/commands/health-runtime.ts +10 -10
  47. package/src/input/commands/knowledge.ts +176 -1
  48. package/src/input/commands/planning-runtime.ts +1 -1
  49. package/src/input/commands/platform-access-runtime.ts +10 -10
  50. package/src/input/commands/policy-dispatch.ts +1 -1
  51. package/src/input/commands/schedule-runtime.ts +42 -5
  52. package/src/input/commands/security-runtime.ts +1 -1
  53. package/src/input/commands/session-content.ts +1 -1
  54. package/src/input/commands/session-workflow.ts +1 -1
  55. package/src/input/commands/shell-core.ts +4 -2
  56. package/src/input/commands/tasks-runtime.ts +3 -3
  57. package/src/input/commands.ts +4 -0
  58. package/src/input/handler-onboarding.ts +4 -4
  59. package/src/input/handler.ts +3 -2
  60. package/src/input/onboarding/onboarding-wizard-helpers.ts +1 -1
  61. package/src/input/onboarding/onboarding-wizard-operator-steps.ts +13 -13
  62. package/src/input/onboarding/onboarding-wizard-steps.ts +8 -8
  63. package/src/input/settings-modal-agent-policy.ts +1 -1
  64. package/src/input/slash-command-parser.ts +60 -0
  65. package/src/panels/builtin/agent.ts +1 -1
  66. package/src/panels/builtin/operations.ts +2 -2
  67. package/src/panels/provider-account-snapshot.ts +1 -1
  68. package/src/panels/provider-health-domains.ts +6 -6
  69. package/src/panels/subscription-panel.ts +1 -1
  70. package/src/panels/tasks-panel.ts +3 -3
  71. package/src/renderer/agent-workspace.ts +43 -30
  72. package/src/renderer/help-overlay.ts +1 -1
  73. package/src/renderer/settings-modal.ts +13 -13
  74. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  75. package/src/runtime/bootstrap.ts +8 -8
  76. package/src/runtime/index.ts +2 -2
  77. package/src/runtime/onboarding/derivation.ts +6 -6
  78. package/src/shell/service-settings-sync.ts +1 -1
  79. package/src/version.ts +1 -1
@@ -0,0 +1,494 @@
1
+ import { createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
2
+ import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
3
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
4
+ import { getModelIdFromProviderModel, getProviderIdFromModel } from '../config/provider-model.ts';
5
+ import { SDK_VERSION } from '../version.ts';
6
+ import {
7
+ resolveAgentDaemonConnection,
8
+ ROUTINE_SCHEDULE_METHOD,
9
+ ROUTINE_SCHEDULE_ROUTE,
10
+ type AgentDaemonConfigReader,
11
+ type AgentDaemonConnection,
12
+ type RoutineScheduleDeliveryKind,
13
+ type RoutineScheduleDeliverySurfaceKind,
14
+ type RoutineScheduleDeliveryTargetSpec,
15
+ type RoutineScheduleKind,
16
+ type RoutineScheduleSpec,
17
+ } from './routine-schedule-promotion.ts';
18
+
19
+ type ScheduleCreateInput = OperatorMethodInput<'schedules.create'>;
20
+ type ScheduleCreateOutput = OperatorMethodOutput<'schedules.create'>;
21
+ type ScheduleDeliveryInput = NonNullable<ScheduleCreateInput['delivery']>;
22
+ type ScheduleDeliveryTargetInput = ScheduleDeliveryInput['targets'] extends readonly (infer T)[] ? T : never;
23
+
24
+ export const REMINDER_SCHEDULE_METHOD = ROUTINE_SCHEDULE_METHOD;
25
+ export const REMINDER_SCHEDULE_ROUTE = ROUTINE_SCHEDULE_ROUTE;
26
+
27
+ export interface ParsedReminderScheduleArgs {
28
+ readonly message: string | null;
29
+ readonly schedule: RoutineScheduleSpec | null;
30
+ readonly deliveryTargets: readonly RoutineScheduleDeliveryTargetSpec[];
31
+ readonly name?: string;
32
+ readonly timezone?: string;
33
+ readonly provider?: string;
34
+ readonly model?: string;
35
+ readonly enabled: boolean;
36
+ readonly yes: boolean;
37
+ readonly errors: readonly string[];
38
+ }
39
+
40
+ export interface ReminderSchedulePreview {
41
+ readonly message: string;
42
+ readonly route: typeof REMINDER_SCHEDULE_ROUTE;
43
+ readonly method: typeof REMINDER_SCHEDULE_METHOD;
44
+ readonly payload: ScheduleCreateInput;
45
+ }
46
+
47
+ export interface ReminderScheduleSuccess {
48
+ readonly ok: true;
49
+ readonly kind: typeof REMINDER_SCHEDULE_METHOD;
50
+ readonly route: typeof REMINDER_SCHEDULE_ROUTE;
51
+ readonly message: string;
52
+ readonly schedule: ScheduleCreateOutput;
53
+ readonly request: ScheduleCreateInput;
54
+ }
55
+
56
+ export interface ReminderScheduleFailure {
57
+ readonly ok: false;
58
+ readonly kind:
59
+ | 'confirmation_required'
60
+ | 'auth_required'
61
+ | 'daemon_unavailable'
62
+ | 'version_mismatch'
63
+ | 'daemon_route_unavailable'
64
+ | 'daemon_error';
65
+ readonly error: string;
66
+ readonly route: typeof REMINDER_SCHEDULE_ROUTE;
67
+ readonly baseUrl?: string;
68
+ readonly daemonVersion?: string;
69
+ readonly expectedSdkVersion?: string;
70
+ }
71
+
72
+ export type ReminderScheduleResult = ReminderScheduleSuccess | ReminderScheduleFailure;
73
+
74
+ const DELIVERY_SURFACE_KINDS: readonly RoutineScheduleDeliverySurfaceKind[] = [
75
+ 'tui',
76
+ 'web',
77
+ 'slack',
78
+ 'discord',
79
+ 'ntfy',
80
+ 'webhook',
81
+ 'telegram',
82
+ 'google-chat',
83
+ 'signal',
84
+ 'whatsapp',
85
+ 'imessage',
86
+ 'msteams',
87
+ 'bluebubbles',
88
+ 'mattermost',
89
+ 'matrix',
90
+ 'service',
91
+ ];
92
+
93
+ function isRecord(value: unknown): value is Record<string, unknown> {
94
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
95
+ }
96
+
97
+ function readString(record: Record<string, unknown>, key: string): string | null {
98
+ const value = record[key];
99
+ return typeof value === 'string' ? value : null;
100
+ }
101
+
102
+ function optionValue(args: readonly string[], index: number, inlineValue: string | undefined): {
103
+ readonly value: string | undefined;
104
+ readonly nextIndex: number;
105
+ } {
106
+ if (inlineValue !== undefined) return { value: inlineValue, nextIndex: index };
107
+ const next = args[index + 1];
108
+ if (next === undefined || next.startsWith('--')) return { value: undefined, nextIndex: index };
109
+ return { value: next, nextIndex: index + 1 };
110
+ }
111
+
112
+ function isDeliverySurfaceKind(value: string): value is RoutineScheduleDeliverySurfaceKind {
113
+ return DELIVERY_SURFACE_KINDS.includes(value as RoutineScheduleDeliverySurfaceKind);
114
+ }
115
+
116
+ function parseChannelDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
117
+ const [surfaceKind = '', routeId, label] = raw.split(':');
118
+ if (!isDeliverySurfaceKind(surfaceKind)) {
119
+ return `Unsupported delivery channel "${surfaceKind}".`;
120
+ }
121
+ return {
122
+ kind: 'surface',
123
+ surfaceKind,
124
+ routeId: routeId?.trim() || undefined,
125
+ label: label?.trim() || undefined,
126
+ };
127
+ }
128
+
129
+ function parseWebhookDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
130
+ const normalized = raw.trim();
131
+ if (!normalized) return '--delivery-webhook requires a URL.';
132
+ try {
133
+ const url = new URL(normalized);
134
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') return '--delivery-webhook must be an http(s) URL.';
135
+ } catch {
136
+ return '--delivery-webhook must be a valid URL.';
137
+ }
138
+ return {
139
+ kind: 'webhook',
140
+ address: normalized,
141
+ };
142
+ }
143
+
144
+ function parseRouteDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
145
+ const [routeId = '', label] = raw.split(':');
146
+ const normalizedRouteId = routeId.trim();
147
+ if (!normalizedRouteId) return '--delivery-route requires a route id.';
148
+ return {
149
+ kind: 'surface',
150
+ routeId: normalizedRouteId,
151
+ label: label?.trim() || undefined,
152
+ };
153
+ }
154
+
155
+ function parseLinkDeliveryTarget(raw: string): RoutineScheduleDeliveryTargetSpec | string {
156
+ const normalized = raw.trim();
157
+ if (!normalized) return '--delivery-link requires a URL or label.';
158
+ return {
159
+ kind: 'link',
160
+ address: normalized,
161
+ };
162
+ }
163
+
164
+ function validateDeliveryTargets(targets: readonly RoutineScheduleDeliveryTargetSpec[]): string | null {
165
+ const kinds = new Set(targets.map((target) => target.kind));
166
+ return kinds.size > 1 ? 'Use one delivery target kind per reminder command.' : null;
167
+ }
168
+
169
+ function normalizeProviderModel(provider: string | undefined, model: string | undefined): {
170
+ readonly provider?: string;
171
+ readonly model?: string;
172
+ } {
173
+ if (!model) return provider ? { provider } : {};
174
+ const normalizedProvider = provider ?? getProviderIdFromModel(model);
175
+ return {
176
+ provider: normalizedProvider,
177
+ model: getModelIdFromProviderModel(model),
178
+ };
179
+ }
180
+
181
+ function deliveryModeFromTargets(targets: readonly RoutineScheduleDeliveryTargetSpec[]): RoutineScheduleDeliveryKind | 'none' {
182
+ const first = targets[0];
183
+ return first ? first.kind : 'none';
184
+ }
185
+
186
+ function toDeliveryTargetInput(target: RoutineScheduleDeliveryTargetSpec): ScheduleDeliveryTargetInput {
187
+ return {
188
+ kind: target.kind,
189
+ surfaceKind: target.surfaceKind,
190
+ address: target.address,
191
+ routeId: target.routeId,
192
+ label: target.label,
193
+ };
194
+ }
195
+
196
+ export function parseReminderScheduleArgs(args: readonly string[]): ParsedReminderScheduleArgs {
197
+ let message: string | null = null;
198
+ let schedule: RoutineScheduleSpec | null = null;
199
+ const deliveryTargets: RoutineScheduleDeliveryTargetSpec[] = [];
200
+ let name: string | undefined;
201
+ let timezone: string | undefined;
202
+ let provider: string | undefined;
203
+ let model: string | undefined;
204
+ let enabled = true;
205
+ let yes = false;
206
+ const errors: string[] = [];
207
+ const positional: string[] = [];
208
+
209
+ for (let index = 0; index < args.length; index += 1) {
210
+ const raw = args[index] ?? '';
211
+ const equals = raw.indexOf('=');
212
+ const optionName = equals >= 0 ? raw.slice(0, equals) : raw;
213
+ const inlineValue = equals >= 0 ? raw.slice(equals + 1) : undefined;
214
+
215
+ if (raw === '--yes') {
216
+ yes = true;
217
+ continue;
218
+ }
219
+ if (raw === '--disabled') {
220
+ enabled = false;
221
+ continue;
222
+ }
223
+ if (
224
+ optionName === '--message'
225
+ || optionName === '--name'
226
+ || optionName === '--timezone'
227
+ || optionName === '--provider'
228
+ || optionName === '--model'
229
+ ) {
230
+ const consumed = optionValue(args, index, inlineValue);
231
+ index = consumed.nextIndex;
232
+ const value = consumed.value?.trim();
233
+ if (!value) {
234
+ errors.push(`${optionName} requires a value.`);
235
+ continue;
236
+ }
237
+ if (optionName === '--message') message = value;
238
+ if (optionName === '--name') name = value;
239
+ if (optionName === '--timezone') timezone = value;
240
+ if (optionName === '--provider') provider = value;
241
+ if (optionName === '--model') model = value;
242
+ continue;
243
+ }
244
+ if (optionName === '--cron' || optionName === '--every' || optionName === '--at') {
245
+ const consumed = optionValue(args, index, inlineValue);
246
+ index = consumed.nextIndex;
247
+ const value = consumed.value?.trim();
248
+ if (!value) {
249
+ errors.push(`${optionName} requires a value.`);
250
+ continue;
251
+ }
252
+ if (schedule) {
253
+ errors.push('Choose exactly one schedule selector: --cron, --every, or --at.');
254
+ continue;
255
+ }
256
+ schedule = {
257
+ kind: optionName === '--cron' ? 'cron' : optionName === '--every' ? 'every' : 'at',
258
+ value,
259
+ };
260
+ continue;
261
+ }
262
+ if (optionName === '--delivery-channel' || optionName === '--deliver-channel') {
263
+ const consumed = optionValue(args, index, inlineValue);
264
+ index = consumed.nextIndex;
265
+ const value = consumed.value?.trim();
266
+ if (!value) {
267
+ errors.push(`${optionName} requires a value.`);
268
+ continue;
269
+ }
270
+ const target = parseChannelDeliveryTarget(value);
271
+ if (typeof target === 'string') errors.push(target);
272
+ else deliveryTargets.push(target);
273
+ continue;
274
+ }
275
+ if (optionName === '--delivery-route' || optionName === '--deliver-route') {
276
+ const consumed = optionValue(args, index, inlineValue);
277
+ index = consumed.nextIndex;
278
+ const value = consumed.value?.trim();
279
+ if (!value) {
280
+ errors.push(`${optionName} requires a value.`);
281
+ continue;
282
+ }
283
+ const target = parseRouteDeliveryTarget(value);
284
+ if (typeof target === 'string') errors.push(target);
285
+ else deliveryTargets.push(target);
286
+ continue;
287
+ }
288
+ if (optionName === '--delivery-webhook' || optionName === '--deliver-webhook') {
289
+ const consumed = optionValue(args, index, inlineValue);
290
+ index = consumed.nextIndex;
291
+ const value = consumed.value?.trim();
292
+ if (!value) {
293
+ errors.push(`${optionName} requires a value.`);
294
+ continue;
295
+ }
296
+ const target = parseWebhookDeliveryTarget(value);
297
+ if (typeof target === 'string') errors.push(target);
298
+ else deliveryTargets.push(target);
299
+ continue;
300
+ }
301
+ if (optionName === '--delivery-link' || optionName === '--deliver-link') {
302
+ const consumed = optionValue(args, index, inlineValue);
303
+ index = consumed.nextIndex;
304
+ const value = consumed.value?.trim();
305
+ if (!value) {
306
+ errors.push(`${optionName} requires a value.`);
307
+ continue;
308
+ }
309
+ const target = parseLinkDeliveryTarget(value);
310
+ if (typeof target === 'string') errors.push(target);
311
+ else deliveryTargets.push(target);
312
+ continue;
313
+ }
314
+ if (raw.startsWith('--')) {
315
+ errors.push(`Unknown option: ${raw}`);
316
+ continue;
317
+ }
318
+ positional.push(raw);
319
+ }
320
+
321
+ if (!message && positional.length > 0) message = positional.join(' ').trim();
322
+ if (!message) errors.push('Reminder message is required: use --message <text> or trailing text.');
323
+ if (!schedule) errors.push('Schedule is required: use --cron <expr>, --every <interval>, or --at <iso-time>.');
324
+ const deliveryError = validateDeliveryTargets(deliveryTargets);
325
+ if (deliveryError) errors.push(deliveryError);
326
+ return { message, schedule, deliveryTargets, name, timezone, provider, model, enabled, yes, errors };
327
+ }
328
+
329
+ export function buildReminderSchedulePrompt(message: string): string {
330
+ return [
331
+ 'GoodVibes Agent scheduled reminder.',
332
+ '',
333
+ 'Reminder:',
334
+ message,
335
+ '',
336
+ 'Operator policy:',
337
+ '- Treat this as a reminder delivery, not an autonomous hidden workflow.',
338
+ '- Use isolated Agent Knowledge routes only when lookup is needed; never use default Knowledge/Wiki or non-Agent knowledge spaces as fallback.',
339
+ '- Do not perform destructive, costly, externally visible, or secret-handling actions from this reminder without explicit approval.',
340
+ '- Do not request WRFC from a reminder. If build/fix/review work is needed, ask the user to delegate it explicitly to GoodVibes TUI.',
341
+ '- Keep the reminder concise and state any next action the user can take.',
342
+ ].join('\n');
343
+ }
344
+
345
+ export function buildReminderSchedulePayload(parsed: ParsedReminderScheduleArgs): ScheduleCreateInput {
346
+ if (!parsed.message) throw new Error('Reminder message is required.');
347
+ if (!parsed.schedule) throw new Error('Schedule is required.');
348
+ const modelRoute = normalizeProviderModel(parsed.provider, parsed.model);
349
+ const payload: ScheduleCreateInput = {
350
+ name: parsed.name ?? `Agent reminder: ${parsed.message.slice(0, 48)}`,
351
+ prompt: buildReminderSchedulePrompt(parsed.message),
352
+ kind: parsed.schedule.kind,
353
+ enabled: parsed.enabled,
354
+ target: {
355
+ kind: 'main',
356
+ surfaceKind: 'service',
357
+ preserveThread: true,
358
+ createIfMissing: true,
359
+ },
360
+ delivery: {
361
+ mode: deliveryModeFromTargets(parsed.deliveryTargets),
362
+ targets: parsed.deliveryTargets.map(toDeliveryTargetInput),
363
+ fallbackTargets: [],
364
+ includeSummary: true,
365
+ includeTranscript: false,
366
+ includeLinks: true,
367
+ },
368
+ failure: {
369
+ action: 'retry',
370
+ maxConsecutiveFailures: 3,
371
+ cooldownMs: 3_600_000,
372
+ retryPolicy: {
373
+ maxAttempts: 2,
374
+ delayMs: 60_000,
375
+ strategy: 'exponential',
376
+ maxDelayMs: 900_000,
377
+ jitterMs: 30_000,
378
+ },
379
+ disableAfterFailures: false,
380
+ },
381
+ lightContext: true,
382
+ autoApprove: false,
383
+ allowUnsafeExternalContent: false,
384
+ ...modelRoute,
385
+ };
386
+ if (parsed.schedule.kind === 'cron') {
387
+ return {
388
+ ...payload,
389
+ cron: parsed.schedule.value,
390
+ timezone: parsed.timezone,
391
+ };
392
+ }
393
+ if (parsed.schedule.kind === 'every') {
394
+ return { ...payload, every: parsed.schedule.value };
395
+ }
396
+ return { ...payload, at: parsed.schedule.value };
397
+ }
398
+
399
+ export function buildReminderSchedulePreview(parsed: ParsedReminderScheduleArgs): ReminderSchedulePreview {
400
+ if (!parsed.message) throw new Error('Reminder message is required.');
401
+ return {
402
+ message: parsed.message,
403
+ route: REMINDER_SCHEDULE_ROUTE,
404
+ method: REMINDER_SCHEDULE_METHOD,
405
+ payload: buildReminderSchedulePayload(parsed),
406
+ };
407
+ }
408
+
409
+ async function fetchDaemonStatus(connection: AgentDaemonConnection): Promise<{
410
+ readonly ok: boolean;
411
+ readonly status: number;
412
+ readonly body: unknown;
413
+ }> {
414
+ try {
415
+ const response = await fetch(`${connection.baseUrl}/status`, {
416
+ headers: connection.token ? { authorization: `Bearer ${connection.token}` } : undefined,
417
+ });
418
+ const text = await response.text();
419
+ let body: unknown = text;
420
+ try {
421
+ body = text.trim() ? JSON.parse(text) as unknown : {};
422
+ } catch {
423
+ body = text;
424
+ }
425
+ return { ok: response.ok, status: response.status, body };
426
+ } catch (error) {
427
+ return { ok: false, status: 0, body: summarizeError(error) };
428
+ }
429
+ }
430
+
431
+ async function classifyReminderScheduleError(
432
+ error: unknown,
433
+ connection: AgentDaemonConnection,
434
+ ): Promise<ReminderScheduleFailure> {
435
+ const message = summarizeError(error);
436
+ const lower = message.toLowerCase();
437
+ if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) {
438
+ return { ok: false, kind: 'auth_required', error: message, route: REMINDER_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
439
+ }
440
+ if (lower.includes('404') || lower.includes('not found')) {
441
+ const daemon = await fetchDaemonStatus(connection);
442
+ const record = isRecord(daemon.body) ? daemon.body : {};
443
+ const daemonVersion = readString(record, 'version') ?? 'unknown';
444
+ if (daemon.ok && daemonVersion !== SDK_VERSION) {
445
+ return {
446
+ ok: false,
447
+ kind: 'version_mismatch',
448
+ error: `Connected GoodVibes service SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.create is unavailable.`,
449
+ route: REMINDER_SCHEDULE_ROUTE,
450
+ baseUrl: connection.baseUrl,
451
+ daemonVersion,
452
+ expectedSdkVersion: SDK_VERSION,
453
+ };
454
+ }
455
+ return { ok: false, kind: 'daemon_route_unavailable', error: message, route: REMINDER_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
456
+ }
457
+ if (lower.includes('fetch') || lower.includes('connect') || lower.includes('econnrefused')) {
458
+ return { ok: false, kind: 'daemon_unavailable', error: message, route: REMINDER_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
459
+ }
460
+ return { ok: false, kind: 'daemon_error', error: message, route: REMINDER_SCHEDULE_ROUTE, baseUrl: connection.baseUrl };
461
+ }
462
+
463
+ export async function createReminderSchedule(
464
+ connection: AgentDaemonConnection,
465
+ preview: ReminderSchedulePreview,
466
+ ): Promise<ReminderScheduleResult> {
467
+ if (!connection.token) {
468
+ return {
469
+ ok: false,
470
+ kind: 'auth_required',
471
+ error: `No daemon operator token found at ${connection.tokenPath}`,
472
+ route: REMINDER_SCHEDULE_ROUTE,
473
+ baseUrl: connection.baseUrl,
474
+ };
475
+ }
476
+ try {
477
+ const sdk = createBrowserGoodVibesSdk({ baseUrl: connection.baseUrl, authToken: connection.token });
478
+ const schedule = await sdk.operator.invoke(REMINDER_SCHEDULE_METHOD, preview.payload);
479
+ return {
480
+ ok: true,
481
+ kind: REMINDER_SCHEDULE_METHOD,
482
+ route: REMINDER_SCHEDULE_ROUTE,
483
+ message: preview.message,
484
+ schedule,
485
+ request: preview.payload,
486
+ };
487
+ } catch (error) {
488
+ return classifyReminderScheduleError(error, connection);
489
+ }
490
+ }
491
+
492
+ export function resolveReminderDaemonConnection(configManager: AgentDaemonConfigReader, homeDirectory: string): AgentDaemonConnection {
493
+ return resolveAgentDaemonConnection(configManager, homeDirectory);
494
+ }
@@ -41,7 +41,7 @@ export function formatRoutineSchedulePreview(preview: RoutineSchedulePromotionPr
41
41
  ` delivery: ${delivery?.mode ?? 'none'}${deliveryTargetCount > 0 ? ` (${deliveryTargetCount} target${deliveryTargetCount === 1 ? '' : 's'})` : ''}`,
42
42
  ' target: GoodVibes runtime/main conversation route',
43
43
  ' policy: isolated Agent Knowledge only; no default wiki/non-Agent fallback; no WRFC unless explicitly delegated',
44
- ' next: rerun with --yes to create this external schedule',
44
+ ' next: rerun with --yes to create this connected schedule',
45
45
  ].join('\n');
46
46
  }
47
47
 
@@ -111,13 +111,13 @@ export function formatRoutineScheduleCorrelation(result: RoutineScheduleCorrelat
111
111
  result.baseUrl ? ` runtime: ${result.baseUrl}` : null,
112
112
  ` route: ${ROUTINE_SCHEDULE_LIST_METHOD} ${result.route}`,
113
113
  result.kind === 'auth_required'
114
- ? ' next: pair/authenticate with the external GoodVibes runtime, then retry.'
114
+ ? ' next: pair/authenticate with connected GoodVibes services, then retry.'
115
115
  : null,
116
116
  result.kind === 'daemon_unavailable'
117
- ? ' next: start/restart the external GoodVibes runtime from TUI or host tooling; Agent does not own runtime lifecycle.'
117
+ ? ' next: make connected GoodVibes services available outside Agent, then retry.'
118
118
  : null,
119
119
  result.kind === 'version_mismatch' || result.kind === 'daemon_route_unavailable'
120
- ? ' next: update/restart the external GoodVibes runtime so public schedules.list is available.'
120
+ ? ' next: update connected GoodVibes services so public schedules.list is available.'
121
121
  : null,
122
122
  ].filter((line): line is string => Boolean(line)).join('\n');
123
123
  }
@@ -166,13 +166,13 @@ export function formatRoutineScheduleFailure(failure: RoutineSchedulePromotionFa
166
166
  ? ` versions: runtime=${failure.daemonVersion} expected=${failure.expectedSdkVersion}`
167
167
  : null,
168
168
  failure.kind === 'auth_required'
169
- ? ' next: pair/authenticate with the external GoodVibes runtime, then retry with --yes.'
169
+ ? ' next: pair/authenticate with connected GoodVibes services, then retry with --yes.'
170
170
  : null,
171
171
  failure.kind === 'daemon_unavailable'
172
- ? ' next: start/restart the external GoodVibes runtime from TUI or host tooling; Agent does not own runtime lifecycle.'
172
+ ? ' next: make connected GoodVibes services available outside Agent, then retry.'
173
173
  : null,
174
174
  failure.kind === 'version_mismatch' || failure.kind === 'daemon_route_unavailable'
175
- ? ' next: update/restart the external GoodVibes runtime so public schedules.create is available.'
175
+ ? ' next: update connected GoodVibes services so public schedules.create is available.'
176
176
  : null,
177
177
  ].filter((line): line is string => Boolean(line)).join('\n');
178
178
  }
@@ -393,7 +393,7 @@ async function classifyScheduleError(
393
393
  return {
394
394
  ok: false,
395
395
  kind: 'version_mismatch',
396
- error: `External runtime SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.create is unavailable.`,
396
+ error: `Connected GoodVibes service SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.create is unavailable.`,
397
397
  route: ROUTINE_SCHEDULE_ROUTE,
398
398
  baseUrl: connection.baseUrl,
399
399
  daemonVersion,
@@ -454,7 +454,7 @@ async function classifyScheduleListError(
454
454
  return {
455
455
  ok: false,
456
456
  kind: 'version_mismatch',
457
- error: `External runtime SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.list is unavailable.`,
457
+ error: `Connected GoodVibes service SDK version ${daemonVersion} does not match Agent SDK pin ${SDK_VERSION}; schedules.list is unavailable.`,
458
458
  route: ROUTINE_SCHEDULE_ROUTE,
459
459
  baseUrl: connection.baseUrl,
460
460
  daemonVersion,
@@ -10,6 +10,7 @@ export interface SkillRecord {
10
10
  description: string;
11
11
  path: string;
12
12
  origin: SkillOrigin;
13
+ body: string;
13
14
  dependencies: string[];
14
15
  includes: string[];
15
16
  frontmatter: Record<string, string>;
@@ -64,6 +65,7 @@ async function readSkillFile(path: string, origin: SkillOrigin): Promise<SkillRe
64
65
  description,
65
66
  path,
66
67
  origin,
68
+ body: body.trim(),
67
69
  dependencies,
68
70
  includes,
69
71
  frontmatter,
@@ -0,0 +1,93 @@
1
+ export function commandValues(args: readonly string[]): string[] {
2
+ const values: string[] = [];
3
+ for (let index = 0; index < args.length; index += 1) {
4
+ const token = args[index]!;
5
+ if (!token.startsWith('--')) {
6
+ values.push(token);
7
+ continue;
8
+ }
9
+ if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
10
+ }
11
+ return values;
12
+ }
13
+
14
+ export function delegationTaskValues(args: readonly string[]): string[] {
15
+ const values: string[] = [];
16
+ for (const token of args) {
17
+ if (token === '--wrfc') continue;
18
+ if (!token.startsWith('--')) values.push(token);
19
+ }
20
+ return values;
21
+ }
22
+
23
+ export function readOptionValue(args: readonly string[], name: string): string | undefined {
24
+ for (let index = 0; index < args.length; index += 1) {
25
+ const token = args[index]!;
26
+ if (token === name) {
27
+ const next = args[index + 1];
28
+ return next && !next.startsWith('--') ? next : undefined;
29
+ }
30
+ if (token.startsWith(`${name}=`)) return token.slice(name.length + 1);
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ export function readPositiveInt(args: readonly string[], name: string, fallback: number): number {
36
+ const raw = readOptionValue(args, name);
37
+ if (!raw) return fallback;
38
+ const parsed = Number.parseInt(raw, 10);
39
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
40
+ }
41
+
42
+ export function readStringList(args: readonly string[], name: string): readonly string[] {
43
+ const raw = readOptionValue(args, name);
44
+ if (!raw) return [];
45
+ return raw.split(',').map((entry) => entry.trim()).filter(Boolean);
46
+ }
47
+
48
+ export function readFirstStringList(args: readonly string[], names: readonly string[]): readonly string[] {
49
+ for (const name of names) {
50
+ const values = readStringList(args, name);
51
+ if (values.length > 0) return values;
52
+ }
53
+ return [];
54
+ }
55
+
56
+ export function readSinceMs(args: readonly string[]): number | undefined {
57
+ const days = readOptionValue(args, '--since-days');
58
+ if (!days) return undefined;
59
+ const parsed = Number.parseInt(days, 10);
60
+ if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
61
+ return Date.now() - parsed * 24 * 60 * 60 * 1000;
62
+ }
63
+
64
+ export function parseConnectorInput(value: string | undefined): unknown {
65
+ if (!value) return undefined;
66
+ const trimmed = value.trim();
67
+ if (!trimmed) return undefined;
68
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
69
+ try {
70
+ return JSON.parse(trimmed) as unknown;
71
+ } catch {
72
+ return trimmed;
73
+ }
74
+ }
75
+ return trimmed;
76
+ }
77
+
78
+ export function hasFlag(args: readonly string[], flag: string): boolean {
79
+ return args.includes(flag);
80
+ }
81
+
82
+ export function stripCommandFlag(args: readonly string[], flag: string): { readonly rest: readonly string[]; readonly present: boolean } {
83
+ const rest: string[] = [];
84
+ let present = false;
85
+ for (const arg of args) {
86
+ if (arg === flag) {
87
+ present = true;
88
+ continue;
89
+ }
90
+ rest.push(arg);
91
+ }
92
+ return { rest, present };
93
+ }