@pellux/goodvibes-agent 1.8.0 → 1.8.2
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 +9 -0
- package/README.md +1 -1
- package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-44ar68xs.node} +0 -0
- package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-asvc2tv1.node} +0 -0
- package/dist/package/main.js +135434 -118764
- package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
- package/docs/tools-and-commands.md +9 -0
- package/package.json +3 -2
- package/src/agent/capability-registry.ts +300 -0
- package/src/agent/email/style-reply-lane.ts +13 -4
- package/src/agent/memory-consolidation-receipts.ts +79 -0
- package/src/agent/memory-prompt.ts +3 -1
- package/src/agent/operator-gateway-call.ts +134 -0
- package/src/agent/principal-attribution.ts +97 -0
- package/src/agent/prompt-context-receipts.ts +5 -1
- package/src/agent/unified-inbox.ts +17 -1
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/channel-profiles-command.ts +168 -0
- package/src/cli/ci-command.ts +214 -0
- package/src/cli/completion.ts +5 -0
- package/src/cli/connected-host-metrics.ts +165 -0
- package/src/cli/entrypoint.ts +25 -1
- package/src/cli/fleet-command.ts +178 -0
- package/src/cli/help.ts +115 -0
- package/src/cli/import-command.ts +208 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/management-commands.ts +9 -8
- package/src/cli/management.ts +42 -0
- package/src/cli/memory-command.ts +4 -3
- package/src/cli/openclaw-import.ts +382 -0
- package/src/cli/operator-command-args.ts +88 -0
- package/src/cli/parser.ts +12 -0
- package/src/cli/personas-command.ts +3 -2
- package/src/cli/principals-command.ts +230 -0
- package/src/cli/profiles-command.ts +2 -1
- package/src/cli/relay-command.ts +100 -0
- package/src/cli/routines-command.ts +4 -3
- package/src/cli/skill-bundle-command.ts +3 -2
- package/src/cli/skills-command.ts +3 -2
- package/src/cli/status.ts +114 -2
- package/src/cli/temporal-label.ts +89 -0
- package/src/cli/types.ts +7 -0
- package/src/cli/workspaces-command.ts +135 -0
- package/src/config/agent-settings-policy.ts +8 -0
- package/src/config/checkpoint-settings.ts +113 -0
- package/src/config/workspace-registration.ts +299 -0
- package/src/core/rewind-turn-anchors.ts +82 -0
- package/src/input/agent-workspace-categories.ts +1 -0
- package/src/input/agent-workspace-channel-triage.ts +27 -8
- package/src/input/agent-workspace-local-library-snapshot.ts +5 -1
- package/src/input/agent-workspace-onboarding-categories.ts +17 -1
- package/src/input/agent-workspace-snapshot.ts +25 -3
- package/src/input/agent-workspace-types.ts +5 -5
- package/src/input/commands/connected-host-admin-runtime.ts +75 -0
- package/src/input/commands.ts +2 -0
- package/src/input/settings-modal-types.ts +16 -5
- package/src/main.ts +17 -12
- package/src/permissions/approval-posture.ts +40 -5
- package/src/permissions/prompt.ts +35 -1
- package/src/renderer/settings-modal-helpers.ts +11 -0
- package/src/renderer/settings-modal.ts +21 -0
- package/src/renderer/terminal-escapes.ts +65 -12
- package/src/runtime/agent-runtime-events.ts +102 -3
- package/src/runtime/bootstrap-core.ts +111 -10
- package/src/runtime/bootstrap-external-services.ts +156 -0
- package/src/runtime/bootstrap.ts +73 -69
- package/src/runtime/context-accounting-source.ts +114 -0
- package/src/runtime/conversation-rewind-port.ts +133 -0
- package/src/runtime/index.ts +8 -28
- package/src/runtime/memory-consolidation-scheduler.ts +67 -0
- package/src/runtime/memory-consolidation-wiring.ts +56 -0
- package/src/runtime/memory-usage-wiring.ts +75 -0
- package/src/runtime/services.ts +257 -6
- package/src/runtime/tool-permission-safety.ts +18 -1
- package/src/shell/blocking-input.ts +38 -8
- package/src/shell/startup-wiring.ts +26 -1
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-cli-command-policy.ts +29 -3
- package/src/tools/agent-harness-learning-auto-promote.ts +7 -3
- package/src/tools/agent-harness-memory-posture.ts +57 -2
- package/src/tools/agent-harness-metadata.ts +12 -0
- package/src/tools/agent-harness-mode-catalog.ts +1 -1
- package/src/tools/agent-harness-personal-ops-lanes.ts +8 -6
- package/src/tools/agent-harness-prompt-context.ts +3 -1
- package/src/tools/agent-harness-tool.ts +3 -2
- package/src/tools/agent-policy-explanation.ts +40 -0
- package/src/tools/tool-definition-compaction.ts +2 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* principal-attribution.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared "honest inbound attribution" helper. Anywhere the Agent displays who a
|
|
5
|
+
* session or message came from, it should show the RESOLVED principal when the
|
|
6
|
+
* sender's channel identity maps to one, and plainly say "unknown principal"
|
|
7
|
+
* when it does not — never a blank field, never a guess.
|
|
8
|
+
*
|
|
9
|
+
* Resolution goes through the connected host's `principals.resolve` operator
|
|
10
|
+
* method: POST /api/principals/resolve with `{ channel, value }`, returning
|
|
11
|
+
* `{ principal, known }`. An unmapped identity resolves to `known: false` —
|
|
12
|
+
* the registry never guesses, and neither does this helper.
|
|
13
|
+
*
|
|
14
|
+
* Every failure mode (empty channel identity, no connected-host token, a
|
|
15
|
+
* failed/unreachable resolve call, or an explicit `known: false`) collapses to
|
|
16
|
+
* the same human-facing label, `UNKNOWN_PRINCIPAL_LABEL`, so the display never
|
|
17
|
+
* over-claims certainty about which failure occurred. Callers that want the
|
|
18
|
+
* underlying detail can read the optional `error` field separately.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createBrowserGoodVibesSdk } from '@pellux/goodvibes-sdk/browser';
|
|
22
|
+
import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
|
|
23
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
24
|
+
|
|
25
|
+
export const UNKNOWN_PRINCIPAL_LABEL = 'unknown principal';
|
|
26
|
+
|
|
27
|
+
export interface PrincipalAttributionConnection {
|
|
28
|
+
readonly baseUrl: string;
|
|
29
|
+
readonly token: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ResolvedPrincipalSummary {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly kind: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ResolvedPrincipalAttribution {
|
|
39
|
+
readonly known: boolean;
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly principal: ResolvedPrincipalSummary | null;
|
|
42
|
+
/** Diagnostic detail only — never surfaced in `label` itself. */
|
|
43
|
+
readonly error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type PrincipalsResolveInput = OperatorMethodInput<'principals.resolve'>;
|
|
47
|
+
type PrincipalsResolveOutput = OperatorMethodOutput<'principals.resolve'>;
|
|
48
|
+
|
|
49
|
+
const UNKNOWN_ATTRIBUTION: ResolvedPrincipalAttribution = {
|
|
50
|
+
known: false,
|
|
51
|
+
label: UNKNOWN_PRINCIPAL_LABEL,
|
|
52
|
+
principal: null,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export function formatPrincipalLabel(principal: ResolvedPrincipalSummary): string {
|
|
56
|
+
return `${principal.name} (${principal.id})`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a channel identity ({channel, value}) to its named principal for
|
|
61
|
+
* honest inbound-attribution display.
|
|
62
|
+
*
|
|
63
|
+
* Never fabricates a name. Returns the shared "unknown principal" label
|
|
64
|
+
* (never a blank string) when: the identity is empty, no connected-host
|
|
65
|
+
* operator token is available, or the resolve call fails or returns
|
|
66
|
+
* `known: false`.
|
|
67
|
+
*/
|
|
68
|
+
export async function resolveChannelPrincipalAttribution(
|
|
69
|
+
connection: PrincipalAttributionConnection,
|
|
70
|
+
channel: string,
|
|
71
|
+
value: string,
|
|
72
|
+
): Promise<ResolvedPrincipalAttribution> {
|
|
73
|
+
if (!channel.trim() || !value.trim()) return UNKNOWN_ATTRIBUTION;
|
|
74
|
+
if (!connection.token) {
|
|
75
|
+
return { ...UNKNOWN_ATTRIBUTION, error: 'No connected-host operator token available for principal resolution.' };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const sdk = createBrowserGoodVibesSdk({
|
|
79
|
+
baseUrl: connection.baseUrl,
|
|
80
|
+
authToken: connection.token,
|
|
81
|
+
// A single attempt is enough for a display-only lookup — this must not
|
|
82
|
+
// stall triage/inbox rendering behind retry backoff.
|
|
83
|
+
retry: { maxAttempts: 1, baseDelayMs: 0, maxDelayMs: 0 },
|
|
84
|
+
});
|
|
85
|
+
const payload: PrincipalsResolveInput = { channel, value };
|
|
86
|
+
const result = await sdk.operator.invoke('principals.resolve', payload) as PrincipalsResolveOutput;
|
|
87
|
+
if (!result.known || !result.principal) return UNKNOWN_ATTRIBUTION;
|
|
88
|
+
const principal: ResolvedPrincipalSummary = {
|
|
89
|
+
id: result.principal.id,
|
|
90
|
+
name: result.principal.name,
|
|
91
|
+
kind: result.principal.kind,
|
|
92
|
+
};
|
|
93
|
+
return { known: true, label: formatPrincipalLabel(principal), principal };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return { ...UNKNOWN_ATTRIBUTION, error: summarizeError(error) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -202,7 +202,11 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
|
|
|
202
202
|
const projectContext = discoverProjectContextFiles(input.shellPaths);
|
|
203
203
|
const projectContextPrompt = buildProjectContextPrompt(input.shellPaths) ?? '';
|
|
204
204
|
const memoryRecords = resolveMemoryRecords(input);
|
|
205
|
-
|
|
205
|
+
// Bound to one arg: Array.filter passes (element, index, array), and
|
|
206
|
+
// isPromptActiveMemory's second param is `now` — passing the callback bare
|
|
207
|
+
// would leak the array index in as `now`, breaking the temporal-validity
|
|
208
|
+
// check for every record past index 0.
|
|
209
|
+
const eligibleMemory = memoryRecords.filter((record) => isPromptActiveMemory(record));
|
|
206
210
|
const memoryRanking = rankMemoryForTurn(input.memoryRegistry, eligibleMemory, input.turnText);
|
|
207
211
|
const activeMemory = memoryRanking.records.slice(0, 10);
|
|
208
212
|
const activeMemoryIds = new Set(activeMemory.map((record) => record.id));
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { AgentWorkspaceChannelTriage } from '../input/agent-workspace-channel-triage.ts';
|
|
20
|
+
import { UNKNOWN_PRINCIPAL_LABEL } from './principal-attribution.ts';
|
|
20
21
|
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Item kinds
|
|
@@ -62,6 +63,10 @@ export interface UnifiedInboxRouteBindingItem {
|
|
|
62
63
|
readonly surfaceId: string | null;
|
|
63
64
|
/** External id is digested — never the raw identifier. */
|
|
64
65
|
readonly externalIdDigest: string | null;
|
|
66
|
+
/** Resolved sender principal, when the channel identity maps to one. Never a guess. */
|
|
67
|
+
readonly principal: { readonly id: string; readonly name: string; readonly kind: string } | null;
|
|
68
|
+
/** Human-facing sender label — the resolved principal's "name (id)", or "unknown principal". */
|
|
69
|
+
readonly principalLabel: string;
|
|
65
70
|
readonly sessionPolicy: string | null;
|
|
66
71
|
readonly threadPolicy: string | null;
|
|
67
72
|
readonly deliveryGuarantee: string | null;
|
|
@@ -202,6 +207,15 @@ function toSurfaceMessageItem(raw: Record<string, unknown>): UnifiedInboxSurface
|
|
|
202
207
|
};
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
function toPrincipalOrNull(raw: Record<string, unknown>): { readonly id: string; readonly name: string; readonly kind: string } | null {
|
|
211
|
+
const principalRaw = isRecord(raw.principal) ? raw.principal : null;
|
|
212
|
+
if (!principalRaw) return null;
|
|
213
|
+
const id = readString(principalRaw, 'id');
|
|
214
|
+
const name = readString(principalRaw, 'name');
|
|
215
|
+
const kind = readString(principalRaw, 'kind');
|
|
216
|
+
return id && name && kind ? { id, name, kind } : null;
|
|
217
|
+
}
|
|
218
|
+
|
|
205
219
|
function toRouteBindingItem(raw: Record<string, unknown>): UnifiedInboxRouteBindingItem {
|
|
206
220
|
return {
|
|
207
221
|
kind: 'route_binding',
|
|
@@ -210,6 +224,8 @@ function toRouteBindingItem(raw: Record<string, unknown>): UnifiedInboxRouteBind
|
|
|
210
224
|
surfaceKind: readString(raw, 'surfaceKind', 'unknown'),
|
|
211
225
|
surfaceId: readStringOrNull(raw, 'surfaceId'),
|
|
212
226
|
externalIdDigest: readStringOrNull(raw, 'externalIdDigest'),
|
|
227
|
+
principal: toPrincipalOrNull(raw),
|
|
228
|
+
principalLabel: readString(raw, 'principalLabel', UNKNOWN_PRINCIPAL_LABEL),
|
|
213
229
|
sessionPolicy: readStringOrNull(raw, 'sessionPolicy'),
|
|
214
230
|
threadPolicy: readStringOrNull(raw, 'threadPolicy'),
|
|
215
231
|
deliveryGuarantee: readStringOrNull(raw, 'deliveryGuarantee'),
|
|
@@ -385,7 +401,7 @@ export function formatUnifiedInbox(inbox: UnifiedInbox): string {
|
|
|
385
401
|
if (routeBindingItems.length > 0) {
|
|
386
402
|
lines.push('', ' Route Bindings');
|
|
387
403
|
for (const item of routeBindingItems.slice(0, 10)) {
|
|
388
|
-
lines.push(` - ${item.id}: ${item.surfaceKind} ${item.bindingKind} ext=${item.externalIdDigest ?? 'none'}`);
|
|
404
|
+
lines.push(` - ${item.id}: ${item.surfaceKind} ${item.bindingKind} ext=${item.externalIdDigest ?? 'none'} sender=${item.principalLabel}`);
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
407
|
|
|
@@ -16,6 +16,7 @@ import { classifyProviderSetup } from './provider-classification.ts';
|
|
|
16
16
|
import { buildCliServicePosture } from './service-posture.ts';
|
|
17
17
|
import { REDACTED_VALUE, collectSensitiveConfigValues, isRedactedValue, redactConfig, redactSerializedSecrets } from './redaction.ts';
|
|
18
18
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
19
|
+
import { appendTemporalLabel } from './temporal-label.ts';
|
|
19
20
|
|
|
20
21
|
interface BundleInspectSummary {
|
|
21
22
|
readonly type: string;
|
|
@@ -142,7 +143,7 @@ export async function handleBundleCommand(runtime: CliCommandRuntime): Promise<C
|
|
|
142
143
|
` type: ${summary.type}`,
|
|
143
144
|
` version: ${summary.version}`,
|
|
144
145
|
` path: ${summary.path}`,
|
|
145
|
-
` capturedAt: ${summary.capturedAt === null ? 'n/a' : new Date(summary.capturedAt).toISOString()}`,
|
|
146
|
+
` capturedAt: ${summary.capturedAt === null ? 'n/a' : appendTemporalLabel(new Date(summary.capturedAt).toISOString(), summary.capturedAt)}`,
|
|
146
147
|
` configKeys: ${summary.configKeys}`,
|
|
147
148
|
` redactedConfigKeys: ${summary.redactedConfigPaths.length}`,
|
|
148
149
|
` diagnostics: ${summary.hasDiagnostics ? 'present' : 'missing'}`,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
|
|
2
|
+
import {
|
|
3
|
+
formatOperatorGatewayFailure,
|
|
4
|
+
invokeOperatorGatewayMethod,
|
|
5
|
+
} from '../agent/operator-gateway-call.ts';
|
|
6
|
+
import { resolveAgentConnectedHostConnection } from '../agent/routine-schedule-promotion.ts';
|
|
7
|
+
import {
|
|
8
|
+
operatorFlagValue,
|
|
9
|
+
parseOperatorCommandArgs,
|
|
10
|
+
} from './operator-command-args.ts';
|
|
11
|
+
import type { CliCommandOutput } from './types.ts';
|
|
12
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
13
|
+
import { appendTemporalLabel } from './temporal-label.ts';
|
|
14
|
+
|
|
15
|
+
const CHANNEL_PROFILES_ROUTE = '/api/channels/profiles';
|
|
16
|
+
const CHANNEL_PROFILE_ROUTE = '/api/channels/profiles/{surfaceKind}';
|
|
17
|
+
|
|
18
|
+
const CHANNEL_PROFILES_GET_USAGE = 'Usage: goodvibes-agent channel-profiles get <surfaceKind> [--channel-id <id>]';
|
|
19
|
+
const CHANNEL_PROFILES_SET_USAGE = 'Usage: goodvibes-agent channel-profiles set <surfaceKind> [--channel-id <id>] [--model <model>] [--provider <provider>] [--permission-mode <plan|normal|accept-edits|auto>] --yes';
|
|
20
|
+
const CHANNEL_PROFILES_DELETE_USAGE = 'Usage: goodvibes-agent channel-profiles delete <surfaceKind> [--channel-id <id>] --yes';
|
|
21
|
+
const CHANNEL_PROFILES_USAGE = 'Usage: goodvibes-agent channel-profiles [list|get <surfaceKind> [--channel-id <id>]|set <surfaceKind> [--channel-id <id>] [--model <model>] [--provider <provider>] [--permission-mode <mode>] --yes|delete <surfaceKind> [--channel-id <id>] --yes]';
|
|
22
|
+
|
|
23
|
+
const PERMISSION_MODES = new Set(['plan', 'normal', 'accept-edits', 'auto']);
|
|
24
|
+
|
|
25
|
+
type ChannelProfile = OperatorMethodOutput<'channels.profiles.list'>['bindings'][number];
|
|
26
|
+
type PermissionMode = NonNullable<OperatorMethodInput<'channels.profiles.set'>['permissionMode']>;
|
|
27
|
+
|
|
28
|
+
function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
|
|
29
|
+
return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function usageFailure(runtime: CliCommandRuntime, error: string): CliCommandOutput {
|
|
33
|
+
return {
|
|
34
|
+
output: jsonOrText(runtime, { ok: false, kind: 'invalid_channel_profiles_command', error }, error),
|
|
35
|
+
exitCode: 2,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parsePermissionMode(value: string | undefined, usage: string): PermissionMode | undefined {
|
|
40
|
+
if (value === undefined) return undefined;
|
|
41
|
+
const normalized = value.trim().toLowerCase();
|
|
42
|
+
if (!PERMISSION_MODES.has(normalized)) throw new Error(`${usage}\n--permission-mode must be one of: plan, normal, accept-edits, auto.`);
|
|
43
|
+
return normalized as PermissionMode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderChannelProfile(binding: ChannelProfile): string {
|
|
47
|
+
return [
|
|
48
|
+
`Channel profile ${binding.id}`,
|
|
49
|
+
` surface ${binding.surfaceKind}${binding.channelId ? ` channel ${binding.channelId}` : ''}`,
|
|
50
|
+
` model ${binding.model ?? '(default)'}`,
|
|
51
|
+
` provider ${binding.provider ?? '(default)'}`,
|
|
52
|
+
` permission mode ${binding.permissionMode ?? '(default)'}`,
|
|
53
|
+
` updated ${appendTemporalLabel(new Date(binding.updatedAt).toISOString(), binding.updatedAt)}`,
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderChannelProfileLine(binding: ChannelProfile): string {
|
|
58
|
+
const surface = binding.channelId ? `${binding.surfaceKind}:${binding.channelId}` : binding.surfaceKind;
|
|
59
|
+
return ` ${surface.padEnd(24)} model ${(binding.model ?? '(default)').padEnd(20)} provider ${(binding.provider ?? '(default)').padEnd(14)} permission ${binding.permissionMode ?? '(default)'}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderChannelProfileList(bindings: readonly ChannelProfile[]): string {
|
|
63
|
+
if (bindings.length === 0) {
|
|
64
|
+
return [
|
|
65
|
+
'Channel profiles',
|
|
66
|
+
' No per-channel profile bindings configured yet.',
|
|
67
|
+
' next goodvibes-agent channel-profiles set <surfaceKind> --model <model> --yes',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
return [`Channel profiles (${bindings.length})`, ...bindings.map(renderChannelProfileLine)].join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function handleChannelProfilesList(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
74
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
75
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.list'>>(connection, 'channels.profiles.list', CHANNEL_PROFILES_ROUTE, {});
|
|
76
|
+
if (!result.ok) {
|
|
77
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
78
|
+
}
|
|
79
|
+
return { output: jsonOrText(runtime, result, renderChannelProfileList(result.data.bindings)), exitCode: 0 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleChannelProfilesGet(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
83
|
+
const parsed = parseOperatorCommandArgs(args, ['channel-id']);
|
|
84
|
+
const surfaceKind = parsed.positionals[0];
|
|
85
|
+
if (!surfaceKind) return usageFailure(runtime, CHANNEL_PROFILES_GET_USAGE);
|
|
86
|
+
const payload: OperatorMethodInput<'channels.profiles.get'> = {
|
|
87
|
+
surfaceKind,
|
|
88
|
+
channelId: operatorFlagValue(parsed, 'channel-id'),
|
|
89
|
+
};
|
|
90
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
91
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.get'>>(connection, 'channels.profiles.get', CHANNEL_PROFILE_ROUTE, payload);
|
|
92
|
+
if (!result.ok) {
|
|
93
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
94
|
+
}
|
|
95
|
+
return { output: jsonOrText(runtime, result, renderChannelProfile(result.data.binding)), exitCode: 0 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function handleChannelProfilesSet(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
99
|
+
const parsed = parseOperatorCommandArgs(args, ['channel-id', 'model', 'provider', 'permission-mode']);
|
|
100
|
+
const surfaceKind = parsed.positionals[0];
|
|
101
|
+
if (!surfaceKind) return usageFailure(runtime, CHANNEL_PROFILES_SET_USAGE);
|
|
102
|
+
let permissionMode: PermissionMode | undefined;
|
|
103
|
+
try {
|
|
104
|
+
permissionMode = parsePermissionMode(operatorFlagValue(parsed, 'permission-mode'), CHANNEL_PROFILES_SET_USAGE);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return usageFailure(runtime, error instanceof Error ? error.message : String(error));
|
|
107
|
+
}
|
|
108
|
+
if (!parsed.yes) {
|
|
109
|
+
return { output: `Refusing to set channel profile ${surfaceKind} without --yes.`, exitCode: 2 };
|
|
110
|
+
}
|
|
111
|
+
const payload: OperatorMethodInput<'channels.profiles.set'> = {
|
|
112
|
+
surfaceKind,
|
|
113
|
+
channelId: operatorFlagValue(parsed, 'channel-id'),
|
|
114
|
+
model: operatorFlagValue(parsed, 'model'),
|
|
115
|
+
provider: operatorFlagValue(parsed, 'provider'),
|
|
116
|
+
permissionMode,
|
|
117
|
+
};
|
|
118
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
119
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.set'>>(connection, 'channels.profiles.set', CHANNEL_PROFILES_ROUTE, payload);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
output: jsonOrText(runtime, result, [`Set channel profile ${result.data.binding.id}`, renderChannelProfile(result.data.binding)].join('\n')),
|
|
125
|
+
exitCode: 0,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleChannelProfilesDelete(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
130
|
+
const parsed = parseOperatorCommandArgs(args, ['channel-id']);
|
|
131
|
+
const surfaceKind = parsed.positionals[0];
|
|
132
|
+
if (!surfaceKind) return usageFailure(runtime, CHANNEL_PROFILES_DELETE_USAGE);
|
|
133
|
+
if (!parsed.yes) {
|
|
134
|
+
return { output: `Refusing to delete channel profile ${surfaceKind} without --yes.`, exitCode: 2 };
|
|
135
|
+
}
|
|
136
|
+
const payload: OperatorMethodInput<'channels.profiles.delete'> = {
|
|
137
|
+
surfaceKind,
|
|
138
|
+
channelId: operatorFlagValue(parsed, 'channel-id'),
|
|
139
|
+
};
|
|
140
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
141
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'channels.profiles.delete'>>(connection, 'channels.profiles.delete', CHANNEL_PROFILE_ROUTE, payload);
|
|
142
|
+
if (!result.ok) {
|
|
143
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
output: jsonOrText(runtime, result, `Deleted channel profile ${result.data.surfaceKind}${result.data.channelId ? `:${result.data.channelId}` : ''}: ${result.data.deleted ? 'ok' : 'not found'}`),
|
|
147
|
+
exitCode: result.data.deleted ? 0 : 1,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function handleChannelProfilesCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
152
|
+
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
153
|
+
switch (sub.toLowerCase()) {
|
|
154
|
+
case 'list':
|
|
155
|
+
return handleChannelProfilesList(runtime);
|
|
156
|
+
case 'get':
|
|
157
|
+
case 'show':
|
|
158
|
+
return handleChannelProfilesGet(runtime, rest);
|
|
159
|
+
case 'set':
|
|
160
|
+
case 'update':
|
|
161
|
+
return handleChannelProfilesSet(runtime, rest);
|
|
162
|
+
case 'delete':
|
|
163
|
+
case 'remove':
|
|
164
|
+
return handleChannelProfilesDelete(runtime, rest);
|
|
165
|
+
default:
|
|
166
|
+
return usageFailure(runtime, CHANNEL_PROFILES_USAGE);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk/contracts';
|
|
2
|
+
import {
|
|
3
|
+
formatOperatorGatewayFailure,
|
|
4
|
+
invokeOperatorGatewayMethod,
|
|
5
|
+
} from '../agent/operator-gateway-call.ts';
|
|
6
|
+
import { resolveAgentConnectedHostConnection } from '../agent/routine-schedule-promotion.ts';
|
|
7
|
+
import {
|
|
8
|
+
operatorFlagValue,
|
|
9
|
+
operatorHasFlag,
|
|
10
|
+
operatorParseIntFlag,
|
|
11
|
+
operatorRequiredFlag,
|
|
12
|
+
parseOperatorCommandArgs,
|
|
13
|
+
} from './operator-command-args.ts';
|
|
14
|
+
import type { CliCommandOutput } from './types.ts';
|
|
15
|
+
import type { CliCommandRuntime } from './management.ts';
|
|
16
|
+
import { appendTemporalLabel } from './temporal-label.ts';
|
|
17
|
+
|
|
18
|
+
const CI_STATUS_ROUTE = '/api/ci/status';
|
|
19
|
+
const CI_WATCHES_ROUTE = '/api/ci/watches';
|
|
20
|
+
const CI_WATCH_ROUTE = '/api/ci/watches/{watchId}';
|
|
21
|
+
const CI_WATCH_RUN_ROUTE = '/api/ci/watches/{watchId}/run';
|
|
22
|
+
|
|
23
|
+
const CI_STATUS_USAGE = 'Usage: goodvibes-agent ci status <repo> [--ref <ref>] [--pr <number>]';
|
|
24
|
+
const CI_WATCHES_CREATE_USAGE = 'Usage: goodvibes-agent ci watches create <repo> --delivery-channel <channel> [--ref <ref>] [--pr <number>] [--trigger-fix-session] --yes';
|
|
25
|
+
const CI_WATCHES_DELETE_USAGE = 'Usage: goodvibes-agent ci watches delete <watchId> --yes';
|
|
26
|
+
const CI_WATCHES_RUN_USAGE = 'Usage: goodvibes-agent ci watches run <watchId>';
|
|
27
|
+
const CI_USAGE = 'Usage: goodvibes-agent ci [status <repo> [--ref <ref>] [--pr <number>]|watches list|watches create <repo> --delivery-channel <channel> [--ref <ref>] [--pr <number>] [--trigger-fix-session] --yes|watches delete <watchId> --yes|watches run <watchId>]';
|
|
28
|
+
|
|
29
|
+
type CiReport = OperatorMethodOutput<'ci.status'>['report'];
|
|
30
|
+
type CiWatch = OperatorMethodOutput<'ci.watches.list'>['watches'][number];
|
|
31
|
+
|
|
32
|
+
function jsonOrText(runtime: CliCommandRuntime, value: unknown, text: string): string {
|
|
33
|
+
return runtime.cli.flags.outputFormat === 'json' ? JSON.stringify(value, null, 2) : text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usageFailure(runtime: CliCommandRuntime, error: string): CliCommandOutput {
|
|
37
|
+
return {
|
|
38
|
+
output: jsonOrText(runtime, { ok: false, kind: 'invalid_ci_command', error }, error),
|
|
39
|
+
exitCode: 2,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderCiJobs(report: CiReport): readonly string[] {
|
|
44
|
+
if (report.jobs.length === 0) return [' jobs (none reported)'];
|
|
45
|
+
return [
|
|
46
|
+
` jobs (${report.jobs.length})`,
|
|
47
|
+
...report.jobs.map((job) => {
|
|
48
|
+
const conclusion = job.conclusion ?? 'pending';
|
|
49
|
+
const continueOnError = job.continueOnError ? ' continue-on-error' : '';
|
|
50
|
+
return ` ${job.name.padEnd(32)} status ${job.status.padEnd(12)} conclusion ${conclusion}${continueOnError}`;
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderCiReport(report: CiReport): string {
|
|
56
|
+
return [
|
|
57
|
+
`CI report ${report.repo}${report.ref ? ` @ ${report.ref}` : ''}${report.prNumber !== undefined ? ` PR #${report.prNumber}` : ''}`,
|
|
58
|
+
` overall ${report.overall}`,
|
|
59
|
+
...renderCiJobs(report),
|
|
60
|
+
report.violations.length > 0 ? ` violations: ${report.violations.join('; ')}` : ' violations: none',
|
|
61
|
+
` checked at ${appendTemporalLabel(new Date(report.checkedAt).toISOString(), report.checkedAt)}`,
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderCiWatch(watch: CiWatch): string {
|
|
66
|
+
return [
|
|
67
|
+
` ${watch.id} ${watch.repo}${watch.ref ? ` @ ${watch.ref}` : ''}${watch.prNumber !== undefined ? ` PR #${watch.prNumber}` : ''}`,
|
|
68
|
+
` delivery ${watch.deliveryChannel} trigger-fix-session ${watch.triggerFixSession ? 'yes' : 'no'} last overall ${watch.lastOverall ?? 'unknown'}`,
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderCiWatchList(watches: readonly CiWatch[]): string {
|
|
73
|
+
if (watches.length === 0) {
|
|
74
|
+
return [
|
|
75
|
+
'CI watches',
|
|
76
|
+
' No CI watches configured yet.',
|
|
77
|
+
' next goodvibes-agent ci watches create <repo> --delivery-channel <channel> --yes',
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
return [`CI watches (${watches.length})`, ...watches.map(renderCiWatch)].join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleCiStatus(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
84
|
+
const parsed = parseOperatorCommandArgs(args, ['ref', 'pr']);
|
|
85
|
+
const repo = parsed.positionals[0];
|
|
86
|
+
if (!repo) return usageFailure(runtime, CI_STATUS_USAGE);
|
|
87
|
+
let prNumber: number | undefined;
|
|
88
|
+
try {
|
|
89
|
+
prNumber = operatorParseIntFlag(parsed, 'pr', CI_STATUS_USAGE);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return usageFailure(runtime, error instanceof Error ? error.message : String(error));
|
|
92
|
+
}
|
|
93
|
+
const payload: OperatorMethodInput<'ci.status'> = {
|
|
94
|
+
repo,
|
|
95
|
+
ref: operatorFlagValue(parsed, 'ref'),
|
|
96
|
+
prNumber,
|
|
97
|
+
};
|
|
98
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
99
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.status'>>(connection, 'ci.status', CI_STATUS_ROUTE, payload);
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
102
|
+
}
|
|
103
|
+
return { output: jsonOrText(runtime, result, renderCiReport(result.data.report)), exitCode: 0 };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function handleCiWatchesList(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
107
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
108
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.list'>>(connection, 'ci.watches.list', CI_WATCHES_ROUTE, {});
|
|
109
|
+
if (!result.ok) {
|
|
110
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
111
|
+
}
|
|
112
|
+
return { output: jsonOrText(runtime, result, renderCiWatchList(result.data.watches)), exitCode: 0 };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function handleCiWatchesCreate(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
116
|
+
const parsed = parseOperatorCommandArgs(args, ['ref', 'pr', 'delivery-channel']);
|
|
117
|
+
const repo = parsed.positionals[0];
|
|
118
|
+
if (!repo) return usageFailure(runtime, CI_WATCHES_CREATE_USAGE);
|
|
119
|
+
let deliveryChannel: string;
|
|
120
|
+
let prNumber: number | undefined;
|
|
121
|
+
try {
|
|
122
|
+
deliveryChannel = operatorRequiredFlag(parsed, 'delivery-channel', CI_WATCHES_CREATE_USAGE);
|
|
123
|
+
prNumber = operatorParseIntFlag(parsed, 'pr', CI_WATCHES_CREATE_USAGE);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return usageFailure(runtime, error instanceof Error ? error.message : String(error));
|
|
126
|
+
}
|
|
127
|
+
if (!parsed.yes) {
|
|
128
|
+
return { output: `Refusing to create a CI watch for ${repo} without --yes.`, exitCode: 2 };
|
|
129
|
+
}
|
|
130
|
+
const payload: OperatorMethodInput<'ci.watches.create'> = {
|
|
131
|
+
repo,
|
|
132
|
+
ref: operatorFlagValue(parsed, 'ref'),
|
|
133
|
+
prNumber,
|
|
134
|
+
deliveryChannel,
|
|
135
|
+
triggerFixSession: operatorHasFlag(parsed, 'trigger-fix-session') || undefined,
|
|
136
|
+
};
|
|
137
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
138
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.create'>>(connection, 'ci.watches.create', CI_WATCHES_ROUTE, payload);
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
output: jsonOrText(runtime, result, [`Created CI watch ${result.data.watch.id}`, renderCiWatch(result.data.watch)].join('\n')),
|
|
144
|
+
exitCode: 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handleCiWatchesDelete(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
149
|
+
const parsed = parseOperatorCommandArgs(args);
|
|
150
|
+
const watchId = parsed.positionals[0];
|
|
151
|
+
if (!watchId) return usageFailure(runtime, CI_WATCHES_DELETE_USAGE);
|
|
152
|
+
if (!parsed.yes) {
|
|
153
|
+
return { output: `Refusing to delete CI watch ${watchId} without --yes.`, exitCode: 2 };
|
|
154
|
+
}
|
|
155
|
+
const payload: OperatorMethodInput<'ci.watches.delete'> = { watchId };
|
|
156
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
157
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.delete'>>(connection, 'ci.watches.delete', CI_WATCH_ROUTE, payload);
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
output: jsonOrText(runtime, result, `Deleted CI watch ${result.data.watchId}: ${result.data.deleted ? 'ok' : 'not found'}`),
|
|
163
|
+
exitCode: result.data.deleted ? 0 : 1,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function handleCiWatchesRun(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
168
|
+
const parsed = parseOperatorCommandArgs(args);
|
|
169
|
+
const watchId = parsed.positionals[0];
|
|
170
|
+
if (!watchId) return usageFailure(runtime, CI_WATCHES_RUN_USAGE);
|
|
171
|
+
const payload: OperatorMethodInput<'ci.watches.run'> = { watchId };
|
|
172
|
+
const connection = resolveAgentConnectedHostConnection(runtime.configManager, runtime.homeDirectory);
|
|
173
|
+
const result = await invokeOperatorGatewayMethod<OperatorMethodOutput<'ci.watches.run'>>(connection, 'ci.watches.run', CI_WATCH_RUN_ROUTE, payload);
|
|
174
|
+
if (!result.ok) {
|
|
175
|
+
return { output: jsonOrText(runtime, result, formatOperatorGatewayFailure(result)), exitCode: 1 };
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
output: jsonOrText(runtime, result, [
|
|
179
|
+
renderCiReport(result.data.report),
|
|
180
|
+
` notified ${result.data.notified ? 'yes' : 'no'}${result.data.notificationId ? ` (${result.data.notificationId})` : ''}`,
|
|
181
|
+
` fix session triggered ${result.data.fixSessionTriggered ? 'yes' : 'no'}${result.data.fixSessionId ? ` (${result.data.fixSessionId})` : ''}`,
|
|
182
|
+
].join('\n')),
|
|
183
|
+
exitCode: 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function handleCiWatchesCommand(runtime: CliCommandRuntime, args: readonly string[]): Promise<CliCommandOutput> {
|
|
188
|
+
const [sub = 'list', ...rest] = args;
|
|
189
|
+
switch (sub.toLowerCase()) {
|
|
190
|
+
case 'list':
|
|
191
|
+
return handleCiWatchesList(runtime);
|
|
192
|
+
case 'create':
|
|
193
|
+
return handleCiWatchesCreate(runtime, rest);
|
|
194
|
+
case 'delete':
|
|
195
|
+
case 'remove':
|
|
196
|
+
return handleCiWatchesDelete(runtime, rest);
|
|
197
|
+
case 'run':
|
|
198
|
+
return handleCiWatchesRun(runtime, rest);
|
|
199
|
+
default:
|
|
200
|
+
return usageFailure(runtime, CI_USAGE);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function handleCiCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
205
|
+
const [sub, ...rest] = runtime.cli.commandArgs;
|
|
206
|
+
switch ((sub ?? '').toLowerCase()) {
|
|
207
|
+
case 'status':
|
|
208
|
+
return handleCiStatus(runtime, rest);
|
|
209
|
+
case 'watches':
|
|
210
|
+
return handleCiWatchesCommand(runtime, rest);
|
|
211
|
+
default:
|
|
212
|
+
return usageFailure(runtime, CI_USAGE);
|
|
213
|
+
}
|
|
214
|
+
}
|