@pellux/goodvibes-tui 1.13.1 → 1.14.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 +96 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +363 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +79 -0
- package/src/runtime/services.ts +33 -33
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/gateway-verbs.ts +0 -47
- package/src/runtime/render-scheduler.ts +0 -90
|
@@ -4,6 +4,7 @@ import type { CommandRegistry } from '../command-registry.ts';
|
|
|
4
4
|
import { VERSION } from '../../version.ts';
|
|
5
5
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
6
6
|
import { handleLocalAuthCommand } from './local-auth-runtime.ts';
|
|
7
|
+
import { registerUpdateCommand } from './update-runtime.ts';
|
|
7
8
|
import { buildAuthInspectionSnapshot, inspectProviderAuth } from '@/runtime/index.ts';
|
|
8
9
|
import { requireProfileManager, requireSecretsManager, requireServiceRegistry, requireShellPaths, requireSubscriptionManager } from './runtime-services.ts';
|
|
9
10
|
|
|
@@ -18,16 +19,6 @@ interface InstallBundle {
|
|
|
18
19
|
readonly setupLinks: readonly string[];
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
interface UpdateBundle {
|
|
22
|
-
readonly version: 1;
|
|
23
|
-
readonly exportedAt: number;
|
|
24
|
-
readonly appVersion: string;
|
|
25
|
-
readonly updateChannel: 'stable' | 'preview';
|
|
26
|
-
readonly subscriptionProviders: readonly string[];
|
|
27
|
-
readonly sandboxProfile: string;
|
|
28
|
-
readonly notes: readonly string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
22
|
interface AuthReviewBundle {
|
|
32
23
|
readonly version: 1;
|
|
33
24
|
readonly exportedAt: number;
|
|
@@ -54,17 +45,6 @@ function inspectInstallBundle(bundle: InstallBundle): string {
|
|
|
54
45
|
].join('\n');
|
|
55
46
|
}
|
|
56
47
|
|
|
57
|
-
function inspectUpdateBundle(bundle: UpdateBundle): string {
|
|
58
|
-
return [
|
|
59
|
-
'Update Bundle Review',
|
|
60
|
-
` appVersion: ${bundle.appVersion}`,
|
|
61
|
-
` updateChannel: ${bundle.updateChannel}`,
|
|
62
|
-
` subscriptionProviders: ${bundle.subscriptionProviders.length}`,
|
|
63
|
-
` sandboxProfile: ${bundle.sandboxProfile}`,
|
|
64
|
-
` notes: ${bundle.notes.length}`,
|
|
65
|
-
].join('\n');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
48
|
function inspectAuthBundle(bundle: AuthReviewBundle): string {
|
|
69
49
|
return [
|
|
70
50
|
'Auth Review Bundle',
|
|
@@ -189,82 +169,7 @@ export function registerPlatformAccessRuntimeCommands(registry: CommandRegistry)
|
|
|
189
169
|
},
|
|
190
170
|
});
|
|
191
171
|
|
|
192
|
-
registry
|
|
193
|
-
name: 'update',
|
|
194
|
-
aliases: ['upgrade'],
|
|
195
|
-
description: 'Review update posture, choose release channel guidance, and package portable update bundles',
|
|
196
|
-
usage: '[review|channel <stable|preview>|bundle export <path>|bundle inspect <path>]',
|
|
197
|
-
handler(args, ctx) {
|
|
198
|
-
const shellPaths = requireShellPaths(ctx);
|
|
199
|
-
const sub = args[0] ?? 'review';
|
|
200
|
-
const subscriptions = requireSubscriptionManager(ctx);
|
|
201
|
-
const serviceRegistry = requireServiceRegistry(ctx);
|
|
202
|
-
const secretsManager = requireSecretsManager(ctx);
|
|
203
|
-
const builtinProviders = listBuiltinSubscriptionProviders().map((entry) => entry.provider);
|
|
204
|
-
const sandboxProfile = [
|
|
205
|
-
`${ctx.platform.configManager.get('sandbox.replIsolation')}`,
|
|
206
|
-
`${ctx.platform.configManager.get('sandbox.mcpIsolation')}`,
|
|
207
|
-
`${ctx.platform.configManager.get('sandbox.vmBackend')}`,
|
|
208
|
-
].join('/');
|
|
209
|
-
const activeSubscriptions = subscriptions.list().map((entry) => entry.provider);
|
|
210
|
-
if (sub === 'review') {
|
|
211
|
-
const channel = ctx.platform.configManager.get('release.channel');
|
|
212
|
-
ctx.print([
|
|
213
|
-
'Update Review',
|
|
214
|
-
` version: ${VERSION}`,
|
|
215
|
-
` channel: ${channel}`,
|
|
216
|
-
` built-in subscription providers: ${builtinProviders.length}${builtinProviders.length > 0 ? ` (${builtinProviders.join(', ')})` : ''}`,
|
|
217
|
-
` active subscriptions: ${activeSubscriptions.length}${activeSubscriptions.length > 0 ? ` (${activeSubscriptions.join(', ')})` : ''}`,
|
|
218
|
-
` sandbox profile: ${sandboxProfile}`,
|
|
219
|
-
' use /update channel <stable|preview> to change release posture',
|
|
220
|
-
].join('\n'));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (sub === 'channel') {
|
|
224
|
-
const channel = args[1];
|
|
225
|
-
if (channel !== 'stable' && channel !== 'preview') {
|
|
226
|
-
ctx.print('Usage: /update channel <stable|preview>');
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
ctx.platform.configManager.setDynamic('release.channel', channel);
|
|
230
|
-
ctx.print(`Update channel set to ${channel}.`);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (sub === 'bundle') {
|
|
234
|
-
const mode = args[1];
|
|
235
|
-
const pathArg = args[2];
|
|
236
|
-
if ((mode === 'export' || mode === 'inspect') && !pathArg) {
|
|
237
|
-
ctx.print(`Usage: /update bundle ${mode} <path>`);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const targetPath = shellPaths.resolveWorkspacePath(pathArg!);
|
|
241
|
-
if (mode === 'export') {
|
|
242
|
-
const bundle: UpdateBundle = {
|
|
243
|
-
version: 1,
|
|
244
|
-
exportedAt: Date.now(),
|
|
245
|
-
appVersion: VERSION,
|
|
246
|
-
updateChannel: ctx.platform.configManager.get('release.channel') as 'stable' | 'preview',
|
|
247
|
-
subscriptionProviders: [...new Set([...builtinProviders, ...activeSubscriptions])],
|
|
248
|
-
sandboxProfile,
|
|
249
|
-
notes: [
|
|
250
|
-
'Preview channel is recommended only when operator review and sandbox isolation are enabled.',
|
|
251
|
-
'OAuth-backed provider subscriptions survive channel changes and continue to apply to supported provider surfaces.',
|
|
252
|
-
],
|
|
253
|
-
};
|
|
254
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
255
|
-
writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
|
|
256
|
-
ctx.print(`Update bundle exported to ${targetPath}`);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (mode === 'inspect') {
|
|
260
|
-
const bundle = JSON.parse(readFileSync(targetPath, 'utf-8')) as UpdateBundle;
|
|
261
|
-
ctx.print(inspectUpdateBundle(bundle));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
ctx.print('Usage: /update [review|channel <stable|preview>|bundle export <path>|bundle inspect <path>]');
|
|
266
|
-
},
|
|
267
|
-
});
|
|
172
|
+
registerUpdateCommand(registry);
|
|
268
173
|
|
|
269
174
|
registry.register({
|
|
270
175
|
name: 'auth',
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* principals-runtime.ts
|
|
3
|
+
*
|
|
4
|
+
* `/principals` — identity-mapping admin over `principals.*`: named
|
|
5
|
+
* principals (people, bots, services, tokens) and the channel identities
|
|
6
|
+
* ({channel, value} pairs, e.g. {channel:'slack', value:'U123'}) mapped to
|
|
7
|
+
* them. Goes over the operator wire (see operator-rpc.ts) for the same
|
|
8
|
+
* reason /ci and /checkin do: this verb family is not yet on the in-process
|
|
9
|
+
* OperatorClient facade.
|
|
10
|
+
*
|
|
11
|
+
* `principals.resolve` is the honest lookup this command exists to make
|
|
12
|
+
* reachable: an unmapped identity resolves to the shared "unknown" principal
|
|
13
|
+
* with `known: false` — the registry never guesses, and this command never
|
|
14
|
+
* papers over that with a name that looks real.
|
|
15
|
+
*/
|
|
16
|
+
import type { CommandRegistry } from '../command-registry.ts';
|
|
17
|
+
import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
|
|
18
|
+
import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
|
|
19
|
+
|
|
20
|
+
type Principal = OperatorMethodOutput<'principals.list'>['principals'][number];
|
|
21
|
+
type PrincipalKind = Principal['kind'];
|
|
22
|
+
|
|
23
|
+
const PRINCIPAL_KINDS: readonly PrincipalKind[] = ['user', 'bot', 'service', 'token'];
|
|
24
|
+
|
|
25
|
+
function isPrincipalKind(value: string | undefined): value is PrincipalKind {
|
|
26
|
+
return PRINCIPAL_KINDS.includes(value as PrincipalKind);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Parse `channel:value` tokens (e.g. `slack:U123`) into identity pairs. Skips malformed tokens. */
|
|
30
|
+
function parseIdentities(tokens: readonly string[]): Array<{ channel: string; value: string }> {
|
|
31
|
+
const identities: Array<{ channel: string; value: string }> = [];
|
|
32
|
+
for (const token of tokens) {
|
|
33
|
+
const idx = token.indexOf(':');
|
|
34
|
+
if (idx <= 0 || idx === token.length - 1) continue;
|
|
35
|
+
identities.push({ channel: token.slice(0, idx), value: token.slice(idx + 1) });
|
|
36
|
+
}
|
|
37
|
+
return identities;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Extract `--flag value` pairs from an args array, returning the remaining positional tokens. */
|
|
41
|
+
function extractFlags(args: readonly string[], flagNames: readonly string[]): { flags: Map<string, string>; positional: string[] } {
|
|
42
|
+
const flags = new Map<string, string>();
|
|
43
|
+
const positional: string[] = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i]!;
|
|
46
|
+
const name = arg.startsWith('--') ? arg.slice(2) : null;
|
|
47
|
+
if (name && flagNames.includes(name)) {
|
|
48
|
+
const value = args[i + 1];
|
|
49
|
+
if (value !== undefined) {
|
|
50
|
+
flags.set(name, value);
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
positional.push(arg);
|
|
56
|
+
}
|
|
57
|
+
return { flags, positional };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderPrincipal(principal: Principal): string {
|
|
61
|
+
const lines = [
|
|
62
|
+
` ${principal.id} ${principal.name} [${principal.kind}]`,
|
|
63
|
+
];
|
|
64
|
+
if (principal.identities.length > 0) {
|
|
65
|
+
lines.push(` identities: ${principal.identities.map((i) => `${i.channel}:${i.value}`).join(', ')}`);
|
|
66
|
+
} else {
|
|
67
|
+
lines.push(' identities: (none)');
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function registerPrincipalsRuntimeCommands(registry: CommandRegistry): void {
|
|
73
|
+
registry.register({
|
|
74
|
+
name: 'principals',
|
|
75
|
+
description: 'Identity mappings: named principals and their channel identities',
|
|
76
|
+
usage: '[list|get <id>|create <name> <kind> [channel:value...]|update <id> [--name x] [--kind x] [--identities c:v,c:v]|delete <id>|resolve <channel> <value>]',
|
|
77
|
+
argsHint: '[list|get|create|update|delete|resolve]',
|
|
78
|
+
async handler(args, ctx) {
|
|
79
|
+
const sub = args[0] ?? 'list';
|
|
80
|
+
const knownSubcommands = ['list', 'get', 'create', 'update', 'delete', 'resolve'];
|
|
81
|
+
if (!knownSubcommands.includes(sub)) {
|
|
82
|
+
ctx.print(
|
|
83
|
+
'Usage: /principals <subcommand>\n'
|
|
84
|
+
+ ' /principals list — list every principal\n'
|
|
85
|
+
+ ' /principals get <id> — show one principal\n'
|
|
86
|
+
+ ' /principals create <name> <user|bot|service|token> [channel:value...]\n'
|
|
87
|
+
+ ' /principals update <id> [--name x] [--kind x] [--identities c:v,c:v]\n'
|
|
88
|
+
+ ' /principals delete <id> — permanently remove a principal\n'
|
|
89
|
+
+ ' /principals resolve <channel> <value> — resolve a sender identity to its principal, or "unknown principal"'
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate each subcommand's required positional arguments before
|
|
95
|
+
// touching the operator connection, so a usage mistake never depends
|
|
96
|
+
// on daemon reachability to be reported.
|
|
97
|
+
if (sub === 'get' && !args[1]) {
|
|
98
|
+
ctx.print('Usage: /principals get <id>');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (sub === 'create' && (!args[1] || !isPrincipalKind(args[2]))) {
|
|
102
|
+
ctx.print(`Usage: /principals create <name> <${PRINCIPAL_KINDS.join('|')}> [channel:value...]`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (sub === 'update' && !args[1]) {
|
|
106
|
+
ctx.print('Usage: /principals update <id> [--name x] [--kind user|bot|service|token] [--identities c:v,c:v]');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (sub === 'delete' && !args[1]) {
|
|
110
|
+
ctx.print('Usage: /principals delete <id>');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (sub === 'resolve' && (!args[1] || !args[2])) {
|
|
114
|
+
ctx.print('Usage: /principals resolve <channel> <value>');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const rpc = getOperatorRpc(ctx);
|
|
119
|
+
if (!rpc.available) {
|
|
120
|
+
ctx.print(`[principals] ${rpc.reason}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (sub === 'list') {
|
|
125
|
+
try {
|
|
126
|
+
const { principals } = await rpc.sdk.operator.invoke('principals.list', {});
|
|
127
|
+
if (principals.length === 0) {
|
|
128
|
+
ctx.print('[principals] no principals registered.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
ctx.print(['Principals:', ...principals.map(renderPrincipal)].join('\n'));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
ctx.print(`[principals list] ${describeOperatorRpcError(error)}`);
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (sub === 'get') {
|
|
139
|
+
const id = args[1]!;
|
|
140
|
+
try {
|
|
141
|
+
const { principal } = await rpc.sdk.operator.invoke('principals.get', { principalId: id });
|
|
142
|
+
ctx.print(renderPrincipal(principal));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
ctx.print(`[principals get] ${describeOperatorRpcError(error)}`);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (sub === 'create') {
|
|
150
|
+
const name = args[1]!;
|
|
151
|
+
const kind = args[2] as PrincipalKind;
|
|
152
|
+
const identities = parseIdentities(args.slice(3));
|
|
153
|
+
try {
|
|
154
|
+
const { principal } = await rpc.sdk.operator.invoke('principals.create', { name, kind, identities });
|
|
155
|
+
ctx.print(`[principals create] created ${principal.id}\n${renderPrincipal(principal)}`);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
ctx.print(`[principals create] ${describeOperatorRpcError(error)}`);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sub === 'update') {
|
|
163
|
+
const id = args[1]!;
|
|
164
|
+
const { flags } = extractFlags(args.slice(2), ['name', 'kind', 'identities']);
|
|
165
|
+
const kind = flags.get('kind');
|
|
166
|
+
if (kind !== undefined && !isPrincipalKind(kind)) {
|
|
167
|
+
ctx.print(`Invalid --kind "${kind}". Valid values: ${PRINCIPAL_KINDS.join(', ')}`);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const identitiesRaw = flags.get('identities');
|
|
171
|
+
try {
|
|
172
|
+
const { principal } = await rpc.sdk.operator.invoke('principals.update', {
|
|
173
|
+
principalId: id,
|
|
174
|
+
...(flags.has('name') ? { name: flags.get('name')! } : {}),
|
|
175
|
+
...(kind !== undefined ? { kind } : {}),
|
|
176
|
+
...(identitiesRaw !== undefined ? { identities: parseIdentities(identitiesRaw.split(',')) } : {}),
|
|
177
|
+
});
|
|
178
|
+
ctx.print(`[principals update] updated ${principal.id}\n${renderPrincipal(principal)}`);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
ctx.print(`[principals update] ${describeOperatorRpcError(error)}`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (sub === 'delete') {
|
|
186
|
+
const id = args[1]!;
|
|
187
|
+
try {
|
|
188
|
+
const result = await rpc.sdk.operator.invoke('principals.delete', { principalId: id });
|
|
189
|
+
ctx.print(result.deleted ? `[principals delete] deleted ${id}.` : `[principals delete] no principal found with id ${id}.`);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
ctx.print(`[principals delete] ${describeOperatorRpcError(error)}`);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (sub === 'resolve') {
|
|
197
|
+
const channel = args[1]!;
|
|
198
|
+
const value = args[2]!;
|
|
199
|
+
try {
|
|
200
|
+
const result = await rpc.sdk.operator.invoke('principals.resolve', { channel, value });
|
|
201
|
+
if (!result.known) {
|
|
202
|
+
ctx.print(`[principals resolve] unknown principal — ${channel}:${value} is not mapped to any registered principal.`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
ctx.print(`[principals resolve] ${channel}:${value} -> ${result.principal.name}\n${renderPrincipal(result.principal)}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
ctx.print(`[principals resolve] ${describeOperatorRpcError(error)}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -154,11 +154,51 @@ function inspectReleaseBundle(path: string): string {
|
|
|
154
154
|
export function registerProductRuntimeCommands(registry: CommandRegistry): void {
|
|
155
155
|
registry.register({
|
|
156
156
|
name: 'trust',
|
|
157
|
-
description: 'Review trust posture
|
|
158
|
-
usage: '[review|bundle export <path>|bundle inspect <path>]',
|
|
157
|
+
description: 'Review trust posture, set this workspace\'s trust level, or export trust bundles',
|
|
158
|
+
usage: '[review|workspace [trusted|restricted]|bundle export <path>|bundle inspect <path>]',
|
|
159
159
|
async handler(args, ctx) {
|
|
160
|
-
const shellPaths = requireShellPaths(ctx);
|
|
161
160
|
const sub = args[0] ?? 'review';
|
|
161
|
+
if (sub === 'workspace') {
|
|
162
|
+
const manager = ctx.workspace.workspaceTrustManager;
|
|
163
|
+
if (!manager) {
|
|
164
|
+
ctx.print('Workspace trust is unavailable in this context.');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const choice = (args[1] ?? '').toLowerCase();
|
|
168
|
+
if (choice === 'trusted' || choice === 'trust' || choice === 'full' || choice === 'yes') {
|
|
169
|
+
await manager.setLevel('trusted');
|
|
170
|
+
ctx.print('This workspace is now TRUSTED — all tools may run, subject to your permission settings.');
|
|
171
|
+
ctx.renderRequest();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (choice === 'restricted' || choice === 'restrict' || choice === 'readonly' || choice === 'read-only' || choice === 'no') {
|
|
175
|
+
await manager.setLevel('restricted');
|
|
176
|
+
ctx.print('This workspace is now RESTRICTED — read-only exploration; write/execute/delegate tools are denied until you run `/trust workspace trusted`.');
|
|
177
|
+
ctx.renderRequest();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (choice && choice !== 'status' && choice !== 'show') {
|
|
181
|
+
ctx.print(`Unknown workspace trust option "${args[1]}". Usage: /trust workspace [trusted|restricted]`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const level = manager.getLevel();
|
|
185
|
+
const lines = [
|
|
186
|
+
`Workspace trust: ${level.toUpperCase()}` +
|
|
187
|
+
(manager.wasGrandfathered() ? ' (grandfathered from prior GoodVibes state)' : ''),
|
|
188
|
+
level === 'trusted'
|
|
189
|
+
? 'All tools may run, subject to your normal permission settings.'
|
|
190
|
+
: 'Read-only exploration: write, execute, and delegate tools are denied until you trust this workspace.',
|
|
191
|
+
];
|
|
192
|
+
if (!manager.isDecided()) lines.push('Not yet decided — run `/trust workspace trusted` to enable full capability.');
|
|
193
|
+
const registrationManager = ctx.workspace.workspaceRegistrationManager;
|
|
194
|
+
if (registrationManager) {
|
|
195
|
+
lines.push(`Registration: ${await registrationManager.describe()}`);
|
|
196
|
+
}
|
|
197
|
+
lines.push('Change with: /trust workspace trusted | /trust workspace restricted');
|
|
198
|
+
ctx.print(lines.join('\n'));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const shellPaths = requireShellPaths(ctx);
|
|
162
202
|
if (sub === 'review') {
|
|
163
203
|
const bundle = await buildTrustReviewBundle(ctx);
|
|
164
204
|
ctx.print(formatTrustReview(bundle));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-files-config.ts — TUI-local synthetic setting for the memory file
|
|
3
|
+
* projection directory (`memory.projection.dir`).
|
|
4
|
+
*
|
|
5
|
+
* `memory.projection.dir` is not in the SDK's ConfigKey union — same
|
|
6
|
+
* situation as worktree.setup.commands (see worktree-setup-config.ts, which
|
|
7
|
+
* this file mirrors): a brand-new top-level config section with no
|
|
8
|
+
* DEFAULT_CONFIG entry, so ConfigManager.get()/set() path resolution can
|
|
9
|
+
* throw for it. Read defensively; the settings-modal write path already
|
|
10
|
+
* degrades a resolution failure into an honest "Save failed" message.
|
|
11
|
+
*
|
|
12
|
+
* Default: `.goodvibes/memory/projection`, relative to the project working
|
|
13
|
+
* directory — under the project's own `.goodvibes/` control directory,
|
|
14
|
+
* alongside the (separate) KV-state memory directory used by the state tool.
|
|
15
|
+
*/
|
|
16
|
+
import type { ConfigKey, ConfigManager, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
|
+
|
|
18
|
+
export const MEMORY_PROJECTION_DIR_CONFIG_KEY = 'memory.projection.dir' as ConfigKey;
|
|
19
|
+
|
|
20
|
+
/** Default projection directory, relative to the project working directory. */
|
|
21
|
+
export const MEMORY_PROJECTION_DIR_DEFAULT = '.goodvibes/memory/projection';
|
|
22
|
+
|
|
23
|
+
export const MEMORY_PROJECTION_DIR_SYNTHETIC_SETTING: ConfigSetting = {
|
|
24
|
+
key: MEMORY_PROJECTION_DIR_CONFIG_KEY,
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: MEMORY_PROJECTION_DIR_DEFAULT,
|
|
27
|
+
description: '/recall files sync writes one markdown file per standing (project/team-scope) memory record here, and commits the directory when it sits inside a git repository. Relative paths resolve against the project working directory. /recall files review reads user edits back from this directory as proposals — a file edit or deletion never mutates the store directly.',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Read the configured projection directory, falling back to the default for an absent/malformed value. */
|
|
31
|
+
export function readMemoryProjectionDir(configManager: Pick<ConfigManager, 'get'>): string {
|
|
32
|
+
let raw: unknown;
|
|
33
|
+
try {
|
|
34
|
+
raw = configManager.get(MEMORY_PROJECTION_DIR_CONFIG_KEY);
|
|
35
|
+
} catch {
|
|
36
|
+
return MEMORY_PROJECTION_DIR_DEFAULT;
|
|
37
|
+
}
|
|
38
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : MEMORY_PROJECTION_DIR_DEFAULT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isMemoryProjectionDirConfigKey(key: ConfigKey): boolean {
|
|
42
|
+
return key === MEMORY_PROJECTION_DIR_CONFIG_KEY;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildMemoryProjectionDirSyntheticEntry(
|
|
46
|
+
configManager: Pick<ConfigManager, 'get'>,
|
|
47
|
+
): { setting: ConfigSetting; currentValue: string; isDefault: boolean } {
|
|
48
|
+
const currentValue = readMemoryProjectionDir(configManager);
|
|
49
|
+
return {
|
|
50
|
+
setting: MEMORY_PROJECTION_DIR_SYNTHETIC_SETTING,
|
|
51
|
+
currentValue,
|
|
52
|
+
isDefault: currentValue === MEMORY_PROJECTION_DIR_DEFAULT,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall-files-git.ts — the synchronous git seam for memory file projection.
|
|
3
|
+
*
|
|
4
|
+
* `projectMemoryToFiles(records, dir, { git })` (SDK, platform/state) is a
|
|
5
|
+
* pure synchronous function and its `MemoryProjectionGit` seam is declared
|
|
6
|
+
* synchronous (`add(dir): void`, `commit(dir, message): void`) — it throws on
|
|
7
|
+
* failure rather than returning a result. The TUI's existing git wrapper,
|
|
8
|
+
* `GitService` (platform/git), is async (wraps simple-git), so it cannot
|
|
9
|
+
* satisfy this seam directly. `GitService`'s own static helpers
|
|
10
|
+
* (initRepo/isGitRepo/getRepoRoot) ARE synchronous, using `Bun.spawnSync`
|
|
11
|
+
* directly (see service.ts) — this file follows the exact same convention
|
|
12
|
+
* for `add`/`commit` so the seam matches the SDK's synchronous contract.
|
|
13
|
+
*/
|
|
14
|
+
import type { MemoryProjectionGit } from '@pellux/goodvibes-sdk/platform/state';
|
|
15
|
+
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
16
|
+
|
|
17
|
+
function decode(bytes: Uint8Array | undefined): string {
|
|
18
|
+
return bytes ? new TextDecoder().decode(bytes).trim() : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build a synchronous git seam rooted at `repoRoot`. `commit()` treats a
|
|
23
|
+
* "nothing to commit" exit as a benign no-op (not an error) so
|
|
24
|
+
* `/recall files sync` stays safely re-runnable when the projection files
|
|
25
|
+
* did not actually change since the last sync — any OTHER commit failure
|
|
26
|
+
* (no git identity configured, hook rejection, etc.) still throws, since the
|
|
27
|
+
* seam's contract has no way to report partial failure back to the caller
|
|
28
|
+
* other than throwing.
|
|
29
|
+
*/
|
|
30
|
+
export function createSyncGitSeam(repoRoot: string): MemoryProjectionGit {
|
|
31
|
+
return {
|
|
32
|
+
add(dir: string): void {
|
|
33
|
+
const result = Bun.spawnSync(['git', 'add', dir], { cwd: repoRoot });
|
|
34
|
+
if (result.exitCode !== 0) {
|
|
35
|
+
throw new Error(`git add failed: ${decode(result.stderr) || 'unknown error'}`);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
commit(dir: string, message: string): void {
|
|
39
|
+
const result = Bun.spawnSync(['git', 'commit', '-m', message, '--', dir], { cwd: repoRoot });
|
|
40
|
+
if (result.exitCode === 0) return;
|
|
41
|
+
const output = `${decode(result.stdout)} ${decode(result.stderr)}`;
|
|
42
|
+
if (/nothing to commit/i.test(output)) return; // benign — no new/changed files this run
|
|
43
|
+
throw new Error(`git commit failed: ${output.trim() || 'unknown error'}`);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the git seam for `dir`, or null when `dir` is not inside a git
|
|
50
|
+
* repository. Never auto-initializes a repository — that is a much bigger
|
|
51
|
+
* side effect than a memory sync command should take unasked.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveSyncGitSeam(dir: string): { seam: MemoryProjectionGit; repoRoot: string } | null {
|
|
54
|
+
const repoRoot = GitService.getRepoRoot(dir);
|
|
55
|
+
if (!repoRoot) return null;
|
|
56
|
+
return { seam: createSyncGitSeam(repoRoot), repoRoot };
|
|
57
|
+
}
|