Files
nanoclaw/scripts/init-cli-agent.ts
T
gavrielc d8d61d3695 fix: Teams user-id prefix + defer cli:local owner grant
parseUserId now falls back to user.kind when the id prefix isn't a
registered adapter — Teams uses `29:` rather than `teams:`, so the
literal prefix wouldn't resolve the channel adapter for cold DMs.

init-cli-agent no longer claims the first-owner slot on `cli:local`.
The CLI identity is scratch; owner promotion belongs to
init-first-agent once the real channel user is wired.
2026-04-21 10:16:13 +03:00

171 lines
5.1 KiB
TypeScript

/**
* Initialize the scratch CLI agent used during `/new-setup`.
*
* Creates the synthetic `cli:local` user, grants owner role if no owner
* exists yet, builds an agent group with a minimal CLAUDE.md, and wires it
* to the CLI messaging group so `pnpm run chat` works immediately.
*
* No welcome is staged — the operator's first `pnpm run chat` is the
* natural wake, and the agent introduces itself on first contact per its
* CLAUDE.md.
*
* Runs alongside the service (WAL-mode sqlite) — does NOT initialize
* channel adapters, so there's no Gateway conflict.
*
* Usage:
* pnpm exec tsx scripts/init-cli-agent.ts \
* --display-name "Gavriel" \
* [--agent-name "Andy"]
*/
import path from 'path';
import { DATA_DIR } from '../src/config.js';
import { createAgentGroup, getAgentGroupByFolder } from '../src/db/agent-groups.js';
import { initDb } from '../src/db/connection.js';
import {
createMessagingGroup,
createMessagingGroupAgent,
getMessagingGroupAgentByPair,
getMessagingGroupByPlatform,
} from '../src/db/messaging-groups.js';
import { runMigrations } from '../src/db/migrations/index.js';
import { normalizeName } from '../src/modules/agent-to-agent/db/agent-destinations.js';
import { upsertUser } from '../src/modules/permissions/db/users.js';
import { initGroupFilesystem } from '../src/group-init.js';
import type { AgentGroup, MessagingGroup } from '../src/types.js';
const CLI_CHANNEL = 'cli';
const CLI_PLATFORM_ID = 'local';
const CLI_SYNTHETIC_USER_ID = `${CLI_CHANNEL}:${CLI_PLATFORM_ID}`;
interface Args {
displayName: string;
agentName: string;
}
function parseArgs(argv: string[]): Args {
let displayName: string | undefined;
let agentName: string | undefined;
for (let i = 0; i < argv.length; i++) {
const key = argv[i];
const val = argv[i + 1];
if (key === '--display-name') {
displayName = val;
i++;
} else if (key === '--agent-name') {
agentName = val;
i++;
}
}
if (!displayName) {
console.error('Missing required arg: --display-name');
console.error('See scripts/init-cli-agent.ts header for usage.');
process.exit(2);
}
return {
displayName,
agentName: agentName?.trim() || displayName,
};
}
function generateId(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
async function main(): Promise<void> {
const args = parseArgs(process.argv.slice(2));
const db = initDb(path.join(DATA_DIR, 'v2.db'));
runMigrations(db);
const now = new Date().toISOString();
// 1. Synthetic CLI user + owner grant if none exists.
upsertUser({
id: CLI_SYNTHETIC_USER_ID,
kind: CLI_CHANNEL,
display_name: args.displayName,
created_at: now,
});
// Owner grant deferred to init-first-agent when the real channel user is
// wired — cli:local is a scratch identity, not the operator.
const promotedToOwner = false;
// 2. Agent group + filesystem.
const folder = `cli-with-${normalizeName(args.displayName)}`;
let ag: AgentGroup | undefined = getAgentGroupByFolder(folder);
if (!ag) {
const agId = generateId('ag');
createAgentGroup({
id: agId,
name: args.agentName,
folder,
agent_provider: null,
created_at: now,
});
ag = getAgentGroupByFolder(folder)!;
console.log(`Created agent group: ${ag.id} (${folder})`);
} else {
console.log(`Reusing agent group: ${ag.id} (${folder})`);
}
initGroupFilesystem(ag, {
instructions:
`# ${args.agentName}\n\n` +
`You are ${args.agentName}, a personal NanoClaw agent for ${args.displayName}. ` +
'When the user first reaches out, introduce yourself briefly and invite them to chat. Keep replies concise.',
});
// 3. CLI messaging group + wiring.
let cliMg: MessagingGroup | undefined = getMessagingGroupByPlatform(CLI_CHANNEL, CLI_PLATFORM_ID);
if (!cliMg) {
cliMg = {
id: generateId('mg'),
channel_type: CLI_CHANNEL,
platform_id: CLI_PLATFORM_ID,
name: 'Local CLI',
is_group: 0,
unknown_sender_policy: 'public',
created_at: now,
};
createMessagingGroup(cliMg);
console.log(`Created CLI messaging group: ${cliMg.id}`);
}
const existing = getMessagingGroupAgentByPair(cliMg.id, ag.id);
if (!existing) {
createMessagingGroupAgent({
id: generateId('mga'),
messaging_group_id: cliMg.id,
agent_group_id: ag.id,
engage_mode: 'pattern',
engage_pattern: '.',
sender_scope: 'all',
ignored_message_policy: 'drop',
session_mode: 'shared',
priority: 0,
created_at: now,
});
console.log(`Wired cli: ${cliMg.id} -> ${ag.id}`);
} else {
console.log(`Wiring already exists: ${existing.id}`);
}
console.log('');
console.log('Init complete.');
console.log(
` owner: ${CLI_SYNTHETIC_USER_ID}${promotedToOwner ? ' (promoted on first owner)' : ''}`,
);
console.log(` agent: ${ag.name} [${ag.id}] @ groups/${folder}`);
console.log(` channel: cli/${CLI_PLATFORM_ID}`);
console.log('');
console.log('Run `pnpm run chat hi` to talk to your agent.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});