@pellux/goodvibes-tui 0.20.3 → 0.22.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 +50 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +4 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +658 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +31 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +14 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/context-auto-compact.ts +77 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +32 -1
- package/src/input/commands/control-room-runtime.ts +10 -10
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -4
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +128 -12
- package/src/input/handler-modal-token-routes.ts +22 -5
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +73 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +6 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +378 -0
- package/src/input/settings-modal-mutations.ts +157 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +236 -232
- package/src/main.ts +93 -85
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +5 -1
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/session-maintenance.ts +66 -15
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +118 -13
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +90 -10
- package/src/renderer/shell-surface.ts +10 -0
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +18 -0
- package/src/runtime/bootstrap-core.ts +145 -13
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +9 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +27 -1
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/panels/knowledge-panel.ts +0 -345
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -196,18 +196,18 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
|
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
registry.register({
|
|
199
|
-
name: '
|
|
200
|
-
aliases: ['
|
|
201
|
-
description: 'Inspect durable project
|
|
199
|
+
name: 'project-memory',
|
|
200
|
+
aliases: ['pmem'],
|
|
201
|
+
description: 'Inspect durable project memory: risks, runbooks, and architecture notes',
|
|
202
202
|
usage: '[open | queue [limit] | explain <task...> [--scope <path> ...]]',
|
|
203
203
|
handler(args, ctx) {
|
|
204
204
|
const subcommand = (args[0] ?? 'open').toLowerCase();
|
|
205
205
|
if (subcommand === 'open') {
|
|
206
|
-
if (ctx.
|
|
207
|
-
ctx.
|
|
206
|
+
if (ctx.openMemoryPanel) {
|
|
207
|
+
ctx.openMemoryPanel();
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
|
-
ctx.print('
|
|
210
|
+
ctx.print('Memory panel is not available in this runtime.');
|
|
211
211
|
return;
|
|
212
212
|
}
|
|
213
213
|
const memory = getMemoryApi(ctx);
|
|
@@ -237,7 +237,7 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
|
|
|
237
237
|
});
|
|
238
238
|
const task = taskTokens.join(' ').trim();
|
|
239
239
|
if (!task) {
|
|
240
|
-
ctx.print('Usage: /
|
|
240
|
+
ctx.print('Usage: /project-memory explain <task...> [--scope <path> ...]');
|
|
241
241
|
return;
|
|
242
242
|
}
|
|
243
243
|
const injections = selectKnowledgeForTask(memory, task, scopeValues);
|
|
@@ -245,11 +245,11 @@ export function registerControlRoomRuntimeCommands(registry: CommandRegistry): v
|
|
|
245
245
|
ctx.print(prompt ?? 'No reviewed project knowledge matched that task.');
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
|
-
if (ctx.
|
|
249
|
-
ctx.
|
|
248
|
+
if (ctx.openMemoryPanel) {
|
|
249
|
+
ctx.openMemoryPanel();
|
|
250
250
|
return;
|
|
251
251
|
}
|
|
252
|
-
ctx.print(`Unknown
|
|
252
|
+
ctx.print(`Unknown project-memory subcommand: ${subcommand}`);
|
|
253
253
|
},
|
|
254
254
|
});
|
|
255
255
|
}
|
|
@@ -222,7 +222,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
|
|
|
222
222
|
|
|
223
223
|
registry.register({
|
|
224
224
|
name: 'voice',
|
|
225
|
-
description: 'Review
|
|
225
|
+
description: 'Review or toggle always-speak mode (same switch as /tts on|off) and package portable voice metadata',
|
|
226
226
|
usage: '[review|enable|disable|bundle export <path>|bundle inspect <path>]',
|
|
227
227
|
handler(args, ctx) {
|
|
228
228
|
const shellPaths = requireShellPaths(ctx);
|
|
@@ -231,8 +231,9 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
|
|
|
231
231
|
const enabled = Boolean(ctx.platform.configManager.get('ui.voiceEnabled') ?? false);
|
|
232
232
|
ctx.print([
|
|
233
233
|
'Voice Review',
|
|
234
|
-
`
|
|
235
|
-
'
|
|
234
|
+
` always-speak: ${enabled ? 'on' : 'off'}`,
|
|
235
|
+
' config key: ui.voiceEnabled (same as /tts on|off)',
|
|
236
|
+
' posture: optional local TTS output; disabled by default',
|
|
236
237
|
' note: voice remains an optional operator convenience, not a required SaaS dependency',
|
|
237
238
|
].join('\n'));
|
|
238
239
|
return;
|
|
@@ -240,7 +241,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
|
|
|
240
241
|
if (sub === 'enable' || sub === 'disable') {
|
|
241
242
|
const next = sub === 'enable';
|
|
242
243
|
ctx.platform.configManager.setDynamic('ui.voiceEnabled', next);
|
|
243
|
-
ctx.print(`
|
|
244
|
+
ctx.print(`Always-speak mode ${next ? 'enabled' : 'disabled'}. ${next ? 'Every submitted turn will be played through live TTS.' : 'Use /tts <prompt> to speak individual turns.'}`);
|
|
244
245
|
return;
|
|
245
246
|
}
|
|
246
247
|
if (sub === 'bundle') {
|
|
@@ -131,7 +131,7 @@ function renderKnowledgeAskResult(result: KnowledgeAskResult): string {
|
|
|
131
131
|
|
|
132
132
|
export const knowledgeCommand: SlashCommand = {
|
|
133
133
|
name: 'knowledge',
|
|
134
|
-
aliases: ['know'
|
|
134
|
+
aliases: ['know'],
|
|
135
135
|
description: 'Structured knowledge graph: ingest URLs/bookmarks, inspect issues, and build compact prompt packets.',
|
|
136
136
|
usage: '<subcommand> [args]',
|
|
137
137
|
argsHint: 'status|ask|ingest-url|import-bookmarks|import-urls|list|search|get|queue|review-issue|candidates|reports|schedules|lint|packet|explain|reindex|consolidate',
|
|
@@ -17,11 +17,22 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
|
|
|
17
17
|
if (sub === 'add-user') {
|
|
18
18
|
const username = args[1];
|
|
19
19
|
const password = args[2];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
if (!username) {
|
|
21
|
+
ctx.print('Usage: /auth local add-user <username> <password> [roles]\nTip: invoke without a password to use the masked panel: /auth local add-user <username>');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!password) {
|
|
25
|
+
// No password supplied — open masked-entry mode on the LocalAuthPanel.
|
|
26
|
+
if (ctx.openLocalAuthMaskedEntry) {
|
|
27
|
+
ctx.openLocalAuthMaskedEntry('add-user', username);
|
|
28
|
+
} else {
|
|
29
|
+
ctx.print('Masked entry unavailable in this context. Use: /auth local add-user <username> <password>');
|
|
30
|
+
}
|
|
23
31
|
return;
|
|
24
32
|
}
|
|
33
|
+
// Password supplied as argv: warn that the history entry has been scrubbed.
|
|
34
|
+
ctx.print('Warning: passwords passed as command arguments are scrubbed from history, but may appear in shell scrollback. The masked entry is preferred: /auth local add-user <username>');
|
|
35
|
+
const roles = args[3]?.split(',').map((value) => value.trim()).filter(Boolean) ?? ['admin'];
|
|
25
36
|
try {
|
|
26
37
|
const added = auth.addUser(username, password, roles);
|
|
27
38
|
ctx.print(`Added local auth user ${added.username} (${formatRoles(added.roles)}).`);
|
|
@@ -49,10 +60,21 @@ export function handleLocalAuthCommand(args: string[], ctx: CommandContext): voi
|
|
|
49
60
|
if (sub === 'rotate-password') {
|
|
50
61
|
const username = args[1];
|
|
51
62
|
const password = args[2];
|
|
52
|
-
if (!username
|
|
53
|
-
ctx.print('Usage: /auth local rotate-password <username> <password>');
|
|
63
|
+
if (!username) {
|
|
64
|
+
ctx.print('Usage: /auth local rotate-password <username> <password>\nTip: invoke without a password to use the masked panel: /auth local rotate-password <username>');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!password) {
|
|
68
|
+
// No password supplied — open masked-entry mode on the LocalAuthPanel.
|
|
69
|
+
if (ctx.openLocalAuthMaskedEntry) {
|
|
70
|
+
ctx.openLocalAuthMaskedEntry('rotate-password', username);
|
|
71
|
+
} else {
|
|
72
|
+
ctx.print('Masked entry unavailable in this context. Use: /auth local rotate-password <username> <password>');
|
|
73
|
+
}
|
|
54
74
|
return;
|
|
55
75
|
}
|
|
76
|
+
// Password supplied as argv: warn that the history entry has been scrubbed.
|
|
77
|
+
ctx.print('Warning: passwords passed as command arguments are scrubbed from history, but may appear in shell scrollback. The masked entry is preferred: /auth local rotate-password <username>');
|
|
56
78
|
try {
|
|
57
79
|
auth.rotatePassword(username, password);
|
|
58
80
|
ctx.print(`Rotated password for ${username}. Existing sessions were revoked.`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from 'path';
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { atomicWriteFileSync } from '../../config/atomic-write.ts';
|
|
3
4
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
5
|
import type { ConfigKey } from '../../config/index.ts';
|
|
5
6
|
import { CONFIG_SCHEMA } from '../../config/index.ts';
|
|
@@ -204,18 +205,15 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
|
|
|
204
205
|
}
|
|
205
206
|
if (bundle.services) {
|
|
206
207
|
const servicesPath = getShellPaths().resolveProjectPath('tui', 'services.json');
|
|
207
|
-
|
|
208
|
-
writeFileSync(servicesPath, JSON.stringify(bundle.services, null, 2) + '\n', 'utf-8');
|
|
208
|
+
atomicWriteFileSync(servicesPath, JSON.stringify(bundle.services, null, 2) + '\n', { mkdirp: true });
|
|
209
209
|
}
|
|
210
210
|
if (bundle.ecosystem?.plugins) {
|
|
211
211
|
const pluginsPath = getShellPaths().resolveProjectPath('tui', 'ecosystem', 'plugins.json');
|
|
212
|
-
|
|
213
|
-
writeFileSync(pluginsPath, JSON.stringify(bundle.ecosystem.plugins, null, 2) + '\n', 'utf-8');
|
|
212
|
+
atomicWriteFileSync(pluginsPath, JSON.stringify(bundle.ecosystem.plugins, null, 2) + '\n', { mkdirp: true });
|
|
214
213
|
}
|
|
215
214
|
if (bundle.ecosystem?.skills) {
|
|
216
215
|
const skillsPath = getShellPaths().resolveProjectPath('tui', 'ecosystem', 'skills.json');
|
|
217
|
-
|
|
218
|
-
writeFileSync(skillsPath, JSON.stringify(bundle.ecosystem.skills, null, 2) + '\n', 'utf-8');
|
|
216
|
+
atomicWriteFileSync(skillsPath, JSON.stringify(bundle.ecosystem.skills, null, 2) + '\n', { mkdirp: true });
|
|
219
217
|
}
|
|
220
218
|
ctx.print(`Imported setup transfer bundle from ${targetPath}`);
|
|
221
219
|
} catch (error) {
|
|
@@ -55,7 +55,7 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
55
55
|
|
|
56
56
|
registry.register({
|
|
57
57
|
name: 'session-memory',
|
|
58
|
-
description: 'Dedicated front-door for session-scoped memory capture and review',
|
|
58
|
+
description: 'Dedicated front-door for session-scoped memory capture and review. All subcommands are filtered to scope=session.',
|
|
59
59
|
usage: '[queue [limit] | export <path> | add <class> <summary...>]',
|
|
60
60
|
async handler(args, ctx) {
|
|
61
61
|
const sub = (args[0] ?? 'queue').toLowerCase();
|
|
@@ -64,7 +64,8 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
66
|
if (sub === 'queue') {
|
|
67
|
-
|
|
67
|
+
// Pass --scope session so only session-scoped records appear in the queue.
|
|
68
|
+
await ctx.executeCommand('recall', ['queue', '--scope', 'session', ...(args[1] ? [args[1]] : [])]);
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
70
71
|
if (sub === 'export' && args[1]) {
|
|
@@ -75,13 +76,13 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
75
76
|
await ctx.executeCommand('recall', ['add', args[1], ...args.slice(2), '--scope', 'session']);
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
|
-
ctx.print('Usage: /session-memory [queue [limit] | export <path> | add <class> <summary...>]');
|
|
79
|
+
ctx.print('Usage: /session-memory [queue [limit] | export <path> | add <class> <summary...>]\nAll subcommands are scoped to session records only.');
|
|
79
80
|
},
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
registry.register({
|
|
83
84
|
name: 'team-memory',
|
|
84
|
-
description: 'Dedicated front-door for team/shared memory review and exchange',
|
|
85
|
+
description: 'Dedicated front-door for team/shared memory review and exchange. The queue and export subcommands are filtered to scope=team.',
|
|
85
86
|
usage: '[queue [limit] | export <path> | import <path> | capture policy]',
|
|
86
87
|
async handler(args, ctx) {
|
|
87
88
|
const sub = (args[0] ?? 'queue').toLowerCase();
|
|
@@ -90,7 +91,8 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
if (sub === 'queue') {
|
|
93
|
-
|
|
94
|
+
// Pass --scope team so only team-scoped records appear in the queue.
|
|
95
|
+
await ctx.executeCommand('recall', ['queue', '--scope', 'team', ...(args[1] ? [args[1]] : [])]);
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
if (sub === 'export' && args[1]) {
|
|
@@ -105,7 +107,7 @@ export function registerMemoryProductRuntimeCommands(registry: CommandRegistry):
|
|
|
105
107
|
await ctx.executeCommand('recall', ['capture', 'policy']);
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
ctx.print('Usage: /team-memory [queue [limit] | export <path> | import <path> | capture policy]');
|
|
110
|
+
ctx.print('Usage: /team-memory [queue [limit] | export <path> | import <path> | capture policy]\nqueue and export are scoped to team records; import applies the bundle\'s own scopes and capture policy is global.');
|
|
109
111
|
},
|
|
110
112
|
});
|
|
111
113
|
}
|
|
@@ -5,7 +5,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
export function registerOperatorPanelCommand(registry: CommandRegistry): void {
|
|
6
6
|
registry.register({
|
|
7
7
|
name: 'panel',
|
|
8
|
-
aliases: ['panels'
|
|
8
|
+
aliases: ['panels'],
|
|
9
9
|
description: 'Open, place, resize, or list panels. Usage: /panel [open <id> [top|bottom]|close <id>|list|toggle|move|focus|split|width|height]',
|
|
10
10
|
usage: '[open <id> [top|bottom]|close <id>|list|toggle|move <top|bottom|other> [id]|focus <top|bottom|toggle>|split [show|hide|toggle]|width <left|right|reset>|height <up|down|reset>]',
|
|
11
11
|
argsHint: '<open|close|list|toggle|move|focus|split|width|height> [id]',
|
|
@@ -5,6 +5,7 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
5
5
|
import { registerOperatorPanelCommand } from './operator-panel-runtime.ts';
|
|
6
6
|
import { requireOpsApi, requireProfileManager, requireReplayEngine } from './runtime-services.ts';
|
|
7
7
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
|
+
import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
8
9
|
|
|
9
10
|
export function registerOperatorRuntimeCommands(registry: CommandRegistry): void {
|
|
10
11
|
registerOperatorPanelCommand(registry);
|
|
@@ -32,18 +33,10 @@ export function registerOperatorRuntimeCommands(registry: CommandRegistry): void
|
|
|
32
33
|
ctx.print('[context] No messages in conversation.');
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
let total = 0;
|
|
36
|
+
const total = estimateConversationTokens(msgs);
|
|
37
37
|
const lines: string[] = ['Context breakdown:'];
|
|
38
38
|
for (const m of msgs) {
|
|
39
|
-
const
|
|
40
|
-
? m.content
|
|
41
|
-
: (m.content as Array<{ type: string; text?: string }>)
|
|
42
|
-
.filter((p) => p.type === 'text')
|
|
43
|
-
.map((p) => p.text ?? '')
|
|
44
|
-
.join('');
|
|
45
|
-
const t = estimateTokens(text);
|
|
46
|
-
total += t;
|
|
39
|
+
const t = estimateConversationTokens([m]);
|
|
47
40
|
lines.push(` ${m.role.padEnd(12)} ~${t.toLocaleString()} tokens`);
|
|
48
41
|
}
|
|
49
42
|
lines.push(` ${'Total'.padEnd(12)} ~${total.toLocaleString()} tokens (${msgs.length} messages)`);
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '@/runtime/index.ts';
|
|
16
16
|
import { requireEcosystemCatalogPaths, requirePluginPathOptions } from './runtime-services.ts';
|
|
17
17
|
|
|
18
|
-
export function
|
|
18
|
+
export function registerPluginRuntimeCommands(registry: CommandRegistry): void {
|
|
19
19
|
registry.register({
|
|
20
20
|
name: 'plugin',
|
|
21
21
|
aliases: [],
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implements the Provider Optimizer panel commands:
|
|
5
5
|
*
|
|
6
|
+
* /provider optimizer on|off — Enable or disable the provider optimizer
|
|
6
7
|
* /provider route auto|manual — Set optimizer routing mode
|
|
7
8
|
* /provider explain-route — Print current route explanation
|
|
8
9
|
* /provider pin <provider:model> — Pin routing to a specific provider/model
|
|
@@ -10,14 +11,19 @@
|
|
|
10
11
|
*
|
|
11
12
|
* When the optimizer is disabled, commands report its status and
|
|
12
13
|
* explain-route still works (reads current model capabilities).
|
|
14
|
+
* Enabling the optimizer persists the change to config so it survives restart.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import type { SlashCommand, CommandContext } from '../command-registry.ts';
|
|
18
|
+
import type { ConfigKey } from '../../config/index.ts';
|
|
16
19
|
import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
|
|
17
20
|
import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
18
21
|
import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
|
|
19
22
|
import { requireProviderApi } from './runtime-services.ts';
|
|
20
23
|
|
|
24
|
+
const PROVIDER_OPTIMIZER_FLAG = 'provider-optimizer';
|
|
25
|
+
const PROVIDER_OPTIMIZER_CONFIG_KEY = `featureFlags.${PROVIDER_OPTIMIZER_FLAG}` as ConfigKey;
|
|
26
|
+
|
|
21
27
|
// ---------------------------------------------------------------------------
|
|
22
28
|
// Formatting helpers
|
|
23
29
|
// ---------------------------------------------------------------------------
|
|
@@ -65,6 +71,49 @@ function fmtExplanation(expl: RouteExplanation, context: CommandContext): void {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// /provider optimizer on|off
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
function handleOptimizerToggle(
|
|
79
|
+
args: string[],
|
|
80
|
+
context: CommandContext,
|
|
81
|
+
): void {
|
|
82
|
+
const optimizer = requireProviderOptimizer(context);
|
|
83
|
+
if (!optimizer) return;
|
|
84
|
+
const sub = args[0];
|
|
85
|
+
|
|
86
|
+
if (sub !== 'on' && sub !== 'off') {
|
|
87
|
+
context.print('[provider] Usage: /provider optimizer on|off');
|
|
88
|
+
context.print(` Current state: optimizer is ${optimizer.enabled ? 'enabled' : 'disabled'}`);
|
|
89
|
+
context.print(' "on" — activates intelligent failover and auto-routing');
|
|
90
|
+
context.print(' "off" — disables optimizer; provider selection is manual only');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const enable = sub === 'on';
|
|
95
|
+
const wasEnabled = optimizer.enabled;
|
|
96
|
+
optimizer.setEnabled(enable);
|
|
97
|
+
|
|
98
|
+
// Persist to config so the setting survives restart.
|
|
99
|
+
const flagValue = enable ? 'enabled' : 'disabled';
|
|
100
|
+
context.platform.configManager.setDynamic(PROVIDER_OPTIMIZER_CONFIG_KEY, flagValue);
|
|
101
|
+
|
|
102
|
+
if (enable && !wasEnabled) {
|
|
103
|
+
context.print('[provider] Optimizer enabled.');
|
|
104
|
+
context.print(' Intelligent failover is now active: on a request error the optimizer');
|
|
105
|
+
context.print(' will attempt the next viable provider and surface a transcript notice');
|
|
106
|
+
context.print(' naming the from→to transition and reason before retrying.');
|
|
107
|
+
context.print(' Use "/provider route auto" to enable fully automatic routing.');
|
|
108
|
+
} else if (!enable && wasEnabled) {
|
|
109
|
+
context.print('[provider] Optimizer disabled.');
|
|
110
|
+
context.print(' Provider selection returns to manual-only mode. No automatic failover.');
|
|
111
|
+
context.print(' Pinned targets and fallback log are preserved; re-enable to resume.');
|
|
112
|
+
} else {
|
|
113
|
+
context.print(`[provider] Optimizer already ${enable ? 'enabled' : 'disabled'} — no change.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
68
117
|
// ---------------------------------------------------------------------------
|
|
69
118
|
// /provider route auto|manual
|
|
70
119
|
// ---------------------------------------------------------------------------
|
|
@@ -85,9 +134,9 @@ function handleRoute(
|
|
|
85
134
|
|
|
86
135
|
if (!optimizer.enabled) {
|
|
87
136
|
context.print(
|
|
88
|
-
'[provider] Optimizer is
|
|
137
|
+
'[provider] Optimizer is off — routing mode recorded but failover will not fire until optimizer is enabled.',
|
|
89
138
|
);
|
|
90
|
-
context.print(
|
|
139
|
+
context.print(' Enable with: /provider optimizer on');
|
|
91
140
|
}
|
|
92
141
|
|
|
93
142
|
optimizer.setMode(sub);
|
|
@@ -318,11 +367,15 @@ export const providerCommand: SlashCommand = {
|
|
|
318
367
|
aliases: ['prov-opt'],
|
|
319
368
|
description: 'Manage provider routing optimizer (route, pin, explain, fallback).',
|
|
320
369
|
usage: '<subcommand> [args]',
|
|
321
|
-
argsHint: 'route|explain-route|pin|fallback',
|
|
370
|
+
argsHint: 'optimizer|route|explain-route|pin|fallback',
|
|
322
371
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
323
372
|
const [sub, ...rest] = args;
|
|
324
373
|
|
|
325
374
|
switch (sub) {
|
|
375
|
+
case 'optimizer':
|
|
376
|
+
handleOptimizerToggle(rest, context);
|
|
377
|
+
break;
|
|
378
|
+
|
|
326
379
|
case 'route':
|
|
327
380
|
handleRoute(rest, context);
|
|
328
381
|
break;
|
|
@@ -345,6 +398,7 @@ export const providerCommand: SlashCommand = {
|
|
|
345
398
|
if (!optimizer) return;
|
|
346
399
|
const lines = [
|
|
347
400
|
'Usage: /provider <subcommand>',
|
|
401
|
+
' optimizer on|off — Enable or disable the provider optimizer',
|
|
348
402
|
' route auto|manual — Set optimizer routing mode',
|
|
349
403
|
' explain-route — Show current route explanation',
|
|
350
404
|
' pin <provider:model> — Pin routing to specific provider/model',
|
|
@@ -7,8 +7,32 @@ export function handleRecallQueue(args: string[], context: CommandContext): void
|
|
|
7
7
|
if (!memory) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
// Extract optional --scope filter before parsing the positional limit argument.
|
|
12
|
+
const scopeIdx = args.indexOf('--scope');
|
|
13
|
+
let scopeFilter: string | undefined;
|
|
14
|
+
let remainingArgs = args;
|
|
15
|
+
if (scopeIdx !== -1 && args[scopeIdx + 1]) {
|
|
16
|
+
const candidateScope = args[scopeIdx + 1];
|
|
17
|
+
if (!isValidScope(candidateScope)) {
|
|
18
|
+
context.print(`[recall] Unknown scope "${candidateScope}". Valid: ${VALID_SCOPES.join(', ')}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
scopeFilter = candidateScope;
|
|
22
|
+
remainingArgs = args.filter((_, i) => i !== scopeIdx && i !== scopeIdx + 1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const limitRaw = remainingArgs.find((a) => !a.startsWith('--'));
|
|
26
|
+
const limit = Math.max(1, parseInt(limitRaw ?? '10', 10) || 10);
|
|
27
|
+
|
|
28
|
+
// When a scope filter is requested, fetch a larger pool then slice to the
|
|
29
|
+
// requested limit so scope-sparse queues still return meaningful results.
|
|
30
|
+
const fetchLimit = scopeFilter ? 1000 : limit;
|
|
31
|
+
const rawQueue = memory.reviewQueue(fetchLimit);
|
|
32
|
+
const queue = scopeFilter
|
|
33
|
+
? rawQueue.filter((record) => record.scope === scopeFilter).slice(0, limit)
|
|
34
|
+
: rawQueue;
|
|
35
|
+
|
|
12
36
|
if (!queue.length) {
|
|
13
37
|
context.print('[recall] Review queue is empty.');
|
|
14
38
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { atomicWriteFileSync } from '../../config/atomic-write.ts';
|
|
3
4
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
4
5
|
import type { SelectionAction, SelectionItem } from '../selection-modal.ts';
|
|
5
6
|
import { openCommandPanel, requireServiceRegistry, requireShellPaths } from './runtime-services.ts';
|
|
@@ -161,8 +162,7 @@ export function registerServicesRuntimeCommands(registry: CommandRegistry): void
|
|
|
161
162
|
try {
|
|
162
163
|
const parsed = JSON.parse(readFileSync(sourcePath, 'utf-8')) as Record<string, unknown>;
|
|
163
164
|
const targetPath = shellPaths.resolveProjectPath('tui', 'services.json');
|
|
164
|
-
|
|
165
|
-
writeFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', 'utf-8');
|
|
165
|
+
atomicWriteFileSync(targetPath, JSON.stringify(parsed, null, 2) + '\n', { mkdirp: true });
|
|
166
166
|
ctx.print(`Imported services config from ${sourcePath}`);
|
|
167
167
|
} catch (error) {
|
|
168
168
|
ctx.print(`Failed to import services config: ${summarizeError(error)}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import type { CommandContext
|
|
3
|
+
import type { CommandContext } from '../command-registry.ts';
|
|
4
4
|
import { type SessionMeta } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
5
5
|
import type { TranscriptEventKind } from '@pellux/goodvibes-sdk/platform/core';
|
|
6
6
|
import type { ConversationTitleSource } from '../../core/conversation';
|
|
@@ -441,21 +441,13 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
|
|
|
441
441
|
return false;
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
async handler(args, ctx) {
|
|
452
|
-
const handled = await handleSessionWorkflowCommand(args, ctx);
|
|
453
|
-
if (!handled) {
|
|
454
|
-
ctx.print('Unknown subcommand: ' + (args[0] ?? '') + '\nUsage: /session [list | rename <name> | resume <id> | fork [name] | save [name] | info [id] | events [kind] | groups [kind] | hotspots | export <id> [format] | search <query> | delete <id>]');
|
|
455
|
-
}
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
}
|
|
444
|
+
// session-mgmt / smgmt was removed in TASK-032.
|
|
445
|
+
// All session lifecycle operations are now first-class subcommands of /session.
|
|
446
|
+
// Use /session list, /session resume, /session save, etc.
|
|
447
|
+
//
|
|
448
|
+
// CommandRegistry.register() throws on duplicate names/aliases, so this
|
|
449
|
+
// registration was intentionally deleted rather than left as dead code.
|
|
450
|
+
|
|
459
451
|
interface SessionExportData {
|
|
460
452
|
readonly messages: object[];
|
|
461
453
|
readonly timestamp?: number;
|
|
@@ -319,18 +319,52 @@ function handleCancel(args: string[], context: CommandContext): void {
|
|
|
319
319
|
/**
|
|
320
320
|
* sessionCommand — The `/session` slash command.
|
|
321
321
|
*
|
|
322
|
-
*
|
|
322
|
+
* The ONE front-door for all session operations. Owns two domains:
|
|
323
|
+
*
|
|
324
|
+
* Lifecycle (continuity, export, resume, pruning):
|
|
325
|
+
* list | rename | resume | fork | save | info | events | groups | hotspots | export | search | delete
|
|
326
|
+
*
|
|
327
|
+
* Orchestration (cross-session task DAG — 40 tests, cycle detection):
|
|
328
|
+
* link-task | handoff | graph | cancel
|
|
329
|
+
*
|
|
330
|
+
* Orchestration-command decision (TASK-032):
|
|
331
|
+
* Both domains live under /session rather than splitting orchestration into
|
|
332
|
+
* a separate /session-orch command. Rationale: they share the same entity
|
|
333
|
+
* (a session) and the same operator mental model ("I am working with sessions").
|
|
334
|
+
* A second front-door would create ambiguity about which command to reach for.
|
|
335
|
+
* Explicit switch routing (not fallthrough) makes both domains first-class;
|
|
336
|
+
* the former /session-mgmt alias (session-mgmt/smgmt) is removed so there
|
|
337
|
+
* is exactly one registration and no silent shadowing.
|
|
323
338
|
*/
|
|
324
339
|
export const sessionCommand: SlashCommand = {
|
|
325
340
|
name: 'session',
|
|
326
341
|
aliases: ['sess'],
|
|
327
|
-
description: '
|
|
342
|
+
description: 'Session lifecycle and orchestration: list, resume, fork, save, export, link-task, handoff, graph, cancel.',
|
|
328
343
|
usage: '<subcommand> [args]',
|
|
329
|
-
argsHint: 'link-task|handoff|graph|cancel',
|
|
344
|
+
argsHint: 'list|rename|resume|fork|save|info|export|search|delete|events|groups|hotspots|link-task|handoff|graph|cancel',
|
|
330
345
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
331
346
|
const [sub, ...rest] = args;
|
|
332
347
|
|
|
333
348
|
switch (sub) {
|
|
349
|
+
// ── Lifecycle subcommands ────────────────────────────────────────────────
|
|
350
|
+
// Each delegates explicitly to handleSessionWorkflowCommand so every
|
|
351
|
+
// subcommand has a deterministic, named path — no silent fallthrough.
|
|
352
|
+
case 'list':
|
|
353
|
+
case 'rename':
|
|
354
|
+
case 'resume':
|
|
355
|
+
case 'fork':
|
|
356
|
+
case 'save':
|
|
357
|
+
case 'info':
|
|
358
|
+
case 'export':
|
|
359
|
+
case 'search':
|
|
360
|
+
case 'delete':
|
|
361
|
+
case 'events':
|
|
362
|
+
case 'groups':
|
|
363
|
+
case 'hotspots':
|
|
364
|
+
await handleSessionWorkflowCommand(args, context);
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
// ── Orchestration subcommands ─────────────────────────────────────────────
|
|
334
368
|
case 'link-task':
|
|
335
369
|
case 'link':
|
|
336
370
|
handleLinkTask(rest, context);
|
|
@@ -350,24 +384,40 @@ export const sessionCommand: SlashCommand = {
|
|
|
350
384
|
handleCancel(rest, context);
|
|
351
385
|
break;
|
|
352
386
|
|
|
387
|
+
// ── No-arg: show current session info ────────────────────────────────────
|
|
388
|
+
case undefined:
|
|
389
|
+
await handleSessionWorkflowCommand([], context);
|
|
390
|
+
break;
|
|
391
|
+
|
|
353
392
|
default: {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
]
|
|
369
|
-
|
|
370
|
-
|
|
393
|
+
const usage = [
|
|
394
|
+
'Usage: /session <subcommand>',
|
|
395
|
+
'',
|
|
396
|
+
'Lifecycle:',
|
|
397
|
+
' list — List saved sessions',
|
|
398
|
+
' rename <name> — Rename the current session',
|
|
399
|
+
' resume <id|name> — Resume a saved session',
|
|
400
|
+
' fork [name] — Fork the current session',
|
|
401
|
+
' save [name] — Save the current session',
|
|
402
|
+
' info [id] — Show session info',
|
|
403
|
+
' export <id|.> [markdown|text] — Export session transcript',
|
|
404
|
+
' search <query> — Search session content',
|
|
405
|
+
' delete <id> — Delete a saved session',
|
|
406
|
+
' events [kind] — Show transcript events',
|
|
407
|
+
' groups [kind] — Show transcript groups',
|
|
408
|
+
' hotspots — Show transcript hotspots',
|
|
409
|
+
'',
|
|
410
|
+
'Orchestration:',
|
|
411
|
+
' link-task <taskId> [--session <sid>] [--depends-on <sid:taskId>] [--label <label>]',
|
|
412
|
+
' — Register a task in the cross-session graph',
|
|
413
|
+
' handoff <taskId> --to <sid> [--session <sid>] [--reason <reason>]',
|
|
414
|
+
' — Hand a task off to another session',
|
|
415
|
+
' graph [--session <sid>] [--format text|json]',
|
|
416
|
+
' — Display the cross-session task dependency graph',
|
|
417
|
+
' cancel <taskId> [--scope task|subtree|session] [--session <sid>] [--reason <reason>]',
|
|
418
|
+
' — Cancel tasks with scoped semantics',
|
|
419
|
+
].join('\n');
|
|
420
|
+
context.print(usage);
|
|
371
421
|
break;
|
|
372
422
|
}
|
|
373
423
|
}
|