@pellux/goodvibes-agent 0.1.70 → 0.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -3
  3. package/docs/README.md +2 -2
  4. package/docs/getting-started.md +1 -1
  5. package/docs/runtime-connection.md +37 -0
  6. package/package.json +43 -2
  7. package/src/agent/skill-discovery.ts +119 -0
  8. package/src/cli/config-overrides.ts +1 -5
  9. package/src/cli/entrypoint.ts +0 -6
  10. package/src/cli/help.ts +0 -43
  11. package/src/cli/index.ts +0 -2
  12. package/src/cli/management-commands.ts +1 -109
  13. package/src/cli/management.ts +1 -32
  14. package/src/cli/package-verification.ts +12 -4
  15. package/src/cli/parser.ts +0 -16
  16. package/src/cli/status.ts +1 -1
  17. package/src/cli/types.ts +0 -8
  18. package/src/input/commands/delegation-runtime.ts +0 -8
  19. package/src/input/commands/experience-runtime.ts +0 -177
  20. package/src/input/commands/guidance-runtime.ts +0 -69
  21. package/src/input/commands/local-runtime.ts +1 -57
  22. package/src/input/commands/local-setup-review.ts +1 -1
  23. package/src/input/commands/operator-runtime.ts +1 -145
  24. package/src/input/commands/platform-access-runtime.ts +2 -195
  25. package/src/input/commands/product-runtime.ts +0 -116
  26. package/src/input/commands/security-runtime.ts +88 -0
  27. package/src/input/commands/session-content.ts +0 -97
  28. package/src/input/commands/shell-core.ts +0 -13
  29. package/src/input/commands.ts +2 -95
  30. package/src/panels/builtin/operations.ts +3 -184
  31. package/src/panels/confirm-state.ts +1 -1
  32. package/src/panels/index.ts +0 -11
  33. package/src/version.ts +1 -1
  34. package/docs/deployment-and-services.md +0 -52
  35. package/src/cli/service-command.ts +0 -26
  36. package/src/cli/surface-command.ts +0 -247
  37. package/src/input/commands/branch-runtime.ts +0 -72
  38. package/src/input/commands/control-room-runtime.ts +0 -234
  39. package/src/input/commands/discovery-runtime.ts +0 -61
  40. package/src/input/commands/hooks-runtime.ts +0 -207
  41. package/src/input/commands/incident-runtime.ts +0 -106
  42. package/src/input/commands/integration-runtime.ts +0 -437
  43. package/src/input/commands/local-setup.ts +0 -288
  44. package/src/input/commands/managed-runtime.ts +0 -240
  45. package/src/input/commands/marketplace-runtime.ts +0 -305
  46. package/src/input/commands/memory-product-runtime.ts +0 -148
  47. package/src/input/commands/operator-panel-runtime.ts +0 -146
  48. package/src/input/commands/platform-services-runtime.ts +0 -271
  49. package/src/input/commands/profile-sync-runtime.ts +0 -110
  50. package/src/input/commands/provider.ts +0 -363
  51. package/src/input/commands/remote-runtime-pool.ts +0 -89
  52. package/src/input/commands/remote-runtime-setup.ts +0 -226
  53. package/src/input/commands/remote-runtime.ts +0 -432
  54. package/src/input/commands/replay-runtime.ts +0 -25
  55. package/src/input/commands/services-runtime.ts +0 -220
  56. package/src/input/commands/settings-sync-runtime.ts +0 -197
  57. package/src/input/commands/share-runtime.ts +0 -127
  58. package/src/input/commands/skills-runtime.ts +0 -226
  59. package/src/input/commands/teleport-runtime.ts +0 -68
  60. package/src/panels/cockpit-panel.ts +0 -183
  61. package/src/panels/communication-panel.ts +0 -153
  62. package/src/panels/control-plane-panel.ts +0 -211
  63. package/src/panels/forensics-panel.ts +0 -364
  64. package/src/panels/hooks-panel.ts +0 -239
  65. package/src/panels/incident-review-panel.ts +0 -197
  66. package/src/panels/marketplace-panel.ts +0 -212
  67. package/src/panels/ops-control-panel.ts +0 -150
  68. package/src/panels/ops-strategy-panel.ts +0 -235
  69. package/src/panels/orchestration-panel.ts +0 -272
  70. package/src/panels/plugins-panel.ts +0 -178
  71. package/src/panels/remote-panel.ts +0 -449
  72. package/src/panels/routes-panel.ts +0 -178
  73. package/src/panels/services-panel.ts +0 -231
  74. package/src/panels/settings-sync-panel.ts +0 -120
  75. package/src/panels/skills-panel.ts +0 -431
  76. package/src/panels/watchers-panel.ts +0 -193
  77. package/src/verification/live-verifier.ts +0 -588
  78. package/src/verification/verification-ledger.ts +0 -239
@@ -1,363 +0,0 @@
1
- /**
2
- * /provider command handler.
3
- *
4
- * Implements the Provider Optimizer panel commands:
5
- *
6
- * /provider route auto|manual — Set optimizer routing mode
7
- * /provider explain-route — Print current route explanation
8
- * /provider pin <provider:model> — Pin routing to a specific provider/model
9
- * /provider fallback test — Simulate the fallback chain
10
- *
11
- * When the optimizer is disabled, commands report its status and
12
- * explain-route still works (reads current model capabilities).
13
- */
14
-
15
- import type { SlashCommand, CommandContext } from '../command-registry.ts';
16
- import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
17
- import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
18
- import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
19
- import { requireProviderApi } from './runtime-services.ts';
20
-
21
- // ---------------------------------------------------------------------------
22
- // Formatting helpers
23
- // ---------------------------------------------------------------------------
24
-
25
- function fmtBool(value: boolean): string {
26
- return value ? 'yes' : 'no';
27
- }
28
-
29
- function fmtTs(epochMs: number): string {
30
- return new Date(epochMs).toISOString().replace('T', ' ').slice(0, 19);
31
- }
32
-
33
- function requireProviderOptimizer(context: CommandContext) {
34
- if (!context.provider.providerOptimizer) {
35
- context.print('[provider] Provider optimizer is not wired into this runtime.');
36
- return null;
37
- }
38
- return context.provider.providerOptimizer;
39
- }
40
-
41
- function fmtExplanation(expl: RouteExplanation, context: CommandContext): void {
42
- const status = expl.accepted ? '[accepted]' : '[rejected]';
43
- context.print(` ${status} ${expl.providerId}/${expl.modelId}`);
44
- context.print(` ${expl.summary}`);
45
- if (!expl.accepted && expl.rejections.length > 0) {
46
- context.print(' Unmet requirements:');
47
- for (const r of expl.rejections) {
48
- context.print(
49
- ` - ${r.code}: ${r.reason} (actual=${r.actual}, required=${r.required})`,
50
- );
51
- }
52
- }
53
- if (expl.accepted) {
54
- const c = expl.capability;
55
- context.print(
56
- ` Capabilities: streaming=${fmtBool(c.streaming)}, tools=${fmtBool(c.toolCalling)}, ` +
57
- `parallel=${fmtBool(c.parallelTools)}, json=${fmtBool(c.jsonMode)}, ` +
58
- `reasoning=${fmtBool(c.reasoningControls)}`,
59
- );
60
- context.print(
61
- ` Context=${c.maxContextTokens.toLocaleString()} tokens, ` +
62
- `output=${c.maxOutputTokens.toLocaleString()} tokens, ` +
63
- `timeout=${c.timeoutMs}ms, caching=${c.caching}`,
64
- );
65
- }
66
- }
67
-
68
- // ---------------------------------------------------------------------------
69
- // /provider route auto|manual
70
- // ---------------------------------------------------------------------------
71
-
72
- function handleRoute(
73
- args: string[],
74
- context: CommandContext,
75
- ): void {
76
- const optimizer = requireProviderOptimizer(context);
77
- if (!optimizer) return;
78
- const sub = args[0];
79
-
80
- if (sub !== 'auto' && sub !== 'manual') {
81
- context.print('[provider] Usage: /provider route auto|manual');
82
- context.print(` Current mode: ${optimizer.mode} (optimizer ${optimizer.enabled ? 'on' : 'off'})`);
83
- return;
84
- }
85
-
86
- if (!optimizer.enabled) {
87
- context.print(
88
- '[provider] Optimizer is currently disabled. Enable it with the provider-optimizer feature flag.',
89
- );
90
- context.print(` Routing mode set to: ${sub} (no-op until optimizer is enabled)`);
91
- }
92
-
93
- optimizer.setMode(sub);
94
- context.print(`[provider] Routing mode → ${sub}`);
95
-
96
- if (sub === 'auto') {
97
- context.print(
98
- ' Auto mode: optimizer selects the best capable provider for each request profile.',
99
- );
100
- } else {
101
- context.print(
102
- ' Manual mode: optimizer is advisory only; provider selection is caller-driven.',
103
- );
104
- }
105
- }
106
-
107
- // ---------------------------------------------------------------------------
108
- // /provider explain-route
109
- // ---------------------------------------------------------------------------
110
-
111
- async function handleExplainRoute(
112
- _args: string[],
113
- context: CommandContext,
114
- ): Promise<void> {
115
- const optimizer = requireProviderOptimizer(context);
116
- if (!optimizer) return;
117
- const providerApi = requireProviderApi(context);
118
-
119
- let currentModel: ProviderApiModelRecord;
120
- try {
121
- currentModel = await providerApi.getCurrentModel();
122
- } catch {
123
- context.print('[provider] No current model selected.');
124
- return;
125
- }
126
-
127
- context.print(
128
- `[provider] Route explanation for current model: ${currentModel.providerId}/${currentModel.modelId}`,
129
- );
130
-
131
- // Always explain current route regardless of optimizer enabled state
132
- const expl = optimizer.explainCurrentRoute();
133
- fmtExplanation(expl, context);
134
-
135
- // Show optimizer status
136
- context.print(`
137
- Optimizer: ${optimizer.enabled ? 'enabled' : 'disabled'}, mode=${optimizer.mode}`);
138
- if (optimizer.pinnedTarget) {
139
- context.print(
140
- ` Pinned to: ${optimizer.pinnedTarget.providerId}/${optimizer.pinnedTarget.modelId}`,
141
- );
142
- }
143
-
144
- // Show recent fallback transitions
145
- const log = optimizer.fallbackLog;
146
- if (log.length > 0) {
147
- const recent = log.slice(-5); // last 5 transitions
148
- context.print(` Fallback log (last ${recent.length} of ${log.length}):`);
149
- for (const t of recent) {
150
- context.print(
151
- ` ${fmtTs(t.ts)} ${t.from} → ${t.to} (${t.reason})`,
152
- );
153
- }
154
- } else {
155
- context.print(' Fallback log: empty');
156
- }
157
- }
158
-
159
- // ---------------------------------------------------------------------------
160
- // /provider pin <provider:model>
161
- // ---------------------------------------------------------------------------
162
-
163
- async function handlePin(
164
- args: string[],
165
- context: CommandContext,
166
- ): Promise<void> {
167
- const optimizer = requireProviderOptimizer(context);
168
- if (!optimizer) return;
169
- const target = args[0];
170
-
171
- if (!target) {
172
- // Show current pin status
173
- if (optimizer.pinnedTarget) {
174
- context.print(
175
- `[provider] Currently pinned to: ${optimizer.pinnedTarget.providerId}/${optimizer.pinnedTarget.modelId}`,
176
- );
177
- context.print(' Use "/provider pin <provider:model>" to change, or "/provider route manual" to unpin.');
178
- } else {
179
- context.print('[provider] No pin active. Usage: /provider pin <provider:model>');
180
- context.print(' Example: /provider pin anthropic:claude-opus-4-5');
181
- }
182
- return;
183
- }
184
-
185
- // Parse provider:model format
186
- const colonIdx = target.indexOf(':');
187
- if (colonIdx === -1) {
188
- context.print(`[provider] Invalid format "${target}". Expected: <provider>:<model>`);
189
- context.print(' Example: /provider pin anthropic:claude-opus-4-5');
190
- return;
191
- }
192
-
193
- const providerId = target.slice(0, colonIdx);
194
- const modelId = target.slice(colonIdx + 1);
195
-
196
- if (!providerId || !modelId) {
197
- context.print(`[provider] Invalid format "${target}". Both provider and model must be non-empty.`);
198
- return;
199
- }
200
-
201
- // Validate that the model exists in registry
202
- const providerApi = requireProviderApi(context);
203
- const models = await providerApi.listModels();
204
- const currentModel = await providerApi.getCurrentModel();
205
- const match = models.find(
206
- (m) => m.providerId === providerId && (m.modelId === modelId || m.registryKey === target),
207
- ) ?? (
208
- currentModel.providerId === providerId
209
- && (currentModel.modelId === modelId || currentModel.registryKey === target)
210
- ? currentModel
211
- : undefined
212
- );
213
-
214
- if (!match) {
215
- context.print(
216
- `[provider] Model not found in registry: ${providerId}/${modelId}`,
217
- );
218
- context.print(' Use "/model" to see available models, or check the provider and model ID.');
219
- return;
220
- }
221
-
222
- // Enable optimizer if it's off
223
- if (!optimizer.enabled) {
224
- optimizer.setEnabled(true);
225
- context.print('⚠ Optimizer was disabled — enabling it for pin to take effect.');
226
- }
227
-
228
- optimizer.pin(match.providerId, match.modelId);
229
- context.print(`[provider] Pinned → ${match.providerId}/${match.modelId}`);
230
- context.print(' All routed requests will target this provider/model.');
231
- context.print(' Unpin with: /provider route manual');
232
- }
233
-
234
- // ---------------------------------------------------------------------------
235
- // /provider fallback test
236
- // ---------------------------------------------------------------------------
237
-
238
- function handleFallbackTest(
239
- args: string[],
240
- context: CommandContext,
241
- ): void {
242
- const optimizer = requireProviderOptimizer(context);
243
- if (!optimizer) return;
244
- const sub = args[0];
245
-
246
- if (sub !== 'test') {
247
- context.print('[provider] Usage: /provider fallback test');
248
- return;
249
- }
250
-
251
- context.print('[provider] Simulating fallback chain (empty request profile — no requirements)...');
252
-
253
- const result: FallbackTestResult = optimizer.testFallback();
254
-
255
- context.print(
256
- `[provider] Fallback chain: ${result.viableCount} capable / ${result.totalCount} total providers`,
257
- );
258
-
259
- // Show first capable then rejected
260
- const capable = result.chain.filter((n) => n.capable);
261
- const rejected = result.chain.filter((n) => !n.capable);
262
-
263
- if (capable.length > 0) {
264
- context.print(' Capable providers (would succeed):');
265
- for (const node of capable.slice(0, 10)) {
266
- context.print(` [${node.position}] ${node.providerId}/${node.modelId}`);
267
- }
268
- if (capable.length > 10) {
269
- context.print(` ... and ${capable.length - 10} more`);
270
- }
271
- } else {
272
- context.print(' No capable providers found for this profile.');
273
- }
274
-
275
- if (rejected.length > 0) {
276
- context.print(` Rejected providers (${rejected.length}):`);
277
- for (const node of rejected.slice(0, 5)) {
278
- const reasons = !node.explanation.accepted
279
- ? node.explanation.rejections.map((r) => r.code).join(', ')
280
- : '';
281
- context.print(
282
- ` [${node.position}] ${node.providerId}/${node.modelId} — ${reasons || 'unknown'}`,
283
- );
284
- }
285
- if (rejected.length > 5) {
286
- context.print(` ... and ${rejected.length - 5} more`);
287
- }
288
- }
289
-
290
- // Show fallback log
291
- const log = optimizer.fallbackLog;
292
- if (log.length > 0) {
293
- context.print(` Logged transitions (${log.length} total):`);
294
- const recent: readonly FallbackTransition[] = log.slice(-5);
295
- for (const t of recent) {
296
- context.print(
297
- ` ${fmtTs(t.ts)} ${t.from} → ${t.to} (${t.reason})`,
298
- );
299
- }
300
- } else {
301
- context.print(' No fallback transitions logged this session.');
302
- }
303
-
304
- context.print(` Test completed at: ${fmtTs(result.testedAt)}`);
305
- }
306
-
307
- // ---------------------------------------------------------------------------
308
- // Top-level command definition
309
- // ---------------------------------------------------------------------------
310
-
311
- /**
312
- * providerCommand — The `/provider` slash command.
313
- *
314
- * Routes to subcommand handlers based on args[0].
315
- */
316
- export const providerCommand: SlashCommand = {
317
- name: 'provider-opt',
318
- aliases: ['prov-opt'],
319
- description: 'Manage provider routing optimizer (route, pin, explain, fallback).',
320
- usage: '<subcommand> [args]',
321
- argsHint: 'route|explain-route|pin|fallback',
322
- handler: async (args: string[], context: CommandContext): Promise<void> => {
323
- const [sub, ...rest] = args;
324
-
325
- switch (sub) {
326
- case 'route':
327
- handleRoute(rest, context);
328
- break;
329
-
330
- case 'explain-route':
331
- case 'explain':
332
- await handleExplainRoute(rest, context);
333
- break;
334
-
335
- case 'pin':
336
- await handlePin(rest, context);
337
- break;
338
-
339
- case 'fallback':
340
- handleFallbackTest(rest, context);
341
- break;
342
-
343
- default: {
344
- const optimizer = requireProviderOptimizer(context);
345
- if (!optimizer) return;
346
- const lines = [
347
- 'Usage: /provider <subcommand>',
348
- ' route auto|manual — Set optimizer routing mode',
349
- ' explain-route — Show current route explanation',
350
- ' pin <provider:model> — Pin routing to specific provider/model',
351
- ' fallback test — Simulate the full fallback chain',
352
- '',
353
- ` Optimizer: ${optimizer.enabled ? 'enabled' : 'disabled'} mode=${optimizer.mode}`,
354
- ];
355
- if (optimizer.pinnedTarget) {
356
- lines.push(` Pinned: ${optimizer.pinnedTarget.providerId}/${optimizer.pinnedTarget.modelId}`);
357
- }
358
- context.print(lines.join('\n'));
359
- break;
360
- }
361
- }
362
- },
363
- };
@@ -1,89 +0,0 @@
1
- import type { CommandContext } from '../command-registry.ts';
2
-
3
- type RemotePoolLike = {
4
- id: string;
5
- label: string;
6
- trustClass: string;
7
- preferredTemplate?: string;
8
- maxRunners?: number;
9
- runnerIds: readonly string[];
10
- description?: string;
11
- };
12
-
13
- type RemoteRegistryLike = {
14
- listPools(): readonly RemotePoolLike[];
15
- getPool(id: string): RemotePoolLike | null | undefined;
16
- };
17
-
18
- export function handleRemotePoolCommand(
19
- args: string[],
20
- ctx: Pick<CommandContext, 'print'>,
21
- remoteRegistry: RemoteRegistryLike,
22
- ): boolean {
23
- const subcommand = args[0]?.toLowerCase() ?? 'show';
24
- if (subcommand !== 'pool') return false;
25
- const mode = args[1]?.toLowerCase() ?? 'list';
26
- if (mode === 'list') {
27
- const pools = remoteRegistry.listPools();
28
- if (pools.length === 0) {
29
- ctx.print('No remote worker pools defined yet.');
30
- return true;
31
- }
32
- ctx.print([
33
- `Remote Worker Pools (${pools.length})`,
34
- ...pools.map((pool) => ` ${pool.id} ${pool.runnerIds.length} workers trust=${pool.trustClass} template=${pool.preferredTemplate ?? '(none)'}`),
35
- ].join('\n'));
36
- return true;
37
- }
38
- if (mode === 'show') {
39
- const poolId = args[2];
40
- if (!poolId) {
41
- ctx.print('Usage: /remote pool show <poolId>');
42
- return true;
43
- }
44
- const pool = remoteRegistry.getPool(poolId);
45
- if (!pool) {
46
- ctx.print(`Unknown remote worker pool: ${poolId}`);
47
- return true;
48
- }
49
- ctx.print([
50
- `Remote Worker Pool ${pool.id}`,
51
- ` label: ${pool.label}`,
52
- ` trustClass: ${pool.trustClass}`,
53
- ` preferredTemplate: ${pool.preferredTemplate ?? '(none)'}`,
54
- ` maxWorkers: ${pool.maxRunners ?? '(unbounded)'}`,
55
- ` workers: ${pool.runnerIds.join(', ') || '(none)'}`,
56
- ` description: ${pool.description ?? '(none)'}`,
57
- ].join('\n'));
58
- return true;
59
- }
60
- if (mode === 'create') {
61
- ctx.print([
62
- 'Remote worker pool mutation is blocked in GoodVibes Agent.',
63
- ' requested: /remote pool create',
64
- ' policy: Agent inspects remote worker pools but does not manage worker topology',
65
- ' next: use the owning GoodVibes runtime or delegated build environment for worker-pool administration',
66
- ].join('\n'));
67
- return true;
68
- }
69
- if (mode === 'assign') {
70
- ctx.print([
71
- 'Remote worker pool mutation is blocked in GoodVibes Agent.',
72
- ' requested: /remote pool assign',
73
- ' policy: Agent inspects remote worker pools but does not manage worker topology',
74
- ' next: use the owning GoodVibes runtime or delegated build environment for worker-pool administration',
75
- ].join('\n'));
76
- return true;
77
- }
78
- if (mode === 'unassign') {
79
- ctx.print([
80
- 'Remote worker pool mutation is blocked in GoodVibes Agent.',
81
- ' requested: /remote pool unassign',
82
- ' policy: Agent inspects remote worker pools but does not manage worker topology',
83
- ' next: use the owning GoodVibes runtime or delegated build environment for worker-pool administration',
84
- ].join('\n'));
85
- return true;
86
- }
87
- ctx.print('Usage: /remote pool <list|show|create|assign|unassign> ...');
88
- return true;
89
- }
@@ -1,226 +0,0 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { getDefaultAcpAgentCommand } from '@pellux/goodvibes-sdk/platform/acp';
4
- import type { CommandContext, RemoteCommandService } from '../command-registry.ts';
5
- import type { RemoteSessionBundle } from '@/runtime/index.ts';
6
- import { requireShellPaths } from './runtime-services.ts';
7
- import { requireYesFlag, stripYesFlag } from './confirmation.ts';
8
-
9
- type RemoteRegistryLike = Pick<RemoteCommandService, 'listContracts' | 'exportSessionBundle' | 'importSessionBundle'>;
10
-
11
- type ActiveConnectionLike = {
12
- agentId: string;
13
- transportState: string;
14
- messageCount: number;
15
- errorCount: number;
16
- label: string;
17
- };
18
-
19
- export function inspectRemoteSessionBundle(bundle: RemoteSessionBundle): string {
20
- return [
21
- 'Remote Session Bundle Review',
22
- ` session: ${bundle.sessionId}`,
23
- ` exportedAt: ${new Date(bundle.exportedAt).toISOString()}`,
24
- ` active connections: ${bundle.activeConnectionIds.length}`,
25
- ` pools: ${bundle.pools.length}`,
26
- ` contracts: ${bundle.contracts.length}`,
27
- ` artifacts: ${bundle.artifacts.length}`,
28
- ].join('\n');
29
- }
30
-
31
- export async function handleRemoteSetupCommand(
32
- args: string[],
33
- ctx: CommandContext,
34
- activeConnections: ActiveConnectionLike[],
35
- remoteRegistry: RemoteRegistryLike,
36
- ): Promise<boolean> {
37
- const parsed = stripYesFlag(args);
38
- const commandArgs = [...parsed.rest];
39
- const subcommand = commandArgs[0]?.toLowerCase() ?? 'show';
40
- if (subcommand === 'setup') {
41
- const command = getDefaultAcpAgentCommand();
42
- const danger = ctx.platform.configManager.getCategory('danger');
43
- const lines = [
44
- 'Remote Setup Review',
45
- ` acp agent command: ${command.join(' ')}`,
46
- ` runtime host enabled: ${danger.daemon ? 'yes' : 'no'}`,
47
- ` inbound listener enabled: ${danger.httpListener ? 'yes' : 'no'}`,
48
- ` remote worker contracts: ${remoteRegistry.listContracts().length}`,
49
- ` active acp connections: ${activeConnections.length}`,
50
- '',
51
- ' guidance:',
52
- ' - set ACP_AGENT_CMD to override the spawned remote agent command',
53
- ' - use /remote env to export a reusable shell snippet',
54
- ' - runtime-host and inbound-listener posture belongs to the runtime owner, not Agent onboarding',
55
- ];
56
- if (commandArgs[1]?.toLowerCase() === 'export') {
57
- const pathArg = commandArgs[2];
58
- if (!pathArg) {
59
- ctx.print('Usage: /remote setup export <path> --yes');
60
- return true;
61
- }
62
- if (!parsed.yes) {
63
- requireYesFlag(ctx, `export remote setup bundle to ${pathArg}`, '/remote setup export <path> --yes');
64
- return true;
65
- }
66
- const shellPaths = requireShellPaths(ctx);
67
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
68
- mkdirSync(dirname(targetPath), { recursive: true });
69
- writeFileSync(targetPath, `${JSON.stringify({
70
- exportedAt: Date.now(),
71
- acpAgentCommand: command,
72
- runtimeHostEnabled: Boolean(danger.daemon),
73
- inboundListenerEnabled: Boolean(danger.httpListener),
74
- remoteRunnerContracts: remoteRegistry.listContracts().length,
75
- }, null, 2)}\n`, 'utf-8');
76
- ctx.print(`Exported remote setup bundle to ${targetPath}`);
77
- return true;
78
- }
79
- ctx.print(lines.join('\n'));
80
- return true;
81
- }
82
-
83
- if (subcommand === 'env') {
84
- const command = getDefaultAcpAgentCommand();
85
- const shellSnippet = [
86
- `export ACP_AGENT_CMD='${command.join(' ')}'`,
87
- `export GOODVIBES_REMOTE_SESSION='${ctx.session.runtime.sessionId}'`,
88
- ].join('\n');
89
- if (commandArgs[1]?.toLowerCase() === 'export') {
90
- const pathArg = commandArgs[2];
91
- if (!pathArg) {
92
- ctx.print('Usage: /remote env export <path> --yes');
93
- return true;
94
- }
95
- if (!parsed.yes) {
96
- requireYesFlag(ctx, `export remote environment snippet to ${pathArg}`, '/remote env export <path> --yes');
97
- return true;
98
- }
99
- const shellPaths = requireShellPaths(ctx);
100
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
101
- mkdirSync(dirname(targetPath), { recursive: true });
102
- writeFileSync(targetPath, `${shellSnippet}\n`, 'utf-8');
103
- ctx.print(`Exported remote environment snippet to ${targetPath}`);
104
- return true;
105
- }
106
- ctx.print(['Remote Environment', shellSnippet].join('\n'));
107
- return true;
108
- }
109
-
110
- if (subcommand === 'tunnel') {
111
- const mode = commandArgs[1]?.toLowerCase() ?? 'review';
112
- const lines = [
113
- 'Remote Tunnel Review',
114
- ' transport: self-hosted ACP / runtime relay',
115
- ` session: ${ctx.session.runtime.sessionId}`,
116
- ` active remote connections: ${activeConnections.length}`,
117
- ' guidance: forward ACP agent traffic through your chosen self-hosted tunnel or SSH transport',
118
- ];
119
- if (mode === 'export') {
120
- const pathArg = commandArgs[2];
121
- if (!pathArg) {
122
- ctx.print('Usage: /remote tunnel export <path> --yes');
123
- return true;
124
- }
125
- if (!parsed.yes) {
126
- requireYesFlag(ctx, `export remote tunnel review to ${pathArg}`, '/remote tunnel export <path> --yes');
127
- return true;
128
- }
129
- const shellPaths = requireShellPaths(ctx);
130
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
131
- mkdirSync(dirname(targetPath), { recursive: true });
132
- writeFileSync(targetPath, `${lines.join('\n')}\n`, 'utf-8');
133
- ctx.print(`Exported remote tunnel review to ${targetPath}`);
134
- return true;
135
- }
136
- ctx.print(lines.join('\n'));
137
- return true;
138
- }
139
-
140
- if (subcommand === 'bootstrap') {
141
- const mode = commandArgs[1]?.toLowerCase() ?? 'export';
142
- const payload = {
143
- exportedAt: Date.now(),
144
- sessionId: ctx.session.runtime.sessionId,
145
- acpAgentCommand: getDefaultAcpAgentCommand(),
146
- env: {
147
- ACP_AGENT_CMD: getDefaultAcpAgentCommand().join(' '),
148
- GOODVIBES_REMOTE_SESSION: ctx.session.runtime.sessionId,
149
- },
150
- links: [
151
- 'goodvibes://open/remote',
152
- 'goodvibes://open/cockpit?target=remote',
153
- ],
154
- };
155
- if (mode === 'inspect') {
156
- const pathArg = commandArgs[2];
157
- if (!pathArg) {
158
- ctx.print('Usage: /remote bootstrap inspect <path>');
159
- return true;
160
- }
161
- const shellPaths = requireShellPaths(ctx);
162
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
163
- const parsed = JSON.parse(readFileSync(targetPath, 'utf-8')) as typeof payload;
164
- ctx.print([
165
- 'Remote Bootstrap Bundle Review',
166
- ` session: ${parsed.sessionId}`,
167
- ` acp agent command: ${parsed.acpAgentCommand.join(' ')}`,
168
- ` links: ${parsed.links.length}`,
169
- ].join('\n'));
170
- return true;
171
- }
172
- const pathArg = commandArgs[2] ?? commandArgs[1];
173
- if (!pathArg || mode !== 'export') {
174
- ctx.print('Usage: /remote bootstrap export <path> --yes | /remote bootstrap inspect <path>');
175
- return true;
176
- }
177
- if (!parsed.yes) {
178
- requireYesFlag(ctx, `export remote bootstrap bundle to ${pathArg}`, '/remote bootstrap export <path> --yes');
179
- return true;
180
- }
181
- const shellPaths = requireShellPaths(ctx);
182
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
183
- mkdirSync(dirname(targetPath), { recursive: true });
184
- writeFileSync(targetPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
185
- ctx.print(`Exported remote bootstrap bundle to ${targetPath}`);
186
- return true;
187
- }
188
-
189
- if (subcommand === 'session') {
190
- const mode = commandArgs[1]?.toLowerCase();
191
- const pathArg = commandArgs[2];
192
- if (!mode || !pathArg) {
193
- ctx.print('Usage: /remote session <export|inspect|import> <path> [--yes]');
194
- return true;
195
- }
196
- const shellPaths = requireShellPaths(ctx);
197
- const targetPath = shellPaths.resolveWorkspacePath(pathArg);
198
- if (mode === 'export') {
199
- if (!parsed.yes) {
200
- requireYesFlag(ctx, `export remote session bundle to ${pathArg}`, '/remote session export <path> --yes');
201
- return true;
202
- }
203
- const exported = await remoteRegistry.exportSessionBundle(targetPath);
204
- ctx.print(`Exported remote session bundle ${exported.bundle.sessionId} to ${exported.path}`);
205
- return true;
206
- }
207
- if (mode === 'inspect') {
208
- const bundle = JSON.parse(readFileSync(targetPath, 'utf-8')) as RemoteSessionBundle;
209
- ctx.print(inspectRemoteSessionBundle(bundle));
210
- return true;
211
- }
212
- if (mode === 'import') {
213
- if (!parsed.yes) {
214
- requireYesFlag(ctx, `import remote session bundle from ${pathArg}`, '/remote session import <path> --yes');
215
- return true;
216
- }
217
- const bundle = await remoteRegistry.importSessionBundle(targetPath);
218
- ctx.print(`Imported remote session bundle ${bundle.sessionId} with ${bundle.contracts.length} contracts.`);
219
- return true;
220
- }
221
- ctx.print('Usage: /remote session <export|inspect|import> <path> [--yes]');
222
- return true;
223
- }
224
-
225
- return false;
226
- }