Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ export function resolveGatewayEndpointForCli(ctx) {
return {
ok: false,
error:
'could not resolve the HypAware gateway endpoint: no running daemon reported a bound port and no `listen` is pinned for @hypaware/ai-gateway. Start the daemon (`hyp start`) or pin a port with `hyp init`.',
'could not resolve the HypAware gateway endpoint: no running daemon reported a bound port and no `listen` is pinned for @hypaware/ai-gateway. Start the daemon (`hyp daemon start`) or pin a port with `hyp init`.',
}
}

Expand Down
2 changes: 1 addition & 1 deletion llp/0178-attach-prompts-to-enable.plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ design's prose):
- id: T4 branch: task/attach-prompts-to-enable/T4 deps: [T3] complexity: 2 -- `src/core/commands/clients.js`, the `hyp attach all` expansion (`expandClientName(parsed.client, gateway)` returning `gateway.listClients()`): after computing the live-registered `clientNames` for the `all` sentinel, diff them against T2's `buildAttachPluginCatalog(ctx).clientDescriptors` keys and, for every catalog-known client missing from the live set, print one line to stdout - `note: ${name} is a known client but its adapter is not enabled; run 'hyp attach ${name}' to enable it` - using T3's classification to skip printing a note for a genuinely `unknown`-to-the-catalog id (there is none, by construction, since the diff is catalog-keys minus live-keys). Do not prompt, error, or change the exit code for these notes; every live-registered client in the `all` expansion still attaches exactly as before. Test: a fixture with two catalog-known clients where only one is live-registered asserts (a) the live one attaches normally, (b) exactly one `note:` line is printed naming the other and the fix command, (c) the command's exit code reflects only real attach failures among the live set, unaffected by the note.
- id: T5 branch: task/attach-prompts-to-enable/T5 deps: [] complexity: 2 -- `src/core/cli/walkthrough.js`: add `export` to `defaultBackfillConsentPromptFactory` (~line 188) and to the private title-builder it and `legacyBackfillConsentPromptFactory`/`tuiBackfillConsentPromptFactory` share (`backfillConsentTitle`), with no change to their behavior, so the interactive attach flow (T9/T10) can ask the identical question instead of re-implementing its copy. No other code in `walkthrough.js` changes. Test: existing wizard/finale tests that exercise the backfill consent prompt (e.g. anything under `test/` covering `runPickerFinale`/`runFinaleBackfill`) pass unchanged; a new test imports `defaultBackfillConsentPromptFactory` directly from outside `walkthrough.js` and asserts it produces the same prompt title/copy the finale shows today for a sample `{ providers, retentionDays }`.
- id: T6 branch: task/attach-prompts-to-enable/T6 deps: [T2] complexity: 3 -- New exported function (co-located in `src/core/cli/walkthrough.js` next to `composePickerConfig`, since it is a one-descriptor slice of that same fold) `resolveSingleSourceEnablement(descriptor)` taking one `PickerDescriptor` (from T2's `buildAttachPluginCatalog(ctx).pickerDescriptors.get(name)`) and returning `{ requiresGateway: boolean, pluginNames: string[], entries: PluginConfigInstance[] }`: `entries` is `[@hypaware/ai-gateway (if descriptor.compose.requires_gateway), ...([descriptor.compose.plugin] filtered to defined), ...(descriptor.compose.plugins ?? [])]`, `pluginNames` their `.name`s in the same order, mirroring exactly the per-descriptor fold inside `composePickerConfig` (~lines 630-666) without the multi-descriptor union/upstream-merge machinery that fold needs for the whole picker. This is the "same composition the picker uses (`requires_gateway` and friends)" the design cites for the prompt's dependency list. Test: unit test asserting the exact `entries`/`pluginNames` for the claude picker descriptor (`@hypaware/claude` + `@hypaware/ai-gateway`) and the openclaw one (`@hypaware/openclaw` + `@hypaware/ai-gateway`), reading the real bundled manifests via `buildPluginCatalog`, not hand-rolled fixtures, so a manifest edit that changes the dependency set fails this test rather than drifting silently.
- id: T7 branch: task/attach-prompts-to-enable/T7 deps: [] complexity: 2 -- `src/core/commands/clients.js`: extend the existing "cannot resolve the gateway endpoint" give-up message (~line 287-296, in the manual-attach endpoint-resolution ladder) to additionally name `hyp daemon install` / `hyp daemon start` when `(await import('../daemon/install.js')).serviceDaemonStatus({ homeDir })` reports the service is not installed, per design `#bootstrap-floor`'s "config exists but no daemon is installed" case. Keep the existing wording and `hyp start` mention for the case where a daemon service *is* installed but not currently reachable. No other behavior in the ladder changes. Test: extend the existing endpoint-fallback test (`test/core/attach-endpoint-fallback.test.js`) with a case asserting the extended message text appears when no daemon service is installed, and that the current message is unchanged when one is installed but unreachable.
- id: T7 branch: task/attach-prompts-to-enable/T7 deps: [] complexity: 2 -- `src/core/commands/clients.js`: extend the existing "cannot resolve the gateway endpoint" give-up message (~line 287-296, in the manual-attach endpoint-resolution ladder) to additionally name `hyp daemon install` / `hyp daemon start` when `(await import('../daemon/install.js')).serviceDaemonStatus({ homeDir })` reports the service is not installed, per design `#bootstrap-floor`'s "config exists but no daemon is installed" case. Keep the existing wording and `hyp start` mention for the case where a daemon service *is* installed but not currently reachable. [Correction, landed after this plan: `hyp start` was never a registered command, so following it produced "hyp: unknown command 'start'"; the shipped message spells the same mention `hyp daemon start` (issue #834). What T7 settled, that the installed-but-unreachable case names the lifecycle start command only and never `hyp daemon install`, is unchanged.] No other behavior in the ladder changes. Test: extend the existing endpoint-fallback test (`test/core/attach-endpoint-fallback.test.js`) with a case asserting the extended message text appears when no daemon service is installed, and that the current message is unchanged when one is installed but unreachable.
- id: T8 branch: task/attach-prompts-to-enable/T8 deps: [T6] complexity: 4 -- New function `enableClientAdapter({ name, entries, ctx })` (new module `src/core/config/client_enable.js`, alongside the existing `client_detach_disk.js`) doing: (1) read the current local config via `loadConfigFile(configPath)` (falling back to `{ version: 2, plugins: [] }` only if `configPath` resolves but the file is genuinely absent - the bootstrap-floor "no config at all" case is gated by the *caller*, T9, before this is ever invoked); (2) additively append each of T6's `entries` whose `name` is not already present in the *effective* merged config (read via `resolveLayeredConfigFromDisk`, so an entry the central layer already names is never duplicated locally); (3) guard the write with the existing `prepareLocalConfigWrite({ targetPath: configPath, force: true })` (LLP 0031's backup-before-replace; `force: true` because this is a program-driven additive edit behind its own consent prompt, not the user-facing overwrite-confirm `init` uses) and write with the same `JSON.stringify(config, null, 2) + '\n'` shape `pick.js`/`init.js` use; (4) if `serviceDaemonStatus({ homeDir })` reports installed, call `restartServiceDaemon({ homeDir })` then a new `waitForGatewayBind({ env, homeDir, timeoutMs, sleep })` (new export in `src/core/cli/remote_commands.js`, next to `waitForClientAttach`, same poll-with-bounded-timeout shape: poll `resolveLiveGatewayEndpointFromStatus({ stateRoot })` until a port is live or the budget elapses; a timeout returns `{ bound: false }`, it does not throw); (5) if not installed, skip restart/wait entirely and return immediately (T7's extended message is what the caller's subsequent endpoint-resolution ladder will show). Return a structured result naming which of the four steps (write / restart / wait / n/a) completed, so T9/T11 can report failures per-step. Test: a fixture asserting the additive write leaves unrelated existing plugins/keys untouched and produces a `.bak-<ts>` backup; a fixture asserting an entry already present in the effective config is not duplicated; a mocked-`restartServiceDaemon`-plus-fake-`sleep` test asserting `waitForGatewayBind` returns once a fake `status.json` reports a bound port, and returns (not throws) `{ bound: false }` on timeout; a not-installed fixture asserting restart/wait are both skipped.
- id: T9 branch: task/attach-prompts-to-enable/T9 deps: [T3, T6, T8] complexity: 5 -- `src/core/commands/clients.js`, `runClientLifecycle`'s attach branch: on state `not_enabled` (T3) with a TTY and no `--json`, prompt before doing anything, per design `#prompt`: for Claude/Codex/other, `The ${clientLabel} adapter is not enabled on this install. Attaching requires it. Enable ${T6-derived plugin list} now? [y/N]`; for OpenClaw specifically, the `#openclaw` disclosure variant naming the periodic sweep import up front, same `[y/N]`. Bootstrap floor (`#bootstrap-floor`): if no local config file exists at all (test via the same existence check `prepareLocalConfigWrite` uses internally), skip the prompt entirely and fall through to T3's existing guided error (which already names `hyp init`) - never prompt when there is nothing to add to. The `disabled_central` state never reaches this task's prompt branch at all; T3's refusal is the only behavior for it, unchanged. On decline, exit 1 with zero side effects (no write, no restart), matching today's refusal shape. On accept: call T8's `enableClientAdapter`, then **resolve the crux left open in "How this refines the design"** - this same CLI invocation must dispatch to the newly-enabled adapter's `attach()` afterward, which needs this process's own `ctx.capabilities`/`gateway.getClient(name)` to see the plugin T8 just wrote. Two mechanisms are viable and the choice is this task's to make and document in-code: (a) generalize `activateSeamCommandPlugins`'s dependency-closure activation (`src/core/cli/dispatch.js` ~line 790) into a helper usable outside the command-dispatch-miss seam, exposed to `runAttach` through a new, narrow `CommandRunContext` capability (this would also require extending `hypaware-plugin-kernel-types.d.ts`'s `CommandRunContext` and the dispatcher that populates it), or (b) after T8 completes, re-run this command's own dispatch from a fresh boot against the just-written config (in-process re-boot-and-redispatch, or a self re-exec) before proceeding to the existing `client.attach()` call. Whichever is chosen, it must not start a second gateway network listener in the CLI process (activation, not binding, is all that is required - see the existing dry-run branch's `gateway.localEndpoint()` try/catch for the precedent that activation and listener-binding are already separate concerns in this codebase). After the adapter is reachable, dispatch continues exactly as today's registered-client path (endpoint resolution ladder unchanged, `client.attach()`, asset materialization). Test: an interactive-prompt-driven attach test (fixture stdin, no daemon) asserting decline exits 1 with no config/backup file written; a matching accept-path test (with a stubbed `enableClientAdapter` and a fake daemon status) asserting the client's settings file ends up attached, proving whichever mechanism was chosen actually reaches `client.attach()`; an exact-string test asserting the OpenClaw prompt copy differs from the Claude/Codex copy per the two quotes in design `#prompt`/`#openclaw`; a no-config-file fixture asserting the prompt is skipped and T3's `hyp init`-naming error is shown instead.
- id: T10 branch: task/attach-prompts-to-enable/T10 deps: [T5, T9] complexity: 3 -- Wire the backfill consent step into T9's accept path, after a successful attach dispatch: ask T5's exported `defaultBackfillConsentPromptFactory`-produced question (same copy the init finale shows) for the just-attached client, and on yes call `runBackfillProvider` (`src/core/commands/backfill.js`, the same function `buildPickerBackfillRunner` in `src/core/commands/init.js` wraps) for that client's provider if one is registered in `ctx.backfills`; on no, or if the client has no registered backfill provider, leave history unimported with no further action, exactly matching the finale's decline behavior. This step never runs for a client whose adapter was already enabled (today's registered-state attach path is completely unchanged - this is reachable only from T9's accept branch). Test: an accept-then-yes fixture asserting the provider's `run` is invoked with the expected `{ provider, dryRun: false, retentionDays, until }` shape and its result is reported; an accept-then-no fixture asserting no backfill run occurs; a client-with-no-provider fixture (e.g. a hypothetical adapter with no `contributes.backfill`) asserting the question is not asked at all.
Expand Down
2 changes: 1 addition & 1 deletion src/core/cli/remote_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const GATEWAY_BIND_WAIT_DEFAULT_MS = 30000
* *return* on timeout rather than throwing. A timeout is not an error here for
* the same reason it is not one there: the caller has a better answer than an
* exception (attach's own endpoint-resolution ladder, which ends in the
* `hyp daemon install` / `hyp start` guidance the give-up message names).
* `hyp daemon install` / `hyp daemon start` guidance the give-up message names).
*
* The probed fact is `resolveLiveGatewayEndpointFromStatus`, which is already
* liveness-gated on the daemon pid, so a stale `status.json` left by the
Expand Down
8 changes: 4 additions & 4 deletions src/core/commands/clients.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,16 +407,16 @@ async function runClientLifecycle(action, argv, ctx) {
} else {
// Which give-up message to show hinges on whether a daemon
// service is installed at all: an install-but-unstarted daemon
// just needs `hyp start`, but with no service installed that
// command has nothing to start, so the message must also point
// at `hyp daemon install` / `hyp daemon start`.
// just needs `hyp daemon start`, but with no service installed
// that command has nothing to start, so the message must also
// point at `hyp daemon install` / `hyp daemon start`.
// @ref LLP 0174#bootstrap-floor [implements]: "config exists but no daemon is installed" extends the endpoint give-up message instead of attach gaining daemon orchestration
const { serviceDaemonStatus } = await import('../daemon/install.js')
const daemonStatus = await serviceDaemonStatus({ homeDir })
const message = daemonStatus.installed
? `cannot resolve the gateway endpoint: the gateway is not running in this ` +
`process and no ai-gateway 'listen' address is configured. Start the daemon ` +
`(hyp start) so it can attach clients, or set 'listen' in the ai-gateway config.`
`(hyp daemon start) so it can attach clients, or set 'listen' in the ai-gateway config.`
: `cannot resolve the gateway endpoint: the gateway is not running in this ` +
`process and no ai-gateway 'listen' address is configured, and no daemon ` +
`service is installed on this machine. Run 'hyp daemon install' then ` +
Expand Down
11 changes: 6 additions & 5 deletions test/core/attach-endpoint-fallback.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,20 +143,21 @@ test('attach without endpoint: no daemon installed names hyp daemon install/star
})
})

test('attach without endpoint: installed-but-unreachable daemon keeps the existing hyp start message', async () => {
test('attach without endpoint: installed-but-unreachable daemon names hyp daemon start', async () => {
await withTempHome(async (home) => {
// A daemon service IS installed (marker on disk) but not currently
// reachable: this is the pre-existing case, and its wording must not
// change or gain the daemon-install mention.
// @ref LLP 0174#bootstrap-floor [tests]: the installed-but-unreachable give-up message is unchanged
// reachable: this case names the lifecycle command only, and must not
// gain the daemon-install mention. The command it names has to be one
// the dispatcher accepts, so it is `hyp daemon start`, never `hyp start`.
// @ref LLP 0174#bootstrap-floor [tests]: only the not-installed case gains the daemon-install mention, so this one still names the start command alone
installFakeDaemonService(home)
/** @type {string[]} */
const attachCalls = []
const { ctx, stderr } = makeCtx({ home, attachCalls })
const code = await runAttach(['claude'], ctx)
assert.equal(code, 1)
assert.match(stderr.text(), /cannot resolve the gateway endpoint/)
assert.match(stderr.text(), /\(hyp start\)/)
assert.match(stderr.text(), /\(hyp daemon start\)/)
assert.doesNotMatch(stderr.text(), /hyp daemon install/)
assert.doesNotMatch(stderr.text(), /localEndpoint\(\) called before/)
assert.deepEqual(attachCalls, [])
Expand Down
Loading
Loading