openclaw-castroom 0.1.0
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/README.md +96 -0
- package/dist/commands/add.js +92 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/populate.js +80 -0
- package/dist/commands/populate.js.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/openclaw/runner.js +84 -0
- package/dist/openclaw/runner.js.map +1 -0
- package/dist/openclaw/workspace.js +186 -0
- package/dist/openclaw/workspace.js.map +1 -0
- package/dist/personas/index.js +44 -0
- package/dist/personas/index.js.map +1 -0
- package/dist/personas/packs/office/andy.js +50 -0
- package/dist/personas/packs/office/andy.js.map +1 -0
- package/dist/personas/packs/office/angela.js +50 -0
- package/dist/personas/packs/office/angela.js.map +1 -0
- package/dist/personas/packs/office/carol.js +49 -0
- package/dist/personas/packs/office/carol.js.map +1 -0
- package/dist/personas/packs/office/creed.js +49 -0
- package/dist/personas/packs/office/creed.js.map +1 -0
- package/dist/personas/packs/office/darryl.js +50 -0
- package/dist/personas/packs/office/darryl.js.map +1 -0
- package/dist/personas/packs/office/david-wallace.js +49 -0
- package/dist/personas/packs/office/david-wallace.js.map +1 -0
- package/dist/personas/packs/office/dwight.js +52 -0
- package/dist/personas/packs/office/dwight.js.map +1 -0
- package/dist/personas/packs/office/erin.js +49 -0
- package/dist/personas/packs/office/erin.js.map +1 -0
- package/dist/personas/packs/office/gabe.js +50 -0
- package/dist/personas/packs/office/gabe.js.map +1 -0
- package/dist/personas/packs/office/index.js +54 -0
- package/dist/personas/packs/office/index.js.map +1 -0
- package/dist/personas/packs/office/jan.js +49 -0
- package/dist/personas/packs/office/jan.js.map +1 -0
- package/dist/personas/packs/office/jim.js +50 -0
- package/dist/personas/packs/office/jim.js.map +1 -0
- package/dist/personas/packs/office/karen.js +48 -0
- package/dist/personas/packs/office/karen.js.map +1 -0
- package/dist/personas/packs/office/kelly.js +49 -0
- package/dist/personas/packs/office/kelly.js.map +1 -0
- package/dist/personas/packs/office/kevin.js +49 -0
- package/dist/personas/packs/office/kevin.js.map +1 -0
- package/dist/personas/packs/office/meredith.js +48 -0
- package/dist/personas/packs/office/meredith.js.map +1 -0
- package/dist/personas/packs/office/michael.js +50 -0
- package/dist/personas/packs/office/michael.js.map +1 -0
- package/dist/personas/packs/office/oscar.js +49 -0
- package/dist/personas/packs/office/oscar.js.map +1 -0
- package/dist/personas/packs/office/pam.js +50 -0
- package/dist/personas/packs/office/pam.js.map +1 -0
- package/dist/personas/packs/office/phyllis.js +48 -0
- package/dist/personas/packs/office/phyllis.js.map +1 -0
- package/dist/personas/packs/office/roy.js +48 -0
- package/dist/personas/packs/office/roy.js.map +1 -0
- package/dist/personas/packs/office/ryan.js +48 -0
- package/dist/personas/packs/office/ryan.js.map +1 -0
- package/dist/personas/packs/office/stanley.js +49 -0
- package/dist/personas/packs/office/stanley.js.map +1 -0
- package/dist/personas/packs/office/toby.js +48 -0
- package/dist/personas/packs/office/toby.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/barbara.js +45 -0
- package/dist/personas/packs/trailer-park-boys/barbara.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/bubbles.js +49 -0
- package/dist/personas/packs/trailer-park-boys/bubbles.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/cory.js +45 -0
- package/dist/personas/packs/trailer-park-boys/cory.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/cyrus.js +45 -0
- package/dist/personas/packs/trailer-park-boys/cyrus.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/george.js +45 -0
- package/dist/personas/packs/trailer-park-boys/george.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/index.js +42 -0
- package/dist/personas/packs/trailer-park-boys/index.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/jroc.js +45 -0
- package/dist/personas/packs/trailer-park-boys/jroc.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/julian.js +49 -0
- package/dist/personas/packs/trailer-park-boys/julian.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/lahey.js +46 -0
- package/dist/personas/packs/trailer-park-boys/lahey.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/lucy.js +46 -0
- package/dist/personas/packs/trailer-park-boys/lucy.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/randy.js +45 -0
- package/dist/personas/packs/trailer-park-boys/randy.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/ray.js +45 -0
- package/dist/personas/packs/trailer-park-boys/ray.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/ricky.js +49 -0
- package/dist/personas/packs/trailer-park-boys/ricky.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/sam.js +45 -0
- package/dist/personas/packs/trailer-park-boys/sam.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/sarah.js +45 -0
- package/dist/personas/packs/trailer-park-boys/sarah.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/trevor.js +45 -0
- package/dist/personas/packs/trailer-park-boys/trevor.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/trinity.js +45 -0
- package/dist/personas/packs/trailer-park-boys/trinity.js.map +1 -0
- package/dist/personas/packs/trailer-park-boys/tyrone.js +45 -0
- package/dist/personas/packs/trailer-park-boys/tyrone.js.map +1 -0
- package/dist/personas/types.js +2 -0
- package/dist/personas/types.js.map +1 -0
- package/dist/templates/base.js +109 -0
- package/dist/templates/base.js.map +1 -0
- package/dist/templates/common.js +79 -0
- package/dist/templates/common.js.map +1 -0
- package/dist/ui/spinner.js +40 -0
- package/dist/ui/spinner.js.map +1 -0
- package/package.json +55 -0
- package/src/commands/add.ts +145 -0
- package/src/commands/populate.ts +109 -0
- package/src/index.ts +112 -0
- package/src/openclaw/runner.ts +121 -0
- package/src/openclaw/workspace.ts +248 -0
- package/src/personas/index.ts +59 -0
- package/src/personas/packs/office/andy.ts +51 -0
- package/src/personas/packs/office/angela.ts +51 -0
- package/src/personas/packs/office/carol.ts +50 -0
- package/src/personas/packs/office/creed.ts +50 -0
- package/src/personas/packs/office/darryl.ts +51 -0
- package/src/personas/packs/office/david-wallace.ts +50 -0
- package/src/personas/packs/office/dwight.ts +53 -0
- package/src/personas/packs/office/erin.ts +50 -0
- package/src/personas/packs/office/gabe.ts +51 -0
- package/src/personas/packs/office/index.ts +56 -0
- package/src/personas/packs/office/jan.ts +50 -0
- package/src/personas/packs/office/jim.ts +51 -0
- package/src/personas/packs/office/karen.ts +49 -0
- package/src/personas/packs/office/kelly.ts +50 -0
- package/src/personas/packs/office/kevin.ts +50 -0
- package/src/personas/packs/office/meredith.ts +49 -0
- package/src/personas/packs/office/michael.ts +51 -0
- package/src/personas/packs/office/oscar.ts +50 -0
- package/src/personas/packs/office/pam.ts +51 -0
- package/src/personas/packs/office/phyllis.ts +49 -0
- package/src/personas/packs/office/roy.ts +49 -0
- package/src/personas/packs/office/ryan.ts +49 -0
- package/src/personas/packs/office/stanley.ts +50 -0
- package/src/personas/packs/office/toby.ts +49 -0
- package/src/personas/packs/trailer-park-boys/barbara.ts +47 -0
- package/src/personas/packs/trailer-park-boys/bubbles.ts +50 -0
- package/src/personas/packs/trailer-park-boys/cory.ts +47 -0
- package/src/personas/packs/trailer-park-boys/cyrus.ts +47 -0
- package/src/personas/packs/trailer-park-boys/george.ts +47 -0
- package/src/personas/packs/trailer-park-boys/index.ts +44 -0
- package/src/personas/packs/trailer-park-boys/jroc.ts +47 -0
- package/src/personas/packs/trailer-park-boys/julian.ts +50 -0
- package/src/personas/packs/trailer-park-boys/lahey.ts +48 -0
- package/src/personas/packs/trailer-park-boys/lucy.ts +48 -0
- package/src/personas/packs/trailer-park-boys/randy.ts +47 -0
- package/src/personas/packs/trailer-park-boys/ray.ts +47 -0
- package/src/personas/packs/trailer-park-boys/ricky.ts +50 -0
- package/src/personas/packs/trailer-park-boys/sam.ts +47 -0
- package/src/personas/packs/trailer-park-boys/sarah.ts +47 -0
- package/src/personas/packs/trailer-park-boys/trevor.ts +47 -0
- package/src/personas/packs/trailer-park-boys/trinity.ts +47 -0
- package/src/personas/packs/trailer-park-boys/tyrone.ts +47 -0
- package/src/personas/types.ts +24 -0
- package/src/templates/base.ts +110 -0
- package/src/templates/common.ts +96 -0
- package/src/ui/spinner.ts +56 -0
- package/test/personas.test.ts +31 -0
- package/test/populate.test.ts +83 -0
- package/test/workspace.test.ts +47 -0
- package/tsconfig.json +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-castroom",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Give your OpenClaw agents personality from TV show characters.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"castroom": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"lint": "node -e \"process.env.ESLINT_USE_FLAT_CONFIG='false'; import('node:child_process').then(({ spawnSync }) => { const result = spawnSync(process.execPath, ['node_modules/eslint/bin/eslint.js', '.'], { stdio: 'inherit', env: process.env }); process.exit(result.status ?? 1); });\"",
|
|
15
|
+
"format": "prettier --check .",
|
|
16
|
+
"format:write": "prettier --write .",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "14.0.3",
|
|
21
|
+
"execa": "9.6.1",
|
|
22
|
+
"fs-extra": "11.3.3",
|
|
23
|
+
"pathe": "2.0.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/fs-extra": "11.0.4",
|
|
27
|
+
"@types/node": "25.2.0",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "8.54.0",
|
|
29
|
+
"@typescript-eslint/parser": "8.54.0",
|
|
30
|
+
"eslint": "9.39.2",
|
|
31
|
+
"prettier": "3.8.1",
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"vitest": "4.0.18"
|
|
34
|
+
},
|
|
35
|
+
"eslintConfig": {
|
|
36
|
+
"root": true,
|
|
37
|
+
"env": {
|
|
38
|
+
"node": true,
|
|
39
|
+
"es2022": true
|
|
40
|
+
},
|
|
41
|
+
"parser": "@typescript-eslint/parser",
|
|
42
|
+
"parserOptions": {
|
|
43
|
+
"sourceType": "module"
|
|
44
|
+
},
|
|
45
|
+
"plugins": ["@typescript-eslint"],
|
|
46
|
+
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
47
|
+
"ignorePatterns": ["dist", "node_modules"]
|
|
48
|
+
},
|
|
49
|
+
"prettier": {
|
|
50
|
+
"singleQuote": true,
|
|
51
|
+
"semi": true,
|
|
52
|
+
"printWidth": 100,
|
|
53
|
+
"trailingComma": "all"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { populateWorkspace } from './populate.js';
|
|
2
|
+
import {
|
|
3
|
+
findPersonaInAnyPack,
|
|
4
|
+
getPersona,
|
|
5
|
+
listPacks,
|
|
6
|
+
listPersonas,
|
|
7
|
+
type Persona,
|
|
8
|
+
} from '../personas/index.js';
|
|
9
|
+
import {
|
|
10
|
+
detectNonInteractiveFlags,
|
|
11
|
+
ensureOpenclawAvailable,
|
|
12
|
+
getOpenclawAddHelp,
|
|
13
|
+
runOpenclawCommand,
|
|
14
|
+
} from '../openclaw/runner.js';
|
|
15
|
+
import { resolveWorkspace } from '../openclaw/workspace.js';
|
|
16
|
+
import { withSpinner } from '../ui/spinner.js';
|
|
17
|
+
|
|
18
|
+
export type AddOptions = {
|
|
19
|
+
pack?: string;
|
|
20
|
+
persona?: string;
|
|
21
|
+
listPacks?: boolean;
|
|
22
|
+
listPersonas?: boolean;
|
|
23
|
+
packListPersonas?: boolean;
|
|
24
|
+
workspace?: string;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
dryRun?: boolean;
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
nonInteractive?: boolean;
|
|
29
|
+
verbose?: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const addCommand = async (
|
|
33
|
+
agentName: string | undefined,
|
|
34
|
+
options: AddOptions,
|
|
35
|
+
): Promise<void> => {
|
|
36
|
+
if (options.listPacks) {
|
|
37
|
+
console.log(listPacks().join('\n'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pack = options.pack ?? 'office';
|
|
42
|
+
|
|
43
|
+
if (options.listPersonas || options.packListPersonas) {
|
|
44
|
+
console.log(listPersonas(pack).join('\n'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!agentName) {
|
|
49
|
+
throw new Error('Agent name is required unless --list-packs or --list-personas is used.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.interactive && options.nonInteractive) {
|
|
53
|
+
throw new Error('Use either --interactive or --non-interactive, not both.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const personaInput = options.persona ?? agentName;
|
|
57
|
+
let persona: Persona;
|
|
58
|
+
try {
|
|
59
|
+
persona = getPersona(pack, personaInput);
|
|
60
|
+
} catch {
|
|
61
|
+
const found = findPersonaInAnyPack(personaInput);
|
|
62
|
+
if (found) {
|
|
63
|
+
persona = found.persona;
|
|
64
|
+
} else {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Unknown persona "${personaInput}". Use --pack <series> to specify, or run --list-personas to see options.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await ensureOpenclawAvailable();
|
|
72
|
+
|
|
73
|
+
let nonInteractiveFlags: string[] = [];
|
|
74
|
+
if (options.nonInteractive) {
|
|
75
|
+
const helpText = await getOpenclawAddHelp();
|
|
76
|
+
nonInteractiveFlags = detectNonInteractiveFlags(helpText);
|
|
77
|
+
if (nonInteractiveFlags.length === 0) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'OpenClaw does not expose a non-interactive flag. Re-run with --interactive or create the agent manually and use populate with --workspace.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const args = ['agents', 'add', agentName, ...nonInteractiveFlags];
|
|
85
|
+
if (options.verbose) {
|
|
86
|
+
console.log(`running: openclaw ${args.join(' ')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await runOpenclawCommand(args, {
|
|
90
|
+
streamOutput: Boolean(options.verbose || !options.nonInteractive),
|
|
91
|
+
stdin: 'inherit',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (result.exitCode !== 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`OpenClaw failed with exit code ${result.exitCode}. Check the OpenClaw output and try again.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const resolution = await resolveWorkspace({
|
|
101
|
+
explicitPath: options.workspace,
|
|
102
|
+
output: result,
|
|
103
|
+
cwd: process.cwd(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
for (const warning of resolution.warnings) {
|
|
107
|
+
console.warn(`warning: ${warning}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.verbose) {
|
|
111
|
+
console.log(`workspace: ${resolution.workspace} (source: ${resolution.source})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pickRandom = (arr: string[]): string => arr[Math.floor(Math.random() * arr.length)] ?? '';
|
|
115
|
+
|
|
116
|
+
const injection = await withSpinner(
|
|
117
|
+
options.dryRun ? 'Previewing persona injection…' : 'Injecting persona…',
|
|
118
|
+
async () =>
|
|
119
|
+
populateWorkspace({
|
|
120
|
+
workspacePath: resolution.workspace,
|
|
121
|
+
persona,
|
|
122
|
+
force: options.force ?? true,
|
|
123
|
+
dryRun: options.dryRun,
|
|
124
|
+
}),
|
|
125
|
+
{
|
|
126
|
+
enabled: Boolean(process.stdout.isTTY) && !options.verbose,
|
|
127
|
+
successText: options.dryRun ? 'Preview ready' : 'Persona ready',
|
|
128
|
+
failureText: 'Persona injection failed',
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const wrote = injection.written.length;
|
|
133
|
+
const skipped = injection.skipped.length;
|
|
134
|
+
if (options.dryRun) {
|
|
135
|
+
console.log(`Would write ${wrote} file(s) (${skipped} skipped).`);
|
|
136
|
+
} else {
|
|
137
|
+
console.log(`Injected persona files (${wrote} written, ${skipped} skipped).`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const greetings = persona.startAgentGreetings?.length ? persona.startAgentGreetings : persona.bootstrapGreetings;
|
|
141
|
+
const line = pickRandom(greetings);
|
|
142
|
+
if (line) {
|
|
143
|
+
console.log(`\n${persona.name}: ${line}`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fsExtra from 'fs-extra';
|
|
2
|
+
const { ensureDir, pathExists, remove, rename, stat, writeFile } = fsExtra;
|
|
3
|
+
import { basename, dirname, join } from 'pathe';
|
|
4
|
+
import { renderPersonaFiles } from '../templates/common.js';
|
|
5
|
+
import type { Persona } from '../personas/types.js';
|
|
6
|
+
import { withSpinner } from '../ui/spinner.js';
|
|
7
|
+
|
|
8
|
+
export type PopulateResult = {
|
|
9
|
+
written: string[];
|
|
10
|
+
skipped: string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const atomicWriteFile = async (filePath: string, content: string): Promise<void> => {
|
|
14
|
+
await ensureDir(dirname(filePath));
|
|
15
|
+
const tempPath = join(dirname(filePath), `.${basename(filePath)}.${Date.now()}.tmp`);
|
|
16
|
+
await writeFile(tempPath, content, { encoding: 'utf8' });
|
|
17
|
+
try {
|
|
18
|
+
await rename(tempPath, filePath);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
await remove(filePath);
|
|
22
|
+
await rename(tempPath, filePath);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const assertWorkspaceDir = async (workspacePath: string): Promise<void> => {
|
|
30
|
+
if (!(await pathExists(workspacePath))) {
|
|
31
|
+
throw new Error(`Workspace not found: ${workspacePath}`);
|
|
32
|
+
}
|
|
33
|
+
const stats = await stat(workspacePath);
|
|
34
|
+
if (!stats.isDirectory()) {
|
|
35
|
+
throw new Error(`Workspace is not a directory: ${workspacePath}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const populateWorkspace = async (options: {
|
|
40
|
+
workspacePath: string;
|
|
41
|
+
persona: Persona;
|
|
42
|
+
force?: boolean;
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
}): Promise<PopulateResult> => {
|
|
45
|
+
await assertWorkspaceDir(options.workspacePath);
|
|
46
|
+
const files = renderPersonaFiles(options.persona);
|
|
47
|
+
const written: string[] = [];
|
|
48
|
+
const skipped: string[] = [];
|
|
49
|
+
|
|
50
|
+
for (const [fileName, content] of Object.entries(files)) {
|
|
51
|
+
const filePath = join(options.workspacePath, fileName);
|
|
52
|
+
const exists = await pathExists(filePath);
|
|
53
|
+
if (exists && !options.force) {
|
|
54
|
+
skipped.push(filePath);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (options.dryRun) {
|
|
59
|
+
written.push(filePath);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await atomicWriteFile(filePath, content);
|
|
64
|
+
written.push(filePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { written, skipped };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const populateCommand = async (options: {
|
|
71
|
+
workspace: string;
|
|
72
|
+
persona: Persona;
|
|
73
|
+
force?: boolean;
|
|
74
|
+
dryRun?: boolean;
|
|
75
|
+
}): Promise<void> => {
|
|
76
|
+
const pickRandom = (arr: string[]): string => arr[Math.floor(Math.random() * arr.length)] ?? '';
|
|
77
|
+
|
|
78
|
+
const result = await withSpinner(
|
|
79
|
+
options.dryRun ? 'Previewing persona injection…' : 'Injecting persona…',
|
|
80
|
+
async () =>
|
|
81
|
+
populateWorkspace({
|
|
82
|
+
workspacePath: options.workspace,
|
|
83
|
+
persona: options.persona,
|
|
84
|
+
force: options.force,
|
|
85
|
+
dryRun: options.dryRun,
|
|
86
|
+
}),
|
|
87
|
+
{
|
|
88
|
+
enabled: Boolean(process.stdout.isTTY),
|
|
89
|
+
successText: options.dryRun ? 'Preview ready' : 'Persona ready',
|
|
90
|
+
failureText: 'Persona injection failed',
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const wrote = result.written.length;
|
|
95
|
+
const skipped = result.skipped.length;
|
|
96
|
+
if (options.dryRun) {
|
|
97
|
+
console.log(`Would write ${wrote} file(s) (${skipped} skipped).`);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(`Injected persona files (${wrote} written, ${skipped} skipped).`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const greetings = options.persona.startAgentGreetings?.length
|
|
103
|
+
? options.persona.startAgentGreetings
|
|
104
|
+
: options.persona.bootstrapGreetings;
|
|
105
|
+
const line = pickRandom(greetings);
|
|
106
|
+
if (line) {
|
|
107
|
+
console.log(`\n${options.persona.name}: ${line}`);
|
|
108
|
+
}
|
|
109
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { addCommand } from './commands/add.js';
|
|
4
|
+
import { populateCommand } from './commands/populate.js';
|
|
5
|
+
import { getPersona, listPacks, listPersonas } from './personas/index.js';
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('castroom')
|
|
11
|
+
.description('Give your OpenClaw agents personality from TV show characters.')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.option('--pack <name>', 'Persona pack (default: "office")', 'office')
|
|
16
|
+
.option('--list-packs', 'List available persona packs and exit')
|
|
17
|
+
.option('--list-personas', 'List personas for the selected pack and exit')
|
|
18
|
+
.option('--pack-list-personas', 'Alias for --list-personas');
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('add')
|
|
22
|
+
.argument('[agentName]', 'Agent name to create with OpenClaw')
|
|
23
|
+
.usage('<agentName> [options]')
|
|
24
|
+
.option('--pack <name>', 'Persona pack (default: "office")', 'office')
|
|
25
|
+
.option('--persona <name>', 'Persona name (default: agentName)')
|
|
26
|
+
.option('--list-packs', 'List available persona packs and exit')
|
|
27
|
+
.option('--list-personas', 'List personas for the selected pack and exit')
|
|
28
|
+
.option('--pack-list-personas', 'Alias for --list-personas')
|
|
29
|
+
.option('--workspace <path>', 'Workspace path override')
|
|
30
|
+
.option('--force', 'Overwrite existing markdown files')
|
|
31
|
+
.option('--dry-run', 'Print actions without writing files')
|
|
32
|
+
.option('--interactive', 'Allow interactive OpenClaw prompts')
|
|
33
|
+
.option('--non-interactive', 'Attempt non-interactive OpenClaw flow')
|
|
34
|
+
.option('--verbose', 'Print extra diagnostics')
|
|
35
|
+
.action(async (agentName, options) => {
|
|
36
|
+
const parentOpts = program.opts();
|
|
37
|
+
await addCommand(agentName, {
|
|
38
|
+
...parentOpts,
|
|
39
|
+
...options,
|
|
40
|
+
pack: options.pack ?? parentOpts.pack ?? 'office',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command('populate')
|
|
46
|
+
.option('--workspace <path>', 'Workspace path to populate')
|
|
47
|
+
.option('--pack <name>', 'Persona pack (default: "office")', 'office')
|
|
48
|
+
.option('--persona <name>', 'Persona name to use')
|
|
49
|
+
.option('--list-packs', 'List available persona packs and exit')
|
|
50
|
+
.option('--list-personas', 'List personas for the selected pack and exit')
|
|
51
|
+
.option('--pack-list-personas', 'Alias for --list-personas')
|
|
52
|
+
.option('--force', 'Overwrite existing markdown files')
|
|
53
|
+
.option('--dry-run', 'Print actions without writing files')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
const parentOpts = program.opts();
|
|
56
|
+
const merged = {
|
|
57
|
+
...parentOpts,
|
|
58
|
+
...options,
|
|
59
|
+
pack: options.pack ?? parentOpts.pack ?? 'office',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (merged.listPacks) {
|
|
63
|
+
console.log(listPacks().join('\n'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (merged.listPersonas || merged.packListPersonas) {
|
|
68
|
+
console.log(listPersonas(merged.pack ?? 'office').join('\n'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!merged.workspace) {
|
|
73
|
+
throw new Error('Workspace is required. Provide --workspace <path>.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!merged.persona) {
|
|
77
|
+
throw new Error('Persona is required. Provide --persona <name>.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const persona = getPersona(merged.pack ?? 'office', merged.persona);
|
|
81
|
+
|
|
82
|
+
await populateCommand({
|
|
83
|
+
workspace: merged.workspace,
|
|
84
|
+
persona,
|
|
85
|
+
force: merged.force,
|
|
86
|
+
dryRun: merged.dryRun,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program.action(async (options) => {
|
|
91
|
+
if (options.listPacks) {
|
|
92
|
+
console.log(listPacks().join('\n'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.listPersonas || options.packListPersonas) {
|
|
97
|
+
console.log(listPersonas(options.pack ?? 'office').join('\n'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
program.help();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const main = async () => {
|
|
105
|
+
await program.parseAsync(process.argv);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
main().catch((error) => {
|
|
109
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
110
|
+
console.error(`error: ${message}`);
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
|
|
3
|
+
export type OpenclawResult = {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
exitCode: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type RunOptions = {
|
|
10
|
+
streamOutput?: boolean;
|
|
11
|
+
stdin?: 'inherit' | 'ignore';
|
|
12
|
+
cwd?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const OPENCLAW_BIN = 'openclaw';
|
|
16
|
+
|
|
17
|
+
const isMissingCommand = (error: unknown) =>
|
|
18
|
+
error instanceof Error && 'code' in error && (error as { code?: string }).code === 'ENOENT';
|
|
19
|
+
|
|
20
|
+
export const ensureOpenclawAvailable = async (): Promise<void> => {
|
|
21
|
+
try {
|
|
22
|
+
const result = await execa(OPENCLAW_BIN, ['--version'], { reject: false });
|
|
23
|
+
if (result.exitCode !== 0) {
|
|
24
|
+
throw new Error('OpenClaw CLI returned a non-zero exit code.');
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (isMissingCommand(error)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'OpenClaw CLI not found. Install OpenClaw and ensure the `openclaw` command is on your PATH.',
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
throw new Error(
|
|
33
|
+
'OpenClaw CLI is not available. Install OpenClaw and ensure the `openclaw` command works.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const runOpenclawCommand = async (
|
|
39
|
+
args: string[],
|
|
40
|
+
{ streamOutput, stdin = 'inherit', cwd }: RunOptions = {},
|
|
41
|
+
): Promise<OpenclawResult> => {
|
|
42
|
+
try {
|
|
43
|
+
const child = execa(OPENCLAW_BIN, args, {
|
|
44
|
+
stdin,
|
|
45
|
+
stdout: 'pipe',
|
|
46
|
+
stderr: 'pipe',
|
|
47
|
+
reject: false,
|
|
48
|
+
cwd,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
let stdout = '';
|
|
52
|
+
let stderr = '';
|
|
53
|
+
|
|
54
|
+
child.stdout?.on('data', (chunk) => {
|
|
55
|
+
const text = chunk.toString();
|
|
56
|
+
stdout += text;
|
|
57
|
+
if (streamOutput) {
|
|
58
|
+
process.stdout.write(text);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.stderr?.on('data', (chunk) => {
|
|
63
|
+
const text = chunk.toString();
|
|
64
|
+
stderr += text;
|
|
65
|
+
if (streamOutput) {
|
|
66
|
+
process.stderr.write(text);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await child;
|
|
71
|
+
return {
|
|
72
|
+
stdout,
|
|
73
|
+
stderr,
|
|
74
|
+
exitCode: result.exitCode ?? 0,
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (isMissingCommand(error)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'OpenClaw CLI not found. Install OpenClaw and ensure the `openclaw` command is on your PATH.',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
|
|
88
|
+
export const detectNonInteractiveFlags = (helpText: string): string[] => {
|
|
89
|
+
const candidates = ['--yes', '-y', '--defaults', '--no-input', '--non-interactive'];
|
|
90
|
+
const normalized = helpText.replace(/\r/g, '');
|
|
91
|
+
const supported = candidates.filter((flag) => {
|
|
92
|
+
const pattern = new RegExp(
|
|
93
|
+
`(^|\\s|[\\[(,])${escapeRegex(flag)}(?=\\s|,|$|[\\])])`,
|
|
94
|
+
'm',
|
|
95
|
+
);
|
|
96
|
+
return pattern.test(normalized);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result: string[] = [];
|
|
100
|
+
for (const flag of candidates) {
|
|
101
|
+
if (!supported.includes(flag)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (flag === '-y' && supported.includes('--yes')) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
result.push(flag);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const getOpenclawAddHelp = async (): Promise<string> => {
|
|
114
|
+
const result = await runOpenclawCommand(['agents', 'add', '--help'], { stdin: 'ignore' });
|
|
115
|
+
if (result.exitCode !== 0) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'Failed to run `openclaw agents add --help`. Try --interactive or create the agent manually.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
121
|
+
};
|