@pellux/goodvibes-tui 1.24.2 → 1.26.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.
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/bin/goodvibes-daemon +24 -0
- package/docs/foundation-artifacts/operator-contract.json +18234 -12253
- package/package.json +8 -7
- package/src/cli/help.ts +10 -0
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +149 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +62 -7
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/handlers/index.ts +7 -6
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +332 -0
- package/src/daemon/send/composition.ts +99 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/input/commands/calendar-runtime.ts +245 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/connection-status.ts +258 -0
- package/src/input/commands/local-provider-runtime.ts +9 -1
- package/src/input/commands/mail-runtime.ts +267 -0
- package/src/input/commands/operator-runtime.ts +6 -1
- package/src/input/commands/payment-card-intake.ts +200 -0
- package/src/input/commands/profile-render.ts +250 -0
- package/src/input/commands/profile-runtime.ts +362 -0
- package/src/input/commands/profile-types.ts +381 -0
- package/src/input/commands/review-runtime.ts +13 -1
- package/src/input/commands.ts +11 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
- package/src/input/payments-config.ts +167 -0
- package/src/input/provider-key-intake.ts +10 -2
- package/src/input/settings-modal-commit.ts +146 -0
- package/src/input/settings-modal-connections.ts +102 -0
- package/src/input/settings-modal-data.ts +30 -0
- package/src/input/settings-modal-reset.ts +6 -2
- package/src/input/settings-modal-secrets.ts +9 -2
- package/src/input/settings-modal-types.ts +19 -3
- package/src/input/settings-modal.ts +54 -107
- package/src/main.ts +7 -2
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/settings-modal-connections.ts +63 -0
- package/src/renderer/settings-modal-helpers.ts +6 -0
- package/src/renderer/settings-modal.ts +21 -1
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap.ts +2 -2
- package/src/runtime/context.ts +9 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +0 -4
- package/src/runtime/install-self-check-startup.ts +23 -0
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/onboarding/types.ts +6 -1
- package/src/runtime/path-shadow-startup.ts +243 -0
- package/src/runtime/path-shadow.ts +418 -0
- package/src/runtime/reachability-notice.ts +100 -0
- package/src/runtime/runtime-services-types.ts +243 -0
- package/src/runtime/services.ts +30 -224
- package/src/shell/ui-openers.ts +7 -0
- package/src/verification/verification-ledger.ts +158 -0
- package/src/version.ts +1 -1
- package/src/daemon/handlers/calendar/caldav-client.ts +0 -662
- package/src/daemon/handlers/calendar/ics.ts +0 -556
- package/src/daemon/handlers/calendar/index.ts +0 -316
- package/src/daemon/handlers/email/config.ts +0 -164
- package/src/daemon/handlers/email/imap-connector.ts +0 -441
- package/src/daemon/handlers/email/imap-parsing.ts +0 -499
- package/src/daemon/handlers/email/index.ts +0 -43
- package/src/daemon/handlers/email/read-handlers.ts +0 -80
- package/src/daemon/handlers/email/runtime.ts +0 -140
- package/src/daemon/handlers/email/smtp-connector.ts +0 -557
- package/src/daemon/handlers/email/validation.ts +0 -147
- package/src/daemon/handlers/email/write-handlers.ts +0 -133
|
@@ -21,16 +21,14 @@ import type { ModelPickerTarget } from './model-picker.ts';
|
|
|
21
21
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
22
22
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
23
23
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
type SettingsSecretsManager,
|
|
28
|
-
} from './settings-modal-secrets.ts';
|
|
24
|
+
import type { SettingsSecretsManager } from './settings-modal-secrets.ts';
|
|
25
|
+
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
26
|
+
import { PAYMENTS_CVV_HANDLING_CONFIG_KEY } from './payments-config.ts';
|
|
29
27
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
30
28
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
29
|
+
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
30
|
+
import type { ConnectionStatus } from './commands/connection-status.ts';
|
|
31
|
+
import { initialConnectionEntries, refreshConnectionEntries } from './settings-modal-connections.ts';
|
|
34
32
|
|
|
35
33
|
import {
|
|
36
34
|
SETTINGS_CATEGORIES,
|
|
@@ -71,6 +69,7 @@ import {
|
|
|
71
69
|
handleResetConfirmKey as _handleResetConfirmKey,
|
|
72
70
|
type ResetConfirmKeyResult,
|
|
73
71
|
} from './settings-modal-reset.ts';
|
|
72
|
+
import { commitEditValue as _commitEditValue } from './settings-modal-commit.ts';
|
|
74
73
|
|
|
75
74
|
export interface SettingsModalChange {
|
|
76
75
|
readonly key: ConfigKey;
|
|
@@ -86,6 +85,10 @@ export type SettingsModalChangeHandler = (change: SettingsModalChange) => Settin
|
|
|
86
85
|
|
|
87
86
|
export interface SettingsModalOpenOptions {
|
|
88
87
|
readonly onSettingApplied?: SettingsModalChangeHandler;
|
|
88
|
+
/** In-process catalog the Connections category probes; see that module. */
|
|
89
|
+
readonly gatewayMethods?: GatewayMethodCatalog;
|
|
90
|
+
/** Called when an async connection refresh has new rows to paint. */
|
|
91
|
+
readonly requestRender?: () => void;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
export {
|
|
@@ -154,6 +157,11 @@ export class SettingsModal {
|
|
|
154
157
|
public mcpEntries: McpEntry[] = [];
|
|
155
158
|
/** Provider subscription entries (populated when subscriptions tab is active). */
|
|
156
159
|
public subscriptionEntries: SubscriptionEntry[] = [];
|
|
160
|
+
/** Connections-tab state; all of it driven by settings-modal-connections.ts. */
|
|
161
|
+
public connectionEntries: ConnectionStatus[] = initialConnectionEntries();
|
|
162
|
+
public gatewayMethods: GatewayMethodCatalog | null = null;
|
|
163
|
+
public requestRender: (() => void) | null = null;
|
|
164
|
+
public connectionsRefreshing = false;
|
|
157
165
|
|
|
158
166
|
/**
|
|
159
167
|
* Whether the user has entered search mode (pressed / or a printable key
|
|
@@ -217,6 +225,9 @@ export class SettingsModal {
|
|
|
217
225
|
this.groups = buildSettingGroups(configManager, featureFlagManager);
|
|
218
226
|
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
219
227
|
this.subscriptionEntries = buildSubscriptionEntries(subscriptionManager, serviceRegistry);
|
|
228
|
+
this.gatewayMethods = options?.gatewayMethods ?? null;
|
|
229
|
+
this.requestRender = options?.requestRender ?? null;
|
|
230
|
+
this.connectionEntries = initialConnectionEntries();
|
|
220
231
|
this.categoryIndex = 0;
|
|
221
232
|
this.selectedIndex = 0;
|
|
222
233
|
this.focusPane = 'categories';
|
|
@@ -256,6 +267,11 @@ export class SettingsModal {
|
|
|
256
267
|
this.searchResults = [];
|
|
257
268
|
this.searchFocused = false;
|
|
258
269
|
this.contextScroll = 0;
|
|
270
|
+
// A probe still in flight checks `active` before writing, so a reopen
|
|
271
|
+
// starts from `checking` rather than from a finished session's answer.
|
|
272
|
+
this.gatewayMethods = null;
|
|
273
|
+
this.requestRender = null;
|
|
274
|
+
this.connectionEntries = initialConnectionEntries();
|
|
259
275
|
this.serviceRegistry = null;
|
|
260
276
|
this.secretsManager = null;
|
|
261
277
|
this.onSettingApplied = null;
|
|
@@ -357,6 +373,8 @@ export class SettingsModal {
|
|
|
357
373
|
} else if (this.currentCategory === 'subscriptions' && this.subscriptionEntries.length > 0) {
|
|
358
374
|
this.selectedIndex = (this.selectedIndex - 1 + this.subscriptionEntries.length) % this.subscriptionEntries.length;
|
|
359
375
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
376
|
+
} else if (this.currentCategory === 'connections' && this.connectionEntries.length > 0) {
|
|
377
|
+
this.selectedIndex = (this.selectedIndex - 1 + this.connectionEntries.length) % this.connectionEntries.length;
|
|
360
378
|
}
|
|
361
379
|
return;
|
|
362
380
|
}
|
|
@@ -380,6 +398,8 @@ export class SettingsModal {
|
|
|
380
398
|
} else if (this.currentCategory === 'subscriptions' && this.subscriptionEntries.length > 0) {
|
|
381
399
|
this.selectedIndex = (this.selectedIndex + 1) % this.subscriptionEntries.length;
|
|
382
400
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
401
|
+
} else if (this.currentCategory === 'connections' && this.connectionEntries.length > 0) {
|
|
402
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.connectionEntries.length;
|
|
383
403
|
}
|
|
384
404
|
return;
|
|
385
405
|
}
|
|
@@ -531,105 +551,22 @@ export class SettingsModal {
|
|
|
531
551
|
* Returns true on success, false if validation failed.
|
|
532
552
|
*/
|
|
533
553
|
commitEdit(): boolean {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const nextMode = this.editBuffer.trim() as McpEntry['trustMode'];
|
|
553
|
-
const validModes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
554
|
-
if (!validModes.includes(nextMode)) {
|
|
555
|
-
this.editingMode = false;
|
|
556
|
-
this.editBuffer = '';
|
|
557
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
if (nextMode === 'allow-all' && entry.trustMode !== 'allow-all') {
|
|
561
|
-
this.mcpAllowAllConfirmationTarget = entry.name;
|
|
562
|
-
this.editBuffer = '';
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
|
-
this.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
566
|
-
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
567
|
-
this.editingMode = false;
|
|
568
|
-
this.editBuffer = '';
|
|
569
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
570
|
-
return true;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
const entry = this.getSelected();
|
|
574
|
-
if (!entry || !this.configManager) return false;
|
|
575
|
-
|
|
576
|
-
const { setting } = entry;
|
|
577
|
-
let parsed: unknown = this.editBuffer;
|
|
578
|
-
|
|
579
|
-
if (setting.type === 'number') {
|
|
580
|
-
parsed = Number(this.editBuffer);
|
|
581
|
-
if (isNaN(parsed as number)) {
|
|
582
|
-
this.editingMode = false;
|
|
583
|
-
this.editBuffer = '';
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (setting.type === 'object') {
|
|
589
|
-
// Object-typed keys (e.g. pricing.modelPrices) edit as JSON; the
|
|
590
|
-
// schema's validate() below still rules on the parsed shape.
|
|
591
|
-
try {
|
|
592
|
-
parsed = JSON.parse(this.editBuffer);
|
|
593
|
-
} catch {
|
|
594
|
-
this.editingMode = false;
|
|
595
|
-
this.editBuffer = '';
|
|
596
|
-
return false;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (setting.validate && !setting.validate(parsed)) {
|
|
601
|
-
this.editingMode = false;
|
|
602
|
-
this.editBuffer = '';
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (isWorktreeSetupListConfigKey(setting.key)) {
|
|
607
|
-
// Comma-separated display/edit convention for the array-backed
|
|
608
|
-
// worktree.setup.* keys — see worktree-setup-config.ts.
|
|
609
|
-
this._setValue(setting.key, parseWorktreeSetupListInput(this.editBuffer));
|
|
610
|
-
} else if (isSandboxExecListConfigKey(setting.key)) {
|
|
611
|
-
// Same comma-separated convention for the array-backed
|
|
612
|
-
// sandbox.egressAllowlist / sandbox.workspaceWritable keys — see
|
|
613
|
-
// sandbox-exec-config.ts.
|
|
614
|
-
this._setValue(setting.key, parseSandboxExecListInput(this.editBuffer));
|
|
615
|
-
} else if (isExecEnvScrubAllowlistConfigKey(setting.key)) {
|
|
616
|
-
// Same comma-separated convention for permissions.execEnvScrubAllowlist
|
|
617
|
-
// — see exec-env-scrub-config.ts.
|
|
618
|
-
this._setValue(setting.key, parseExecEnvScrubAllowlistInput(this.editBuffer));
|
|
619
|
-
} else if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
|
|
620
|
-
setSecretBackedSettingValue({
|
|
621
|
-
key: setting.key,
|
|
622
|
-
value: String(parsed ?? ''),
|
|
623
|
-
configManager: this.configManager,
|
|
624
|
-
secretsManager: this.secretsManager,
|
|
625
|
-
setConfigValue: (key, value) => this._setValue(key, value),
|
|
626
|
-
});
|
|
627
|
-
} else {
|
|
628
|
-
this._setValue(setting.key as ConfigKey, parsed);
|
|
629
|
-
}
|
|
630
|
-
this.editingMode = false;
|
|
631
|
-
this.editBuffer = '';
|
|
632
|
-
return true;
|
|
554
|
+
return _commitEditValue({
|
|
555
|
+
editingMode: this.editingMode,
|
|
556
|
+
currentCategory: this.currentCategory,
|
|
557
|
+
editBuffer: this.editBuffer,
|
|
558
|
+
configManager: this.configManager,
|
|
559
|
+
secretsManager: this.secretsManager,
|
|
560
|
+
mcpRegistry: this.mcpRegistry,
|
|
561
|
+
mcpAllowAllConfirmationTarget: this.mcpAllowAllConfirmationTarget,
|
|
562
|
+
getSelectedMcp: () => this.getSelectedMcp(),
|
|
563
|
+
getSelected: () => this.getSelected(),
|
|
564
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
565
|
+
setEditingMode: (v) => { this.editingMode = v; },
|
|
566
|
+
setEditBuffer: (v) => { this.editBuffer = v; },
|
|
567
|
+
setMcpEntries: (entries) => { this.mcpEntries = entries; },
|
|
568
|
+
setMcpAllowAllConfirmationTarget: (v) => { this.mcpAllowAllConfirmationTarget = v; },
|
|
569
|
+
});
|
|
633
570
|
}
|
|
634
571
|
|
|
635
572
|
/** Cancel inline edit without saving. */
|
|
@@ -702,6 +639,8 @@ export class SettingsModal {
|
|
|
702
639
|
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
703
640
|
} else if (this.currentCategory === 'subscriptions') {
|
|
704
641
|
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
642
|
+
} else if (this.currentCategory === 'connections') {
|
|
643
|
+
void refreshConnectionEntries(this);
|
|
705
644
|
}
|
|
706
645
|
}
|
|
707
646
|
|
|
@@ -714,6 +653,7 @@ export class SettingsModal {
|
|
|
714
653
|
if (
|
|
715
654
|
this.currentCategory === 'mcp'
|
|
716
655
|
|| this.currentCategory === 'subscriptions'
|
|
656
|
+
|| this.currentCategory === 'connections'
|
|
717
657
|
) return [];
|
|
718
658
|
const items = this.groups.get(this.currentCategory) ?? [];
|
|
719
659
|
if (this.currentCategory === 'network') {
|
|
@@ -754,6 +694,13 @@ export class SettingsModal {
|
|
|
754
694
|
this.lastSettingEffectMessage = result.effectMessage;
|
|
755
695
|
}
|
|
756
696
|
// No-op (result.changed === false, effectMessage === null): leave lastSettingEffectMessage untouched.
|
|
697
|
+
|
|
698
|
+
// Selecting 'prompt' for payments.cvvHandling states the tradeoff at the
|
|
699
|
+
// moment of selection, not just in the documentation pane — see the
|
|
700
|
+
// SDK's own CVV_PROMPT_TRADEOFF_WARNING (platform/payments).
|
|
701
|
+
if (key === PAYMENTS_CVV_HANDLING_CONFIG_KEY && value === 'prompt') {
|
|
702
|
+
this.lastSettingEffectMessage = CVV_PROMPT_TRADEOFF_WARNING;
|
|
703
|
+
}
|
|
757
704
|
}
|
|
758
705
|
|
|
759
706
|
/** Refresh live/persisted/pending state for every feature header bound to `key`. */
|
package/src/main.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
2
|
+
import { resolveGoodVibesDaemonHome, resolveGoodVibesHome } from './config/goodvibes-home.ts';
|
|
3
3
|
import { Compositor } from './renderer/compositor.ts';
|
|
4
4
|
import { type Line } from './types/grid.ts';
|
|
5
5
|
import { UIFactory } from './renderer/ui-factory.ts';
|
|
@@ -87,9 +87,12 @@ import { VERSION } from './version.ts';
|
|
|
87
87
|
async function main() {
|
|
88
88
|
const stdout = process.stdout;
|
|
89
89
|
const stdin = process.stdin;
|
|
90
|
+
// Both roots come from the one resolver the daemon CLI also uses. This line
|
|
91
|
+
// called homedir() unconditionally, so a harness that redirected the tree got
|
|
92
|
+
// a client that wrote the real one — secret store included.
|
|
90
93
|
const { cli, configManager, bootstrapWorkingDir, bootstrapHomeDirectory } = await prepareShellCliRuntime(process.argv.slice(2), {
|
|
91
94
|
defaultWorkingDirectory: process.env['GOODVIBES_WORKING_DIR'] ?? process.cwd(),
|
|
92
|
-
homeDirectory:
|
|
95
|
+
homeDirectory: resolveGoodVibesHome(),
|
|
93
96
|
}, 'goodvibes');
|
|
94
97
|
|
|
95
98
|
// Between binary start and the first frame, boot used to be silent (config
|
|
@@ -106,6 +109,8 @@ async function main() {
|
|
|
106
109
|
configManager,
|
|
107
110
|
workingDir: bootstrapWorkingDir,
|
|
108
111
|
homeDirectory: bootstrapHomeDirectory,
|
|
112
|
+
// Named, so GOODVIBES_DAEMON_HOME on its own moves the client's daemon tier.
|
|
113
|
+
daemonHomeDirectory: resolveGoodVibesDaemonHome(bootstrapHomeDirectory),
|
|
109
114
|
});
|
|
110
115
|
const {
|
|
111
116
|
conversation,
|
|
@@ -207,6 +207,11 @@ export function createMarketplaceModalSurface(deps: MarketplaceModalDeps): Confi
|
|
|
207
207
|
* missing paths with existsSync → [], so refresh() finds nothing and renders the
|
|
208
208
|
* honest empty-state copy). The random tmp path never appears in the
|
|
209
209
|
* rendered lines (the empty-state copy is static), so the golden is byte-stable.
|
|
210
|
+
*
|
|
211
|
+
* This is production code (ships in the real binary), not test scratch, so
|
|
212
|
+
* it stays rooted at the real OS temp dir rather than the test-only
|
|
213
|
+
* makeProjectTempDir helper. Created and removed synchronously in the same
|
|
214
|
+
* call, so the on-disk window is negligible.
|
|
210
215
|
*/
|
|
211
216
|
export function marketplaceModalGoldenSurface(): ConfigModalSurface {
|
|
212
217
|
const root = mkdtempSync(join(tmpdir(), 'gv-marketplace-golden-'));
|
|
@@ -180,6 +180,11 @@ export function createSkillsModalSurface(deps: SkillsModalDeps): ConfigModalSurf
|
|
|
180
180
|
* homeDir and removed immediately (scanSkillDirectorySync catches missing dirs →
|
|
181
181
|
* []), so refresh() finds no `.goodvibes/skills` directories and renders the
|
|
182
182
|
* static empty-state copy. The random tmp path never appears in the output.
|
|
183
|
+
*
|
|
184
|
+
* This is production code (ships in the real binary), not test scratch, so
|
|
185
|
+
* it stays rooted at the real OS temp dir rather than the test-only
|
|
186
|
+
* makeProjectTempDir helper. Created and removed synchronously in the same
|
|
187
|
+
* call, so the on-disk window is negligible.
|
|
183
188
|
*/
|
|
184
189
|
export function skillsModalGoldenSurface(): ConfigModalSurface {
|
|
185
190
|
const root = mkdtempSync(join(tmpdir(), 'gv-skills-golden-'));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* settings-modal-connections.ts — rendering for the Connections category of the
|
|
3
|
+
* settings workspace.
|
|
4
|
+
*
|
|
5
|
+
* Split out of settings-modal.ts for the 800-line file cap, the same reason
|
|
6
|
+
* settings-modal-helpers.ts exists. Pure rendering: every value shown here was
|
|
7
|
+
* decided by the daemon probe in input/commands/connection-status.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SettingsModal } from '../input/settings-modal.ts';
|
|
11
|
+
import { selectedConnectionEntry } from '../input/settings-modal-connections.ts';
|
|
12
|
+
import { connectionSurfaceLabel, type ConnectionStatus } from '../input/commands/connection-status.ts';
|
|
13
|
+
import { GLYPHS } from './ui-primitives.ts';
|
|
14
|
+
import { clamp, padDisplay, stableWindow } from './fullscreen-workspace.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The Connections rows: one line per surface, saying what it actually is.
|
|
18
|
+
*
|
|
19
|
+
* `checking` is rendered as its own state rather than blanked, because a row
|
|
20
|
+
* that showed nothing while the probe was in flight would read as "nothing
|
|
21
|
+
* configured" — the one thing this category exists to stop claiming falsely.
|
|
22
|
+
*/
|
|
23
|
+
export function renderConnectionRows(modal: SettingsModal, width: number, height: number): string[] {
|
|
24
|
+
const rows: string[] = [];
|
|
25
|
+
const items = modal.connectionEntries;
|
|
26
|
+
if (items.length === 0) return ['No connection surfaces are known to this build.'];
|
|
27
|
+
const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
|
|
28
|
+
const surfaceWidth = clamp(Math.floor(width * 0.16), 10, 20);
|
|
29
|
+
const stateWidth = 12;
|
|
30
|
+
const detailWidth = Math.max(16, width - surfaceWidth - stateWidth - 8);
|
|
31
|
+
rows.push(` ${padDisplay('Surface', surfaceWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Detail', detailWidth)}`);
|
|
32
|
+
const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
|
|
33
|
+
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more connection(s) above`);
|
|
34
|
+
for (let index = window.start; index < window.end; index += 1) {
|
|
35
|
+
const entry = items[index]!;
|
|
36
|
+
const selected = index === selectedIndex;
|
|
37
|
+
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '\u2022') : ' ';
|
|
38
|
+
rows.push(`${marker} ${padDisplay(connectionSurfaceLabel(entry.surface), surfaceWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.detail, detailWidth)}`);
|
|
39
|
+
}
|
|
40
|
+
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more connection(s) below`);
|
|
41
|
+
return rows.slice(0, height);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The detail pane for one connection: the state, what it means, and every next
|
|
46
|
+
* step verbatim. The next steps are the daemon's requirements, so they are
|
|
47
|
+
* listed in full rather than summarized — a truncated instruction is a wrong
|
|
48
|
+
* instruction.
|
|
49
|
+
*/
|
|
50
|
+
export function buildConnectionContext(modal: SettingsModal): string[] {
|
|
51
|
+
const entry = selectedConnectionEntry(modal);
|
|
52
|
+
if (!entry) return ['Connections', 'No connection surface is selected.'];
|
|
53
|
+
return [
|
|
54
|
+
connectionSurfaceLabel(entry.surface),
|
|
55
|
+
`State: ${entry.state}`,
|
|
56
|
+
entry.detail,
|
|
57
|
+
...(entry.nextActions.length > 0 ? ['', 'Next steps:', ...entry.nextActions] : []),
|
|
58
|
+
'',
|
|
59
|
+
entry.surface === 'mail'
|
|
60
|
+
? 'Use it from the transcript with /mail (status, list, read, draft, send).'
|
|
61
|
+
: 'Use it from the transcript with /calendar (status, list, get, create).',
|
|
62
|
+
];
|
|
63
|
+
}
|
|
@@ -107,10 +107,13 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
107
107
|
agents: 'Sub-agent context-window awareness and per-turn passive knowledge/code injection: token budget, relevance floor, code-chunk limit, and the compaction threshold. Gated by the agent context/injection features.',
|
|
108
108
|
security: 'API token scope and rotation auditing: rotation cadence, warning lead time, and whether overdue/over-scoped tokens are blocked or only reported. Gated by the Token Scope and Rotation Audit feature.',
|
|
109
109
|
integrations: 'Integration delivery reliability (Slack/Discord/webhook): retry counts, backoff bounds, dead-letter queue size, and SLO enforcement. Gated by the Integration Delivery SLO feature.',
|
|
110
|
+
connections: 'Whether mail and calendar are actually usable right now. Each row is answered by asking the daemon that owns the connection, not by reading config here, so a row says ready only when a real request succeeded.',
|
|
110
111
|
policy: 'Policy-as-code bundle loading: where the startup policy bundle is loaded from and its file path. Gated by the Policy-as-Code feature.',
|
|
111
112
|
notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
|
|
112
113
|
relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
|
|
113
114
|
learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
|
|
115
|
+
payments: 'Payment card on file for daemon-initiated purchases: card number, expiry, CVV, cardholder name, billing and shipping address, and whether the CVV is stored or requested at purchase time. Card fields are entered through /payments card (concealed input) and are secret-tier — never rendered in plaintext.',
|
|
116
|
+
profile: 'What the platform knows about you — your name, how to reach you, where you are, how you like answers written, the people and places you mention — kept as one Markdown file at daemon scope that you can open and edit by hand at any time; your edits win and are never rewritten. These rows decide whether it is loaded at all, whether the assistant records facts it learns from things you say directly to it, whether it tells you in one line each time it does, whether the harmless part (how you are addressed, your city, your timezone, your formatting preferences, your style) is put in the model\'s context so it stops guessing, whether using a private detail such as your address is announced in the reply, whether settings left unset read their value from the profile, and where the file lives. Everything private — your name, contact details, home address, billing and shipping details, quiet hours, and the People, Places, Work and Notes sections — is never put in context in bulk and is reachable only by a named lookup that is disclosed when it happens. Run /profile to read the whole thing, /profile where <field> to see the surface, date and your exact words behind any recorded line, and /profile forget <field> to delete one.',
|
|
114
117
|
};
|
|
115
118
|
|
|
116
119
|
export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], string> = {
|
|
@@ -161,8 +164,11 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
161
164
|
agents: 'Agents & Context',
|
|
162
165
|
security: 'Token Security',
|
|
163
166
|
integrations: 'Integration Delivery',
|
|
167
|
+
connections: 'Connections',
|
|
164
168
|
policy: 'Policy-as-Code',
|
|
165
169
|
notifications: 'Notifications',
|
|
170
|
+
payments: 'Payments',
|
|
171
|
+
profile: 'Owner Profile',
|
|
166
172
|
};
|
|
167
173
|
|
|
168
174
|
const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
@@ -11,7 +11,10 @@ import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings
|
|
|
11
11
|
import { FEATURE_SETTINGS_BY_ID, isFeatureValueEnabled } from '../runtime/feature-settings.ts';
|
|
12
12
|
import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
13
13
|
import { CATEGORY_INFO, CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
14
|
+
import { buildConnectionContext, renderConnectionRows } from './settings-modal-connections.ts';
|
|
14
15
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
16
|
+
import { maskConcealedText } from '../input/concealed-input.ts';
|
|
17
|
+
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
15
18
|
import { GLYPHS } from './ui-primitives.ts';
|
|
16
19
|
import { formatHints, joinHints } from './hint-grammar.ts';
|
|
17
20
|
import {
|
|
@@ -104,6 +107,10 @@ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
|
|
|
104
107
|
'meta-cloud': 'Use Meta Cloud API credentials and identifiers.',
|
|
105
108
|
bridge: 'Use a bridge service URL/token flow instead of direct Meta Cloud API delivery.',
|
|
106
109
|
},
|
|
110
|
+
'payments.cvvHandling': {
|
|
111
|
+
stored: 'Store the CVV through the secret manager so the daemon can complete a purchase unattended.',
|
|
112
|
+
prompt: CVV_PROMPT_TRADEOFF_WARNING,
|
|
113
|
+
},
|
|
107
114
|
};
|
|
108
115
|
|
|
109
116
|
function paddedWrapped(text: string, width: number, prefix = ''): string[] {
|
|
@@ -120,7 +127,16 @@ function formatDefaultValue(value: unknown): string {
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
function currentSettingValue(modal: SettingsModal, entry: SettingEntry, selected: boolean): string {
|
|
123
|
-
if (selected && modal.editingMode)
|
|
130
|
+
if (selected && modal.editingMode) {
|
|
131
|
+
// Secret-backed keys (surfaces.*.botToken, .signingSecret, etc. — see
|
|
132
|
+
// secret-config.ts) must never echo the in-progress plaintext buffer:
|
|
133
|
+
// not in the row, not in the "Current: ..." context line, not in search
|
|
134
|
+
// results. Reuse the composer's concealed-input mask (concealed-input.ts)
|
|
135
|
+
// rather than a second masking implementation — same bullet-per-character
|
|
136
|
+
// shape, so keystrokes still visibly register without revealing content.
|
|
137
|
+
const buffer = isSecretConfigKey(entry.setting.key) ? maskConcealedText(modal.editBuffer) : modal.editBuffer;
|
|
138
|
+
return `${buffer}${GLYPHS.surface.cursor}`;
|
|
139
|
+
}
|
|
124
140
|
return formatValue(entry);
|
|
125
141
|
}
|
|
126
142
|
|
|
@@ -360,6 +376,8 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
|
360
376
|
lines.push(...buildMcpContext(modal, modal.getSelectedMcp()));
|
|
361
377
|
} else if (category === 'subscriptions') {
|
|
362
378
|
lines.push(...buildSubscriptionContext(modal, modal.getSelectedSubscription()));
|
|
379
|
+
} else if (category === 'connections') {
|
|
380
|
+
lines.push(...buildConnectionContext(modal));
|
|
363
381
|
} else {
|
|
364
382
|
const selected = modal.getSelected();
|
|
365
383
|
// A feature-unit header shows the feature's documentation (full
|
|
@@ -386,6 +404,7 @@ function buildContextLines(modal: SettingsModal, width: number): string[] {
|
|
|
386
404
|
function categoryItemCount(modal: SettingsModal, category: SettingsCategory): number {
|
|
387
405
|
if (category === 'mcp') return modal.mcpEntries.length;
|
|
388
406
|
if (category === 'subscriptions') return modal.subscriptionEntries.length;
|
|
407
|
+
if (category === 'connections') return modal.connectionEntries.length;
|
|
389
408
|
return modal.groups.get(category)?.length ?? 0;
|
|
390
409
|
}
|
|
391
410
|
|
|
@@ -601,6 +620,7 @@ function renderControlRows(modal: SettingsModal, width: number, height: number):
|
|
|
601
620
|
if (modal.searchFocused) return renderSearchRows(modal, width, height);
|
|
602
621
|
if (modal.currentCategory === 'mcp') return renderMcpRows(modal, width, height);
|
|
603
622
|
if (modal.currentCategory === 'subscriptions') return renderSubscriptionRows(modal, width, height);
|
|
623
|
+
if (modal.currentCategory === 'connections') return renderConnectionRows(modal, width, height);
|
|
604
624
|
return renderSettingRows(modal, width, height);
|
|
605
625
|
}
|
|
606
626
|
|
|
@@ -248,7 +248,7 @@ export async function initializeBootstrapCore(
|
|
|
248
248
|
runtimeBus,
|
|
249
249
|
runtimeStore: store,
|
|
250
250
|
getConversationTitle: () => getConversationTitle(),
|
|
251
|
-
workingDir, homeDirectory,
|
|
251
|
+
workingDir, homeDirectory, ...(options.daemonHomeDirectory === undefined ? {} : { daemonHomeDirectory: options.daemonHomeDirectory }),
|
|
252
252
|
// Embedded topology: the in-process runtime IS the daemon and holds its own OS
|
|
253
253
|
// inhibitor, so opt into the real host seam (fork default is non-spawning). See services.ts.
|
|
254
254
|
powerSeam: createHostPowerSeam(),
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { initializeBootstrapCore } from './bootstrap-core.ts';
|
|
|
41
41
|
import { ensureBootModelResolvable } from './provider-fallback.ts';
|
|
42
42
|
import { createBootstrapShell } from './bootstrap-shell.ts';
|
|
43
43
|
import { announceResumeState } from './resume-notice.ts';
|
|
44
|
-
import {
|
|
44
|
+
import { announceInstallHealth } from './install-self-check-startup.ts';
|
|
45
45
|
import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './orchestrator-core-services.ts';
|
|
46
46
|
import { consumeDaemonAttachNotices, consumeExternalDaemonAttachNotices } from './daemon-attach-notices.ts';
|
|
47
47
|
import { wireContextAccountingSource } from './context-accounting-source.ts';
|
|
@@ -322,7 +322,7 @@ export async function bootstrapRuntime(
|
|
|
322
322
|
}).catch(() => {
|
|
323
323
|
// Best-effort — never let the resume notice block or crash boot.
|
|
324
324
|
});
|
|
325
|
-
|
|
325
|
+
announceInstallHealth(systemMessageRouter);
|
|
326
326
|
const { gitStatusProvider, inputHistory, lastGitInfoRef } = shell;
|
|
327
327
|
// FIX 2: dispose the header's live-repo-state poll (git-status.ts
|
|
328
328
|
// startPolling) on shutdown, same pattern as acpTaskSyncInterval above.
|
package/src/runtime/context.ts
CHANGED
|
@@ -31,6 +31,15 @@ export interface BootstrapOptions {
|
|
|
31
31
|
workingDir: string;
|
|
32
32
|
/** App-owned home directory for this runtime instance. */
|
|
33
33
|
homeDirectory: string;
|
|
34
|
+
/**
|
|
35
|
+
* The daemon's identity directory for this runtime instance.
|
|
36
|
+
*
|
|
37
|
+
* Named rather than derived so the client's daemon-TIER secret reads and
|
|
38
|
+
* writes follow a GOODVIBES_DAEMON_HOME set on its own. Absent, the secret
|
|
39
|
+
* store falls back to `<homeDirectory>/.goodvibes/daemon`, which is the same
|
|
40
|
+
* answer whenever that variable is unset.
|
|
41
|
+
*/
|
|
42
|
+
daemonHomeDirectory?: string;
|
|
34
43
|
/** Explicit app-owned config manager for this runtime instance. */
|
|
35
44
|
configManager: import('@pellux/goodvibes-sdk/platform/config').ConfigManager;
|
|
36
45
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential-composition.ts
|
|
3
|
+
*
|
|
4
|
+
* The credential and identity seam: the secret store, the step-up ceremony
|
|
5
|
+
* service that verifies against it, and the per-device pairing tokens.
|
|
6
|
+
*
|
|
7
|
+
* These are grouped because they share one fact, and it is the fact this round
|
|
8
|
+
* had to fix: WHERE the daemon's own credentials live. `daemonHome` is threaded
|
|
9
|
+
* rather than defaulted, because without it a daemon told to run out of a temp
|
|
10
|
+
* tree moved its identity directory and nothing else — the credential store
|
|
11
|
+
* stayed in the real home, so an "isolated" test daemon held live credentials
|
|
12
|
+
* and long-polled a real account. A default here would silently restore that.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from services.ts because that file sits at the 800-line
|
|
15
|
+
* architecture cap. Moving a coherent seam out is what the cap is for; trimming
|
|
16
|
+
* unrelated comments to buy headroom is the thing it exists to prevent.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { SecretsManager } from '../config/secrets.ts';
|
|
20
|
+
import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
|
|
21
|
+
import { PairingTokenManager } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
22
|
+
|
|
23
|
+
interface CredentialCompositionInput {
|
|
24
|
+
readonly workingDirectory: string;
|
|
25
|
+
readonly homeDirectory: string;
|
|
26
|
+
/**
|
|
27
|
+
* The daemon's state root when the host was told one. Absent means "not
|
|
28
|
+
* overridden" and must stay absent rather than being defaulted — see above.
|
|
29
|
+
*/
|
|
30
|
+
readonly daemonHomeDirectory?: string | undefined;
|
|
31
|
+
readonly configManager: ConstructorParameters<typeof SecretsManager>[0]['configManager'];
|
|
32
|
+
readonly pairingTokenPath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function composeCredentialServices(input: CredentialCompositionInput): {
|
|
36
|
+
readonly secretsManager: SecretsManager;
|
|
37
|
+
readonly stepUpService: StepUpService;
|
|
38
|
+
readonly pairingTokens: PairingTokenManager;
|
|
39
|
+
} {
|
|
40
|
+
const secretsManager = new SecretsManager({
|
|
41
|
+
projectRoot: input.workingDirectory,
|
|
42
|
+
globalHome: input.homeDirectory,
|
|
43
|
+
// Threaded, not defaulted: else an isolated daemon reads the real store.
|
|
44
|
+
...(input.daemonHomeDirectory === undefined ? {} : { daemonHome: input.daemonHomeDirectory }),
|
|
45
|
+
configManager: input.configManager,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
secretsManager,
|
|
49
|
+
// Shared between the ceremony gateway verbs and the relay gate's verifier,
|
|
50
|
+
// so both check a step-up against the same store.
|
|
51
|
+
stepUpService: new StepUpService({ secrets: secretsManager }),
|
|
52
|
+
pairingTokens: new PairingTokenManager(input.pairingTokenPath),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -27,8 +27,6 @@ import { registerRouting } from '../daemon/handlers/routing/index.ts';
|
|
|
27
27
|
import { registerInboxMethods } from '../daemon/handlers/inbox/index.ts';
|
|
28
28
|
import { registerTriagedInbox } from '../daemon/handlers/triage/index.ts';
|
|
29
29
|
import { registerDraftMethods } from '../daemon/handlers/drafts/index.ts';
|
|
30
|
-
import { registerCalendar } from '../daemon/handlers/calendar/index.ts';
|
|
31
|
-
import { registerEmailMethods } from '../daemon/handlers/email/index.ts';
|
|
32
30
|
import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
|
|
33
31
|
import { inboxPollerGate } from './cluster-composition.ts';
|
|
34
32
|
|
|
@@ -73,8 +71,6 @@ export function createDaemonHandlerComposition(
|
|
|
73
71
|
options.clusterCoordinator.register(inboxPollerGate(providerId, control)),
|
|
74
72
|
})).unregister,
|
|
75
73
|
registerDrafts: (ctx) => registerDraftMethods(ctx),
|
|
76
|
-
registerCalendar,
|
|
77
|
-
registerEmail: (ctx) => registerEmailMethods(ctx),
|
|
78
74
|
registerRemote: (ctx) => registerRemoteSurface(ctx, { manager: options.distributedRuntime }),
|
|
79
75
|
});
|
|
80
76
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { getGoodVibesPackageRoot, resolveGoodVibesDaemonExecutable } from '../cli/service-posture.ts';
|
|
19
19
|
import { runInstallSelfCheck } from './install-self-check.ts';
|
|
20
|
+
import { announceReachability } from './path-shadow-startup.ts';
|
|
20
21
|
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
21
22
|
|
|
22
23
|
export function announceInstallSelfCheck(router: SystemMessageRouter): void {
|
|
@@ -34,3 +35,25 @@ export function announceInstallSelfCheck(router: SystemMessageRouter): void {
|
|
|
34
35
|
// Best-effort — an install self-check must never block or crash boot.
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The one call boot makes about the state of this install: is it complete
|
|
41
|
+
* (the check above), is it the copy the shell actually reaches, and is it the
|
|
42
|
+
* current release (path-shadow-startup).
|
|
43
|
+
*
|
|
44
|
+
* They are announced together because they are the same question from the
|
|
45
|
+
* user's side — "am I looking at the build I think I am" — and because the
|
|
46
|
+
* three answers are useless apart: a complete install nobody can reach, or a
|
|
47
|
+
* reachable install three versions old, both end with the product appearing
|
|
48
|
+
* to have lost a capability it never had.
|
|
49
|
+
*
|
|
50
|
+
* The reachability half is async (it may look up the latest release) and is
|
|
51
|
+
* deliberately not awaited: boot continues, the notice arrives through the
|
|
52
|
+
* same router a moment later.
|
|
53
|
+
*/
|
|
54
|
+
export function announceInstallHealth(router: SystemMessageRouter): void {
|
|
55
|
+
announceInstallSelfCheck(router);
|
|
56
|
+
void announceReachability(router).catch(() => {
|
|
57
|
+
// Best-effort — a reachability check must never block or crash boot.
|
|
58
|
+
});
|
|
59
|
+
}
|