@pellux/goodvibes-agent 0.1.55 → 0.1.57
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/.goodvibes/GOODVIBES.md +1 -1
- package/CHANGELOG.md +19 -8
- package/README.md +10 -3
- package/docs/README.md +1 -1
- package/docs/getting-started.md +10 -3
- package/docs/release-and-publishing.md +12 -3
- package/package.json +1 -3
- package/scripts/check-bun.sh +5 -1
- package/src/agent/routine-schedule-args.ts +219 -0
- package/src/agent/routine-schedule-format.ts +173 -0
- package/src/agent/routine-schedule-promotion.ts +3 -811
- package/src/agent/routine-schedule-receipts.ts +502 -0
- package/src/cli/agent-knowledge-command.ts +6 -6
- package/src/cli/help.ts +3 -25
- package/src/cli/package-verification.ts +23 -16
- package/src/cli/redaction.ts +4 -1
- package/src/cli/routines-command.ts +10 -6
- package/src/cli/service-posture.ts +47 -280
- package/src/cli/status.ts +0 -1
- package/src/config/secret-config.ts +0 -2
- package/src/input/agent-workspace-categories.ts +219 -0
- package/src/input/agent-workspace-editors.ts +143 -0
- package/src/input/agent-workspace-snapshot.ts +265 -0
- package/src/input/agent-workspace-types.ts +142 -0
- package/src/input/agent-workspace.ts +22 -766
- package/src/input/commands/agent-runtime-profile-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -4
- package/src/input/commands/guidance-runtime.ts +1 -2
- package/src/input/commands/health-runtime.ts +3 -65
- package/src/input/commands/knowledge.ts +7 -7
- package/src/input/commands/local-setup-review.ts +0 -61
- package/src/input/commands/local-setup-transfer.ts +0 -3
- package/src/input/commands/local-setup.ts +2 -15
- package/src/input/commands/planning-runtime.ts +4 -1
- package/src/input/commands/platform-access-runtime.ts +1 -10
- package/src/input/commands/platform-services-runtime.ts +0 -1
- package/src/input/commands/recall-query.ts +1 -1
- package/src/input/commands/routines-runtime.ts +10 -6
- package/src/input/commands/schedule-runtime.ts +10 -6
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +1 -14
- package/src/input/commands.ts +0 -4
- package/src/input/handler-onboarding.ts +10 -120
- package/src/input/onboarding/onboarding-wizard-apply.ts +5 -196
- package/src/input/onboarding/onboarding-wizard-constants.ts +8 -119
- package/src/input/onboarding/onboarding-wizard-helpers.ts +2 -53
- package/src/input/onboarding/onboarding-wizard-rules.ts +2 -236
- package/src/input/onboarding/onboarding-wizard-state.ts +1 -69
- package/src/input/onboarding/onboarding-wizard-steps.ts +584 -737
- package/src/input/onboarding/onboarding-wizard-types.ts +8 -26
- package/src/input/onboarding/onboarding-wizard.ts +4 -109
- package/src/input/settings-modal-agent-policy.ts +10 -0
- package/src/input/settings-modal-types.ts +2 -4
- package/src/input/settings-modal.ts +3 -1
- package/src/input/submission-router.ts +0 -1
- package/src/panels/approval-panel.ts +1 -2
- package/src/panels/builtin/operations.ts +1 -2
- package/src/panels/knowledge-panel.ts +2 -2
- package/src/panels/project-planning-panel.ts +4 -1
- package/src/panels/provider-health-domains.ts +0 -22
- package/src/panels/provider-health-panel.ts +1 -5
- package/src/panels/session-browser-panel.ts +0 -5
- package/src/panels/tasks-panel.ts +2 -64
- package/src/renderer/agent-workspace.ts +1 -1
- package/src/renderer/help-overlay.ts +1 -2
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/settings-modal-helpers.ts +0 -16
- package/src/renderer/settings-modal.ts +3 -5
- package/src/runtime/bootstrap-hook-bridge.ts +0 -3
- package/src/runtime/bootstrap-shell.ts +2 -1
- package/src/runtime/bootstrap.ts +1 -1
- package/src/runtime/index.ts +0 -1
- package/src/runtime/onboarding/derivation.ts +1 -28
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +1 -4
- package/src/runtime/services.ts +4 -23
- package/src/runtime/ui-read-models.ts +4 -3
- package/src/shell/service-settings-sync.ts +15 -244
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/wrfc-agent-guard.ts +3 -3
- package/src/verification/live-verifier.ts +11 -5
- package/src/verification/verification-ledger.ts +3 -6
- package/src/version.ts +1 -1
- package/src/input/commands/agent-externalized-tui.ts +0 -73
- package/src/input/commands/cloudflare-runtime.ts +0 -385
- package/src/input/handler-onboarding-cloudflare.ts +0 -322
- package/src/input/onboarding/onboarding-runtime-status.ts +0 -87
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +0 -494
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +0 -199
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +0 -762
- package/src/runtime/cloudflare-control-plane.ts +0 -350
- package/src/runtime/sandbox-public-gaps.ts +0 -358
|
@@ -4,6 +4,12 @@ import { spawn } from 'node:child_process';
|
|
|
4
4
|
import { buildVerificationLedger } from './verification-ledger.ts';
|
|
5
5
|
import { SDK_VERSION } from '../version.ts';
|
|
6
6
|
|
|
7
|
+
const AGENT_KNOWLEDGE_FORBIDDEN_RESPONSE_MARKERS = [
|
|
8
|
+
['home', ' assistant'].join(''),
|
|
9
|
+
['home', 'graph'].join(''),
|
|
10
|
+
['home ', 'graph'].join(''),
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
7
13
|
export type LiveVerificationStatus = 'pass' | 'warn' | 'fail' | 'skip';
|
|
8
14
|
|
|
9
15
|
export interface LiveVerificationCheck {
|
|
@@ -284,7 +290,7 @@ export function buildAgentKnowledgeLiveSkipCheck(
|
|
|
284
290
|
summary: `Skipped because external daemon SDK ${daemonVersion} does not match Agent SDK pin ${expectedSdkVersion}.`,
|
|
285
291
|
detail: [
|
|
286
292
|
'Agent Knowledge is intentionally isolated under /api/goodvibes-agent/knowledge/*.',
|
|
287
|
-
'An older daemon cannot validate those routes, and Agent must not fall back to default Knowledge/Wiki or
|
|
293
|
+
'An older daemon cannot validate those routes, and Agent must not fall back to default Knowledge/Wiki or non-Agent knowledge segments.',
|
|
288
294
|
'Update/restart the external daemon, then rerun live verification.',
|
|
289
295
|
].join('\n'),
|
|
290
296
|
};
|
|
@@ -495,8 +501,8 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
495
501
|
return { status: 'fail', summary: 'Agent Knowledge ask was not parseable JSON.' };
|
|
496
502
|
}
|
|
497
503
|
const lower = body.toLowerCase();
|
|
498
|
-
if (
|
|
499
|
-
return { status: 'fail', summary: 'Agent Knowledge ask returned
|
|
504
|
+
if (AGENT_KNOWLEDGE_FORBIDDEN_RESPONSE_MARKERS.some((marker) => lower.includes(marker))) {
|
|
505
|
+
return { status: 'fail', summary: 'Agent Knowledge ask returned non-Agent knowledge contamination.' };
|
|
500
506
|
}
|
|
501
507
|
return { status: 'pass', summary: 'Agent Knowledge ask stayed on the isolated Agent route.' };
|
|
502
508
|
},
|
|
@@ -519,8 +525,8 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
519
525
|
return { status: 'fail', summary: 'Agent Knowledge search was not parseable JSON.' };
|
|
520
526
|
}
|
|
521
527
|
const lower = body.toLowerCase();
|
|
522
|
-
if (
|
|
523
|
-
return { status: 'fail', summary: 'Agent Knowledge search returned
|
|
528
|
+
if (AGENT_KNOWLEDGE_FORBIDDEN_RESPONSE_MARKERS.some((marker) => lower.includes(marker))) {
|
|
529
|
+
return { status: 'fail', summary: 'Agent Knowledge search returned non-Agent knowledge contamination.' };
|
|
524
530
|
}
|
|
525
531
|
return { status: 'pass', summary: 'Agent Knowledge search stayed on the isolated Agent route.' };
|
|
526
532
|
},
|
|
@@ -30,7 +30,6 @@ export interface VerificationLedger {
|
|
|
30
30
|
const EXTERNAL_SLASH_COMMANDS = new Set([
|
|
31
31
|
'auth',
|
|
32
32
|
'bridge',
|
|
33
|
-
'cloudflare',
|
|
34
33
|
'health',
|
|
35
34
|
'listener',
|
|
36
35
|
'login',
|
|
@@ -71,14 +70,12 @@ const ONBOARDING_CAPABILITIES = [
|
|
|
71
70
|
'network-access',
|
|
72
71
|
'webhook-events',
|
|
73
72
|
'external-integrations',
|
|
74
|
-
'cloudflare-batch',
|
|
75
73
|
] as const;
|
|
76
74
|
|
|
77
75
|
const EXTERNAL_SURFACES = [
|
|
78
76
|
'bluebubbles',
|
|
79
77
|
'discord',
|
|
80
78
|
'googleChat',
|
|
81
|
-
'homeassistant',
|
|
82
79
|
'imessage',
|
|
83
80
|
'matrix',
|
|
84
81
|
'mattermost',
|
|
@@ -185,9 +182,9 @@ export function buildVerificationLedger(root: string): VerificationLedger {
|
|
|
185
182
|
area: 'Onboarding capability bundles',
|
|
186
183
|
total: ONBOARDING_CAPABILITIES.length,
|
|
187
184
|
localSignalVerified: ONBOARDING_CAPABILITIES.length,
|
|
188
|
-
localBehaviorVerified:
|
|
189
|
-
externalOutcomeRequired:
|
|
190
|
-
notes: 'Wizard state derivation/apply
|
|
185
|
+
localBehaviorVerified: ONBOARDING_CAPABILITIES.length,
|
|
186
|
+
externalOutcomeRequired: 0,
|
|
187
|
+
notes: 'Wizard state derivation/apply is local; daemon-backed outcomes stay external to Agent ownership.',
|
|
191
188
|
},
|
|
192
189
|
];
|
|
193
190
|
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.1.
|
|
9
|
+
let _version = '0.1.57';
|
|
10
10
|
let _sdkVersion = '0.33.35';
|
|
11
11
|
try {
|
|
12
12
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8')) as {
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { CommandRegistry, SlashCommand } from '../command-registry.ts';
|
|
2
|
-
|
|
3
|
-
type ExternalizedCommandId = 'git' | 'diff' | 'worktree' | 'sandbox';
|
|
4
|
-
|
|
5
|
-
interface ExternalizedCommandSpec {
|
|
6
|
-
readonly name: ExternalizedCommandId;
|
|
7
|
-
readonly aliases?: readonly string[];
|
|
8
|
-
readonly description: string;
|
|
9
|
-
readonly usage: string;
|
|
10
|
-
readonly owner: string;
|
|
11
|
-
readonly reason: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const EXTERNALIZED_TUI_COMMANDS: readonly ExternalizedCommandSpec[] = [
|
|
15
|
-
{
|
|
16
|
-
name: 'git',
|
|
17
|
-
aliases: ['g'],
|
|
18
|
-
description: 'Externalized: git workflows are owned by GoodVibes TUI',
|
|
19
|
-
usage: '[status|log|diff]',
|
|
20
|
-
owner: 'GoodVibes TUI',
|
|
21
|
-
reason: 'Git status, commits, and repository mutation belong to the coding TUI, not the serial Agent surface.',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: 'diff',
|
|
25
|
-
aliases: ['d'],
|
|
26
|
-
description: 'Externalized: code diff review is owned by GoodVibes TUI',
|
|
27
|
-
usage: '[session|head|working|staged|<git-ref>]',
|
|
28
|
-
owner: 'GoodVibes TUI',
|
|
29
|
-
reason: 'Diff views are part of delegated build/fix/review work and should stay attached to the coding TUI execution context.',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'worktree',
|
|
33
|
-
aliases: ['worktrees'],
|
|
34
|
-
description: 'Externalized: git worktree lifecycle is owned by GoodVibes TUI',
|
|
35
|
-
usage: '[review|inspect|attach|recover|cleanup]',
|
|
36
|
-
owner: 'GoodVibes TUI',
|
|
37
|
-
reason: 'Worktree creation, attachment, recovery, and cleanup are coding-session lifecycle operations.',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: 'sandbox',
|
|
41
|
-
description: 'Externalized: sandbox and QEMU workflows are owned by GoodVibes TUI',
|
|
42
|
-
usage: '[review|probe|session|qemu|bundle|preset]',
|
|
43
|
-
owner: 'GoodVibes TUI',
|
|
44
|
-
reason: 'Sandbox/QEMU setup and session execution are terminal-native coding/runtime isolation workflows.',
|
|
45
|
-
},
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
function makeExternalizedCommand(spec: ExternalizedCommandSpec): SlashCommand {
|
|
49
|
-
return {
|
|
50
|
-
name: spec.name,
|
|
51
|
-
aliases: spec.aliases ? [...spec.aliases] : undefined,
|
|
52
|
-
description: spec.description,
|
|
53
|
-
usage: spec.usage,
|
|
54
|
-
argsHint: spec.usage,
|
|
55
|
-
handler(_args, ctx) {
|
|
56
|
-
ctx.print([
|
|
57
|
-
`${spec.name} is externalized in GoodVibes Agent.`,
|
|
58
|
-
` owner: ${spec.owner}`,
|
|
59
|
-
` reason: ${spec.reason}`,
|
|
60
|
-
' Agent policy: no local coding TUI, git/worktree, or sandbox execution from this surface.',
|
|
61
|
-
' build/fix/review: ask Agent to delegate the full request to GoodVibes TUI, or run goodvibes-tui in the target workspace.',
|
|
62
|
-
' result: blocked here; no local files, config, worktrees, sandbox sessions, or repository state were changed.',
|
|
63
|
-
].join('\n'));
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function registerAgentExternalizedTuiCommands(registry: CommandRegistry): void {
|
|
69
|
-
for (const spec of EXTERNALIZED_TUI_COMMANDS) {
|
|
70
|
-
registry.register(makeExternalizedCommand(spec));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CLOUDFLARE_COMPONENT_IDS,
|
|
3
|
-
CLOUDFLARE_COMPONENT_LABELS,
|
|
4
|
-
DEFAULT_CLOUDFLARE_COMPONENT_SELECTION,
|
|
5
|
-
CloudflareDaemonRouteError,
|
|
6
|
-
createCloudflareDaemonClient,
|
|
7
|
-
type CloudflareComponent,
|
|
8
|
-
type CloudflareComponentSelection,
|
|
9
|
-
type CloudflareDaemonClient,
|
|
10
|
-
type CloudflareProvisionStep,
|
|
11
|
-
} from '../../runtime/cloudflare-control-plane.ts';
|
|
12
|
-
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
13
|
-
import { requireShellPaths } from './runtime-services.ts';
|
|
14
|
-
import { requireYesFlag, stripYesFlag } from './confirmation.ts';
|
|
15
|
-
|
|
16
|
-
interface ParsedCloudflareArgs {
|
|
17
|
-
readonly positional: readonly string[];
|
|
18
|
-
readonly flags: ReadonlyMap<string, readonly string[]>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function registerCloudflareRuntimeCommands(registry: CommandRegistry): void {
|
|
22
|
-
registry.register({
|
|
23
|
-
name: 'cloudflare',
|
|
24
|
-
aliases: ['cf'],
|
|
25
|
-
description: 'Inspect and manage optional Cloudflare batch/control-plane integration through daemon SDK routes',
|
|
26
|
-
usage: '[status|setup|requirements|create-token --yes|discover|validate|provision --yes|verify|disable --yes] [flags]',
|
|
27
|
-
async handler(args, ctx) {
|
|
28
|
-
const confirmation = stripYesFlag(args);
|
|
29
|
-
const commandArgs = [...confirmation.rest];
|
|
30
|
-
const subcommand = (commandArgs[0] ?? 'status').toLowerCase();
|
|
31
|
-
const parsed = parseCloudflareArgs(commandArgs.slice(1));
|
|
32
|
-
if (subcommand === 'setup' || subcommand === 'onboarding') {
|
|
33
|
-
ctx.openOnboardingWizard?.({ mode: 'edit', reset: true });
|
|
34
|
-
ctx.print('Opening onboarding wizard. Select the Cloudflare batch capability to configure Cloudflare.');
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let client: CloudflareDaemonClient;
|
|
39
|
-
try {
|
|
40
|
-
client = createCloudflareClient(ctx);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
ctx.print(`Cloudflare command unavailable: ${formatCloudflareError(error)}`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
if (subcommand === 'status' || subcommand === 'show') {
|
|
48
|
-
const status = await client.status();
|
|
49
|
-
ctx.print([
|
|
50
|
-
'Cloudflare Status',
|
|
51
|
-
` enabled: ${status.enabled ? 'yes' : 'no'}`,
|
|
52
|
-
` ready: ${status.ready ? 'yes' : 'no'}`,
|
|
53
|
-
` account: ${status.config.accountId || '(not set)'}`,
|
|
54
|
-
` token ref: ${status.config.apiTokenRef || '(CLOUDFLARE_API_TOKEN fallback)'}`,
|
|
55
|
-
` worker: ${status.config.workerName || '(not set)'}`,
|
|
56
|
-
` worker URL: ${status.config.workerBaseUrl || '(not set)'}`,
|
|
57
|
-
` queue: ${status.config.queueName || '(not set)'}`,
|
|
58
|
-
` DLQ: ${status.config.deadLetterQueueName || '(not set)'}`,
|
|
59
|
-
` tunnel: ${status.config.tunnelName || '(not set)'} ${status.config.tunnelId ? `(${status.config.tunnelId})` : ''}`.trimEnd(),
|
|
60
|
-
` access app: ${status.config.accessAppId || '(not set)'}`,
|
|
61
|
-
` batch mode: ${String(ctx.platform.configManager.get('batch.mode') ?? 'off')}`,
|
|
62
|
-
` queue backend: ${String(ctx.platform.configManager.get('batch.queueBackend') ?? 'local')}`,
|
|
63
|
-
...status.warnings.map((warning) => ` warning: ${warning}`),
|
|
64
|
-
].join('\n'));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (subcommand === 'requirements') {
|
|
69
|
-
const result = await client.tokenRequirements({
|
|
70
|
-
components: componentsFromArgs(parsed),
|
|
71
|
-
includeBootstrap: true,
|
|
72
|
-
});
|
|
73
|
-
ctx.print([
|
|
74
|
-
'Cloudflare Token Requirements',
|
|
75
|
-
` components: ${formatComponents(result.components)}`,
|
|
76
|
-
' permissions:',
|
|
77
|
-
...result.permissions.map((permission) => ` ${permission.scope}: ${permission.permission} (${permission.component}) - ${permission.reason}`),
|
|
78
|
-
...(result.bootstrapToken.instructions.length > 0
|
|
79
|
-
? [' bootstrap token:', ...result.bootstrapToken.instructions.map((line) => ` ${line}`)]
|
|
80
|
-
: []),
|
|
81
|
-
].join('\n'));
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (subcommand === 'create-token') {
|
|
86
|
-
if (!confirmation.yes) {
|
|
87
|
-
requireYesFlag(ctx, 'create and store a Cloudflare operational token', '/cloudflare create-token [flags] --yes');
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const bootstrapToken = getFlag(parsed, 'bootstrap-token') || readTokenEnv(parsed, 'bootstrap-env');
|
|
91
|
-
if (!bootstrapToken) {
|
|
92
|
-
ctx.print('Usage: /cloudflare create-token --account <account-id> (--bootstrap-token <token> | --bootstrap-env <env-name>) --yes');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const result = await client.createOperationalToken({
|
|
96
|
-
components: componentsFromArgs(parsed),
|
|
97
|
-
...optionalString('accountId', getFlag(parsed, 'account') || getFlag(parsed, 'account-id')),
|
|
98
|
-
...optionalString('zoneId', getFlag(parsed, 'zone-id')),
|
|
99
|
-
...optionalString('zoneName', getFlag(parsed, 'zone') || getFlag(parsed, 'zone-name')),
|
|
100
|
-
bootstrapToken,
|
|
101
|
-
storeApiToken: true,
|
|
102
|
-
persistConfig: true,
|
|
103
|
-
});
|
|
104
|
-
ctx.print([
|
|
105
|
-
'Cloudflare Operational Token Created',
|
|
106
|
-
` token: ${result.tokenName}${result.tokenId ? ` (${result.tokenId})` : ''}`,
|
|
107
|
-
` account: ${result.accountId}`,
|
|
108
|
-
` zone: ${result.zoneId || '(none)'}`,
|
|
109
|
-
` stored ref: ${result.apiTokenRef ?? '(not stored)'}`,
|
|
110
|
-
' revoke or expire the temporary bootstrap token after validation.',
|
|
111
|
-
].join('\n'));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (subcommand === 'discover') {
|
|
116
|
-
const result = await client.discover({
|
|
117
|
-
...cloudflareAuthInput(parsed),
|
|
118
|
-
components: componentsFromArgs(parsed),
|
|
119
|
-
...optionalString('zoneId', getFlag(parsed, 'zone-id')),
|
|
120
|
-
...optionalString('zoneName', getFlag(parsed, 'zone') || getFlag(parsed, 'zone-name')),
|
|
121
|
-
includeResources: !hasFlag(parsed, 'fast'),
|
|
122
|
-
});
|
|
123
|
-
ctx.print([
|
|
124
|
-
'Cloudflare Discovery',
|
|
125
|
-
` token source: ${result.tokenSource}`,
|
|
126
|
-
` accounts: ${result.accounts.length}`,
|
|
127
|
-
...result.accounts.slice(0, 12).map((account) => ` account ${account.id}: ${account.name}`),
|
|
128
|
-
` zones: ${result.zones.length}`,
|
|
129
|
-
...result.zones.slice(0, 12).map((zone) => ` zone ${zone.id}: ${zone.name}${zone.status ? ` (${zone.status})` : ''}`),
|
|
130
|
-
` worker subdomain: ${result.workerSubdomain || '(not detected)'}`,
|
|
131
|
-
` queues: ${result.queues?.length ?? 0}`,
|
|
132
|
-
` KV namespaces: ${result.kvNamespaces?.length ?? 0}`,
|
|
133
|
-
` R2 buckets: ${result.r2Buckets?.length ?? 0}`,
|
|
134
|
-
...result.warnings.map((warning) => ` warning: ${warning}`),
|
|
135
|
-
].join('\n'));
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (subcommand === 'validate') {
|
|
140
|
-
const result = await client.validate(cloudflareAuthInput(parsed));
|
|
141
|
-
ctx.print([
|
|
142
|
-
'Cloudflare Token Validation',
|
|
143
|
-
` ok: ${result.ok ? 'yes' : 'no'}`,
|
|
144
|
-
` token source: ${result.tokenSource}`,
|
|
145
|
-
result.account ? ` account: ${result.account.name} (${result.account.id})` : ' account: not resolved',
|
|
146
|
-
].join('\n'));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (subcommand === 'provision') {
|
|
151
|
-
if (!confirmation.yes) {
|
|
152
|
-
requireYesFlag(ctx, 'provision Cloudflare resources and persist config', '/cloudflare provision [flags] --yes');
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const result = await client.provision({
|
|
156
|
-
...cloudflareAuthInput(parsed),
|
|
157
|
-
components: componentsFromArgs(parsed),
|
|
158
|
-
...optionalString('accountId', getFlag(parsed, 'account') || getFlag(parsed, 'account-id')),
|
|
159
|
-
...optionalString('zoneId', getFlag(parsed, 'zone-id')),
|
|
160
|
-
...optionalString('zoneName', getFlag(parsed, 'zone') || getFlag(parsed, 'zone-name')),
|
|
161
|
-
...optionalString('daemonBaseUrl', getFlag(parsed, 'daemon-url')),
|
|
162
|
-
...optionalString('daemonHostname', getFlag(parsed, 'daemon-hostname')),
|
|
163
|
-
...optionalString('workerName', getFlag(parsed, 'worker-name')),
|
|
164
|
-
...optionalString('workerSubdomain', getFlag(parsed, 'worker-subdomain')),
|
|
165
|
-
...optionalString('workerHostname', getFlag(parsed, 'worker-hostname')),
|
|
166
|
-
...optionalString('workerBaseUrl', getFlag(parsed, 'worker-url')),
|
|
167
|
-
...optionalString('queueName', getFlag(parsed, 'queue') || getFlag(parsed, 'queue-name')),
|
|
168
|
-
...optionalString('deadLetterQueueName', getFlag(parsed, 'dlq') || getFlag(parsed, 'dead-letter-queue')),
|
|
169
|
-
...optionalString('tunnelName', getFlag(parsed, 'tunnel-name')),
|
|
170
|
-
...optionalString('tunnelId', getFlag(parsed, 'tunnel-id')),
|
|
171
|
-
...optionalString('tunnelServiceUrl', getFlag(parsed, 'tunnel-service-url')),
|
|
172
|
-
...optionalString('tunnelTokenRef', getFlag(parsed, 'tunnel-token-ref')),
|
|
173
|
-
...optionalString('accessAppId', getFlag(parsed, 'access-app-id')),
|
|
174
|
-
...optionalString('accessServiceTokenId', getFlag(parsed, 'access-service-token-id')),
|
|
175
|
-
...optionalString('accessServiceTokenRef', getFlag(parsed, 'access-service-token-ref')),
|
|
176
|
-
...optionalString('kvNamespaceName', getFlag(parsed, 'kv-namespace-name')),
|
|
177
|
-
...optionalString('kvNamespaceId', getFlag(parsed, 'kv-namespace-id')),
|
|
178
|
-
...optionalString('durableObjectNamespaceName', getFlag(parsed, 'do-namespace-name') || getFlag(parsed, 'durable-object-namespace-name')),
|
|
179
|
-
...optionalString('durableObjectNamespaceId', getFlag(parsed, 'do-namespace-id') || getFlag(parsed, 'durable-object-namespace-id')),
|
|
180
|
-
...optionalString('r2BucketName', getFlag(parsed, 'r2-bucket-name')),
|
|
181
|
-
...optionalString('secretsStoreName', getFlag(parsed, 'secrets-store-name')),
|
|
182
|
-
...optionalString('secretsStoreId', getFlag(parsed, 'secrets-store-id')),
|
|
183
|
-
...optionalString('workerCron', getFlag(parsed, 'worker-cron')),
|
|
184
|
-
...optionalString('operatorToken', getFlag(parsed, 'operator-token')),
|
|
185
|
-
...optionalString('operatorTokenRef', getFlag(parsed, 'operator-token-ref')),
|
|
186
|
-
...optionalString('workerClientToken', getFlag(parsed, 'worker-client-token')),
|
|
187
|
-
...optionalString('workerClientTokenRef', getFlag(parsed, 'worker-client-token-ref')),
|
|
188
|
-
...optionalBatchMode(readBatchMode(parsed)),
|
|
189
|
-
persistConfig: true,
|
|
190
|
-
verify: !hasFlag(parsed, 'no-verify'),
|
|
191
|
-
storeApiToken: !hasFlag(parsed, 'no-store-token'),
|
|
192
|
-
enableWorkersDev: !hasFlag(parsed, 'no-workers-dev'),
|
|
193
|
-
});
|
|
194
|
-
ctx.print([
|
|
195
|
-
'Cloudflare Provisioning',
|
|
196
|
-
` ok: ${result.ok ? 'yes' : 'no'}`,
|
|
197
|
-
...(result.worker ? [` worker: ${result.worker.name}${result.worker.baseUrl ? ` at ${result.worker.baseUrl}` : ''}`] : []),
|
|
198
|
-
...(result.queues ? [` queues: ${result.queues.queueName}; DLQ ${result.queues.deadLetterQueueName}`] : []),
|
|
199
|
-
...(result.tunnel ? [` tunnel: ${result.tunnel.name} (${result.tunnel.id})${result.tunnel.hostname ? ` ${result.tunnel.hostname}` : ''}`] : []),
|
|
200
|
-
...(result.access ? [` access: app=${result.access.appId || '(none)'} serviceToken=${result.access.serviceTokenId || '(none)'}`] : []),
|
|
201
|
-
...(result.dns ? [` dns: ${result.dns.zoneName || result.dns.zoneId} records=${result.dns.records.length}`] : []),
|
|
202
|
-
...(result.kv ? [` kv: ${result.kv.namespaceName} (${result.kv.namespaceId})`] : []),
|
|
203
|
-
...(result.r2 ? [` r2: ${result.r2.bucketName}`] : []),
|
|
204
|
-
...(result.secretsStore ? [` secrets store: ${result.secretsStore.storeName} (${result.secretsStore.storeId})`] : []),
|
|
205
|
-
...formatProvisionSteps(result.steps),
|
|
206
|
-
].join('\n'));
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (subcommand === 'verify') {
|
|
211
|
-
const result = await client.verify({
|
|
212
|
-
...optionalString('workerBaseUrl', getFlag(parsed, 'worker-url')),
|
|
213
|
-
...optionalString('workerClientToken', getFlag(parsed, 'worker-token')),
|
|
214
|
-
...optionalString('workerClientTokenRef', getFlag(parsed, 'worker-token-ref')),
|
|
215
|
-
});
|
|
216
|
-
ctx.print([
|
|
217
|
-
'Cloudflare Verification',
|
|
218
|
-
` ok: ${result.ok ? 'yes' : 'no'}`,
|
|
219
|
-
` worker health: ${result.workerHealth.ok ? 'ok' : 'failed'} (HTTP ${result.workerHealth.status})${result.workerHealth.error ? ` - ${result.workerHealth.error}` : ''}`,
|
|
220
|
-
...(result.daemonBatchProxy ? [` daemon batch proxy: ${result.daemonBatchProxy.ok ? 'ok' : 'failed'} (HTTP ${result.daemonBatchProxy.status})${result.daemonBatchProxy.error ? ` - ${result.daemonBatchProxy.error}` : ''}`] : []),
|
|
221
|
-
].join('\n'));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (subcommand === 'disable') {
|
|
226
|
-
if (!confirmation.yes) {
|
|
227
|
-
requireYesFlag(ctx, 'disable Cloudflare integration and persist config', '/cloudflare disable [flags] --yes');
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
const result = await client.disable({
|
|
231
|
-
...cloudflareAuthInput(parsed),
|
|
232
|
-
...optionalString('workerName', getFlag(parsed, 'worker-name')),
|
|
233
|
-
disableWorkerSubdomain: hasFlag(parsed, 'disable-worker-subdomain'),
|
|
234
|
-
disableCron: !hasFlag(parsed, 'keep-cron'),
|
|
235
|
-
persistConfig: true,
|
|
236
|
-
});
|
|
237
|
-
ctx.print([
|
|
238
|
-
'Cloudflare Disabled',
|
|
239
|
-
` ok: ${result.ok ? 'yes' : 'no'}`,
|
|
240
|
-
...formatProvisionSteps(result.steps),
|
|
241
|
-
].join('\n'));
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
ctx.print('Usage: /cloudflare [status|setup|requirements|create-token --yes|discover|validate|provision --yes|verify|disable --yes] [flags]');
|
|
246
|
-
} catch (error) {
|
|
247
|
-
ctx.print(`Cloudflare ${subcommand} failed: ${formatCloudflareError(error)}`);
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function createCloudflareClient(ctx: CommandContext): CloudflareDaemonClient {
|
|
254
|
-
const shellPaths = requireShellPaths(ctx);
|
|
255
|
-
return createCloudflareDaemonClient({
|
|
256
|
-
configManager: ctx.platform.configManager,
|
|
257
|
-
homeDirectory: shellPaths.homeDirectory,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function parseCloudflareArgs(args: readonly string[]): ParsedCloudflareArgs {
|
|
262
|
-
const positional: string[] = [];
|
|
263
|
-
const flags = new Map<string, string[]>();
|
|
264
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
265
|
-
const arg = args[index]!;
|
|
266
|
-
if (!arg.startsWith('--')) {
|
|
267
|
-
positional.push(arg);
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
const raw = arg.slice(2);
|
|
271
|
-
const equalsIndex = raw.indexOf('=');
|
|
272
|
-
if (equalsIndex >= 0) {
|
|
273
|
-
const key = raw.slice(0, equalsIndex);
|
|
274
|
-
const value = raw.slice(equalsIndex + 1);
|
|
275
|
-
flags.set(key, [...(flags.get(key) ?? []), value]);
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
const next = args[index + 1];
|
|
279
|
-
if (next && !next.startsWith('--')) {
|
|
280
|
-
flags.set(raw, [...(flags.get(raw) ?? []), next]);
|
|
281
|
-
index += 1;
|
|
282
|
-
} else {
|
|
283
|
-
flags.set(raw, [...(flags.get(raw) ?? []), 'true']);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return { positional, flags };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function hasFlag(args: ParsedCloudflareArgs, key: string): boolean {
|
|
290
|
-
return args.flags.has(key);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function getFlag(args: ParsedCloudflareArgs, key: string): string {
|
|
294
|
-
const values = args.flags.get(key);
|
|
295
|
-
const value = values?.[values.length - 1] ?? '';
|
|
296
|
-
return value === 'true' ? '' : value.trim();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function getFlagValues(args: ParsedCloudflareArgs, key: string): readonly string[] {
|
|
300
|
-
return args.flags.get(key)?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function componentsFromArgs(args: ParsedCloudflareArgs): Record<CloudflareComponent, boolean> {
|
|
304
|
-
const components: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
|
|
305
|
-
if (hasFlag(args, 'all') || hasFlag(args, 'advanced')) {
|
|
306
|
-
for (const component of CLOUDFLARE_COMPONENT_IDS) components[component] = true;
|
|
307
|
-
}
|
|
308
|
-
for (const raw of [...args.positional, ...getFlagValues(args, 'component')]) {
|
|
309
|
-
const normalized = normalizeComponent(raw);
|
|
310
|
-
if (normalized) components[normalized] = true;
|
|
311
|
-
}
|
|
312
|
-
for (const raw of getFlagValues(args, 'no-component')) {
|
|
313
|
-
const normalized = normalizeComponent(raw);
|
|
314
|
-
if (normalized) components[normalized] = false;
|
|
315
|
-
}
|
|
316
|
-
return components;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function normalizeComponent(value: string): CloudflareComponent | null {
|
|
320
|
-
const normalized = value.trim().toLowerCase().replace(/[-_]/g, '');
|
|
321
|
-
for (const component of CLOUDFLARE_COMPONENT_IDS) {
|
|
322
|
-
if (component.toLowerCase() === normalized) return component;
|
|
323
|
-
}
|
|
324
|
-
if (normalized === 'workerscript' || normalized === 'worker') return 'workers';
|
|
325
|
-
if (normalized === 'queue') return 'queues';
|
|
326
|
-
if (normalized === 'tunnel') return 'zeroTrustTunnel';
|
|
327
|
-
if (normalized === 'access') return 'zeroTrustAccess';
|
|
328
|
-
if (normalized === 'domain' || normalized === 'hostname') return 'dns';
|
|
329
|
-
if (normalized === 'do' || normalized === 'durableobject') return 'durableObjects';
|
|
330
|
-
if (normalized === 'secret' || normalized === 'secrets') return 'secretsStore';
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function formatComponents(components: CloudflareComponentSelection): string {
|
|
335
|
-
const selected = CLOUDFLARE_COMPONENT_IDS
|
|
336
|
-
.filter((component) => components[component] === true)
|
|
337
|
-
.map((component) => CLOUDFLARE_COMPONENT_LABELS[component]);
|
|
338
|
-
return selected.length > 0 ? selected.join(', ') : 'none';
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function cloudflareAuthInput(args: ParsedCloudflareArgs): {
|
|
342
|
-
readonly accountId?: string;
|
|
343
|
-
readonly apiToken?: string;
|
|
344
|
-
readonly apiTokenRef?: string;
|
|
345
|
-
} {
|
|
346
|
-
const token = getFlag(args, 'token') || readTokenEnv(args, 'token-env');
|
|
347
|
-
const tokenRef = getFlag(args, 'token-ref');
|
|
348
|
-
return {
|
|
349
|
-
...optionalString('accountId', getFlag(args, 'account') || getFlag(args, 'account-id')),
|
|
350
|
-
...(token ? { apiToken: token } : tokenRef ? { apiTokenRef: tokenRef } : {}),
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function readTokenEnv(args: ParsedCloudflareArgs, key: string): string {
|
|
355
|
-
const envName = getFlag(args, key);
|
|
356
|
-
if (!envName) return '';
|
|
357
|
-
return process.env[envName] ?? '';
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function readBatchMode(args: ParsedCloudflareArgs): 'off' | 'explicit' | 'eligible-by-default' | undefined {
|
|
361
|
-
const value = getFlag(args, 'batch-mode');
|
|
362
|
-
if (value === 'off' || value === 'explicit' || value === 'eligible-by-default') return value;
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function optionalString<K extends string>(key: K, value: string): Partial<Record<K, string>> {
|
|
367
|
-
return value.trim().length > 0 ? { [key]: value.trim() } as Partial<Record<K, string>> : {};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function optionalBatchMode(value: ReturnType<typeof readBatchMode>): { readonly batchMode?: 'off' | 'explicit' | 'eligible-by-default' } {
|
|
371
|
-
return value ? { batchMode: value } : {};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function formatProvisionSteps(steps: readonly CloudflareProvisionStep[]): string[] {
|
|
375
|
-
return steps.length > 0
|
|
376
|
-
? steps.map((step) => ` ${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`)
|
|
377
|
-
: [' no steps returned'];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function formatCloudflareError(error: unknown): string {
|
|
381
|
-
if (error instanceof CloudflareDaemonRouteError) {
|
|
382
|
-
return `${error.message} (HTTP ${error.status}, ${error.code})`;
|
|
383
|
-
}
|
|
384
|
-
return error instanceof Error ? error.message : String(error);
|
|
385
|
-
}
|