@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.
@@ -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("<", "&lt;")
52
+ .replaceAll(">", "&gt;")
53
+ .replaceAll('"', "&quot;")
54
+ .replaceAll("'", "&apos;");
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.replace(/\s+/g, "").replace(/=+$/g, "").toUpperCase();
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\s+(.+)$/i);
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.0",
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.9.0",
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.3.5",
55
+ "@biomejs/biome": "2.4.15",
52
56
  "@types/better-sqlite3": "^7.6.13",
53
- "@types/node": "^24.3.0",
54
- "tsx": "^4.20.3",
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"