Skip to content
Open
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
12 changes: 8 additions & 4 deletions src/core/cli/core_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,13 @@ function buildCoreCommands(registry) {
},
{
name: 'attach',
summary: 'Attach an AI client to the local gateway',
usage: 'hyp attach [client] [--client <name>] [--dry-run] [--json]',
summary: 'Attach AI clients to the local gateway',
usage: 'hyp attach [client|all] [--client <name>] [--dry-run] [--json]',
help: [
'Points an AI client at the local gateway so its traffic is captured, by',
'writing HypAware-managed settings into that client\'s own config file.',
'With no client name, attaches every active client adapter. Pass a client',
'name to limit the command to that client.',
'Idempotent: re-running is a no-op. Reversible with hyp detach, which',
'removes only the managed settings.',
'',
Expand All @@ -297,11 +299,13 @@ function buildCoreCommands(registry) {
},
{
name: 'detach',
summary: 'Detach an AI client from the local gateway',
usage: 'hyp detach [client] [--client <name>] [--dry-run] [--json]',
summary: 'Detach AI clients from the local gateway',
usage: 'hyp detach [client|all] [--client <name>] [--dry-run] [--json]',
help: [
'Removes the HypAware-managed settings hyp attach wrote, leaving the',
'client\'s own configuration otherwise intact. hyp unattach is an alias.',
'With no client name, detaches every known client integration. Pass a',
'client name to limit the command to that client.',
'',
'Detaching stops future capture for that client; it does not delete',
'anything already recorded (see hyp purge for that).',
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 @@ -45,7 +45,7 @@ import { pluginStateDir } from './plugin.js'
*/

/**
* `hyp attach [client] [--client <name>] [--yes]`
* `hyp attach [client|all] [--client <name>] [--yes]`
*
* Resolves the `hypaware.ai-gateway` capability, looks up the named
* client adapter, and dispatches to the adapter's `attach()`. Each
Expand All @@ -61,7 +61,7 @@ export async function runAttach(argv, ctx) {
}

/**
* `hyp detach [client] [--client <name>]`
* `hyp detach [client|all] [--client <name>]`
*
* Reverses a client's attach. Unlike `attach`, detach does **not**
* dispatch to a per-adapter hook: it routes through the single core,
Expand Down Expand Up @@ -613,12 +613,12 @@ function writeCoreDetachOutput({ ctx, name, json, result }) {

/**
* Parse an optional positional client name plus `--client <name>`,
* `--dry-run`, and `--json` from argv.
* `--dry-run`, and `--json` from argv. With no name, target all clients.
* @param {string[]} argv
*/
function parseClientArgs(argv) {
/** @type {{ client: string, dryRun: boolean, json: boolean, error?: string }} */
const r = { client: 'claude', dryRun: false, json: false }
const r = { client: 'all', dryRun: false, json: false }
/** @type {string | undefined} */
let requestedClient
/**
Expand Down
62 changes: 62 additions & 0 deletions test/core/command-dispatch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,68 @@ test('attach accepts a positional client name', async () => {
])
})

test('bare attach expands to every registered client', async () => {
const { registry, kernel, calls } = fakeClientKernel()
const stdout = makeBuf()
const stderr = makeBuf()

const code = await dispatch(['attach', '--dry-run'], {
stdout,
stderr,
registry,
kernel,
env: { ...process.env, HYP_HOME: await fs.mkdtemp(path.join(os.tmpdir(), 'hypaware-attach-all-')) },
})

assert.equal(code, 0)
assert.equal(stderr.text(), '')
assert.deepEqual(calls, [
{ action: 'attach', client: 'claude', dryRun: true, json: false },
{ action: 'attach', client: 'codex', dryRun: true, json: false },
])
})

test('bare detach expands to every known client descriptor', async () => {
const { registry, kernel, calls } = fakeClientKernel()
const stdout = makeBuf()
const stderr = makeBuf()
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'hypaware-detach-all-'))

const code = await dispatch(['detach', '--json'], {
stdout,
stderr,
registry,
kernel,
env: { ...process.env, HOME: home, CODEX_HOME: home, HYP_HOME: home },
})

assert.equal(code, 0)
assert.equal(stderr.text(), '')
assert.deepEqual(calls, [])
const clients = stdout.text()
.split('\n')
.filter((line) => line.trim())
.map((line) => JSON.parse(line).client)
assert.ok(clients.includes('claude'))
assert.ok(clients.includes('codex'))
})

test('attach and detach help document the all-client default', async () => {
const registry = createCommandRegistry()
registerCoreCommands(registry)

for (const command of ['attach', 'detach']) {
const stdout = makeBuf()
const stderr = makeBuf()
const code = await dispatch([command, '--help'], { stdout, stderr, registry })

assert.equal(code, 0)
assert.equal(stderr.text(), '')
assert.match(stdout.text(), new RegExp(`hyp ${command} \\[client\\|all\\]`))
assert.match(stdout.text(), /With no client name/)
}
})

test('unattach alias routes a positional client through the core disk undo', async () => {
const { registry, kernel, calls } = fakeClientKernel()
const stdout = makeBuf()
Expand Down
Loading