friend-assistant 0.1.2 → 0.1.4
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 +5 -1
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +217 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ Every command starts with `friend`. If you run Friend from the source code, writ
|
|
|
51
51
|
|
|
52
52
|
| Command | What it does | Use it when |
|
|
53
53
|
|---|---|---|
|
|
54
|
-
| `friend start` | Starts Friend: the web app, the assistant, the speech engine if installed, and your connectors. Prints the address (and your network address when that is on). Ctrl+C stops it. | Every day. Open the address in a browser and sign in. |
|
|
54
|
+
| `friend start` | Starts Friend: the web app, the assistant, the speech engine if installed, and your connectors. Prints the address (and your network address when that is on). Ctrl+C stops it. Friend's screen works in one browser window at a time: open it in a second place and that window shows "Use here", which moves Friend there and stops the first. | Every day. Open the address in a browser and sign in. |
|
|
55
55
|
| `friend pair` | Prints a one-time pairing code, valid for 5 minutes, and the command to run on the other PC. | You want Friend to also work on a second PC. |
|
|
56
56
|
| `friend node --url ws://HOST:8787/device --code CODE` | Run on the other PC: connects it to your Friend. Add `--roots C:/work,D:/files` for the folders it may use freely and `--name laptop` to name it. | The second step after `friend pair`. |
|
|
57
57
|
| `friend devices list` | Lists the paired PCs and when each was last seen. | Checking what is connected. |
|
|
@@ -85,6 +85,10 @@ Every command starts with `friend`. If you run Friend from the source code, writ
|
|
|
85
85
|
|
|
86
86
|
| Command | What it does | Use it when |
|
|
87
87
|
|---|---|---|
|
|
88
|
+
| `friend daemon start` | Runs Friend in the background, with no terminal window needed. Prints the address, keeps a log in the data folder (`friend.log`). | You want Friend always available, even after you close the terminal. |
|
|
89
|
+
| `friend daemon stop` | Stops the background Friend and the speech engine with it. | Shutting it down. |
|
|
90
|
+
| `friend daemon restart` | Stops it and starts it again. | After changing a setting such as `friend lan on` or a different model. |
|
|
91
|
+
| `friend daemon status` | Says whether the background Friend is running, for how long, and at which address. Exit code 0 when running, 1 when not. | Checking, or in a script. |
|
|
88
92
|
| `friend lan` (or `friend lan status`) | Shows what Friend listens on and the addresses to open. | Checking, or finding the address for your phone. |
|
|
89
93
|
| `friend lan on` | Lets other devices on your network open Friend. Prints the addresses and the things to know (plain http, no microphone on other devices, firewall prompt). | Using Friend from a phone or another PC. |
|
|
90
94
|
| `friend lan on --https` | The same, with a secure `https://` address (Friend makes its own certificate), so a phone may use the microphone and camera. | Talking to Friend from a phone. |
|
package/dist/BUILD.json
CHANGED
package/dist/friend.mjs
CHANGED
|
@@ -1269,8 +1269,8 @@ function repairDangling(db) {
|
|
|
1269
1269
|
}
|
|
1270
1270
|
function loadHistory(db) {
|
|
1271
1271
|
const msgs = loadAll(db, WINDOW);
|
|
1272
|
-
const
|
|
1273
|
-
return
|
|
1272
|
+
const start2 = msgs.findIndex((m) => m.role === "user" && m.content.some((b) => b.type === "text"));
|
|
1273
|
+
return start2 === -1 ? [] : msgs.slice(start2);
|
|
1274
1274
|
}
|
|
1275
1275
|
|
|
1276
1276
|
// ../server/src/agent/prompt.ts
|
|
@@ -3619,9 +3619,9 @@ import { z as z10 } from "zod";
|
|
|
3619
3619
|
|
|
3620
3620
|
// ../server/src/http-error.ts
|
|
3621
3621
|
var HttpError = class extends Error {
|
|
3622
|
-
constructor(
|
|
3622
|
+
constructor(status2, message) {
|
|
3623
3623
|
super(message);
|
|
3624
|
-
this.status =
|
|
3624
|
+
this.status = status2;
|
|
3625
3625
|
}
|
|
3626
3626
|
status;
|
|
3627
3627
|
};
|
|
@@ -4075,6 +4075,7 @@ function createGateway(deps) {
|
|
|
4075
4075
|
const fwd = config.trustProxy ? String(req.headers["x-forwarded-for"] ?? "").split(",").at(-1)?.trim() : "";
|
|
4076
4076
|
return fwd || req.socket.remoteAddress || "unknown";
|
|
4077
4077
|
};
|
|
4078
|
+
let activeOwner = null;
|
|
4078
4079
|
function handleClient(ws, req) {
|
|
4079
4080
|
if (!originAllowed(req)) {
|
|
4080
4081
|
audit.log("anonymous", "ws.origin_refused", { origin: req.headers.origin });
|
|
@@ -4083,6 +4084,19 @@ function createGateway(deps) {
|
|
|
4083
4084
|
const token = parseCookies(req.headers.cookie)[COOKIE_NAME];
|
|
4084
4085
|
const session = token ? auth.resolveSession(token) : null;
|
|
4085
4086
|
if (!session) return ws.close(4401, "unauthorized");
|
|
4087
|
+
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
4088
|
+
const tab = (params.get("tab") ?? "").slice(0, 64);
|
|
4089
|
+
if (tab) {
|
|
4090
|
+
if (activeOwner && activeOwner.ws.readyState === WebSocket.OPEN) {
|
|
4091
|
+
if (params.get("takeover") !== "1" && activeOwner.tab !== tab) return ws.close(4409, "in use elsewhere");
|
|
4092
|
+
activeOwner.ws.close(4410, "replaced");
|
|
4093
|
+
}
|
|
4094
|
+
const mine = { ws, tab };
|
|
4095
|
+
activeOwner = mine;
|
|
4096
|
+
ws.on("close", () => {
|
|
4097
|
+
if (activeOwner === mine) activeOwner = null;
|
|
4098
|
+
});
|
|
4099
|
+
}
|
|
4086
4100
|
const conn = {
|
|
4087
4101
|
ip: clientIp(req),
|
|
4088
4102
|
send: (m) => {
|
|
@@ -4093,6 +4107,7 @@ function createGateway(deps) {
|
|
|
4093
4107
|
const dispose = deps.onClientConnect?.(session, conn);
|
|
4094
4108
|
ws.on("close", () => dispose?.());
|
|
4095
4109
|
ws.on("message", (data) => {
|
|
4110
|
+
if (tab && activeOwner?.ws !== ws) return;
|
|
4096
4111
|
let json;
|
|
4097
4112
|
try {
|
|
4098
4113
|
json = JSON.parse(String(data));
|
|
@@ -6342,11 +6357,10 @@ async function createApp(opts) {
|
|
|
6342
6357
|
camera.cancelAll(broadcast);
|
|
6343
6358
|
agent.cancel();
|
|
6344
6359
|
approvals.denyAll("Shutting down");
|
|
6345
|
-
await gateway.close()
|
|
6346
|
-
|
|
6347
|
-
})
|
|
6348
|
-
|
|
6349
|
-
});
|
|
6360
|
+
await Promise.all([gateway.close().catch(() => {
|
|
6361
|
+
}), provider.close?.().catch(() => {
|
|
6362
|
+
}), hub.close().catch(() => {
|
|
6363
|
+
})]);
|
|
6350
6364
|
db.close();
|
|
6351
6365
|
}
|
|
6352
6366
|
};
|
|
@@ -6897,7 +6911,7 @@ function startDeviceNode(opts) {
|
|
|
6897
6911
|
state = s;
|
|
6898
6912
|
opts.onState?.(s);
|
|
6899
6913
|
};
|
|
6900
|
-
function
|
|
6914
|
+
function connect2() {
|
|
6901
6915
|
if (stopped) return;
|
|
6902
6916
|
setState("connecting");
|
|
6903
6917
|
const sock = new WebSocket2(opts.url, { headers: { authorization: `Bearer ${opts.token}` }, ...opts.ca ? { ca: opts.ca } : {} });
|
|
@@ -6945,10 +6959,10 @@ function startDeviceNode(opts) {
|
|
|
6945
6959
|
if (opts.reconnect === false) return;
|
|
6946
6960
|
const delay = Math.min(base * 2 ** attempt, 1e4);
|
|
6947
6961
|
attempt += 1;
|
|
6948
|
-
timer = setTimeout(
|
|
6962
|
+
timer = setTimeout(connect2, delay);
|
|
6949
6963
|
});
|
|
6950
6964
|
}
|
|
6951
|
-
|
|
6965
|
+
connect2();
|
|
6952
6966
|
return {
|
|
6953
6967
|
connected,
|
|
6954
6968
|
state: () => state,
|
|
@@ -7380,8 +7394,8 @@ async function startFriend(opts) {
|
|
|
7380
7394
|
async stop() {
|
|
7381
7395
|
if (watcher) clearInterval(watcher);
|
|
7382
7396
|
node.stop();
|
|
7383
|
-
|
|
7384
|
-
await app.stop();
|
|
7397
|
+
const wait = (ms) => new Promise((r) => setTimeout(r, ms).unref?.());
|
|
7398
|
+
await Promise.race([Promise.allSettled([sidecar?.stop(), app.stop()]), wait(6e3)]);
|
|
7385
7399
|
}
|
|
7386
7400
|
};
|
|
7387
7401
|
}
|
|
@@ -7923,14 +7937,14 @@ async function checkDocker(d) {
|
|
|
7923
7937
|
async function checkOllama(d) {
|
|
7924
7938
|
const base = { id: "ollama", label: "Ollama", need: "optional", unlocks: "a free, private model that runs on this PC" };
|
|
7925
7939
|
const v = await d.run("ollama", ["--version"]);
|
|
7926
|
-
let
|
|
7940
|
+
let running2 = false;
|
|
7927
7941
|
try {
|
|
7928
|
-
|
|
7942
|
+
running2 = (await d.fetch("http://127.0.0.1:11434/api/version", { signal: AbortSignal.timeout(2500) })).ok;
|
|
7929
7943
|
} catch {
|
|
7930
|
-
|
|
7944
|
+
running2 = false;
|
|
7931
7945
|
}
|
|
7932
|
-
if (v.code === 0) return { ...base, ok: true, detail: `${first(v.stdout).replace(/^.*version is /i, "Ollama ")}${
|
|
7933
|
-
if (
|
|
7946
|
+
if (v.code === 0) return { ...base, ok: true, detail: `${first(v.stdout).replace(/^.*version is /i, "Ollama ")}${running2 ? ", running" : ", installed (start the Ollama app if it is not running)"}` };
|
|
7947
|
+
if (running2) return { ...base, ok: true, detail: "an Ollama server is running at 127.0.0.1:11434" };
|
|
7934
7948
|
return { ...base, ok: false, detail: "not installed" };
|
|
7935
7949
|
}
|
|
7936
7950
|
async function checkClaude(d, command = "claude") {
|
|
@@ -8550,6 +8564,175 @@ async function doctorCommand(io2, partial = {}) {
|
|
|
8550
8564
|
return requiredMissing ? 1 : 0;
|
|
8551
8565
|
}
|
|
8552
8566
|
|
|
8567
|
+
// ../agent-cli/src/commands/daemon.ts
|
|
8568
|
+
import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
|
|
8569
|
+
import { closeSync as closeSync2, existsSync as existsSync14, mkdirSync as mkdirSync13, openSync as openSync2, readFileSync as readFileSync10, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync3, writeFileSync as writeFileSync12 } from "node:fs";
|
|
8570
|
+
import { connect } from "node:net";
|
|
8571
|
+
import { join as join18 } from "node:path";
|
|
8572
|
+
var USAGE3 = "Usage: friend daemon start | stop | restart | status";
|
|
8573
|
+
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
8574
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8575
|
+
var realDeps = {
|
|
8576
|
+
launch(logFile) {
|
|
8577
|
+
const fd = openSync2(logFile, "a");
|
|
8578
|
+
try {
|
|
8579
|
+
const child = spawn6(process.execPath, [process.argv[1], "start"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true });
|
|
8580
|
+
child.unref();
|
|
8581
|
+
return child.pid;
|
|
8582
|
+
} finally {
|
|
8583
|
+
closeSync2(fd);
|
|
8584
|
+
}
|
|
8585
|
+
},
|
|
8586
|
+
alive(pid) {
|
|
8587
|
+
try {
|
|
8588
|
+
process.kill(pid, 0);
|
|
8589
|
+
return true;
|
|
8590
|
+
} catch (e) {
|
|
8591
|
+
return e.code === "EPERM";
|
|
8592
|
+
}
|
|
8593
|
+
},
|
|
8594
|
+
async kill(pid) {
|
|
8595
|
+
if (process.platform === "win32") {
|
|
8596
|
+
spawnSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
|
|
8597
|
+
} else {
|
|
8598
|
+
try {
|
|
8599
|
+
process.kill(pid, "SIGTERM");
|
|
8600
|
+
} catch {
|
|
8601
|
+
return;
|
|
8602
|
+
}
|
|
8603
|
+
for (let i = 0; i < 40 && realDeps.alive(pid); i++) await sleep(200);
|
|
8604
|
+
if (realDeps.alive(pid)) {
|
|
8605
|
+
try {
|
|
8606
|
+
process.kill(pid, "SIGKILL");
|
|
8607
|
+
} catch {
|
|
8608
|
+
}
|
|
8609
|
+
}
|
|
8610
|
+
}
|
|
8611
|
+
for (let i = 0; i < 25 && realDeps.alive(pid); i++) await sleep(200);
|
|
8612
|
+
},
|
|
8613
|
+
portOpen(port, withinMs) {
|
|
8614
|
+
return new Promise((resolve10) => {
|
|
8615
|
+
const s = connect({ port, host: "127.0.0.1" });
|
|
8616
|
+
const done = (ok) => {
|
|
8617
|
+
s.destroy();
|
|
8618
|
+
resolve10(ok);
|
|
8619
|
+
};
|
|
8620
|
+
s.setTimeout(withinMs, () => done(false));
|
|
8621
|
+
s.once("connect", () => done(true));
|
|
8622
|
+
s.once("error", () => done(false));
|
|
8623
|
+
});
|
|
8624
|
+
},
|
|
8625
|
+
now: Date.now
|
|
8626
|
+
};
|
|
8627
|
+
var pidFile = (dataDir) => join18(dataDir, "friend.pid");
|
|
8628
|
+
var logFileOf = (dataDir) => join18(dataDir, "friend.log");
|
|
8629
|
+
function readPid(dataDir) {
|
|
8630
|
+
try {
|
|
8631
|
+
const r = JSON.parse(readFileSync10(pidFile(dataDir), "utf8"));
|
|
8632
|
+
return Number.isInteger(r.pid) && r.pid > 0 ? r : null;
|
|
8633
|
+
} catch {
|
|
8634
|
+
return null;
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
function running(dataDir, deps) {
|
|
8638
|
+
const r = readPid(dataDir);
|
|
8639
|
+
if (r && deps.alive(r.pid)) return r;
|
|
8640
|
+
if (r) rmSync6(pidFile(dataDir), { force: true });
|
|
8641
|
+
return null;
|
|
8642
|
+
}
|
|
8643
|
+
function since(ms) {
|
|
8644
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
8645
|
+
if (s < 90) return `${s} seconds`;
|
|
8646
|
+
if (s < 5400) return `${Math.round(s / 60)} minutes`;
|
|
8647
|
+
if (s < 129600) return `${Math.round(s / 3600)} hours`;
|
|
8648
|
+
return `${Math.round(s / 86400)} days`;
|
|
8649
|
+
}
|
|
8650
|
+
async function start(io2, deps) {
|
|
8651
|
+
const existing = running(io2.dataDir, deps);
|
|
8652
|
+
const cfg = loadConfig(io2.dataDir);
|
|
8653
|
+
const urls = urlsFor(cfg.bind, cfg.port, void 0, cfg.https);
|
|
8654
|
+
if (existing) {
|
|
8655
|
+
io2.out(`Friend is already running in the background (pid ${existing.pid}) at ${urls.local}`);
|
|
8656
|
+
return 0;
|
|
8657
|
+
}
|
|
8658
|
+
if (await deps.portOpen(cfg.port, 500)) {
|
|
8659
|
+
io2.err(`Port ${cfg.port} is already in use. Friend may be running in a terminal (stop it with Ctrl+C), or another program uses that port (friend lan port N).`);
|
|
8660
|
+
return 1;
|
|
8661
|
+
}
|
|
8662
|
+
mkdirSync13(io2.dataDir, { recursive: true });
|
|
8663
|
+
const log = logFileOf(io2.dataDir);
|
|
8664
|
+
try {
|
|
8665
|
+
if (existsSync14(log) && statSync3(log).size > MAX_LOG_BYTES) renameSync4(log, `${log}.old`);
|
|
8666
|
+
} catch {
|
|
8667
|
+
}
|
|
8668
|
+
const pid = deps.launch(log);
|
|
8669
|
+
if (!pid) {
|
|
8670
|
+
io2.err("Could not start Friend in the background.");
|
|
8671
|
+
return 1;
|
|
8672
|
+
}
|
|
8673
|
+
writeFileSync12(pidFile(io2.dataDir), JSON.stringify({ pid, startedAt: deps.now() }));
|
|
8674
|
+
for (let i = 0; i < 60; i++) {
|
|
8675
|
+
if (await deps.portOpen(cfg.port, 300)) {
|
|
8676
|
+
io2.out(`Friend is running in the background (pid ${pid}) at ${urls.local}`);
|
|
8677
|
+
for (const n of urls.network) io2.out(`Also on your network: ${n}`);
|
|
8678
|
+
io2.out(`Stop it with: friend daemon stop (log: ${log})`);
|
|
8679
|
+
return 0;
|
|
8680
|
+
}
|
|
8681
|
+
if (!deps.alive(pid)) break;
|
|
8682
|
+
await sleep(250);
|
|
8683
|
+
}
|
|
8684
|
+
rmSync6(pidFile(io2.dataDir), { force: true });
|
|
8685
|
+
io2.err(`Friend did not start. See the log: ${log}`);
|
|
8686
|
+
return 1;
|
|
8687
|
+
}
|
|
8688
|
+
async function stop(io2, deps) {
|
|
8689
|
+
const r = running(io2.dataDir, deps);
|
|
8690
|
+
if (!r) {
|
|
8691
|
+
io2.out("Friend is not running in the background.");
|
|
8692
|
+
return 0;
|
|
8693
|
+
}
|
|
8694
|
+
await deps.kill(r.pid);
|
|
8695
|
+
if (deps.alive(r.pid)) {
|
|
8696
|
+
io2.err(`Could not stop Friend (pid ${r.pid}). End that process yourself.`);
|
|
8697
|
+
return 1;
|
|
8698
|
+
}
|
|
8699
|
+
rmSync6(pidFile(io2.dataDir), { force: true });
|
|
8700
|
+
io2.out("Friend stopped.");
|
|
8701
|
+
return 0;
|
|
8702
|
+
}
|
|
8703
|
+
async function status(io2, deps) {
|
|
8704
|
+
const cfg = loadConfig(io2.dataDir);
|
|
8705
|
+
const urls = urlsFor(cfg.bind, cfg.port, void 0, cfg.https);
|
|
8706
|
+
const r = running(io2.dataDir, deps);
|
|
8707
|
+
if (r) {
|
|
8708
|
+
io2.out(`Friend is running in the background (pid ${r.pid}, for ${since(deps.now() - r.startedAt)}).`);
|
|
8709
|
+
io2.out(`Address: ${urls.local}`);
|
|
8710
|
+
for (const n of urls.network) io2.out(`Also on your network: ${n}`);
|
|
8711
|
+
io2.out(`Log: ${logFileOf(io2.dataDir)}`);
|
|
8712
|
+
return 0;
|
|
8713
|
+
}
|
|
8714
|
+
if (await deps.portOpen(cfg.port, 500)) io2.out(`Friend is not running in the background, but port ${cfg.port} is in use. It may be running in a terminal.`);
|
|
8715
|
+
else io2.out("Friend is not running. Start it in the background with: friend daemon start");
|
|
8716
|
+
return 1;
|
|
8717
|
+
}
|
|
8718
|
+
async function daemonCommand(args, io2, deps = realDeps) {
|
|
8719
|
+
switch (args[0]) {
|
|
8720
|
+
case "start":
|
|
8721
|
+
return start(io2, deps);
|
|
8722
|
+
case "stop":
|
|
8723
|
+
return stop(io2, deps);
|
|
8724
|
+
case "restart": {
|
|
8725
|
+
const code = await stop(io2, deps);
|
|
8726
|
+
return code === 0 ? start(io2, deps) : code;
|
|
8727
|
+
}
|
|
8728
|
+
case "status":
|
|
8729
|
+
return status(io2, deps);
|
|
8730
|
+
default:
|
|
8731
|
+
io2.err(USAGE3);
|
|
8732
|
+
return 1;
|
|
8733
|
+
}
|
|
8734
|
+
}
|
|
8735
|
+
|
|
8553
8736
|
// ../agent-cli/src/commands/claude.ts
|
|
8554
8737
|
async function check2(run2, command) {
|
|
8555
8738
|
const v = await run2(command, ["--version"]);
|
|
@@ -8618,7 +8801,7 @@ async function claudeCommand(args, io2, deps = {}) {
|
|
|
8618
8801
|
}
|
|
8619
8802
|
|
|
8620
8803
|
// ../agent-cli/src/commands/setup.ts
|
|
8621
|
-
import { mkdirSync as
|
|
8804
|
+
import { mkdirSync as mkdirSync14 } from "node:fs";
|
|
8622
8805
|
import { resolve as resolve9 } from "node:path";
|
|
8623
8806
|
async function setupCommand3(io2) {
|
|
8624
8807
|
const db = openDb(io2.dataDir);
|
|
@@ -8644,7 +8827,7 @@ async function setupCommand3(io2) {
|
|
|
8644
8827
|
const cfg = loadConfig(io2.dataDir);
|
|
8645
8828
|
if (workspace2) {
|
|
8646
8829
|
const abs = resolve9(workspace2);
|
|
8647
|
-
|
|
8830
|
+
mkdirSync14(abs, { recursive: true });
|
|
8648
8831
|
cfg.roots = [abs];
|
|
8649
8832
|
}
|
|
8650
8833
|
if (apiKey) cfg.apiKey = apiKey;
|
|
@@ -8680,17 +8863,18 @@ async function statusCommand3(io2) {
|
|
|
8680
8863
|
}
|
|
8681
8864
|
|
|
8682
8865
|
// ../agent-cli/src/cli.ts
|
|
8683
|
-
var
|
|
8866
|
+
var USAGE4 = `Usage: friend <command>
|
|
8684
8867
|
|
|
8685
8868
|
onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
|
|
8686
8869
|
doctor check what this PC has and what is missing (changes nothing)
|
|
8687
8870
|
setup create the owner account (password, PIN, assistant name)
|
|
8688
8871
|
start run the gateway and a local device node
|
|
8872
|
+
daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
|
|
8689
8873
|
pair print a one-time code to connect another PC
|
|
8690
8874
|
node --url <ws-url> --code <code> [--roots a,b] [--name pc]
|
|
8691
8875
|
connect this PC to a Friend gateway
|
|
8692
8876
|
ollama setup|status use a local model through Ollama (no API key needed)
|
|
8693
|
-
lan status|on|off|port N
|
|
8877
|
+
lan status|on|off|port N|https on|off let other devices on your network open Friend, change the port, or turn on the secure (https) address a phone needs for the microphone (restart to apply)
|
|
8694
8878
|
claude setup|status use your Claude subscription through Claude Code (no API key needed)
|
|
8695
8879
|
voice setup|list|status|forget install local speech and choose a female or male voice; check it; forget deletes your saved voice
|
|
8696
8880
|
memory list|forget <n>|clear see and delete the notes Friend keeps about you
|
|
@@ -8710,6 +8894,8 @@ async function run(argv, io2) {
|
|
|
8710
8894
|
return await setupCommand3(io2);
|
|
8711
8895
|
case "start":
|
|
8712
8896
|
return await startCommand(io2);
|
|
8897
|
+
case "daemon":
|
|
8898
|
+
return await daemonCommand(rest, io2);
|
|
8713
8899
|
case "pair":
|
|
8714
8900
|
return await pairCommand(io2);
|
|
8715
8901
|
case "node":
|
|
@@ -8722,10 +8908,15 @@ async function run(argv, io2) {
|
|
|
8722
8908
|
return await onboardCommand(rest, io2);
|
|
8723
8909
|
case "doctor":
|
|
8724
8910
|
return await doctorCommand(io2);
|
|
8911
|
+
case "help":
|
|
8912
|
+
case "--help":
|
|
8913
|
+
case "-h":
|
|
8914
|
+
io2.out(USAGE4);
|
|
8915
|
+
return 0;
|
|
8725
8916
|
case "version":
|
|
8726
8917
|
case "--version":
|
|
8727
8918
|
case "-v":
|
|
8728
|
-
io2.out(true ? "0.1.
|
|
8919
|
+
io2.out(true ? "0.1.4" : "development");
|
|
8729
8920
|
return 0;
|
|
8730
8921
|
case "lan":
|
|
8731
8922
|
return await lanCommand(rest, io2);
|
|
@@ -8744,7 +8935,7 @@ async function run(argv, io2) {
|
|
|
8744
8935
|
case "config":
|
|
8745
8936
|
return await configCommand(rest, io2);
|
|
8746
8937
|
default:
|
|
8747
|
-
io2.err(
|
|
8938
|
+
io2.err(USAGE4);
|
|
8748
8939
|
return 1;
|
|
8749
8940
|
}
|
|
8750
8941
|
} catch (e) {
|
|
@@ -8805,4 +8996,5 @@ var io = {
|
|
|
8805
8996
|
};
|
|
8806
8997
|
run(process.argv.slice(2), io).then((code) => {
|
|
8807
8998
|
if (code !== 0) process.exitCode = code;
|
|
8999
|
+
if (process.argv[2] === "start") setTimeout(() => process.exit(), 1e3).unref();
|
|
8808
9000
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "friend-assistant",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A self-hosted personal AI assistant for your own PC: a full-screen stage, voice, tools with your approval, and your Claude subscription or a local model.",
|
|
5
5
|
"keywords": ["ai", "assistant", "jarvis", "voice", "claude", "ollama", "mcp", "self-hosted"],
|
|
6
6
|
"license": "UNLICENSED",
|