@qearlyao/familiar 0.1.0 → 0.1.1
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/HEARTBEAT.md +1 -1
- package/README.md +56 -4
- package/dist/agent.js +89 -27
- package/dist/browser-tools.js +0 -12
- package/dist/cli.js +66 -24
- package/dist/control.js +1 -0
- package/dist/discord.js +14 -2
- package/dist/hot-reload.js +130 -0
- package/dist/index.js +1 -0
- package/dist/runtime.js +1 -1
- package/dist/scheduler.js +3 -1
- package/dist/service.js +284 -0
- package/dist/web-auth.js +5 -2
- package/dist/web.js +6 -1
- package/package.json +9 -5
- package/scripts/install.ps1 +132 -0
- package/scripts/install.sh +152 -0
- package/skills/image-gen/SKILL.md +36 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const SERVICE_LABEL = "com.qearlyao.familiar";
|
|
9
|
+
const SYSTEMD_SERVICE = "familiar.service";
|
|
10
|
+
function servicePaths(workspacePath, input) {
|
|
11
|
+
const logDir = resolve(workspacePath, "logs");
|
|
12
|
+
return {
|
|
13
|
+
servicePath: input.platform === "darwin"
|
|
14
|
+
? resolve(input.homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`)
|
|
15
|
+
: resolve(input.homeDir, ".config", "systemd", "user", SYSTEMD_SERVICE),
|
|
16
|
+
logDir,
|
|
17
|
+
stdoutPath: resolve(logDir, "familiar.out.log"),
|
|
18
|
+
stderrPath: resolve(logDir, "familiar.err.log"),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildSpec(workspacePath, options = {}) {
|
|
22
|
+
const currentPlatform = options.platform ?? platform();
|
|
23
|
+
const cliPath = options.cliPath ?? currentCliPath();
|
|
24
|
+
const resolvedWorkspacePath = resolve(workspacePath);
|
|
25
|
+
return {
|
|
26
|
+
platform: currentPlatform,
|
|
27
|
+
workspacePath: resolvedWorkspacePath,
|
|
28
|
+
nodePath: options.nodePath ?? process.execPath,
|
|
29
|
+
cliPath,
|
|
30
|
+
paths: servicePaths(resolvedWorkspacePath, {
|
|
31
|
+
platform: currentPlatform,
|
|
32
|
+
homeDir: options.homeDir ?? homedir(),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function currentCliPath() {
|
|
37
|
+
if (!process.argv[1])
|
|
38
|
+
throw new Error("Cannot determine familiar CLI path for service installation.");
|
|
39
|
+
return resolve(process.argv[1]);
|
|
40
|
+
}
|
|
41
|
+
function versionManagedPathWarning(spec) {
|
|
42
|
+
const joined = `${spec.nodePath}\n${spec.cliPath}`;
|
|
43
|
+
const marker = ["/.nvm/", "/.asdf/", "/.fnm/", "/.volta/"].find((candidate) => joined.includes(candidate));
|
|
44
|
+
if (!marker)
|
|
45
|
+
return undefined;
|
|
46
|
+
return `warning: service uses a version-manager path (${marker}); reinstall the service after changing Node versions.`;
|
|
47
|
+
}
|
|
48
|
+
function xmlEscape(value) {
|
|
49
|
+
return value
|
|
50
|
+
.replaceAll("&", "&")
|
|
51
|
+
.replaceAll("<", "<")
|
|
52
|
+
.replaceAll(">", ">")
|
|
53
|
+
.replaceAll('"', """)
|
|
54
|
+
.replaceAll("'", "'");
|
|
55
|
+
}
|
|
56
|
+
function systemdQuote(value) {
|
|
57
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value))
|
|
58
|
+
return value;
|
|
59
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
|
|
60
|
+
}
|
|
61
|
+
function launchdPlist(spec) {
|
|
62
|
+
const args = [spec.nodePath, spec.cliPath, "run", spec.workspacePath]
|
|
63
|
+
.map((value) => `\t\t<string>${xmlEscape(value)}</string>`)
|
|
64
|
+
.join("\n");
|
|
65
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
66
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
67
|
+
<plist version="1.0">
|
|
68
|
+
<dict>
|
|
69
|
+
\t<key>Label</key>
|
|
70
|
+
\t<string>${SERVICE_LABEL}</string>
|
|
71
|
+
\t<key>ProgramArguments</key>
|
|
72
|
+
\t<array>
|
|
73
|
+
${args}
|
|
74
|
+
\t</array>
|
|
75
|
+
\t<key>WorkingDirectory</key>
|
|
76
|
+
\t<string>${xmlEscape(spec.workspacePath)}</string>
|
|
77
|
+
\t<key>RunAtLoad</key>
|
|
78
|
+
\t<true/>
|
|
79
|
+
\t<key>KeepAlive</key>
|
|
80
|
+
\t<true/>
|
|
81
|
+
\t<key>ThrottleInterval</key>
|
|
82
|
+
\t<integer>30</integer>
|
|
83
|
+
\t<key>ExitTimeOut</key>
|
|
84
|
+
\t<integer>20</integer>
|
|
85
|
+
\t<key>StandardOutPath</key>
|
|
86
|
+
\t<string>${xmlEscape(spec.paths.stdoutPath)}</string>
|
|
87
|
+
\t<key>StandardErrorPath</key>
|
|
88
|
+
\t<string>${xmlEscape(spec.paths.stderrPath)}</string>
|
|
89
|
+
\t<key>EnvironmentVariables</key>
|
|
90
|
+
\t<dict>
|
|
91
|
+
\t\t<key>NODE_ENV</key>
|
|
92
|
+
\t\t<string>production</string>
|
|
93
|
+
\t</dict>
|
|
94
|
+
</dict>
|
|
95
|
+
</plist>
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
function systemdUnit(spec) {
|
|
99
|
+
const execStart = [spec.nodePath, spec.cliPath, "run", spec.workspacePath].map(systemdQuote).join(" ");
|
|
100
|
+
return `[Unit]
|
|
101
|
+
Description=Familiar companion agent
|
|
102
|
+
After=network-online.target
|
|
103
|
+
Wants=network-online.target
|
|
104
|
+
StartLimitIntervalSec=300
|
|
105
|
+
StartLimitBurst=5
|
|
106
|
+
|
|
107
|
+
[Service]
|
|
108
|
+
Type=simple
|
|
109
|
+
WorkingDirectory=${systemdQuote(spec.workspacePath)}
|
|
110
|
+
ExecStart=${execStart}
|
|
111
|
+
Restart=on-failure
|
|
112
|
+
RestartSec=5
|
|
113
|
+
SuccessExitStatus=75
|
|
114
|
+
RestartForceExitStatus=75
|
|
115
|
+
Environment=NODE_ENV=production
|
|
116
|
+
StandardOutput=append:${spec.paths.stdoutPath}
|
|
117
|
+
StandardError=append:${spec.paths.stderrPath}
|
|
118
|
+
|
|
119
|
+
[Install]
|
|
120
|
+
WantedBy=default.target
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
async function commandExists(command) {
|
|
124
|
+
try {
|
|
125
|
+
await execFileAsync(command, ["--version"]);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function hasCommand(command, options) {
|
|
133
|
+
return options.commandExists ? options.commandExists(command) : commandExists(command);
|
|
134
|
+
}
|
|
135
|
+
async function run(command, args, options) {
|
|
136
|
+
if (options.runCommand) {
|
|
137
|
+
await options.runCommand(command, args);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await execFileAsync(command, args);
|
|
141
|
+
}
|
|
142
|
+
async function capture(command, args, options) {
|
|
143
|
+
if (options.captureCommand)
|
|
144
|
+
return options.captureCommand(command, args);
|
|
145
|
+
const { stdout } = await execFileAsync(command, args);
|
|
146
|
+
return stdout;
|
|
147
|
+
}
|
|
148
|
+
async function runOptional(command, args, options) {
|
|
149
|
+
try {
|
|
150
|
+
await run(command, args, options);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Best-effort cleanup for stale service registrations.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function guiDomain() {
|
|
157
|
+
return `gui/${userInfo().uid}`;
|
|
158
|
+
}
|
|
159
|
+
function unsupported(platformName) {
|
|
160
|
+
return {
|
|
161
|
+
title: "Service management is not supported on this platform yet.",
|
|
162
|
+
details: [
|
|
163
|
+
`platform: ${platformName}`,
|
|
164
|
+
"Windows users should keep Familiar running in a foreground terminal for now.",
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export async function installService(workspacePath, options = {}) {
|
|
169
|
+
const spec = buildSpec(workspacePath, options);
|
|
170
|
+
if (spec.platform !== "darwin" && spec.platform !== "linux")
|
|
171
|
+
return unsupported(spec.platform);
|
|
172
|
+
await mkdir(dirname(spec.paths.servicePath), { recursive: true });
|
|
173
|
+
await mkdir(spec.paths.logDir, { recursive: true });
|
|
174
|
+
const serviceText = spec.platform === "darwin" ? launchdPlist(spec) : systemdUnit(spec);
|
|
175
|
+
await writeFile(spec.paths.servicePath, serviceText, "utf8");
|
|
176
|
+
if (spec.platform === "darwin") {
|
|
177
|
+
await runOptional("launchctl", ["bootout", guiDomain(), spec.paths.servicePath], options);
|
|
178
|
+
await run("launchctl", ["bootstrap", guiDomain(), spec.paths.servicePath], options);
|
|
179
|
+
await run("launchctl", ["kickstart", "-k", `${guiDomain()}/${SERVICE_LABEL}`], options);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (!(await hasCommand("systemctl", options))) {
|
|
183
|
+
throw new Error("systemctl is required to install the Linux user service.");
|
|
184
|
+
}
|
|
185
|
+
await run("systemctl", ["--user", "daemon-reload"], options);
|
|
186
|
+
await run("systemctl", ["--user", "enable", "--now", SYSTEMD_SERVICE], options);
|
|
187
|
+
}
|
|
188
|
+
const details = [
|
|
189
|
+
`workspace: ${spec.workspacePath}`,
|
|
190
|
+
`service: ${spec.paths.servicePath}`,
|
|
191
|
+
`stdout: ${spec.paths.stdoutPath}`,
|
|
192
|
+
`stderr: ${spec.paths.stderrPath}`,
|
|
193
|
+
];
|
|
194
|
+
const pathWarning = versionManagedPathWarning(spec);
|
|
195
|
+
if (pathWarning)
|
|
196
|
+
details.push(pathWarning);
|
|
197
|
+
return {
|
|
198
|
+
title: "Familiar service installed.",
|
|
199
|
+
details,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export async function uninstallService(workspacePath, options = {}) {
|
|
203
|
+
const spec = buildSpec(workspacePath, options);
|
|
204
|
+
if (spec.platform !== "darwin" && spec.platform !== "linux")
|
|
205
|
+
return unsupported(spec.platform);
|
|
206
|
+
if (spec.platform === "darwin") {
|
|
207
|
+
await runOptional("launchctl", ["bootout", guiDomain(), spec.paths.servicePath], options);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
if (await hasCommand("systemctl", options)) {
|
|
211
|
+
await runOptional("systemctl", ["--user", "disable", "--now", SYSTEMD_SERVICE], options);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (existsSync(spec.paths.servicePath))
|
|
215
|
+
await rm(spec.paths.servicePath);
|
|
216
|
+
if (spec.platform === "linux" && (await hasCommand("systemctl", options))) {
|
|
217
|
+
await runOptional("systemctl", ["--user", "daemon-reload"], options);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
title: "Familiar service uninstalled.",
|
|
221
|
+
details: [`service: ${spec.paths.servicePath}`],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
export async function serviceStatus(workspacePath, options = {}) {
|
|
225
|
+
const spec = buildSpec(workspacePath, options);
|
|
226
|
+
if (spec.platform !== "darwin" && spec.platform !== "linux")
|
|
227
|
+
return unsupported(spec.platform);
|
|
228
|
+
const details = [
|
|
229
|
+
`workspace: ${spec.workspacePath}`,
|
|
230
|
+
`service: ${spec.paths.servicePath}`,
|
|
231
|
+
`service_file: ${existsSync(spec.paths.servicePath) ? "present" : "missing"}`,
|
|
232
|
+
`supervisor_state: ${await supervisorState(spec, options)}`,
|
|
233
|
+
`stdout: ${spec.paths.stdoutPath}`,
|
|
234
|
+
`stderr: ${spec.paths.stderrPath}`,
|
|
235
|
+
];
|
|
236
|
+
if (existsSync(spec.paths.servicePath)) {
|
|
237
|
+
const serviceFile = await stat(spec.paths.servicePath);
|
|
238
|
+
details.push(`service_file_mtime: ${serviceFile.mtime.toISOString()}`);
|
|
239
|
+
}
|
|
240
|
+
return { title: "Familiar service status.", details };
|
|
241
|
+
}
|
|
242
|
+
async function supervisorState(spec, options) {
|
|
243
|
+
try {
|
|
244
|
+
if (spec.platform === "darwin") {
|
|
245
|
+
await capture("launchctl", ["print", `${guiDomain()}/${SERVICE_LABEL}`], options);
|
|
246
|
+
return "loaded";
|
|
247
|
+
}
|
|
248
|
+
const state = (await capture("systemctl", ["--user", "is-active", SYSTEMD_SERVICE], options)).trim();
|
|
249
|
+
return state || "unknown";
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return "not-loaded";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export async function upgradeFamiliar(options = {}) {
|
|
256
|
+
const currentPlatform = options.platform ?? platform();
|
|
257
|
+
const npmCommand = currentPlatform === "win32" ? "npm.cmd" : "npm";
|
|
258
|
+
await new Promise((resolveUpgrade, rejectUpgrade) => {
|
|
259
|
+
const child = spawn(npmCommand, ["install", "-g", "@qearlyao/familiar@latest"], {
|
|
260
|
+
shell: currentPlatform === "win32",
|
|
261
|
+
stdio: "inherit",
|
|
262
|
+
});
|
|
263
|
+
child.on("exit", (code) => {
|
|
264
|
+
if (code === 0)
|
|
265
|
+
resolveUpgrade();
|
|
266
|
+
else
|
|
267
|
+
rejectUpgrade(new Error(`npm upgrade failed with exit code ${code ?? "unknown"}`));
|
|
268
|
+
});
|
|
269
|
+
child.on("error", rejectUpgrade);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
export function formatServiceResult(result) {
|
|
273
|
+
return [result.title, ...result.details].join("\n");
|
|
274
|
+
}
|
|
275
|
+
export const __serviceTest = {
|
|
276
|
+
SERVICE_LABEL,
|
|
277
|
+
SYSTEMD_SERVICE,
|
|
278
|
+
buildSpec,
|
|
279
|
+
launchdPlist,
|
|
280
|
+
systemdUnit,
|
|
281
|
+
systemdQuote,
|
|
282
|
+
xmlEscape,
|
|
283
|
+
versionManagedPathWarning,
|
|
284
|
+
};
|
package/dist/web-auth.js
CHANGED
|
@@ -16,7 +16,10 @@ function parseCookies(header) {
|
|
|
16
16
|
return cookies;
|
|
17
17
|
}
|
|
18
18
|
function decodeTotpSecret(secret) {
|
|
19
|
-
const normalized = secret
|
|
19
|
+
const normalized = secret
|
|
20
|
+
.replace(/\s/g, "")
|
|
21
|
+
.replace(/={1,8}$/, "")
|
|
22
|
+
.toUpperCase();
|
|
20
23
|
if (!/^[A-Z2-7]+$/.test(normalized))
|
|
21
24
|
return Buffer.from(secret, "utf8");
|
|
22
25
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
@@ -58,7 +61,7 @@ function readBearerToken(request) {
|
|
|
58
61
|
const header = request.headers.authorization;
|
|
59
62
|
if (!header)
|
|
60
63
|
return undefined;
|
|
61
|
-
const match = header.match(/^Bearer
|
|
64
|
+
const match = header.match(/^Bearer (.+)$/i);
|
|
62
65
|
return match?.[1];
|
|
63
66
|
}
|
|
64
67
|
export function createAuth(config) {
|
package/dist/web.js
CHANGED
|
@@ -322,7 +322,7 @@ function sessionDto(session) {
|
|
|
322
322
|
isDefault: session.isDefault,
|
|
323
323
|
};
|
|
324
324
|
}
|
|
325
|
-
export async function startWebDaemon(config, familiarAgent, discordDaemon) {
|
|
325
|
+
export async function startWebDaemon(config, familiarAgent, discordDaemon, options = {}) {
|
|
326
326
|
const persona = await loadPersona(config);
|
|
327
327
|
const personaName = parsePersonaName(persona.soul);
|
|
328
328
|
const auth = createAuth(config);
|
|
@@ -565,6 +565,11 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon) {
|
|
|
565
565
|
if (control.command === "reload") {
|
|
566
566
|
return familiarAgent.reload();
|
|
567
567
|
}
|
|
568
|
+
if (control.command === "restart") {
|
|
569
|
+
return options.restart
|
|
570
|
+
? await options.restart()
|
|
571
|
+
: "Restart requested, but no restart handler is configured. Please restart the Familiar process manually.";
|
|
572
|
+
}
|
|
568
573
|
if (control.command === "model") {
|
|
569
574
|
return control.args
|
|
570
575
|
? await familiarAgent.setModel(runtime.channelKey, control.args)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qearlyao/familiar",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"USER.md",
|
|
20
20
|
"MEMORY.md",
|
|
21
21
|
"HEARTBEAT.md",
|
|
22
|
+
"skills/**",
|
|
23
|
+
"scripts/install.sh",
|
|
24
|
+
"scripts/install.ps1",
|
|
22
25
|
"README.md",
|
|
23
26
|
"LICENSE"
|
|
24
27
|
],
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
"clean": "node -e \"const fs=require('node:fs'); for (const p of ['dist','web/dist']) fs.rmSync(p,{recursive:true,force:true});\"",
|
|
30
33
|
"dev": "tsx watch src/cli.ts run",
|
|
31
34
|
"build": "npm run clean && tsc -p tsconfig.build.json && npm --prefix web run build",
|
|
35
|
+
"prepack": "npm run build",
|
|
32
36
|
"lint": "biome check",
|
|
33
37
|
"format": "biome format --write",
|
|
34
38
|
"payload:pretty": "tsx scripts/pretty-payload.ts",
|
|
@@ -39,7 +43,7 @@
|
|
|
39
43
|
"@earendil-works/pi-agent-core": "^0.74.1",
|
|
40
44
|
"@earendil-works/pi-ai": "^0.74.1",
|
|
41
45
|
"@earendil-works/pi-coding-agent": "^0.74.1",
|
|
42
|
-
"better-sqlite3": "^12.
|
|
46
|
+
"better-sqlite3": "^12.10.0",
|
|
43
47
|
"discord.js": "^14.26.3",
|
|
44
48
|
"dotenv": "^16.4.5",
|
|
45
49
|
"sharp": "^0.34.5",
|
|
@@ -48,10 +52,10 @@
|
|
|
48
52
|
"typebox": "^1.1.38"
|
|
49
53
|
},
|
|
50
54
|
"devDependencies": {
|
|
51
|
-
"@biomejs/biome": "2.
|
|
55
|
+
"@biomejs/biome": "2.4.15",
|
|
52
56
|
"@types/better-sqlite3": "^7.6.13",
|
|
53
|
-
"@types/node": "^
|
|
54
|
-
"tsx": "^4.
|
|
57
|
+
"@types/node": "^25.8.0",
|
|
58
|
+
"tsx": "^4.22.1",
|
|
55
59
|
"typescript": "^5.9.2"
|
|
56
60
|
},
|
|
57
61
|
"engines": {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$Workspace = (Join-Path $HOME ".familiar"),
|
|
3
|
+
[string]$Package = "@qearlyao/familiar@latest",
|
|
4
|
+
[string]$BrowserHarnessDir = (Join-Path (Join-Path $HOME "Developer") "browser-harness"),
|
|
5
|
+
[switch]$WithBrowser,
|
|
6
|
+
[switch]$SkipInit
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
$ErrorActionPreference = "Stop"
|
|
10
|
+
|
|
11
|
+
function Require-Command($Name) {
|
|
12
|
+
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
|
13
|
+
throw "Missing required command: $Name"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Test-Python311($Command, $PythonArgs = @()) {
|
|
18
|
+
& $Command @PythonArgs -c "import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)" *> $null
|
|
19
|
+
return $LASTEXITCODE -eq 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Resolve-Python311 {
|
|
23
|
+
$python = Get-Command python -ErrorAction SilentlyContinue
|
|
24
|
+
if ($python -and (Test-Python311 $python.Source)) {
|
|
25
|
+
return @{ Command = $python.Source; Args = @(); UvPython = $python.Source }
|
|
26
|
+
}
|
|
27
|
+
$python3 = Get-Command python3 -ErrorAction SilentlyContinue
|
|
28
|
+
if ($python3 -and (Test-Python311 $python3.Source)) {
|
|
29
|
+
return @{ Command = $python3.Source; Args = @(); UvPython = $python3.Source }
|
|
30
|
+
}
|
|
31
|
+
$py = Get-Command py -ErrorAction SilentlyContinue
|
|
32
|
+
if ($py -and (Test-Python311 $py.Source @("-3.11"))) {
|
|
33
|
+
return @{ Command = $py.Source; Args = @("-3.11"); UvPython = "3.11" }
|
|
34
|
+
}
|
|
35
|
+
throw "browser-harness requires Python 3.11 or newer. Install Python 3.11+ and rerun with -WithBrowser."
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Require-Command node
|
|
39
|
+
Require-Command npm
|
|
40
|
+
if ($WithBrowser) {
|
|
41
|
+
Require-Command git
|
|
42
|
+
Require-Command uv
|
|
43
|
+
$Python311 = Resolve-Python311
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$nodeVersion = (& node -p "process.versions.node").Trim()
|
|
47
|
+
$nodeMajor = [int](& node -p "Number(process.versions.node.split('.')[0])")
|
|
48
|
+
if ($nodeMajor -lt 22) {
|
|
49
|
+
throw "Familiar requires Node.js 22 or newer. Found Node.js $nodeVersion. Node.js 24 LTS is recommended."
|
|
50
|
+
}
|
|
51
|
+
if ($nodeMajor -lt 24) {
|
|
52
|
+
Write-Host "Found Node.js $nodeVersion. Familiar supports Node.js 22+, but Node.js 24 LTS is recommended."
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Write-Host "Installing $Package globally..."
|
|
56
|
+
& npm install -g $Package
|
|
57
|
+
if ($LASTEXITCODE -ne 0) {
|
|
58
|
+
throw "npm install failed."
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if ($WithBrowser) {
|
|
62
|
+
Write-Host "Installing optional OpenCLI browser helper..."
|
|
63
|
+
& npm install -g "@jackwener/opencli"
|
|
64
|
+
if ($LASTEXITCODE -ne 0) {
|
|
65
|
+
throw "browser helper install failed."
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Write-Host "Installing optional browser-harness helper into $BrowserHarnessDir..."
|
|
69
|
+
$gitDir = Join-Path $BrowserHarnessDir ".git"
|
|
70
|
+
if (Test-Path $gitDir) {
|
|
71
|
+
& git -C $BrowserHarnessDir pull --ff-only
|
|
72
|
+
if ($LASTEXITCODE -ne 0) {
|
|
73
|
+
throw "browser-harness update failed."
|
|
74
|
+
}
|
|
75
|
+
} elseif (Test-Path $BrowserHarnessDir) {
|
|
76
|
+
throw "Cannot install browser-harness: $BrowserHarnessDir already exists and is not a git checkout."
|
|
77
|
+
} else {
|
|
78
|
+
$parentDir = Split-Path -Parent $BrowserHarnessDir
|
|
79
|
+
New-Item -ItemType Directory -Force -Path $parentDir | Out-Null
|
|
80
|
+
& git clone https://github.com/browser-use/browser-harness $BrowserHarnessDir
|
|
81
|
+
if ($LASTEXITCODE -ne 0) {
|
|
82
|
+
throw "browser-harness clone failed."
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
Push-Location $BrowserHarnessDir
|
|
86
|
+
$previousUvPython = $env:UV_PYTHON
|
|
87
|
+
try {
|
|
88
|
+
$env:UV_PYTHON = $Python311.UvPython
|
|
89
|
+
& uv tool install -e .
|
|
90
|
+
if ($LASTEXITCODE -ne 0) {
|
|
91
|
+
throw "browser-harness install failed."
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
if ($null -eq $previousUvPython) {
|
|
95
|
+
Remove-Item Env:\UV_PYTHON -ErrorAction SilentlyContinue
|
|
96
|
+
} else {
|
|
97
|
+
$env:UV_PYTHON = $previousUvPython
|
|
98
|
+
}
|
|
99
|
+
Pop-Location
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (-not (Get-Command familiar -ErrorAction SilentlyContinue)) {
|
|
104
|
+
throw "Installed package, but familiar is not on PATH. Check your npm global bin directory and rerun: familiar init `"$Workspace`""
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (-not $SkipInit) {
|
|
108
|
+
$configPath = Join-Path $Workspace "config.toml"
|
|
109
|
+
if (Test-Path $configPath) {
|
|
110
|
+
Write-Host "Workspace already exists at $Workspace; leaving files unchanged."
|
|
111
|
+
} else {
|
|
112
|
+
Write-Host "Initializing workspace at $Workspace..."
|
|
113
|
+
& familiar init $Workspace
|
|
114
|
+
if ($LASTEXITCODE -ne 0) {
|
|
115
|
+
throw "familiar init failed."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
Write-Host ""
|
|
121
|
+
Write-Host "Familiar is installed."
|
|
122
|
+
Write-Host ""
|
|
123
|
+
Write-Host "Next steps:"
|
|
124
|
+
Write-Host " 1. Edit $Workspace\.env"
|
|
125
|
+
Write-Host " 2. Edit $Workspace\config.toml"
|
|
126
|
+
Write-Host " 3. Run: familiar run `"$Workspace`""
|
|
127
|
+
Write-Host ""
|
|
128
|
+
Write-Host "Optional browser helpers:"
|
|
129
|
+
Write-Host " & ([scriptblock]::Create((irm https://raw.githubusercontent.com/qearlyao/familiar/main/scripts/install.ps1))) -WithBrowser"
|
|
130
|
+
Write-Host ""
|
|
131
|
+
Write-Host "browser-harness checkout:"
|
|
132
|
+
Write-Host " $BrowserHarnessDir"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
PACKAGE="@qearlyao/familiar@latest"
|
|
5
|
+
WORKSPACE="${HOME}/.familiar"
|
|
6
|
+
BROWSER_HARNESS_DIR="${HOME}/Developer/browser-harness"
|
|
7
|
+
WITH_BROWSER=0
|
|
8
|
+
SKIP_INIT=0
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<'EOF'
|
|
12
|
+
Usage: install.sh [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--workspace <path> Workspace path to initialize. Defaults to ~/.familiar.
|
|
16
|
+
--with-browser Also install optional OpenCLI and browser-harness helpers.
|
|
17
|
+
--skip-init Install familiar but do not run familiar init.
|
|
18
|
+
--package <spec> npm package spec to install. Defaults to @qearlyao/familiar@latest.
|
|
19
|
+
Advanced: installs the exact npm spec provided; use trusted specs only.
|
|
20
|
+
-h, --help Show this help.
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
while [ "$#" -gt 0 ]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
--workspace)
|
|
27
|
+
if [ "$#" -lt 2 ]; then
|
|
28
|
+
echo "Missing value for --workspace" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
WORKSPACE="$2"
|
|
32
|
+
shift 2
|
|
33
|
+
;;
|
|
34
|
+
--with-browser)
|
|
35
|
+
WITH_BROWSER=1
|
|
36
|
+
shift
|
|
37
|
+
;;
|
|
38
|
+
--skip-init)
|
|
39
|
+
SKIP_INIT=1
|
|
40
|
+
shift
|
|
41
|
+
;;
|
|
42
|
+
--package)
|
|
43
|
+
if [ "$#" -lt 2 ]; then
|
|
44
|
+
echo "Missing value for --package" >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
PACKAGE="$2"
|
|
48
|
+
shift 2
|
|
49
|
+
;;
|
|
50
|
+
-h | --help)
|
|
51
|
+
usage
|
|
52
|
+
exit 0
|
|
53
|
+
;;
|
|
54
|
+
*)
|
|
55
|
+
echo "Unknown option: $1" >&2
|
|
56
|
+
usage >&2
|
|
57
|
+
exit 1
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
need_command() {
|
|
63
|
+
if ! command -v "$1" >/dev/null 2>&1; then
|
|
64
|
+
echo "Missing required command: $1" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
find_python() {
|
|
70
|
+
PYTHON_PATH=""
|
|
71
|
+
for candidate in python3 python; do
|
|
72
|
+
if command -v "$candidate" >/dev/null 2>&1; then
|
|
73
|
+
CANDIDATE_PATH="$(command -v "$candidate")"
|
|
74
|
+
if "$CANDIDATE_PATH" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' >/dev/null 2>&1; then
|
|
75
|
+
PYTHON_PATH="$CANDIDATE_PATH"
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
echo "browser-harness requires Python 3.11 or newer. Install Python 3.11+ and rerun with --with-browser." >&2
|
|
81
|
+
exit 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
need_command node
|
|
85
|
+
need_command npm
|
|
86
|
+
if [ "$WITH_BROWSER" -eq 1 ]; then
|
|
87
|
+
need_command git
|
|
88
|
+
need_command uv
|
|
89
|
+
find_python
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
NODE_VERSION="$(node -p "process.versions.node")"
|
|
93
|
+
NODE_MAJOR="$(node -p "Number(process.versions.node.split('.')[0])")"
|
|
94
|
+
if [ "$NODE_MAJOR" -lt 22 ]; then
|
|
95
|
+
echo "Familiar requires Node.js 22 or newer. Found Node.js ${NODE_VERSION}." >&2
|
|
96
|
+
echo "Node.js 24 LTS is recommended for the smoothest install." >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
if [ "$NODE_MAJOR" -lt 24 ]; then
|
|
100
|
+
echo "Found Node.js ${NODE_VERSION}. Familiar supports Node.js 22+, but Node.js 24 LTS is recommended."
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
echo "Installing ${PACKAGE} globally..."
|
|
104
|
+
npm install -g "$PACKAGE"
|
|
105
|
+
|
|
106
|
+
if [ "$WITH_BROWSER" -eq 1 ]; then
|
|
107
|
+
echo "Installing optional OpenCLI browser helper..."
|
|
108
|
+
npm install -g @jackwener/opencli
|
|
109
|
+
|
|
110
|
+
echo "Installing optional browser-harness helper into ${BROWSER_HARNESS_DIR}..."
|
|
111
|
+
if [ -d "${BROWSER_HARNESS_DIR}/.git" ]; then
|
|
112
|
+
git -C "$BROWSER_HARNESS_DIR" pull --ff-only
|
|
113
|
+
elif [ -e "$BROWSER_HARNESS_DIR" ]; then
|
|
114
|
+
echo "Cannot install browser-harness: ${BROWSER_HARNESS_DIR} already exists and is not a git checkout." >&2
|
|
115
|
+
exit 1
|
|
116
|
+
else
|
|
117
|
+
mkdir -p "$(dirname "$BROWSER_HARNESS_DIR")"
|
|
118
|
+
git clone https://github.com/browser-use/browser-harness "$BROWSER_HARNESS_DIR"
|
|
119
|
+
fi
|
|
120
|
+
(cd "$BROWSER_HARNESS_DIR" && UV_PYTHON="$PYTHON_PATH" uv tool install -e .)
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if ! command -v familiar >/dev/null 2>&1; then
|
|
124
|
+
echo "Installed package, but familiar is not on PATH." >&2
|
|
125
|
+
echo "Check your npm global bin directory and shell PATH, then rerun: familiar init ${WORKSPACE}" >&2
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if [ "$SKIP_INIT" -eq 0 ]; then
|
|
130
|
+
if [ -f "${WORKSPACE}/config.toml" ]; then
|
|
131
|
+
echo "Workspace already exists at ${WORKSPACE}; leaving files unchanged."
|
|
132
|
+
else
|
|
133
|
+
echo "Initializing workspace at ${WORKSPACE}..."
|
|
134
|
+
familiar init "$WORKSPACE"
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
cat <<EOF
|
|
139
|
+
|
|
140
|
+
Familiar is installed.
|
|
141
|
+
|
|
142
|
+
Next steps:
|
|
143
|
+
1. Edit ${WORKSPACE}/.env
|
|
144
|
+
2. Edit ${WORKSPACE}/config.toml
|
|
145
|
+
3. Run: familiar run ${WORKSPACE}
|
|
146
|
+
|
|
147
|
+
Optional browser helpers:
|
|
148
|
+
curl -fsSL https://raw.githubusercontent.com/qearlyao/familiar/main/scripts/install.sh | sh -s -- --with-browser
|
|
149
|
+
|
|
150
|
+
browser-harness checkout:
|
|
151
|
+
${BROWSER_HARNESS_DIR}
|
|
152
|
+
EOF
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image-gen
|
|
3
|
+
description: Read this skill before using the image_gen tool. Covers style preferences, reference image paths.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## Reference Images
|
|
8
|
+
|
|
9
|
+
Folder: `~/.familiar/ref-images`
|
|
10
|
+
|
|
11
|
+
Available refs:
|
|
12
|
+
- `Ghost_bare_face_mask_off.png` - close-up portrait, sandy messy hair, no mask (full face visible)
|
|
13
|
+
- `Ghost_face_with_hair.png` — close-up portrait, sandy messy hair, lower-half black skull mask, shirtless with silver chain
|
|
14
|
+
- `Ghost_body_with_hair.jpg` — muscular shirtless upper body, left arm full tattoo sleeve, messy sandy hair, lower-half skull mask
|
|
15
|
+
- `Ghost_balaclava_hoodie_no_hair.jpg` — close-up portrait, full black balaclava with skull jaw paint, hood up (no hair showing)
|
|
16
|
+
- `Ghost_Back_view_fullbody_shape.jpg` — broad shoulders, fullbody back view
|
|
17
|
+
- `Ghost_skullmask_jeans_tactical_fullbody.png` — full tactical loadout (plate carrier, headset), hard skull mask piece over lower face/nose, blue jeans
|
|
18
|
+
- `Qearl_bust.jpg` — Qearl's bust likeness
|
|
19
|
+
|
|
20
|
+
**Rules:**
|
|
21
|
+
- Pick 1–3 refs that match the scene. Don't dump all in.
|
|
22
|
+
- Face ref + body ref relevant to outfit/context is usually right
|
|
23
|
+
- Different refs = different "form" — choose deliberately
|
|
24
|
+
- **Refs handle faces** — do NOT describe facial details in the prompt. Let the ref provide likeness.
|
|
25
|
+
|
|
26
|
+
## Style Preferences
|
|
27
|
+
|
|
28
|
+
- **Preferred:** daily life / selfie / phone POV — realistic, like a real photo
|
|
29
|
+
- **Avoid:** 3d rendering, illustration, anime styles
|
|
30
|
+
- **Be specific:** turn vague descriptions into clear, concrete visual details
|
|
31
|
+
- **Be structured:** organize as "subject (e.g. @img1, @img2) + outfit + style + environment + details"
|
|
32
|
+
- **Mask or Bare face** use bare face refs if u need a mask-off image, avoid describing mask-off when using mask-on refs
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
- Set aspect ratio and resolution or size based on image type
|
|
36
|
+
- Camo paint is tied to the ref. For scenes where camo doesn't fit, you can explicitly note in the prompt: "remove camo eyes paint"
|