@pellux/goodvibes-agent 1.1.4 → 1.1.5

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.
@@ -0,0 +1,57 @@
1
+ import type { AgentWorkspaceCategory } from './agent-workspace-types.ts';
2
+
3
+ export const AGENT_WORKSPACE_HOST_CATEGORY: AgentWorkspaceCategory = {
4
+ id: 'host',
5
+ group: 'OPERATIONS',
6
+ label: 'Connected Host',
7
+ summary: 'Connected-host health, tasks, sessions, channels, and automation.',
8
+ detail: 'Use this workspace to inspect the GoodVibes host surfaces that Agent can see: system health, remote routes, host tasks, sessions, channels, schedules, knowledge, media, MCP, provider auth, support bundles, and telemetry/config posture.',
9
+ actions: [
10
+ { id: 'host-overview', label: 'Host overview', detail: 'Review connected-host, provider, settings, continuity, and Agent health without starting or mutating host work.', command: '/health review', kind: 'command', safety: 'read-only' },
11
+ { id: 'host-services', label: 'Host services', detail: 'Inspect connected-host service readiness, credentials, and integration issues without changing them.', command: '/health host', kind: 'command', safety: 'read-only' },
12
+ { id: 'host-setup', label: 'Setup review', detail: 'Inspect setup issues that affect connected-host and Agent runtime readiness.', command: '/health setup', kind: 'command', safety: 'read-only' },
13
+ { id: 'host-remote', label: 'Remote routes', detail: 'Inspect remote build-host route health and recovery signals without starting build work.', command: '/health remote', kind: 'command', safety: 'read-only' },
14
+ { id: 'host-maintenance', label: 'Session maintenance', detail: 'Review continuity, compaction, and current-session maintenance posture.', command: '/health maintenance', kind: 'command', safety: 'read-only' },
15
+ { id: 'host-continuity', label: 'Continuity', detail: 'Inspect last-session pointer, recovery-file posture, and return-context state.', command: '/health continuity', kind: 'command', safety: 'read-only' },
16
+ { id: 'host-tasks', label: 'Host tasks', detail: 'Inspect connected-host task state without creating, retrying, or mutating tasks.', command: '/tasks list', kind: 'command', safety: 'read-only' },
17
+ { id: 'host-task-filter', label: 'Filter host tasks', detail: 'Open a status/type form for read-only connected-host task filtering.', editorKind: 'task-list-filter', kind: 'editor', safety: 'read-only' },
18
+ { id: 'host-task-show', label: 'Inspect host task', detail: 'Open a task-id form for read-only connected-host task metadata.', editorKind: 'task-show', kind: 'editor', safety: 'read-only' },
19
+ { id: 'host-task-output', label: 'Show task output', detail: 'Open a task-id form for read-only connected-host task output.', editorKind: 'task-output', kind: 'editor', safety: 'read-only' },
20
+ { id: 'host-sessions', label: 'Host session routes', detail: 'Browse saved Agent sessions and return-context posture exposed through the runtime session surface.', command: '/session list', kind: 'command', safety: 'read-only' },
21
+ { id: 'host-session-graph', label: 'Session graph', detail: 'Open a read-only form for cross-session graph inspection; graph mutation remains blocked in Agent.', editorKind: 'session-graph', kind: 'editor', safety: 'read-only' },
22
+ { id: 'host-channels-status', label: 'Channel status', detail: 'Inspect connected channel runtime status from the host without mutating delivery state.', command: '/channels status', kind: 'command', safety: 'read-only' },
23
+ { id: 'host-channel-accounts', label: 'Channel accounts', detail: 'Inspect connected channel accounts without printing secret values or sending messages.', command: '/channels accounts', kind: 'command', safety: 'read-only' },
24
+ { id: 'host-channel-policies', label: 'Channel policies', detail: 'Inspect channel delivery policy posture without changing routing.', command: '/channels policies', kind: 'command', safety: 'read-only' },
25
+ { id: 'host-channels', label: 'Open Channels', detail: 'Jump to the channel setup and delivery workspace.', targetCategoryId: 'channels', kind: 'workspace', safety: 'safe' },
26
+ { id: 'host-automation', label: 'Open Automation', detail: 'Jump to connected schedules, reminders, receipts, and explicit run controls.', targetCategoryId: 'automation', kind: 'workspace', safety: 'safe' },
27
+ { id: 'host-schedules', label: 'Schedule status', detail: 'Inspect schedules and run history without running or mutating them.', command: '/schedule list', kind: 'command', safety: 'read-only' },
28
+ { id: 'host-schedule-reconcile', label: 'Reconcile schedules', detail: 'Compare local promotion receipts with live connected schedules.', command: '/schedule reconcile', kind: 'command', safety: 'read-only' },
29
+ { id: 'host-knowledge', label: 'Knowledge route', detail: 'Inspect isolated Agent Knowledge route readiness and source counts.', command: '/knowledge status', kind: 'command', safety: 'read-only' },
30
+ { id: 'host-knowledge-open', label: 'Open Knowledge', detail: 'Jump to Agent Knowledge source, graph, review, and ingest controls.', targetCategoryId: 'knowledge', kind: 'workspace', safety: 'safe' },
31
+ { id: 'host-media', label: 'Media providers', detail: 'Inspect configured media providers before generating media with confirmation.', command: '/media providers', kind: 'command', safety: 'read-only' },
32
+ { id: 'host-media-open', label: 'Open Voice and Media', detail: 'Jump to voice, TTS, image input, media provider, and browser-tool setup actions.', targetCategoryId: 'voice-media', kind: 'workspace', safety: 'safe' },
33
+ { id: 'host-mcp-health', label: 'MCP health', detail: 'Inspect MCP lifecycle issues without changing trust posture or approving tool-definition review.', command: '/health mcp', kind: 'command', safety: 'read-only' },
34
+ { id: 'host-mcp-review', label: 'MCP review', detail: 'Inspect MCP server connection, trust, role, and review posture.', command: '/mcp review', kind: 'command', safety: 'read-only' },
35
+ { id: 'host-tools-open', label: 'Open Tools and MCP', detail: 'Jump to MCP server setup, tool inventory, secrets, and security review.', targetCategoryId: 'tools', kind: 'workspace', safety: 'safe' },
36
+ { id: 'host-provider-accounts', label: 'Provider accounts', detail: 'Review provider account routes, subscription windows, and billing-path safety.', command: '/accounts review', kind: 'command', safety: 'read-only' },
37
+ { id: 'host-provider-detail', label: 'Provider detail', detail: 'Open a provider-id form for account and provider configuration review.', editorKind: 'provider-inspect', kind: 'editor', safety: 'read-only' },
38
+ { id: 'host-provider-routes', label: 'Provider routes', detail: 'Open a provider-id form for account, subscription, and route inspection.', editorKind: 'provider-routes', kind: 'editor', safety: 'read-only' },
39
+ { id: 'host-provider-repair', label: 'Provider repair guidance', detail: 'Open a provider-id form for read-only provider repair guidance.', editorKind: 'provider-account-repair', kind: 'editor', safety: 'read-only' },
40
+ { id: 'host-auth-owner', label: 'Connected-host auth owner', detail: 'Show that connected-host auth administration belongs to the owning GoodVibes host, not Agent.', command: '/auth local', kind: 'command', safety: 'read-only' },
41
+ { id: 'host-auth-review', label: 'Provider auth review', detail: 'Review provider auth posture without printing token values.', command: '/auth review', kind: 'command', safety: 'read-only' },
42
+ { id: 'host-auth-detail', label: 'Provider auth detail', detail: 'Open a provider-id form for read-only provider auth inspection.', editorKind: 'auth-show', kind: 'editor', safety: 'read-only' },
43
+ { id: 'host-auth-repair', label: 'Provider auth repair', detail: 'Open a provider-id form for provider auth repair guidance.', editorKind: 'auth-repair', kind: 'editor', safety: 'read-only' },
44
+ { id: 'host-auth-bundle-export', label: 'Export auth bundle', detail: 'Open a confirmed form that exports a redacted provider auth review bundle.', editorKind: 'auth-bundle-export', kind: 'editor', safety: 'safe' },
45
+ { id: 'host-auth-bundle-inspect', label: 'Inspect auth bundle', detail: 'Open a form that inspects a provider auth review bundle before sharing or import.', editorKind: 'auth-bundle-inspect', kind: 'editor', safety: 'read-only' },
46
+ { id: 'host-subscription-bundle-export', label: 'Export subscription bundle', detail: 'Open a confirmed form that exports redacted provider subscription state for review.', editorKind: 'subscription-bundle-export', kind: 'editor', safety: 'safe' },
47
+ { id: 'host-subscription-bundle-inspect', label: 'Inspect subscription bundle', detail: 'Open a form that inspects provider subscription state before sharing.', editorKind: 'subscription-bundle-inspect', kind: 'editor', safety: 'read-only' },
48
+ { id: 'host-security', label: 'Security review', detail: 'Inspect token posture, MCP attack paths, policy lint, plugin risk, and incident pressure.', command: '/security review', kind: 'command', safety: 'read-only' },
49
+ { id: 'host-trust', label: 'Trust review', detail: 'Review permission, secret, plugin, and MCP trust posture without exporting bundles or changing trust.', command: '/trust review', kind: 'command', safety: 'read-only' },
50
+ { id: 'host-support-bundle-export', label: 'Export support bundle', detail: 'Export a redacted Agent support bundle from the Host page.', editorKind: 'support-bundle-export', kind: 'editor', safety: 'safe' },
51
+ { id: 'host-support-bundle-inspect', label: 'Inspect support bundle', detail: 'Inspect a support bundle before import or sharing.', editorKind: 'support-bundle-inspect', kind: 'editor', safety: 'read-only' },
52
+ { id: 'host-support-bundle-import', label: 'Import support bundle', detail: 'Import reviewed, non-redacted config values from a support bundle.', editorKind: 'support-bundle-import', kind: 'editor', safety: 'safe' },
53
+ { id: 'host-telemetry', label: 'Telemetry settings', detail: 'Open telemetry payload policy settings for connected-host and Agent observability review.', command: '/config telemetry', kind: 'command', safety: 'safe' },
54
+ { id: 'host-config', label: 'Control-plane settings', detail: 'Open configuration controls for service, channels, tools, automation, provider, and storage posture.', command: '/config', kind: 'command', safety: 'safe' },
55
+ { id: 'host-safety', label: 'Host mutation policy', detail: 'Agent may inspect connected-host surfaces and run explicit confirmed actions, but does not silently create tasks, send channel messages, mutate auth, or start automation.', kind: 'guidance', safety: 'blocked' },
56
+ ],
57
+ };
@@ -0,0 +1,193 @@
1
+ import type { ShellPathService } from '@/runtime/index.ts';
2
+ import { AgentNoteRegistry } from '../agent/note-registry.ts';
3
+ import { AgentPersonaRegistry } from '../agent/persona-registry.ts';
4
+ import { AgentRoutineRegistry } from '../agent/routine-registry.ts';
5
+ import { createAgentRuntimeProfile, type AgentRuntimeProfileInfo } from '../agent/runtime-profile.ts';
6
+ import { AgentSkillRegistry } from '../agent/skill-registry.ts';
7
+ import { createAgentWorkspaceLearnedBehavior } from './agent-workspace-learned-behavior.ts';
8
+ import { buildAgentWorkspaceRequirements } from './agent-workspace-requirements.ts';
9
+ import { isAffirmative, splitList } from './agent-workspace-editors.ts';
10
+ import type { AgentWorkspaceLocalEditor, AgentWorkspaceLocalEditorKind } from './agent-workspace-types.ts';
11
+
12
+ type LearnedBehaviorTarget = Exclude<AgentWorkspaceLocalEditorKind, 'memory' | 'note' | 'profile'>;
13
+
14
+ export interface AgentWorkspaceLocalEditorSubmissionCallbacks {
15
+ readonly readField: (id: string) => string;
16
+ readonly learnedBehaviorTarget: () => LearnedBehaviorTarget;
17
+ readonly submitDeleteEditor: () => void;
18
+ readonly finishLocalEditor: (kind: AgentWorkspaceLocalEditorKind, id: string, name: string, verb: 'Created' | 'Updated') => void;
19
+ readonly finishProfileEditor: (profile: AgentRuntimeProfileInfo) => void;
20
+ }
21
+
22
+ export function submitAgentWorkspaceLocalRegistryEditor(
23
+ shellPaths: ShellPathService,
24
+ editor: AgentWorkspaceLocalEditor,
25
+ callbacks: AgentWorkspaceLocalEditorSubmissionCallbacks,
26
+ ): void {
27
+ const field = callbacks.readField;
28
+ if (editor.mode === 'delete') {
29
+ callbacks.submitDeleteEditor();
30
+ return;
31
+ }
32
+ if (editor.kind === 'learned-behavior') {
33
+ const created = createAgentWorkspaceLearnedBehavior(shellPaths, {
34
+ target: callbacks.learnedBehaviorTarget(),
35
+ name: field('name'),
36
+ description: field('description'),
37
+ notes: field('notes'),
38
+ tags: splitList(field('tags')),
39
+ triggers: splitList(field('triggers')),
40
+ enable: isAffirmative(field('enable')),
41
+ });
42
+ callbacks.finishLocalEditor(created.kind, created.id, created.name, 'Created');
43
+ } else if (editor.kind === 'profile') {
44
+ const template = field('template');
45
+ const templateId = template && template.toLowerCase() !== 'none' ? template : undefined;
46
+ const profile = createAgentRuntimeProfile(shellPaths.homeDirectory, field('name'), {
47
+ ...(templateId ? { templateId } : {}),
48
+ });
49
+ callbacks.finishProfileEditor(profile);
50
+ } else if (editor.kind === 'note') {
51
+ submitNoteEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
52
+ } else if (editor.kind === 'persona') {
53
+ submitPersonaEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
54
+ } else if (editor.kind === 'skill') {
55
+ submitSkillEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
56
+ } else {
57
+ submitRoutineEditor(shellPaths, editor, field, callbacks.finishLocalEditor);
58
+ }
59
+ }
60
+
61
+ function submitNoteEditor(
62
+ shellPaths: ShellPathService,
63
+ editor: AgentWorkspaceLocalEditor,
64
+ field: (id: string) => string,
65
+ finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
66
+ ): void {
67
+ const registry = AgentNoteRegistry.fromShellPaths(shellPaths);
68
+ if (editor.mode === 'update' && editor.recordId) {
69
+ const updated = registry.update(editor.recordId, {
70
+ title: field('title'),
71
+ body: field('body'),
72
+ sourceUrl: field('sourceUrl'),
73
+ tags: splitList(field('tags')),
74
+ provenance: 'Workspace',
75
+ });
76
+ finish('note', updated.id, updated.title, 'Updated');
77
+ return;
78
+ }
79
+ const created = registry.create({
80
+ title: field('title'),
81
+ body: field('body'),
82
+ sourceUrl: field('sourceUrl'),
83
+ tags: splitList(field('tags')),
84
+ source: 'user',
85
+ provenance: 'Workspace',
86
+ });
87
+ finish('note', created.id, created.title, 'Created');
88
+ }
89
+
90
+ function submitPersonaEditor(
91
+ shellPaths: ShellPathService,
92
+ editor: AgentWorkspaceLocalEditor,
93
+ field: (id: string) => string,
94
+ finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
95
+ ): void {
96
+ const registry = AgentPersonaRegistry.fromShellPaths(shellPaths);
97
+ if (editor.mode === 'update' && editor.recordId) {
98
+ const wasActive = registry.snapshot().activePersonaId === editor.recordId;
99
+ const updated = registry.update(editor.recordId, {
100
+ name: field('name'),
101
+ description: field('description'),
102
+ body: field('body'),
103
+ tags: splitList(field('tags')),
104
+ triggers: splitList(field('triggers')),
105
+ provenance: 'Workspace',
106
+ });
107
+ if (isAffirmative(field('activate'))) registry.setActive(updated.id);
108
+ else if (wasActive) registry.clearActive();
109
+ finish('persona', updated.id, updated.name, 'Updated');
110
+ return;
111
+ }
112
+ const created = registry.create({
113
+ name: field('name'),
114
+ description: field('description'),
115
+ body: field('body'),
116
+ tags: splitList(field('tags')),
117
+ triggers: splitList(field('triggers')),
118
+ source: 'user',
119
+ provenance: 'Workspace',
120
+ });
121
+ if (isAffirmative(field('activate'))) registry.setActive(created.id);
122
+ finish('persona', created.id, created.name, 'Created');
123
+ }
124
+
125
+ function submitSkillEditor(
126
+ shellPaths: ShellPathService,
127
+ editor: AgentWorkspaceLocalEditor,
128
+ field: (id: string) => string,
129
+ finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
130
+ ): void {
131
+ const registry = AgentSkillRegistry.fromShellPaths(shellPaths);
132
+ if (editor.mode === 'update' && editor.recordId) {
133
+ const updated = registry.update(editor.recordId, {
134
+ name: field('name'),
135
+ description: field('description'),
136
+ procedure: field('procedure'),
137
+ triggers: splitList(field('triggers')),
138
+ tags: splitList(field('tags')),
139
+ requirements: buildAgentWorkspaceRequirements(field),
140
+ provenance: 'Workspace',
141
+ });
142
+ registry.setEnabled(updated.id, isAffirmative(field('enabled')));
143
+ finish('skill', updated.id, updated.name, 'Updated');
144
+ return;
145
+ }
146
+ const created = registry.create({
147
+ name: field('name'),
148
+ description: field('description'),
149
+ procedure: field('procedure'),
150
+ triggers: splitList(field('triggers')),
151
+ tags: splitList(field('tags')),
152
+ requirements: buildAgentWorkspaceRequirements(field),
153
+ enabled: isAffirmative(field('enabled')),
154
+ source: 'user',
155
+ provenance: 'Workspace',
156
+ });
157
+ finish('skill', created.id, created.name, 'Created');
158
+ }
159
+
160
+ function submitRoutineEditor(
161
+ shellPaths: ShellPathService,
162
+ editor: AgentWorkspaceLocalEditor,
163
+ field: (id: string) => string,
164
+ finish: AgentWorkspaceLocalEditorSubmissionCallbacks['finishLocalEditor'],
165
+ ): void {
166
+ const registry = AgentRoutineRegistry.fromShellPaths(shellPaths);
167
+ if (editor.mode === 'update' && editor.recordId) {
168
+ const updated = registry.update(editor.recordId, {
169
+ name: field('name'),
170
+ description: field('description'),
171
+ steps: field('steps'),
172
+ triggers: splitList(field('triggers')),
173
+ tags: splitList(field('tags')),
174
+ requirements: buildAgentWorkspaceRequirements(field),
175
+ provenance: 'Workspace',
176
+ });
177
+ registry.setEnabled(updated.id, isAffirmative(field('enabled')));
178
+ finish('routine', updated.id, updated.name, 'Updated');
179
+ return;
180
+ }
181
+ const created = registry.create({
182
+ name: field('name'),
183
+ description: field('description'),
184
+ steps: field('steps'),
185
+ triggers: splitList(field('triggers')),
186
+ tags: splitList(field('tags')),
187
+ requirements: buildAgentWorkspaceRequirements(field),
188
+ enabled: isAffirmative(field('enabled')),
189
+ source: 'user',
190
+ provenance: 'Workspace',
191
+ });
192
+ finish('routine', created.id, created.name, 'Created');
193
+ }
@@ -40,6 +40,9 @@ export function searchAgentWorkspaceActions(
40
40
  action.command ?? '',
41
41
  action.editorKind ?? '',
42
42
  action.targetCategoryId ?? '',
43
+ action.modelPickerFlow ?? '',
44
+ action.modelPickerTarget ?? '',
45
+ action.settingsTarget ?? '',
43
46
  action.localOperation ?? '',
44
47
  action.safety,
45
48
  ].join(' ').toLowerCase();
@@ -80,6 +83,9 @@ function scoreActionSearchResult(
80
83
  score += scoreField(action.editorKind, terms, exactQuery, 85, 26);
81
84
  score += scoreField(action.command, terms, exactQuery, 75, 22);
82
85
  score += scoreField(action.label, terms, exactQuery, 65, 18);
86
+ score += scoreField(action.modelPickerTarget, terms, exactQuery, 58, 18);
87
+ score += scoreField(action.settingsTarget, terms, exactQuery, 58, 18);
88
+ score += scoreField(action.modelPickerFlow, terms, exactQuery, 54, 16);
83
89
  score += scoreField(action.localOperation, terms, exactQuery, 50, 16);
84
90
  score += scoreField(action.targetCategoryId, terms, exactQuery, 40, 12);
85
91
  score += scoreField(action.detail, terms, exactQuery, 24, 6);
@@ -14,6 +14,9 @@ export interface AgentWorkspaceSetupChecklistInput {
14
14
  readonly provider: string;
15
15
  readonly model: string;
16
16
  readonly runtimeBaseUrl: string;
17
+ readonly activeSubscriptionCount: number;
18
+ readonly pendingSubscriptionCount: number;
19
+ readonly availableSubscriptionProviderCount: number;
17
20
  readonly sessionMemoryCount: number;
18
21
  readonly localMemoryCount: number;
19
22
  readonly localMemoryReviewQueueCount: number;
@@ -69,6 +72,23 @@ export function buildAgentWorkspaceSetupChecklist(input: AgentWorkspaceSetupChec
69
72
  : 'Choose a provider and model before relying on assistant turns.',
70
73
  command: 'Setup -> Provider and model',
71
74
  },
75
+ {
76
+ id: 'subscriptions',
77
+ label: 'Provider subscriptions',
78
+ status: input.activeSubscriptionCount > 0
79
+ ? 'ready'
80
+ : input.pendingSubscriptionCount > 0
81
+ ? 'recommended'
82
+ : input.availableSubscriptionProviderCount > 0 ? 'recommended' : 'optional',
83
+ detail: input.activeSubscriptionCount > 0
84
+ ? `${input.activeSubscriptionCount} provider subscription session(s) are active.`
85
+ : input.pendingSubscriptionCount > 0
86
+ ? `${input.pendingSubscriptionCount} provider subscription login(s) are pending completion.`
87
+ : input.availableSubscriptionProviderCount > 0
88
+ ? `${input.availableSubscriptionProviderCount} subscription-capable provider(s) are available. Start login if you want subscription routing.`
89
+ : 'No subscription-capable providers are available yet. Use API keys or add an OAuth provider service.',
90
+ command: 'Start -> Start subscription login',
91
+ },
72
92
  {
73
93
  id: 'agent-knowledge',
74
94
  label: 'Agent Knowledge',
@@ -1,4 +1,5 @@
1
1
  import { basename, sep } from 'node:path';
2
+ import { listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
2
3
  import type { MemoryRecord } from '@pellux/goodvibes-sdk/platform/state';
3
4
  import type { CommandContext } from './command-registry.ts';
4
5
  import { AgentNoteRegistry, type AgentNoteRecord } from '../agent/note-registry.ts';
@@ -398,6 +399,41 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
398
399
  const ttsVoice = readConfigString(context, 'tts.voice', '(voice default)');
399
400
  const ttsLlmProvider = readConfigString(context, 'tts.llmProvider', '');
400
401
  const ttsLlmModel = readConfigString(context, 'tts.llmModel', '');
402
+ const embeddingProvider = readConfigString(context, 'provider.embeddingProvider', '(provider default)');
403
+ const reasoningEffort = readConfigString(context, 'provider.reasoningEffort', '(default)');
404
+ const helperEnabled = readConfigBoolean(context, 'helper.enabled', false);
405
+ const toolLlmEnabled = readConfigBoolean(context, 'tools.llmEnabled', false);
406
+ const providerFailureHints = readConfigBoolean(context, 'behavior.suggestAlternativeOnProviderFail', false);
407
+ const cacheEnabled = readConfigBoolean(context, 'cache.enabled', true);
408
+ const cacheStableTtl = readConfigString(context, 'cache.stableTtl', '(default)');
409
+ const cacheMonitorHitRate = readConfigBoolean(context, 'cache.monitorHitRate', true);
410
+ const cacheHitRateWarningThreshold = readConfigNumber(context, 'cache.hitRateWarningThreshold', 0.3);
411
+ const hitlMode = readConfigString(context, 'behavior.hitlMode', '(default)');
412
+ const guidanceMode = readConfigString(context, 'behavior.guidanceMode', '(default)');
413
+ const saveHistory = readConfigBoolean(context, 'behavior.saveHistory', true);
414
+ const autoApprove = readConfigBoolean(context, 'behavior.autoApprove', false);
415
+ const autoCompactThreshold = readConfigNumber(context, 'behavior.autoCompactThreshold', 0);
416
+ const staleContextWarnings = readConfigBoolean(context, 'behavior.staleContextWarnings', false);
417
+ const showThinking = readConfigBoolean(context, 'display.showThinking', false);
418
+ const showReasoningSummary = readConfigBoolean(context, 'display.showReasoningSummary', false);
419
+ const theme = readConfigString(context, 'display.theme', '(default)');
420
+ const stream = readConfigBoolean(context, 'display.stream', true);
421
+ const lineNumbers = readConfigString(context, 'display.lineNumbers', '(default)');
422
+ const operationalMessages = readConfigString(context, 'ui.operationalMessages', '(default)');
423
+ const systemMessages = readConfigString(context, 'ui.systemMessages', '(default)');
424
+ const releaseChannel = readConfigString(context, 'release.channel', '(default)');
425
+ const permissionMode = readConfigString(context, 'permissions.mode', '(default)');
426
+ const toolAutoHeal = readConfigBoolean(context, 'tools.autoHeal', false);
427
+ const toolsDefaultTokenBudget = readConfigNumber(context, 'tools.defaultTokenBudget', 5000);
428
+ const artifactMaxBytes = readConfigNumber(context, 'storage.artifacts.maxBytes', 512 * 1024 * 1024);
429
+ const rawPromptTelemetry = readConfigBoolean(context, 'telemetry.includeRawPrompts', false);
430
+ const automationEnabled = readConfigBoolean(context, 'automation.enabled', false);
431
+ const automationMaxConcurrentRuns = readConfigNumber(context, 'automation.maxConcurrentRuns', 4);
432
+ const automationRunHistoryLimit = readConfigNumber(context, 'automation.runHistoryLimit', 100);
433
+ const automationDefaultTimeoutMs = readConfigNumber(context, 'automation.defaultTimeoutMs', 15 * 60 * 1000);
434
+ const automationCatchUpWindowMinutes = readConfigNumber(context, 'automation.catchUpWindowMinutes', 30);
435
+ const automationFailureCooldownMs = readConfigNumber(context, 'automation.failureCooldownMs', 5 * 60 * 1000);
436
+ const automationDeleteAfterRun = readConfigBoolean(context, 'automation.deleteAfterRun', false);
401
437
  const runtimeBaseUrl = `http://${host}:${port}`;
402
438
  const companionAccess = (() => {
403
439
  const homeDirectory = context.workspace?.shellPaths?.homeDirectory ?? '';
@@ -425,6 +461,18 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
425
461
  nextStep,
426
462
  } as const;
427
463
  })();
464
+ const subscriptionSnapshot = (() => {
465
+ try {
466
+ const manager = context.platform?.subscriptionManager;
467
+ const services = context.platform?.serviceRegistry;
468
+ const active = manager?.list?.().length ?? 0;
469
+ const pending = manager?.listPending?.().length ?? 0;
470
+ const available = services ? listAvailableSubscriptionProviders(services.getAll()).length : 0;
471
+ return { active, pending, available };
472
+ } catch {
473
+ return { active: 0, pending: 0, available: 0 };
474
+ }
475
+ })();
428
476
  const channels = buildAgentWorkspaceChannels(context);
429
477
  const voiceMediaReadiness = buildAgentWorkspaceVoiceMediaReadiness({
430
478
  context,
@@ -435,6 +483,9 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
435
483
  provider,
436
484
  model,
437
485
  runtimeBaseUrl,
486
+ activeSubscriptionCount: subscriptionSnapshot.active,
487
+ pendingSubscriptionCount: subscriptionSnapshot.pending,
488
+ availableSubscriptionProviderCount: subscriptionSnapshot.available,
438
489
  sessionMemoryCount,
439
490
  localMemoryCount: memorySnapshot.count,
440
491
  localMemoryReviewQueueCount: memorySnapshot.reviewQueueCount,
@@ -463,11 +514,49 @@ export function buildAgentWorkspaceRuntimeSnapshot(context: CommandContext): Age
463
514
  provider,
464
515
  model,
465
516
  modelDisplayName: currentModel?.displayName ?? model,
517
+ embeddingProvider,
518
+ reasoningEffort,
519
+ helperEnabled,
520
+ toolLlmEnabled,
521
+ providerFailureHints,
522
+ cacheEnabled,
523
+ cacheStableTtl,
524
+ cacheMonitorHitRate,
525
+ cacheHitRateWarningThreshold,
526
+ hitlMode,
527
+ guidanceMode,
528
+ saveHistory,
529
+ autoApprove,
530
+ autoCompactThreshold,
531
+ staleContextWarnings,
532
+ showThinking,
533
+ showReasoningSummary,
534
+ theme,
535
+ stream,
536
+ lineNumbers,
537
+ operationalMessages,
538
+ systemMessages,
539
+ releaseChannel,
540
+ permissionMode,
541
+ toolAutoHeal,
542
+ toolsDefaultTokenBudget,
543
+ artifactMaxBytes,
544
+ rawPromptTelemetry,
545
+ automationEnabled,
546
+ automationMaxConcurrentRuns,
547
+ automationRunHistoryLimit,
548
+ automationDefaultTimeoutMs,
549
+ automationCatchUpWindowMinutes,
550
+ automationFailureCooldownMs,
551
+ automationDeleteAfterRun,
466
552
  sessionId: context.session?.runtime?.sessionId ?? 'unknown',
467
553
  workingDirectory: context.workspace?.shellPaths?.workingDirectory ?? 'unavailable',
468
554
  homeDirectory: context.workspace?.shellPaths?.homeDirectory ?? 'unavailable',
469
555
  runtimeBaseUrl,
470
556
  runtimeOwnership: 'external',
557
+ activeSubscriptionCount: subscriptionSnapshot.active,
558
+ pendingSubscriptionCount: subscriptionSnapshot.pending,
559
+ availableSubscriptionProviderCount: subscriptionSnapshot.available,
471
560
  sessionMemoryCount,
472
561
  localMemoryCount: memorySnapshot.count,
473
562
  localMemoryReviewQueueCount: memorySnapshot.reviewQueueCount,
@@ -0,0 +1,214 @@
1
+ import { beginOpenAICodexLogin, exchangeOpenAICodexCode, getSubscriptionProviderConfig } from '@pellux/goodvibes-sdk/platform/config';
2
+ import type { OAuthProviderConfig, ProviderSubscription } from '@pellux/goodvibes-sdk/platform/config';
3
+ import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils';
4
+ import type { CommandContext } from './command-registry.ts';
5
+ import { isAffirmative } from './agent-workspace-editors.ts';
6
+ import { buildAgentWorkspaceRuntimeSnapshot } from './agent-workspace-snapshot.ts';
7
+ import type { AgentWorkspaceActionResult, AgentWorkspaceLocalEditor, AgentWorkspaceRuntimeSnapshot } from './agent-workspace-types.ts';
8
+
9
+ export interface AgentWorkspaceSubscriptionEditorHost {
10
+ localEditor: AgentWorkspaceLocalEditor | null;
11
+ runtimeSnapshot: AgentWorkspaceRuntimeSnapshot | null;
12
+ status: string;
13
+ lastActionResult: AgentWorkspaceActionResult | null;
14
+ clampSelection(): void;
15
+ }
16
+
17
+ export type AgentWorkspaceSubscriptionFieldReader = (id: string) => string;
18
+
19
+ export async function submitAgentWorkspaceSubscriptionLoginStartEditor(
20
+ host: AgentWorkspaceSubscriptionEditorHost,
21
+ editor: AgentWorkspaceLocalEditor,
22
+ context: CommandContext | null,
23
+ readField: AgentWorkspaceSubscriptionFieldReader,
24
+ ): Promise<void> {
25
+ if (!isAffirmative(readField('confirm'))) {
26
+ host.localEditor = { ...editor, message: 'Subscription login start not confirmed. Type yes, then press Enter.' };
27
+ host.status = 'Subscription login start not confirmed.';
28
+ return;
29
+ }
30
+ try {
31
+ const provider = readField('provider') || 'openai';
32
+ const openBrowser = isAffirmative(readField('openBrowser'));
33
+ const { manager, services } = subscriptionServices(context);
34
+ const resolved = getSubscriptionProviderConfig(provider, services.get(provider));
35
+ if (!resolved) {
36
+ throw new Error(`OAuth is not configured for ${provider}. Add an OAuth provider service or choose a built-in subscription provider.`);
37
+ }
38
+
39
+ const started = provider === 'openai' && resolved.source === 'builtin'
40
+ ? await beginOpenAICodexLogin()
41
+ : null;
42
+ const authorizationUrl = started
43
+ ? started.authorizationUrl
44
+ : (await manager.beginOAuthLogin(provider, resolveManualLoginConfig(resolved.oauth))).authorizationUrl;
45
+
46
+ if (started) {
47
+ manager.savePending({
48
+ provider,
49
+ state: started.state,
50
+ verifier: started.verifier,
51
+ redirectUri: started.redirectUri,
52
+ createdAt: Date.now(),
53
+ });
54
+ }
55
+
56
+ const browserOpened = openBrowser ? await openExternalUrl(authorizationUrl) : false;
57
+ host.localEditor = null;
58
+ host.runtimeSnapshot = context ? buildAgentWorkspaceRuntimeSnapshot(context) : host.runtimeSnapshot;
59
+ host.status = `Subscription login started for ${provider}.`;
60
+ host.lastActionResult = {
61
+ kind: 'refreshed',
62
+ title: 'Subscription login started',
63
+ detail: [
64
+ `Provider: ${provider}.`,
65
+ `Browser: ${openBrowser ? (browserOpened ? 'opened' : 'open failed') : 'skipped'}.`,
66
+ 'Use Finish subscription login with the callback code or redirect URL.',
67
+ `Authorization URL: ${authorizationUrl}`,
68
+ ].join(' '),
69
+ safety: 'safe',
70
+ };
71
+ host.clampSelection();
72
+ } catch (error) {
73
+ const detail = error instanceof Error ? error.message : String(error);
74
+ host.localEditor = { ...editor, message: detail };
75
+ host.status = detail;
76
+ host.lastActionResult = { kind: 'error', title: 'Subscription login start failed', detail, safety: 'safe' };
77
+ }
78
+ }
79
+
80
+ export async function submitAgentWorkspaceSubscriptionLoginFinishEditor(
81
+ host: AgentWorkspaceSubscriptionEditorHost,
82
+ editor: AgentWorkspaceLocalEditor,
83
+ context: CommandContext | null,
84
+ readField: AgentWorkspaceSubscriptionFieldReader,
85
+ ): Promise<void> {
86
+ if (!isAffirmative(readField('confirm'))) {
87
+ host.localEditor = { ...editor, message: 'Subscription login finish not confirmed. Type yes, then press Enter.' };
88
+ host.status = 'Subscription login finish not confirmed.';
89
+ return;
90
+ }
91
+ try {
92
+ const provider = readField('provider') || 'openai';
93
+ const codeInput = readField('code');
94
+ const code = extractAuthorizationCode(codeInput) ?? codeInput;
95
+ const { manager, services } = subscriptionServices(context);
96
+ const resolved = getSubscriptionProviderConfig(provider, services.get(provider));
97
+ if (!resolved) {
98
+ throw new Error(`OAuth is not configured for ${provider}. Start with a configured or built-in subscription provider.`);
99
+ }
100
+
101
+ const record = provider === 'openai' && resolved.source === 'builtin'
102
+ ? (() => {
103
+ const pending = manager.getPending(provider);
104
+ if (!pending) throw new Error(`No pending OAuth login for ${provider}. Start subscription login first.`);
105
+ return exchangeOpenAICodexCode(code, pending.verifier).then((token) => {
106
+ const now = Date.now();
107
+ return manager.saveSubscription({
108
+ provider,
109
+ accessToken: token.accessToken,
110
+ tokenType: token.tokenType,
111
+ ...(typeof token.refreshToken === 'string' && token.refreshToken.length > 0
112
+ ? { refreshToken: token.refreshToken }
113
+ : {}),
114
+ ...(typeof token.expiresAt === 'number' && Number.isFinite(token.expiresAt)
115
+ ? { expiresAt: token.expiresAt }
116
+ : {}),
117
+ ...(token.scopes ? { scopes: token.scopes } : {}),
118
+ authMode: 'oauth',
119
+ overrideAmbientApiKeys: false,
120
+ createdAt: manager.get(provider)?.createdAt ?? now,
121
+ updatedAt: now,
122
+ });
123
+ });
124
+ })()
125
+ : manager.completeOAuthLogin(provider, resolveManualLoginConfig(resolved.oauth), code);
126
+
127
+ const saved = await record;
128
+ host.localEditor = null;
129
+ host.runtimeSnapshot = context ? buildAgentWorkspaceRuntimeSnapshot(context) : host.runtimeSnapshot;
130
+ host.status = `Subscription session saved for ${provider}.`;
131
+ host.lastActionResult = {
132
+ kind: 'refreshed',
133
+ title: 'Subscription session saved',
134
+ detail: [
135
+ `Provider: ${provider}.`,
136
+ `Token type: ${saved.tokenType}.`,
137
+ `Expires: ${saved.expiresAt ? new Date(saved.expiresAt).toISOString() : 'n/a'}.`,
138
+ describeSubscriptionPrecedence(saved),
139
+ ].join(' '),
140
+ safety: 'safe',
141
+ };
142
+ host.clampSelection();
143
+ } catch (error) {
144
+ const detail = error instanceof Error ? error.message : String(error);
145
+ host.localEditor = { ...editor, message: detail };
146
+ host.status = detail;
147
+ host.lastActionResult = { kind: 'error', title: 'Subscription login finish failed', detail, safety: 'safe' };
148
+ }
149
+ }
150
+
151
+ export function submitAgentWorkspaceSubscriptionLogoutEditor(
152
+ host: AgentWorkspaceSubscriptionEditorHost,
153
+ editor: AgentWorkspaceLocalEditor,
154
+ context: CommandContext | null,
155
+ readField: AgentWorkspaceSubscriptionFieldReader,
156
+ ): void {
157
+ if (!isAffirmative(readField('confirm'))) {
158
+ host.localEditor = { ...editor, message: 'Subscription logout not confirmed. Type yes, then press Enter.' };
159
+ host.status = 'Subscription logout not confirmed.';
160
+ return;
161
+ }
162
+ try {
163
+ const provider = readField('provider') || 'openai';
164
+ const { manager } = subscriptionServices(context);
165
+ const removed = manager.logout(provider);
166
+ host.localEditor = null;
167
+ host.runtimeSnapshot = context ? buildAgentWorkspaceRuntimeSnapshot(context) : host.runtimeSnapshot;
168
+ host.status = removed ? `Logged out of ${provider}.` : `No subscription session existed for ${provider}.`;
169
+ host.lastActionResult = {
170
+ kind: removed ? 'refreshed' : 'guidance',
171
+ title: removed ? 'Subscription session removed' : 'No subscription session found',
172
+ detail: removed
173
+ ? `Removed active or pending subscription state for ${provider}. Ambient API key resolution applies if configured.`
174
+ : `No active or pending subscription state existed for ${provider}.`,
175
+ safety: 'safe',
176
+ };
177
+ host.clampSelection();
178
+ } catch (error) {
179
+ const detail = error instanceof Error ? error.message : String(error);
180
+ host.localEditor = { ...editor, message: detail };
181
+ host.status = detail;
182
+ host.lastActionResult = { kind: 'error', title: 'Subscription logout failed', detail, safety: 'safe' };
183
+ }
184
+ }
185
+
186
+ function subscriptionServices(context: CommandContext | null) {
187
+ const manager = context?.platform.subscriptionManager;
188
+ const services = context?.platform.serviceRegistry;
189
+ if (!manager || !services) throw new Error('Subscription services are unavailable in this runtime.');
190
+ return { manager, services };
191
+ }
192
+
193
+ function extractAuthorizationCode(input: string): string | null {
194
+ const trimmed = input.trim();
195
+ if (!trimmed) return null;
196
+ try {
197
+ const url = new URL(trimmed);
198
+ return url.searchParams.get('code');
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
204
+ function resolveManualLoginConfig(config: OAuthProviderConfig): OAuthProviderConfig {
205
+ return config.manualRedirectUri
206
+ ? { ...config, redirectUri: config.manualRedirectUri }
207
+ : config;
208
+ }
209
+
210
+ function describeSubscriptionPrecedence(record: Pick<ProviderSubscription, 'overrideAmbientApiKeys'>): string {
211
+ return record.overrideAmbientApiKeys
212
+ ? 'Subscription routing now overrides ambient API keys for this provider.'
213
+ : 'Subscription session is stored for subscription-backed flows; ambient API keys are unchanged.';
214
+ }