Add ccc-server/client/queue bins and blessed queue TUI

- bin/ccc-server, ccc-client, ccc-queue wired up via package.json bin
- client/config.mjs: shared secrets/user resolution from CLI args or
  CCC_SECRETS/CCC_USER env vars
- ccc-queue: blessed TUI with two-pane layout (list + detail), polls
  every 2s, y/n to approve/deny selected item, r to refresh, q to quit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 20:49:12 +00:00
parent 0a3ab14053
commit e8bbcb293f
6 changed files with 255 additions and 51 deletions

View File

@@ -1,71 +1,46 @@
#!/usr/bin/env node
// Conduit client — thin CLI wrapper for Claude to call the conduit server.
// Usage:
// node client/index.mjs --secrets /path/to/secrets.json --user agent '{"action": "list-actions"}'
// node client/index.mjs --secrets /path/to/secrets.json --user agent '{"action":' '"edit-file",' '"filename": "/workspace/foo.mjs"}'
// ccc-client '{"action": "list-actions"}'
// ccc-client '{"action":' '"edit-file",' '"filename": "/workspace/foo.mjs"}'
import { readFileSync } from 'fs';
import { sign_request } from './auth.mjs';
import { load_client_config, get_remaining } from './config.mjs';
const BASE_URL = process.env.CONDUIT_URL || 'http://localhost:3015';
function get_arg(argv, flag) {
const i = argv.indexOf(flag);
return i !== -1 ? argv[i + 1] : null;
}
function get_remaining(argv) {
// Collect all args that aren't --secrets or --user and their values
const result = [];
let i = 2;
while (i < argv.length) {
if (argv[i] === '--secrets' || argv[i] === '--user') {
i += 2;
} else {
result.push(argv[i]);
i++;
}
}
return result;
}
async function call_action(payload, auth_headers) {
export async function call_action(payload, username, secret) {
const body_string = JSON.stringify(payload);
const res = await fetch(`${BASE_URL}/action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...auth_headers(body_string) },
headers: { 'Content-Type': 'application/json', ...sign_request(secret, username, body_string) },
body: body_string,
});
const body = await res.json();
return { status: res.status, body };
}
export async function get_queue(username, secret) {
const res = await fetch(`${BASE_URL}/queue`, {
headers: sign_request(secret, username, ''),
});
return res.json();
}
export async function resolve_queue_item(id, decision, username, secret) {
const res = await fetch(`${BASE_URL}/queue/${id}/${decision}`, {
method: 'POST',
headers: sign_request(secret, username, ''),
});
return res.json();
}
async function main() {
const secrets_path = get_arg(process.argv, '--secrets');
const username = get_arg(process.argv, '--user');
if (!secrets_path || !username) {
console.error('Usage: conduit --secrets <path> --user <name> <json payload>');
process.exit(1);
}
let secrets;
try {
secrets = JSON.parse(readFileSync(secrets_path, 'utf8'));
} catch (err) {
console.error(`Cannot read secrets file: ${err.message}`);
process.exit(1);
}
const user_entry = secrets.users?.[username];
if (!user_entry) {
console.error(`User '${username}' not found in secrets file`);
process.exit(1);
}
const { username, secret } = load_client_config(process.argv);
const remaining = get_remaining(process.argv);
if (!remaining.length) {
console.error('Usage: conduit --secrets <path> --user <name> <json payload>');
console.error('Usage: ccc-client <json payload>');
process.exit(1);
}
@@ -77,9 +52,7 @@ async function main() {
process.exit(1);
}
const auth_headers = (body_string) => sign_request(user_entry.secret, username, body_string);
const { status, body } = await call_action(payload, auth_headers);
const { status, body } = await call_action(payload, username, secret);
console.log(JSON.stringify(body, null, 2));
process.exit(status >= 400 ? 1 : 0);
}