friend-assistant 0.1.44 → 0.1.45
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 +4 -1
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +616 -108
- package/dist/web/assets/{index-D6qxQkPK.js → index-BrLi-8KB.js} +23 -23
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/dist/friend.mjs
CHANGED
|
@@ -3347,6 +3347,7 @@ function sanitizeLook(raw) {
|
|
|
3347
3347
|
};
|
|
3348
3348
|
}
|
|
3349
3349
|
var DEFAULT_OLLAMA = { url: "http://127.0.0.1:11434", model: "qwen2.5:7b", contextTokens: 8192, vision: false };
|
|
3350
|
+
var sanitizeUpdates = (u) => ({ auto: typeof u?.auto === "boolean" ? u.auto : true });
|
|
3350
3351
|
var DEFAULT_VOICE = {
|
|
3351
3352
|
enabled: true,
|
|
3352
3353
|
sttModel: "small",
|
|
@@ -3412,6 +3413,7 @@ var defaults = (dataDir) => ({
|
|
|
3412
3413
|
openai: { ...DEFAULT_OPENAI },
|
|
3413
3414
|
claudeCode: { ...DEFAULT_CLAUDE_CODE },
|
|
3414
3415
|
voice: { ...DEFAULT_VOICE },
|
|
3416
|
+
updates: sanitizeUpdates(void 0),
|
|
3415
3417
|
view: "companion",
|
|
3416
3418
|
look: { ...DEFAULT_LOOK },
|
|
3417
3419
|
ownerName: "",
|
|
@@ -3458,6 +3460,7 @@ function loadConfig(dataDir = defaultDataDir()) {
|
|
|
3458
3460
|
openai: { ...DEFAULT_OPENAI, ...stored.openai },
|
|
3459
3461
|
claudeCode: sanitizeClaudeCode(stored.claudeCode),
|
|
3460
3462
|
voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
|
|
3463
|
+
updates: sanitizeUpdates(stored.updates),
|
|
3461
3464
|
view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
|
|
3462
3465
|
look: sanitizeLook(stored.look),
|
|
3463
3466
|
ownerName: typeof stored.ownerName === "string" ? stored.ownerName.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 40) : "",
|
|
@@ -4884,9 +4887,9 @@ function chromeToken(home = homedir3()) {
|
|
|
4884
4887
|
return null;
|
|
4885
4888
|
}
|
|
4886
4889
|
}
|
|
4887
|
-
function sameKey(given,
|
|
4888
|
-
if (!given || !
|
|
4889
|
-
return timingSafeEqual(createHash3("sha256").update(given).digest(), createHash3("sha256").update(
|
|
4890
|
+
function sameKey(given, real3) {
|
|
4891
|
+
if (!given || !real3) return false;
|
|
4892
|
+
return timingSafeEqual(createHash3("sha256").update(given).digest(), createHash3("sha256").update(real3).digest());
|
|
4890
4893
|
}
|
|
4891
4894
|
function keyFromRequest(headers, search) {
|
|
4892
4895
|
const bearer = /^Bearer (.+)$/i.exec(headers.authorization ?? "")?.[1];
|
|
@@ -7122,16 +7125,16 @@ function parseCsv(text3) {
|
|
|
7122
7125
|
const rows3 = [];
|
|
7123
7126
|
let row = [];
|
|
7124
7127
|
let cell = "";
|
|
7125
|
-
let
|
|
7128
|
+
let quoted2 = false;
|
|
7126
7129
|
for (let i = 0; i < text3.length; i++) {
|
|
7127
7130
|
const ch = text3[i];
|
|
7128
|
-
if (
|
|
7131
|
+
if (quoted2) {
|
|
7129
7132
|
if (ch === '"' && text3[i + 1] === '"') {
|
|
7130
7133
|
cell += '"';
|
|
7131
7134
|
i++;
|
|
7132
|
-
} else if (ch === '"')
|
|
7135
|
+
} else if (ch === '"') quoted2 = false;
|
|
7133
7136
|
else cell += ch;
|
|
7134
|
-
} else if (ch === '"')
|
|
7137
|
+
} else if (ch === '"') quoted2 = true;
|
|
7135
7138
|
else if (ch === ",") {
|
|
7136
7139
|
row.push(cell);
|
|
7137
7140
|
cell = "";
|
|
@@ -7184,59 +7187,59 @@ var MediaStore = class {
|
|
|
7184
7187
|
/** A file the assistant wants to send to the owner's chat: checked here (it must exist, be a real file, not be Friend's own data, not a program). */
|
|
7185
7188
|
async sendable(rawPath) {
|
|
7186
7189
|
if (!isAbsolute(rawPath)) return { ok: false, error: "Give the full path of the file, for example D:\\Pictures\\photo.png." };
|
|
7187
|
-
let
|
|
7190
|
+
let real3;
|
|
7188
7191
|
try {
|
|
7189
|
-
|
|
7192
|
+
real3 = await fs.realpath(resolve4(rawPath));
|
|
7190
7193
|
} catch {
|
|
7191
7194
|
return { ok: false, error: "That file does not exist." };
|
|
7192
7195
|
}
|
|
7193
|
-
if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir),
|
|
7194
|
-
const stat2 = await fs.stat(
|
|
7196
|
+
if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real3)) return { ok: false, error: "Friend's own data folder cannot be sent." };
|
|
7197
|
+
const stat2 = await fs.stat(real3).catch(() => null);
|
|
7195
7198
|
if (!stat2 || !stat2.isFile()) return { ok: false, error: "That path is not a file." };
|
|
7196
|
-
const name = basename(
|
|
7197
|
-
const ext = extname(
|
|
7199
|
+
const name = basename(real3);
|
|
7200
|
+
const ext = extname(real3).slice(1).toLowerCase();
|
|
7198
7201
|
if (NOT_SENT.has(ext)) return { ok: false, error: `Friend cannot send .${ext} files.` };
|
|
7199
7202
|
if (stat2.size === 0) return { ok: false, error: "That file is empty." };
|
|
7200
7203
|
if (stat2.size > MAX_SEND) return { ok: false, error: `That file is too big to send (${Math.round(stat2.size / 1048576)} MB).` };
|
|
7201
7204
|
const mime = IMAGE[ext] ?? VIDEO[ext] ?? DOC_MIME[ext] ?? "application/octet-stream";
|
|
7202
7205
|
const kind = mime.startsWith("image/") ? "image" : mime.startsWith("audio/") ? "audio" : mime.startsWith("video/") ? "video" : "document";
|
|
7203
|
-
return { ok: true, path:
|
|
7206
|
+
return { ok: true, path: real3, name, mime, size: stat2.size, kind };
|
|
7204
7207
|
}
|
|
7205
7208
|
async present(rawPath, title2) {
|
|
7206
7209
|
if (!isAbsolute(rawPath)) return { ok: false, error: "Give the full path of the file, for example D:\\Pictures\\photo.png." };
|
|
7207
|
-
let
|
|
7210
|
+
let real3;
|
|
7208
7211
|
try {
|
|
7209
|
-
|
|
7212
|
+
real3 = await fs.realpath(resolve4(rawPath));
|
|
7210
7213
|
} catch {
|
|
7211
7214
|
return { ok: false, error: "That file does not exist." };
|
|
7212
7215
|
}
|
|
7213
|
-
if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir),
|
|
7214
|
-
const stat2 = await fs.stat(
|
|
7216
|
+
if (insideOf(await fs.realpath(this.dataDir).catch(() => this.dataDir), real3)) return { ok: false, error: "Friend's own data folder cannot be shown." };
|
|
7217
|
+
const stat2 = await fs.stat(real3).catch(() => null);
|
|
7215
7218
|
if (!stat2 || !stat2.isFile()) return { ok: false, error: "That path is not a file." };
|
|
7216
|
-
const name = basename(
|
|
7217
|
-
const ext = extname(
|
|
7219
|
+
const name = basename(real3);
|
|
7220
|
+
const ext = extname(real3).slice(1).toLowerCase();
|
|
7218
7221
|
const label3 = title2?.trim() || name;
|
|
7219
7222
|
const tooBig = (limit, what) => stat2.size > limit ? { ok: false, error: `That ${what} is too large to show (${Math.round(stat2.size / 1048576)} MB).` } : null;
|
|
7220
7223
|
if (IMAGE[ext]) {
|
|
7221
7224
|
const big = tooBig(MAX_IMAGE, "image");
|
|
7222
7225
|
if (big) return big;
|
|
7223
|
-
const id2 = this.grant(
|
|
7226
|
+
const id2 = this.grant(real3, IMAGE[ext], stat2.size);
|
|
7224
7227
|
return { ok: true, type: "image", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the image ${name}.` };
|
|
7225
7228
|
}
|
|
7226
7229
|
if (VIDEO[ext]) {
|
|
7227
7230
|
const big = tooBig(MAX_VIDEO, "file");
|
|
7228
7231
|
if (big) return big;
|
|
7229
|
-
const id2 = this.grant(
|
|
7232
|
+
const id2 = this.grant(real3, VIDEO[ext], stat2.size);
|
|
7230
7233
|
return { ok: true, type: "video", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing ${name}.` };
|
|
7231
7234
|
}
|
|
7232
7235
|
if (ext === "pdf") {
|
|
7233
7236
|
const big = tooBig(MAX_PDF, "PDF");
|
|
7234
7237
|
if (big) return big;
|
|
7235
|
-
const id2 = this.grant(
|
|
7238
|
+
const id2 = this.grant(real3, "application/pdf", stat2.size);
|
|
7236
7239
|
return { ok: true, type: "pdf", props: { title: label3, src: `/api/media/${id2}` }, summary: `Showing the PDF ${name}.` };
|
|
7237
7240
|
}
|
|
7238
7241
|
if (MARKDOWN.has(ext) || ext === "csv" || CODE_LANG[ext]) {
|
|
7239
|
-
const handle = await fs.open(
|
|
7242
|
+
const handle = await fs.open(real3, "r");
|
|
7240
7243
|
let text3;
|
|
7241
7244
|
try {
|
|
7242
7245
|
const buf = Buffer.alloc(Math.min(stat2.size, MAX_TEXT));
|
|
@@ -9400,8 +9403,8 @@ var MusicLibrary = class {
|
|
|
9400
9403
|
try {
|
|
9401
9404
|
const info = await lstat(path3);
|
|
9402
9405
|
if (!info.isFile() || info.isSymbolicLink()) return null;
|
|
9403
|
-
const
|
|
9404
|
-
return
|
|
9406
|
+
const real3 = await realpath(path3);
|
|
9407
|
+
return real3.startsWith(this.root + sep2) ? path3 : null;
|
|
9405
9408
|
} catch {
|
|
9406
9409
|
return null;
|
|
9407
9410
|
}
|
|
@@ -10608,7 +10611,7 @@ var Memory = class {
|
|
|
10608
10611
|
};
|
|
10609
10612
|
|
|
10610
10613
|
// ../server/src/app.ts
|
|
10611
|
-
import { dirname as
|
|
10614
|
+
import { dirname as dirname6, extname as extname3, resolve as resolve5 } from "node:path";
|
|
10612
10615
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10613
10616
|
import { join as join18 } from "node:path";
|
|
10614
10617
|
|
|
@@ -11582,6 +11585,45 @@ function createGateway(deps) {
|
|
|
11582
11585
|
if (!deps.video) return reply.code(404).send({ error: "Not found" });
|
|
11583
11586
|
return deps.video.prepare();
|
|
11584
11587
|
});
|
|
11588
|
+
const systemWindow = new SlidingWindow(30, 6e4);
|
|
11589
|
+
const systemGuard = async (req, reply) => {
|
|
11590
|
+
const token = req.cookies[COOKIE_NAME];
|
|
11591
|
+
const session = token ? auth.resolveSession(token) : null;
|
|
11592
|
+
if (!session) return reply.code(401).send({ error: "Unauthorized" });
|
|
11593
|
+
if (!systemWindow.allow(String(session.id))) return reply.code(429).header("retry-after", 10).send({ error: "Too many requests" });
|
|
11594
|
+
if (!deps.system) return reply.code(503).send({ error: "Not available" });
|
|
11595
|
+
};
|
|
11596
|
+
const systemError = (reply, e, what) => {
|
|
11597
|
+
if (e instanceof HttpError) return reply.code(e.status).send({ error: e.message });
|
|
11598
|
+
audit.log("owner", "system.failed", { what, message: e.message });
|
|
11599
|
+
return reply.code(500).send({ error: "That did not work. Try again." });
|
|
11600
|
+
};
|
|
11601
|
+
app.get("/api/system", { onRequest: systemGuard }, async () => deps.system.info());
|
|
11602
|
+
app.post("/api/system/update/check", { onRequest: systemGuard }, async (_req, reply) => {
|
|
11603
|
+
try {
|
|
11604
|
+
return await deps.system.check();
|
|
11605
|
+
} catch (e) {
|
|
11606
|
+
return systemError(reply, e, "update.check");
|
|
11607
|
+
}
|
|
11608
|
+
});
|
|
11609
|
+
app.post("/api/system/update/apply", { onRequest: systemGuard }, async (req, reply) => {
|
|
11610
|
+
const body = z10.object({ pin: z10.string().min(1).max(32) }).safeParse(req.body);
|
|
11611
|
+
if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
|
|
11612
|
+
try {
|
|
11613
|
+
return await deps.system.update(body.data.pin, req.ip);
|
|
11614
|
+
} catch (e) {
|
|
11615
|
+
return systemError(reply, e, "update.apply");
|
|
11616
|
+
}
|
|
11617
|
+
});
|
|
11618
|
+
app.post("/api/system/autostart", { onRequest: systemGuard }, async (req, reply) => {
|
|
11619
|
+
const body = z10.object({ enabled: z10.boolean(), pin: z10.string().min(1).max(32) }).safeParse(req.body);
|
|
11620
|
+
if (!body.success) return reply.code(400).send({ error: "Enter your PIN" });
|
|
11621
|
+
try {
|
|
11622
|
+
return await deps.system.setAutostart(body.data.enabled, body.data.pin, req.ip);
|
|
11623
|
+
} catch (e) {
|
|
11624
|
+
return systemError(reply, e, "autostart");
|
|
11625
|
+
}
|
|
11626
|
+
});
|
|
11585
11627
|
const chromeFailures = new SlidingWindow(20, 6e4);
|
|
11586
11628
|
const chromeWss = new WebSocketServer({ noServer: true, maxPayload: MAX_DEVICE_FRAME_BYTES });
|
|
11587
11629
|
if (deps.chrome) {
|
|
@@ -12999,7 +13041,8 @@ var SettingsPatch = z11.object({
|
|
|
12999
13041
|
sttModel: z11.string().trim().min(1).max(80).regex(/^[\w./-]+$/),
|
|
13000
13042
|
duck: z11.boolean(),
|
|
13001
13043
|
duckPercent: z11.number().int().min(0).max(100)
|
|
13002
|
-
}).partial().strict()
|
|
13044
|
+
}).partial().strict(),
|
|
13045
|
+
updates: z11.object({ auto: z11.boolean() }).partial().strict()
|
|
13003
13046
|
}).partial().strict();
|
|
13004
13047
|
function settingsView(c) {
|
|
13005
13048
|
return {
|
|
@@ -13062,7 +13105,8 @@ function settingsView(c) {
|
|
|
13062
13105
|
marathiVoice: c.voice.marathiVoice,
|
|
13063
13106
|
duck: c.voice.duck,
|
|
13064
13107
|
duckPercent: c.voice.duckPercent
|
|
13065
|
-
}
|
|
13108
|
+
},
|
|
13109
|
+
updates: { auto: c.updates.auto }
|
|
13066
13110
|
};
|
|
13067
13111
|
}
|
|
13068
13112
|
var SettingsError = class extends Error {
|
|
@@ -13074,6 +13118,7 @@ function refuseForAgent(raw) {
|
|
|
13074
13118
|
if ("brokers" in r || "trading" in r) throw new SettingsError("Broker accounts and the trading level can only be changed by the owner on the settings screen.");
|
|
13075
13119
|
if ("music" in r || "hud" in r) throw new SettingsError("The music folder and the screen widgets can only be changed by the owner on the settings screen.");
|
|
13076
13120
|
if ("video" in r) throw new SettingsError("Video settings (which programs run, where videos play, which sites) can only be changed by the owner on the settings screen.");
|
|
13121
|
+
if ("updates" in r) throw new SettingsError("Whether Friend updates itself can only be changed by the owner on the settings screen.");
|
|
13077
13122
|
if ("alexa" in r) throw new SettingsError("Alexa settings can only be changed by the owner on the settings screen.");
|
|
13078
13123
|
if ("channels" in r) throw new SettingsError("Messaging channels can only be set up by the owner on the settings screen.");
|
|
13079
13124
|
if ("approvals" in r) throw new SettingsError("How much Friend asks before acting can only be changed by the owner on the settings screen.");
|
|
@@ -13218,6 +13263,7 @@ function applySettings(config, raw) {
|
|
|
13218
13263
|
if (apiKey) config.openai.apiKey = apiKey;
|
|
13219
13264
|
}
|
|
13220
13265
|
if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
|
|
13266
|
+
if (p.updates) Object.assign(config.updates, p.updates);
|
|
13221
13267
|
if (p.voice) {
|
|
13222
13268
|
const safe = next.voice;
|
|
13223
13269
|
for (const k of Object.keys(p.voice)) config.voice[k] = safe[k];
|
|
@@ -13689,6 +13735,368 @@ var DuckController = class {
|
|
|
13689
13735
|
}
|
|
13690
13736
|
};
|
|
13691
13737
|
|
|
13738
|
+
// ../server/src/system/autostart.ts
|
|
13739
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
13740
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync10, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "node:fs";
|
|
13741
|
+
import { homedir as homedir4 } from "node:os";
|
|
13742
|
+
import { dirname as dirname4, posix as posix2, win32 as win322 } from "node:path";
|
|
13743
|
+
var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
13744
|
+
var NAME2 = "Friend";
|
|
13745
|
+
var LABEL = "com.friend.assistant";
|
|
13746
|
+
var UNIT = "friend.service";
|
|
13747
|
+
var quoted = (s) => `"${s}"`;
|
|
13748
|
+
function vbsLauncher(self) {
|
|
13749
|
+
const cmd = `${quoted(self.node)} ${quoted(self.script)} daemon start`;
|
|
13750
|
+
const inVbs = cmd.replace(/"/g, '""');
|
|
13751
|
+
return [`' Starts Friend in the background when you sign in. Made by: friend autostart on`, `CreateObject("WScript.Shell").Run "${inVbs}", 0, False`, ""].join("\r\n");
|
|
13752
|
+
}
|
|
13753
|
+
var xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13754
|
+
function plistFor(self, o) {
|
|
13755
|
+
const log = `${o.dataDir}/friend-autostart.log`;
|
|
13756
|
+
return [
|
|
13757
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
13758
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
13759
|
+
`<plist version="1.0">`,
|
|
13760
|
+
`<dict>`,
|
|
13761
|
+
` <key>Label</key><string>${LABEL}</string>`,
|
|
13762
|
+
` <key>ProgramArguments</key>`,
|
|
13763
|
+
` <array>`,
|
|
13764
|
+
` <string>${xml(self.node)}</string>`,
|
|
13765
|
+
` <string>${xml(self.script)}</string>`,
|
|
13766
|
+
` <string>daemon</string>`,
|
|
13767
|
+
` <string>start</string>`,
|
|
13768
|
+
` </array>`,
|
|
13769
|
+
` <key>RunAtLoad</key>`,
|
|
13770
|
+
` <true/>`,
|
|
13771
|
+
` <key>EnvironmentVariables</key>`,
|
|
13772
|
+
` <dict><key>PATH</key><string>${xml(o.path)}</string></dict>`,
|
|
13773
|
+
` <key>StandardOutPath</key><string>${xml(log)}</string>`,
|
|
13774
|
+
` <key>StandardErrorPath</key><string>${xml(log)}</string>`,
|
|
13775
|
+
`</dict>`,
|
|
13776
|
+
`</plist>`,
|
|
13777
|
+
""
|
|
13778
|
+
].join("\n");
|
|
13779
|
+
}
|
|
13780
|
+
var unitWord = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
|
|
13781
|
+
function systemdUnit(self, o) {
|
|
13782
|
+
return [
|
|
13783
|
+
"[Unit]",
|
|
13784
|
+
"Description=Friend assistant",
|
|
13785
|
+
"After=network-online.target",
|
|
13786
|
+
"",
|
|
13787
|
+
"[Service]",
|
|
13788
|
+
"Type=oneshot",
|
|
13789
|
+
"RemainAfterExit=yes",
|
|
13790
|
+
`Environment=${unitWord(`PATH=${o.path}`)}`,
|
|
13791
|
+
`ExecStart=${unitWord(self.node)} ${unitWord(self.script)} daemon start`,
|
|
13792
|
+
`ExecStop=${unitWord(self.node)} ${unitWord(self.script)} daemon stop`,
|
|
13793
|
+
"",
|
|
13794
|
+
"[Install]",
|
|
13795
|
+
"WantedBy=default.target",
|
|
13796
|
+
""
|
|
13797
|
+
].join("\n");
|
|
13798
|
+
}
|
|
13799
|
+
var desktopWord = (s) => `"${s.replace(/(["`$\\])/g, "\\$1")}"`;
|
|
13800
|
+
function desktopEntry(self) {
|
|
13801
|
+
return ["[Desktop Entry]", "Type=Application", "Name=Friend", "Comment=Starts Friend in the background", `Exec=${desktopWord(self.node)} ${desktopWord(self.script)} daemon start`, "Terminal=false", "X-GNOME-Autostart-enabled=true", ""].join("\n");
|
|
13802
|
+
}
|
|
13803
|
+
var Autostart = class {
|
|
13804
|
+
constructor(d, self) {
|
|
13805
|
+
this.d = d;
|
|
13806
|
+
this.self = self;
|
|
13807
|
+
}
|
|
13808
|
+
d;
|
|
13809
|
+
self;
|
|
13810
|
+
get win() {
|
|
13811
|
+
return this.d.platform === "win32";
|
|
13812
|
+
}
|
|
13813
|
+
get mac() {
|
|
13814
|
+
return this.d.platform === "darwin";
|
|
13815
|
+
}
|
|
13816
|
+
get linux() {
|
|
13817
|
+
return this.d.platform === "linux";
|
|
13818
|
+
}
|
|
13819
|
+
p() {
|
|
13820
|
+
return this.win ? win322 : posix2;
|
|
13821
|
+
}
|
|
13822
|
+
vbs() {
|
|
13823
|
+
return win322.join(this.d.dataDir, "autostart.vbs");
|
|
13824
|
+
}
|
|
13825
|
+
plist() {
|
|
13826
|
+
return posix2.join(this.d.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
13827
|
+
}
|
|
13828
|
+
configDir() {
|
|
13829
|
+
return this.d.configHome || posix2.join(this.d.home, ".config");
|
|
13830
|
+
}
|
|
13831
|
+
unitFile() {
|
|
13832
|
+
return posix2.join(this.configDir(), "systemd", "user", UNIT);
|
|
13833
|
+
}
|
|
13834
|
+
desktopFile() {
|
|
13835
|
+
return posix2.join(this.configDir(), "autostart", "friend.desktop");
|
|
13836
|
+
}
|
|
13837
|
+
async hasSystemd() {
|
|
13838
|
+
return (await this.d.run("systemctl", ["--user", "--version"])).code === 0;
|
|
13839
|
+
}
|
|
13840
|
+
async status() {
|
|
13841
|
+
if (this.win) return { supported: true, enabled: (await this.d.run("reg", ["query", RUN_KEY, "/v", NAME2])).code === 0, kind: "registry" };
|
|
13842
|
+
if (this.mac) return { supported: true, enabled: this.d.exists(this.plist()), kind: "launchd" };
|
|
13843
|
+
if (this.linux) {
|
|
13844
|
+
if (await this.hasSystemd()) {
|
|
13845
|
+
const r = await this.d.run("systemctl", ["--user", "is-enabled", UNIT]);
|
|
13846
|
+
if (r.code === 0 && /enabled/.test(r.stdout)) return { supported: true, enabled: true, kind: "systemd" };
|
|
13847
|
+
}
|
|
13848
|
+
return { supported: true, enabled: this.d.exists(this.desktopFile()), kind: this.d.exists(this.desktopFile()) ? "xdg" : "none" };
|
|
13849
|
+
}
|
|
13850
|
+
return { supported: false, enabled: false, kind: "none" };
|
|
13851
|
+
}
|
|
13852
|
+
async enable() {
|
|
13853
|
+
const self = this.self();
|
|
13854
|
+
if (!self.node || !self.script) return { ok: false, message: "Friend does not know which program to start. Run it with the friend command and try again." };
|
|
13855
|
+
if (this.win) return this.enableWindows(self);
|
|
13856
|
+
if (this.mac) return this.enableMac(self);
|
|
13857
|
+
if (this.linux) return this.enableLinux(self);
|
|
13858
|
+
return { ok: false, message: "Starting Friend by itself is not supported on this kind of computer." };
|
|
13859
|
+
}
|
|
13860
|
+
async disable() {
|
|
13861
|
+
if (this.win) {
|
|
13862
|
+
await this.d.run("reg", ["delete", RUN_KEY, "/v", NAME2, "/f"]);
|
|
13863
|
+
this.d.remove(this.vbs());
|
|
13864
|
+
} else if (this.mac) {
|
|
13865
|
+
await this.d.run("launchctl", ["bootout", `gui/${this.d.uid}`, this.plist()]);
|
|
13866
|
+
this.d.remove(this.plist());
|
|
13867
|
+
} else if (this.linux) {
|
|
13868
|
+
if (await this.hasSystemd()) {
|
|
13869
|
+
await this.d.run("systemctl", ["--user", "disable", UNIT]);
|
|
13870
|
+
this.d.remove(this.unitFile());
|
|
13871
|
+
await this.d.run("systemctl", ["--user", "daemon-reload"]);
|
|
13872
|
+
}
|
|
13873
|
+
this.d.remove(this.desktopFile());
|
|
13874
|
+
} else return { ok: false, message: "Starting Friend by itself is not supported on this kind of computer." };
|
|
13875
|
+
return { ok: true, message: "Friend will no longer start by itself when you sign in." };
|
|
13876
|
+
}
|
|
13877
|
+
/** Writes the setup again with the program's current place, when it is on. Used after an upgrade. */
|
|
13878
|
+
async refresh() {
|
|
13879
|
+
if ((await this.status()).enabled) await this.enable();
|
|
13880
|
+
}
|
|
13881
|
+
async enableWindows(self) {
|
|
13882
|
+
this.d.write(this.vbs(), vbsLauncher(self));
|
|
13883
|
+
const r = await this.d.run("reg", ["add", RUN_KEY, "/v", NAME2, "/t", "REG_SZ", "/d", `wscript.exe ${quoted(this.vbs())}`, "/f"]);
|
|
13884
|
+
if (r.code !== 0) return { ok: false, message: "Could not add Friend to the programs that start when you sign in." };
|
|
13885
|
+
return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
|
|
13886
|
+
}
|
|
13887
|
+
async enableMac(self) {
|
|
13888
|
+
this.d.write(this.plist(), plistFor(self, { dataDir: this.d.dataDir, path: this.d.path }));
|
|
13889
|
+
await this.d.run("launchctl", ["bootout", `gui/${this.d.uid}`, this.plist()]);
|
|
13890
|
+
let r = await this.d.run("launchctl", ["bootstrap", `gui/${this.d.uid}`, this.plist()]);
|
|
13891
|
+
if (r.code !== 0) r = await this.d.run("launchctl", ["load", "-w", this.plist()]);
|
|
13892
|
+
if (r.code !== 0) return { ok: false, message: "Could not switch the launch agent on. It will still be used the next time you sign in." };
|
|
13893
|
+
return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
|
|
13894
|
+
}
|
|
13895
|
+
async enableLinux(self) {
|
|
13896
|
+
if (await this.hasSystemd()) {
|
|
13897
|
+
this.d.write(this.unitFile(), systemdUnit(self, { path: this.d.path }));
|
|
13898
|
+
await this.d.run("systemctl", ["--user", "daemon-reload"]);
|
|
13899
|
+
const r = await this.d.run("systemctl", ["--user", "enable", UNIT]);
|
|
13900
|
+
if (r.code === 0) {
|
|
13901
|
+
this.d.remove(this.desktopFile());
|
|
13902
|
+
return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
|
|
13903
|
+
}
|
|
13904
|
+
}
|
|
13905
|
+
this.d.write(this.desktopFile(), desktopEntry(self));
|
|
13906
|
+
return { ok: true, message: "Friend will start by itself when you sign in to this computer." };
|
|
13907
|
+
}
|
|
13908
|
+
};
|
|
13909
|
+
function realAutostartDeps(dataDir) {
|
|
13910
|
+
return {
|
|
13911
|
+
platform: process.platform,
|
|
13912
|
+
home: homedir4(),
|
|
13913
|
+
dataDir,
|
|
13914
|
+
uid: typeof process.getuid === "function" ? process.getuid() : 0,
|
|
13915
|
+
path: process.env.PATH ?? "",
|
|
13916
|
+
...process.env.XDG_CONFIG_HOME ? { configHome: process.env.XDG_CONFIG_HOME } : {},
|
|
13917
|
+
exists: (p) => existsSync16(p),
|
|
13918
|
+
read: (p) => {
|
|
13919
|
+
try {
|
|
13920
|
+
return readFileSync10(p, "utf8");
|
|
13921
|
+
} catch {
|
|
13922
|
+
return null;
|
|
13923
|
+
}
|
|
13924
|
+
},
|
|
13925
|
+
write: (p, text3) => {
|
|
13926
|
+
mkdirSync11(dirname4(p), { recursive: true });
|
|
13927
|
+
writeFileSync10(p, text3);
|
|
13928
|
+
},
|
|
13929
|
+
remove: (p) => rmSync9(p, { force: true }),
|
|
13930
|
+
run: (cmd, args) => new Promise((resolve13) => {
|
|
13931
|
+
let out = "";
|
|
13932
|
+
const child = spawn6(cmd, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
13933
|
+
child.stdout?.on("data", (d) => out += d.toString());
|
|
13934
|
+
child.stderr?.on("data", (d) => out += d.toString());
|
|
13935
|
+
child.on("error", () => resolve13({ code: 127, stdout: out }));
|
|
13936
|
+
child.on("close", (code) => resolve13({ code: code ?? 1, stdout: out }));
|
|
13937
|
+
})
|
|
13938
|
+
};
|
|
13939
|
+
}
|
|
13940
|
+
function currentSelf() {
|
|
13941
|
+
const script = process.argv[1] ?? "";
|
|
13942
|
+
return { node: process.execPath, script: /\.(mjs|js|cjs)$/i.test(script) ? script : "" };
|
|
13943
|
+
}
|
|
13944
|
+
|
|
13945
|
+
// ../server/src/system/npm.ts
|
|
13946
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
13947
|
+
var PACKAGE = "friend-assistant";
|
|
13948
|
+
var runningVersion = () => true ? "0.1.45" : "development";
|
|
13949
|
+
async function npmLatest(fetchFn = fetch, timeoutMs = 1e4) {
|
|
13950
|
+
try {
|
|
13951
|
+
const res = await fetchFn(`https://registry.npmjs.org/${PACKAGE}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(timeoutMs) });
|
|
13952
|
+
if (!res.ok) return null;
|
|
13953
|
+
const v = (await res.json()).version;
|
|
13954
|
+
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v) ? v : null;
|
|
13955
|
+
} catch {
|
|
13956
|
+
return null;
|
|
13957
|
+
}
|
|
13958
|
+
}
|
|
13959
|
+
var ps = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
13960
|
+
function spawnUpgrade(self, platform = process.platform) {
|
|
13961
|
+
if (!self.node || !self.script) return false;
|
|
13962
|
+
try {
|
|
13963
|
+
const child = platform === "win32" ? spawn7("powershell.exe", ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", `Start-Process -WindowStyle Hidden -FilePath ${ps(self.node)} -ArgumentList ${ps(`"${self.script}"`)},'upgrade','--auto'`], { detached: true, stdio: "ignore", windowsHide: true }) : spawn7(self.node, [self.script, "upgrade", "--auto"], { detached: true, stdio: "ignore" });
|
|
13964
|
+
child.on("error", () => {
|
|
13965
|
+
});
|
|
13966
|
+
child.unref();
|
|
13967
|
+
return child.pid !== void 0;
|
|
13968
|
+
} catch {
|
|
13969
|
+
return false;
|
|
13970
|
+
}
|
|
13971
|
+
}
|
|
13972
|
+
|
|
13973
|
+
// ../server/src/system/updater.ts
|
|
13974
|
+
function isNewerVersion(a, b) {
|
|
13975
|
+
if (!/^\d/.test(a) || !/^\d/.test(b)) return false;
|
|
13976
|
+
const n = (v) => v.split(/[.-]/).slice(0, 3).map((x2) => Number(x2) || 0);
|
|
13977
|
+
const [x, y] = [n(a), n(b)];
|
|
13978
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
13979
|
+
return false;
|
|
13980
|
+
}
|
|
13981
|
+
var Updater = class {
|
|
13982
|
+
constructor(d, t = {}) {
|
|
13983
|
+
this.d = d;
|
|
13984
|
+
this.firstMs = t.firstMs ?? 2e4;
|
|
13985
|
+
this.everyMs = t.everyMs ?? 6 * 36e5;
|
|
13986
|
+
this.retryMs = t.retryMs ?? 5 * 6e4;
|
|
13987
|
+
}
|
|
13988
|
+
d;
|
|
13989
|
+
latest = null;
|
|
13990
|
+
checkedAt = null;
|
|
13991
|
+
applying = false;
|
|
13992
|
+
error = null;
|
|
13993
|
+
notified = null;
|
|
13994
|
+
inflight = null;
|
|
13995
|
+
first = null;
|
|
13996
|
+
every = null;
|
|
13997
|
+
retry = null;
|
|
13998
|
+
firstMs;
|
|
13999
|
+
everyMs;
|
|
14000
|
+
retryMs;
|
|
14001
|
+
state() {
|
|
14002
|
+
const current = this.d.current();
|
|
14003
|
+
return { current, latest: this.latest, checkedAt: this.checkedAt, available: this.latest !== null && isNewerVersion(this.latest, current), applying: this.applying, canApply: this.d.canApply(), error: this.error };
|
|
14004
|
+
}
|
|
14005
|
+
start() {
|
|
14006
|
+
this.stop();
|
|
14007
|
+
this.first = setTimeout(() => {
|
|
14008
|
+
this.first = null;
|
|
14009
|
+
void this.check();
|
|
14010
|
+
this.every = setInterval(() => void this.check(), this.everyMs);
|
|
14011
|
+
this.every.unref?.();
|
|
14012
|
+
}, this.firstMs);
|
|
14013
|
+
this.first.unref?.();
|
|
14014
|
+
}
|
|
14015
|
+
stop() {
|
|
14016
|
+
if (this.first) clearTimeout(this.first);
|
|
14017
|
+
if (this.every) clearInterval(this.every);
|
|
14018
|
+
if (this.retry) clearTimeout(this.retry);
|
|
14019
|
+
this.first = this.every = this.retry = null;
|
|
14020
|
+
}
|
|
14021
|
+
/** Asks npm for the newest version (one question at a time), tells the owner, and updates when that is switched on. */
|
|
14022
|
+
check() {
|
|
14023
|
+
this.inflight ??= (async () => {
|
|
14024
|
+
try {
|
|
14025
|
+
const latest = await this.d.latest();
|
|
14026
|
+
this.checkedAt = Date.now();
|
|
14027
|
+
if (latest) {
|
|
14028
|
+
this.latest = latest;
|
|
14029
|
+
this.error = null;
|
|
14030
|
+
await this.consider();
|
|
14031
|
+
} else {
|
|
14032
|
+
this.error = "Could not ask npm for the newest version. Check the internet connection.";
|
|
14033
|
+
}
|
|
14034
|
+
} catch {
|
|
14035
|
+
this.error = "Could not ask npm for the newest version. Check the internet connection.";
|
|
14036
|
+
} finally {
|
|
14037
|
+
this.inflight = null;
|
|
14038
|
+
}
|
|
14039
|
+
return this.state();
|
|
14040
|
+
})();
|
|
14041
|
+
return this.inflight;
|
|
14042
|
+
}
|
|
14043
|
+
/** The owner pressed Update now: installs even when automatic updates are off, but never in the middle of an answer. */
|
|
14044
|
+
async applyNow() {
|
|
14045
|
+
if (this.applying) return { ok: true, message: "The update is already running. Reload this page in a minute." };
|
|
14046
|
+
if (!this.latest) await this.check();
|
|
14047
|
+
const s = this.state();
|
|
14048
|
+
if (!s.latest) return { ok: false, message: s.error ?? "Could not ask npm for the newest version." };
|
|
14049
|
+
if (this.applying) return { ok: true, message: "The update is already running. Reload this page in a minute." };
|
|
14050
|
+
if (!s.available) return { ok: false, message: `Friend is up to date (${s.current}).` };
|
|
14051
|
+
if (!s.canApply) return { ok: false, message: "This Friend is running in a terminal and cannot restart itself. Close it and run: friend upgrade" };
|
|
14052
|
+
if (this.d.busy()) return { ok: false, message: "Friend is busy with a request. Try again in a moment." };
|
|
14053
|
+
return await this.begin(s.latest) ? { ok: true, message: `Updating to ${s.latest}. Friend restarts in a moment: reload this page afterwards.` } : { ok: false, message: this.error ?? "Could not start the update." };
|
|
14054
|
+
}
|
|
14055
|
+
async consider() {
|
|
14056
|
+
const s = this.state();
|
|
14057
|
+
if (!s.available || !s.latest) return;
|
|
14058
|
+
const canApply = this.d.canApply();
|
|
14059
|
+
if (this.notified !== s.latest) {
|
|
14060
|
+
this.notified = s.latest;
|
|
14061
|
+
this.d.notify(
|
|
14062
|
+
canApply ? this.d.auto() ? `Friend ${s.latest} is available. It will update by itself when Friend is not busy.` : `Friend ${s.latest} is available. Turn on automatic updates in Settings, or press Update now there.` : `Friend ${s.latest} is available. Close this Friend and run: friend upgrade`
|
|
14063
|
+
);
|
|
14064
|
+
}
|
|
14065
|
+
if (this.d.auto() && canApply) await this.maybeApply();
|
|
14066
|
+
}
|
|
14067
|
+
async maybeApply() {
|
|
14068
|
+
const s = this.state();
|
|
14069
|
+
if (this.applying || !s.available || !s.latest) return;
|
|
14070
|
+
if (this.d.busy()) {
|
|
14071
|
+
if (!this.retry) {
|
|
14072
|
+
this.retry = setTimeout(() => {
|
|
14073
|
+
this.retry = null;
|
|
14074
|
+
void this.maybeApply();
|
|
14075
|
+
}, this.retryMs);
|
|
14076
|
+
this.retry.unref?.();
|
|
14077
|
+
}
|
|
14078
|
+
return;
|
|
14079
|
+
}
|
|
14080
|
+
await this.begin(s.latest);
|
|
14081
|
+
}
|
|
14082
|
+
async begin(version) {
|
|
14083
|
+
this.applying = true;
|
|
14084
|
+
this.error = null;
|
|
14085
|
+
this.d.notify(`Updating Friend to ${version}. It restarts in a moment: reload this page afterwards.`);
|
|
14086
|
+
let ok = false;
|
|
14087
|
+
try {
|
|
14088
|
+
ok = await this.d.apply(version);
|
|
14089
|
+
} catch {
|
|
14090
|
+
ok = false;
|
|
14091
|
+
}
|
|
14092
|
+
if (!ok) {
|
|
14093
|
+
this.applying = false;
|
|
14094
|
+
this.error = "Could not start the update.";
|
|
14095
|
+
}
|
|
14096
|
+
return ok;
|
|
14097
|
+
}
|
|
14098
|
+
};
|
|
14099
|
+
|
|
13692
14100
|
// ../server/src/voice/challenge.ts
|
|
13693
14101
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
13694
14102
|
var Challenges = class {
|
|
@@ -13750,20 +14158,20 @@ var Challenges = class {
|
|
|
13750
14158
|
|
|
13751
14159
|
// ../server/src/voice/crypto.ts
|
|
13752
14160
|
import { createCipheriv, createDecipheriv, randomBytes as randomBytes9 } from "node:crypto";
|
|
13753
|
-
import { chmodSync as chmodSync9, existsSync as
|
|
13754
|
-
import { dirname as
|
|
14161
|
+
import { chmodSync as chmodSync9, existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
14162
|
+
import { dirname as dirname5 } from "node:path";
|
|
13755
14163
|
var KEY_BYTES = 32;
|
|
13756
14164
|
var IV_BYTES = 12;
|
|
13757
14165
|
var TAG_BYTES = 16;
|
|
13758
14166
|
function loadOrCreateKey(path3) {
|
|
13759
|
-
if (
|
|
13760
|
-
const key2 =
|
|
14167
|
+
if (existsSync17(path3)) {
|
|
14168
|
+
const key2 = readFileSync11(path3);
|
|
13761
14169
|
if (key2.length !== KEY_BYTES) throw new Error(`invalid voice key file (expected ${KEY_BYTES} bytes): ${path3}`);
|
|
13762
14170
|
return key2;
|
|
13763
14171
|
}
|
|
13764
|
-
|
|
14172
|
+
mkdirSync12(dirname5(path3), { recursive: true });
|
|
13765
14173
|
const key = randomBytes9(KEY_BYTES);
|
|
13766
|
-
|
|
14174
|
+
writeFileSync11(path3, key, { mode: 384 });
|
|
13767
14175
|
try {
|
|
13768
14176
|
chmodSync9(path3, 384);
|
|
13769
14177
|
} catch {
|
|
@@ -14848,7 +15256,7 @@ async function createApp(opts) {
|
|
|
14848
15256
|
broadcast({ type: "voice.heard", text: held.text, trust: "owner" });
|
|
14849
15257
|
await runVoiceTurn(held.text, conn.ip, epoch);
|
|
14850
15258
|
}
|
|
14851
|
-
const here2 =
|
|
15259
|
+
const here2 = dirname6(fileURLToPath2(import.meta.url));
|
|
14852
15260
|
const catalogDir = opts.pluginCatalogDir ?? firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve5(here2, "plugins"), resolve5(here2, "../../..", "plugins")]) ?? resolve5(here2, "plugins");
|
|
14853
15261
|
const pluginsApi = {
|
|
14854
15262
|
list: () => {
|
|
@@ -15020,11 +15428,42 @@ async function createApp(opts) {
|
|
|
15020
15428
|
});
|
|
15021
15429
|
}
|
|
15022
15430
|
});
|
|
15431
|
+
const self = opts.system?.self ?? currentSelf();
|
|
15432
|
+
const autostart = new Autostart(opts.system?.autostartDeps ?? realAutostartDeps(opts.dataDir), () => self);
|
|
15433
|
+
const updater = new Updater(
|
|
15434
|
+
{
|
|
15435
|
+
current: opts.system?.current ?? runningVersion,
|
|
15436
|
+
latest: opts.system?.latest ?? (() => npmLatest()),
|
|
15437
|
+
auto: () => config.updates.auto,
|
|
15438
|
+
busy: () => agent.isBusy(),
|
|
15439
|
+
canApply: opts.system?.canApply ?? (() => process.env.FRIEND_DAEMON === "1"),
|
|
15440
|
+
// only Friend in the background can replace itself
|
|
15441
|
+
apply: opts.system?.apply ?? (() => spawnUpgrade(self)),
|
|
15442
|
+
notify: (text3) => broadcast({ type: "alert", level: "info", text: text3 })
|
|
15443
|
+
},
|
|
15444
|
+
opts.system?.times
|
|
15445
|
+
);
|
|
15446
|
+
const systemApi = {
|
|
15447
|
+
info: async () => ({ version: updater.state().current, update: updater.state(), autostart: await autostart.status(), auto: config.updates.auto }),
|
|
15448
|
+
check: () => updater.check(),
|
|
15449
|
+
update: async (pin, ip) => {
|
|
15450
|
+
await requirePin(pin, ip);
|
|
15451
|
+
audit.log("owner", "system.update_now", { ip });
|
|
15452
|
+
return updater.applyNow();
|
|
15453
|
+
},
|
|
15454
|
+
setAutostart: async (enabled, pin, ip) => {
|
|
15455
|
+
await requirePin(pin, ip);
|
|
15456
|
+
audit.log("owner", enabled ? "system.autostart_on" : "system.autostart_off", { ip });
|
|
15457
|
+
const r = enabled ? await autostart.enable() : await autostart.disable();
|
|
15458
|
+
return { ...r, status: await autostart.status() };
|
|
15459
|
+
}
|
|
15460
|
+
};
|
|
15023
15461
|
const tls = config.https ? await ensureTlsCert(opts.dataDir, [...config.allowedHosts, ...config.publicUrl ? [new URL(config.publicUrl).hostname] : []]) : void 0;
|
|
15024
15462
|
const gateway = createGateway({
|
|
15025
15463
|
...tls ? { tls } : {},
|
|
15026
15464
|
connectors: connectorsApi,
|
|
15027
15465
|
chrome: chromeRemoteApi,
|
|
15466
|
+
system: systemApi,
|
|
15028
15467
|
video: {
|
|
15029
15468
|
stream: (id2) => videoHub.streamOf(id2),
|
|
15030
15469
|
status: async () => videoHub.status() ?? await videoHub.refresh(),
|
|
@@ -15594,12 +16033,14 @@ ${url}`);
|
|
|
15594
16033
|
for (const id2 of gone) broadcast({ type: "stage", command: { cmd: "hide", id: id2 } });
|
|
15595
16034
|
if (gone.length) for (const follow of stage.settleNow()) broadcast({ type: "stage", command: follow });
|
|
15596
16035
|
}, opts.expireIntervalMs ?? 1e3);
|
|
16036
|
+
if (opts.system?.checkForUpdates) updater.start();
|
|
15597
16037
|
const listening = await gateway.listen();
|
|
15598
16038
|
listenPort = listening.port;
|
|
15599
16039
|
return listening;
|
|
15600
16040
|
},
|
|
15601
16041
|
async stop() {
|
|
15602
16042
|
stopped = true;
|
|
16043
|
+
updater.stop();
|
|
15603
16044
|
hostPlayer.dispose();
|
|
15604
16045
|
videoHub.dispose();
|
|
15605
16046
|
heartbeat.stop();
|
|
@@ -15622,7 +16063,7 @@ ${url}`);
|
|
|
15622
16063
|
}
|
|
15623
16064
|
|
|
15624
16065
|
// ../server/src/voice/sidecar.ts
|
|
15625
|
-
import { spawn as
|
|
16066
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
15626
16067
|
import { createInterface } from "node:readline";
|
|
15627
16068
|
var SidecarProcess = class {
|
|
15628
16069
|
constructor(opts) {
|
|
@@ -15659,7 +16100,7 @@ var SidecarProcess = class {
|
|
|
15659
16100
|
settled = true;
|
|
15660
16101
|
reject(new Error(message));
|
|
15661
16102
|
};
|
|
15662
|
-
const child =
|
|
16103
|
+
const child = spawn8(this.opts.command, this.opts.args, {
|
|
15663
16104
|
cwd: this.opts.cwd,
|
|
15664
16105
|
env: { ...this.opts.env ?? process.env, FRIEND_VOICE_TOKEN: this.opts.token },
|
|
15665
16106
|
windowsHide: true,
|
|
@@ -15807,6 +16248,9 @@ var PARSERS = {
|
|
|
15807
16248
|
trustProxy: bool
|
|
15808
16249
|
};
|
|
15809
16250
|
var GROUPS = {
|
|
16251
|
+
updates: {
|
|
16252
|
+
auto: bool
|
|
16253
|
+
},
|
|
15810
16254
|
voice: {
|
|
15811
16255
|
enabled: bool,
|
|
15812
16256
|
sttModel: nonEmpty,
|
|
@@ -15880,6 +16324,7 @@ var KEY_HELP = {
|
|
|
15880
16324
|
allowedOrigins: "extra web addresses allowed to connect, separated by commas",
|
|
15881
16325
|
allowedHosts: "extra host names that may reach Friend (for example my-pc), separated by commas",
|
|
15882
16326
|
trustProxy: "true when Friend sits behind your own reverse proxy",
|
|
16327
|
+
"updates.auto": "true or false: look for a newer Friend when it starts and install it by itself while Friend is not busy (false: only when you run friend upgrade or press Update now)",
|
|
15883
16328
|
"voice.enabled": "true or false: use speech at all",
|
|
15884
16329
|
"voice.sttModel": "listening model: tiny, base, small, large-v3-turbo \u2026 (bigger is more accurate and slower; Marathi and far-from-mic speech do best with small or large-v3-turbo)",
|
|
15885
16330
|
"voice.ttsVoice": "speaking voice (easier: friend voice setup, or Settings)",
|
|
@@ -15947,12 +16392,12 @@ import { resolve as resolve9 } from "node:path";
|
|
|
15947
16392
|
import { WebSocket as WebSocket3 } from "ws";
|
|
15948
16393
|
|
|
15949
16394
|
// ../agent-cli/src/tools/fs.ts
|
|
15950
|
-
import { closeSync, mkdirSync as
|
|
15951
|
-
import { dirname as
|
|
16395
|
+
import { closeSync, mkdirSync as mkdirSync13, openSync, readSync, readdirSync as readdirSync4, statSync as statSync4, writeFileSync as writeFileSync12 } from "node:fs";
|
|
16396
|
+
import { dirname as dirname8, join as join19 } from "node:path";
|
|
15952
16397
|
|
|
15953
16398
|
// ../agent-cli/src/tools/roots.ts
|
|
15954
16399
|
import { lstatSync as lstatSync2, realpathSync } from "node:fs";
|
|
15955
|
-
import { dirname as
|
|
16400
|
+
import { dirname as dirname7, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve6, sep as sep3 } from "node:path";
|
|
15956
16401
|
var isWin = process.platform === "win32";
|
|
15957
16402
|
var norm = (p) => isWin ? p.toLowerCase() : p;
|
|
15958
16403
|
var exists = (p) => {
|
|
@@ -15968,18 +16413,18 @@ function realish(abs) {
|
|
|
15968
16413
|
let cur = abs;
|
|
15969
16414
|
const tail2 = [];
|
|
15970
16415
|
while (!exists(cur)) {
|
|
15971
|
-
const parent =
|
|
16416
|
+
const parent = dirname7(cur);
|
|
15972
16417
|
if (parent === cur) return abs;
|
|
15973
16418
|
tail2.unshift(cur.slice(parent.length).replace(/^[\\/]+/, ""));
|
|
15974
16419
|
cur = parent;
|
|
15975
16420
|
}
|
|
15976
|
-
let
|
|
16421
|
+
let real3;
|
|
15977
16422
|
try {
|
|
15978
|
-
|
|
16423
|
+
real3 = realpathSync.native(cur);
|
|
15979
16424
|
} catch {
|
|
15980
16425
|
return UNRESOLVABLE + abs;
|
|
15981
16426
|
}
|
|
15982
|
-
return tail2.length ? resolve6(
|
|
16427
|
+
return tail2.length ? resolve6(real3, ...tail2) : real3;
|
|
15983
16428
|
}
|
|
15984
16429
|
function within(root, target) {
|
|
15985
16430
|
if (target.startsWith(UNRESOLVABLE)) return false;
|
|
@@ -16051,8 +16496,8 @@ function fsWrite(args, ctx) {
|
|
|
16051
16496
|
if ("error" in g) return { ok: false, error: g.error };
|
|
16052
16497
|
if (Buffer.byteLength(args.content) > MAX_WRITE) return { ok: false, error: "Content too large (5 MB limit)" };
|
|
16053
16498
|
try {
|
|
16054
|
-
|
|
16055
|
-
|
|
16499
|
+
mkdirSync13(dirname8(g.abs), { recursive: true });
|
|
16500
|
+
writeFileSync12(g.abs, args.content);
|
|
16056
16501
|
return { ok: true, output: `Wrote ${Buffer.byteLength(args.content)} bytes to ${g.abs}`, truncated: false };
|
|
16057
16502
|
} catch (e) {
|
|
16058
16503
|
return { ok: false, error: e.message };
|
|
@@ -16060,7 +16505,7 @@ function fsWrite(args, ctx) {
|
|
|
16060
16505
|
}
|
|
16061
16506
|
|
|
16062
16507
|
// ../agent-cli/src/tools/shell.ts
|
|
16063
|
-
import { spawn as
|
|
16508
|
+
import { spawn as spawn9, spawnSync } from "node:child_process";
|
|
16064
16509
|
function killTree2(child) {
|
|
16065
16510
|
if (!child.pid) return;
|
|
16066
16511
|
if (process.platform === "win32") {
|
|
@@ -16086,7 +16531,7 @@ function runCommand2(command, cwdArg, ctx, argv) {
|
|
|
16086
16531
|
const file = argv?.file ?? (win ? "powershell.exe" : "sh");
|
|
16087
16532
|
const args = argv?.args ?? (win ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command]);
|
|
16088
16533
|
return new Promise((resolve13) => {
|
|
16089
|
-
const child =
|
|
16534
|
+
const child = spawn9(file, args, { cwd, windowsHide: true, detached: !win, stdio: ["ignore", "pipe", "pipe"] });
|
|
16090
16535
|
let out = "";
|
|
16091
16536
|
let truncated = false;
|
|
16092
16537
|
let timedOut = false;
|
|
@@ -16288,8 +16733,8 @@ function startDeviceNode(opts) {
|
|
|
16288
16733
|
|
|
16289
16734
|
// ../agent-cli/src/commands/start.ts
|
|
16290
16735
|
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
16291
|
-
import { existsSync as
|
|
16292
|
-
import { dirname as
|
|
16736
|
+
import { existsSync as existsSync19, readFileSync as readFileSync13, writeFileSync as writeFileSync14 } from "node:fs";
|
|
16737
|
+
import { dirname as dirname10, join as join21, resolve as resolve8 } from "node:path";
|
|
16293
16738
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
16294
16739
|
|
|
16295
16740
|
// ../agent-cli/src/commands/lan.ts
|
|
@@ -16394,9 +16839,9 @@ async function lanCommand(args, io2, ifaces = networkInterfaces2) {
|
|
|
16394
16839
|
}
|
|
16395
16840
|
|
|
16396
16841
|
// ../agent-cli/src/commands/voice.ts
|
|
16397
|
-
import { spawn as
|
|
16398
|
-
import { existsSync as
|
|
16399
|
-
import { dirname as
|
|
16842
|
+
import { spawn as spawn10 } from "node:child_process";
|
|
16843
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync13 } from "node:fs";
|
|
16844
|
+
import { dirname as dirname9, join as join20, resolve as resolve7 } from "node:path";
|
|
16400
16845
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
16401
16846
|
function voicePaths(dataDir) {
|
|
16402
16847
|
const home = join20(dataDir, "voice");
|
|
@@ -16411,19 +16856,19 @@ function voicePaths(dataDir) {
|
|
|
16411
16856
|
}
|
|
16412
16857
|
function readMarker(dataDir) {
|
|
16413
16858
|
try {
|
|
16414
|
-
return JSON.parse(
|
|
16859
|
+
return JSON.parse(readFileSync12(voicePaths(dataDir).marker, "utf8"));
|
|
16415
16860
|
} catch {
|
|
16416
16861
|
return null;
|
|
16417
16862
|
}
|
|
16418
16863
|
}
|
|
16419
16864
|
function findVoiceDir() {
|
|
16420
|
-
const here2 =
|
|
16865
|
+
const here2 = dirname9(fileURLToPath3(import.meta.url));
|
|
16421
16866
|
return firstExistingDir([process.env.FRIEND_VOICE_DIR, resolve7(here2, "voice"), resolve7(here2, "../../../voice")], "friend_voice") ?? resolve7(here2, "voice");
|
|
16422
16867
|
}
|
|
16423
16868
|
var QUIET_ENV = { ...process.env, HF_HUB_DISABLE_SYMLINKS_WARNING: "1", PYTHONUTF8: "1" };
|
|
16424
16869
|
var defaultRunner = (cmd, args, opts) => new Promise((res) => {
|
|
16425
16870
|
let stdout = "";
|
|
16426
|
-
const child =
|
|
16871
|
+
const child = spawn10(cmd, args, { cwd: opts?.cwd, env: QUIET_ENV, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
16427
16872
|
const feed = (d) => {
|
|
16428
16873
|
const s = d.toString();
|
|
16429
16874
|
stdout += s;
|
|
@@ -16508,8 +16953,8 @@ async function voiceSetupCommand(io2, deps = {}, args = []) {
|
|
|
16508
16953
|
io2.err("Python 3.10 or newer is required. Install it from https://www.python.org/downloads/ and run this again.");
|
|
16509
16954
|
return 1;
|
|
16510
16955
|
}
|
|
16511
|
-
|
|
16512
|
-
if (!
|
|
16956
|
+
mkdirSync14(p.home, { recursive: true });
|
|
16957
|
+
if (!existsSync18(p.python)) {
|
|
16513
16958
|
io2.out("1/3 Creating a private Python environment\u2026");
|
|
16514
16959
|
const r = await run2(py.cmd, [...py.args, "-m", "venv", p.venv], { onLine });
|
|
16515
16960
|
if (r.code !== 0) {
|
|
@@ -16543,7 +16988,7 @@ ${tail(dl.stdout)}`);
|
|
|
16543
16988
|
return 1;
|
|
16544
16989
|
}
|
|
16545
16990
|
const marker = { python: p.python, sttModel: cfg.voice.sttModel, ttsVoice: cfg.voice.ttsVoice, at: Date.now() };
|
|
16546
|
-
|
|
16991
|
+
writeFileSync13(p.marker, JSON.stringify(marker, null, 2));
|
|
16547
16992
|
io2.out("Voice is ready. Start Friend with `friend start`, then hold Space to talk.");
|
|
16548
16993
|
return 0;
|
|
16549
16994
|
}
|
|
@@ -16559,7 +17004,7 @@ async function voiceStatusCommand(io2) {
|
|
|
16559
17004
|
io2.out("Voice engine: not installed. Run: friend voice setup");
|
|
16560
17005
|
return 0;
|
|
16561
17006
|
}
|
|
16562
|
-
if (!
|
|
17007
|
+
if (!existsSync18(marker.python)) {
|
|
16563
17008
|
io2.out("Voice engine: installed, but its Python is missing. Run: friend voice setup");
|
|
16564
17009
|
return 0;
|
|
16565
17010
|
}
|
|
@@ -16630,9 +17075,9 @@ async function voiceCommand(args, io2) {
|
|
|
16630
17075
|
|
|
16631
17076
|
// ../agent-cli/src/commands/start.ts
|
|
16632
17077
|
function findWebDir(explicit) {
|
|
16633
|
-
const here2 =
|
|
17078
|
+
const here2 = dirname10(fileURLToPath4(import.meta.url));
|
|
16634
17079
|
const candidates = [explicit, process.env.FRIEND_WEB_DIR, resolve8(here2, "../../../web/dist"), resolve8(here2, "web")];
|
|
16635
|
-
return candidates.find((c) => !!c &&
|
|
17080
|
+
return candidates.find((c) => !!c && existsSync19(join21(c, "index.html")));
|
|
16636
17081
|
}
|
|
16637
17082
|
async function startFriend(opts) {
|
|
16638
17083
|
const stored = loadConfig(opts.dataDir);
|
|
@@ -16657,6 +17102,8 @@ async function startFriend(opts) {
|
|
|
16657
17102
|
}) : null;
|
|
16658
17103
|
const voice2 = sidecar ? new HttpVoiceService({ baseUrl: () => sidecar.baseUrl(), token: secret }) : void 0;
|
|
16659
17104
|
const app = await createApp({
|
|
17105
|
+
system: { checkForUpdates: true },
|
|
17106
|
+
// looks for a newer Friend after it starts, and updates by itself when that is on
|
|
16660
17107
|
dataDir: opts.dataDir,
|
|
16661
17108
|
webDir: findWebDir(opts.webDir),
|
|
16662
17109
|
voice: voice2,
|
|
@@ -16667,16 +17114,16 @@ async function startFriend(opts) {
|
|
|
16667
17114
|
});
|
|
16668
17115
|
const { port } = await app.start();
|
|
16669
17116
|
const tokenFile = join21(opts.dataDir, "local-device.token");
|
|
16670
|
-
let token =
|
|
17117
|
+
let token = existsSync19(tokenFile) ? readFileSync13(tokenFile, "utf8").trim() : "";
|
|
16671
17118
|
if (!token || !app.auth.resolveDevice(token)) {
|
|
16672
17119
|
token = app.auth.redeemPairCode(app.auth.createPairCode(), "this-pc");
|
|
16673
|
-
|
|
17120
|
+
writeFileSync14(tokenFile, token, { mode: 384 });
|
|
16674
17121
|
}
|
|
16675
17122
|
const node = startDeviceNode({
|
|
16676
17123
|
url: `${app.config.https ? "wss" : "ws"}://127.0.0.1:${port}/device`,
|
|
16677
17124
|
token,
|
|
16678
17125
|
roots: app.config.roots,
|
|
16679
|
-
...app.config.https ? { ca:
|
|
17126
|
+
...app.config.https ? { ca: readFileSync13(join21(opts.dataDir, "tls", "cert.pem"), "utf8") } : {}
|
|
16680
17127
|
// this PC trusts only its own Friend's certificate
|
|
16681
17128
|
});
|
|
16682
17129
|
const urls = urlsFor(app.config.bind, port, void 0, app.config.https);
|
|
@@ -16904,12 +17351,12 @@ async function memoryCommand(args, io2) {
|
|
|
16904
17351
|
}
|
|
16905
17352
|
|
|
16906
17353
|
// ../agent-cli/src/commands/plugin.ts
|
|
16907
|
-
import { existsSync as
|
|
16908
|
-
import { dirname as
|
|
17354
|
+
import { existsSync as existsSync20, readdirSync as readdirSync5, statSync as statSync5 } from "node:fs";
|
|
17355
|
+
import { dirname as dirname11, join as join22, resolve as resolve10 } from "node:path";
|
|
16909
17356
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
16910
17357
|
var USAGE2 = "Usage: friend plugin catalog | list | install <name|folder> [--yes] | enable <id> | disable <id> | remove <id>";
|
|
16911
17358
|
function defaultCatalog() {
|
|
16912
|
-
const here2 =
|
|
17359
|
+
const here2 = dirname11(fileURLToPath5(import.meta.url));
|
|
16913
17360
|
return firstExistingDir([process.env.FRIEND_PLUGIN_CATALOG, resolve10(here2, "plugins"), resolve10(here2, "../../../../plugins")]) ?? resolve10(here2, "plugins");
|
|
16914
17361
|
}
|
|
16915
17362
|
var count = (n, word2) => `${n} ${word2}${n === 1 ? "" : "s"}`;
|
|
@@ -16919,7 +17366,7 @@ async function pluginCommand(args, io2, deps = {}) {
|
|
|
16919
17366
|
const flags = rest.filter((a) => a.startsWith("--"));
|
|
16920
17367
|
const arg = rest.find((a) => !a.startsWith("--"));
|
|
16921
17368
|
if (sub === "catalog") {
|
|
16922
|
-
const dirs =
|
|
17369
|
+
const dirs = existsSync20(catalogDir) ? readdirSync5(catalogDir).filter((d) => statSync5(join22(catalogDir, d)).isDirectory()) : [];
|
|
16923
17370
|
if (dirs.length === 0) io2.out("The catalog is empty.");
|
|
16924
17371
|
for (const d of dirs.sort()) {
|
|
16925
17372
|
const r = inspectPlugin(join22(catalogDir, d));
|
|
@@ -16952,8 +17399,8 @@ async function pluginCommand(args, io2, deps = {}) {
|
|
|
16952
17399
|
io2.err(USAGE2);
|
|
16953
17400
|
return 1;
|
|
16954
17401
|
}
|
|
16955
|
-
const inCatalog = !/[\\/]/.test(arg) &&
|
|
16956
|
-
const source = inCatalog ? join22(catalogDir, arg) :
|
|
17402
|
+
const inCatalog = !/[\\/]/.test(arg) && existsSync20(join22(catalogDir, arg));
|
|
17403
|
+
const source = inCatalog ? join22(catalogDir, arg) : existsSync20(resolve10(arg)) ? resolve10(arg) : null;
|
|
16957
17404
|
if (!source) {
|
|
16958
17405
|
io2.err(`"${arg}" is not in the catalog and there is no such folder. See: friend plugin catalog`);
|
|
16959
17406
|
return 1;
|
|
@@ -17008,7 +17455,7 @@ async function pluginCommand(args, io2, deps = {}) {
|
|
|
17008
17455
|
|
|
17009
17456
|
// ../agent-cli/src/commands/onboard/index.ts
|
|
17010
17457
|
import crossSpawn4 from "cross-spawn";
|
|
17011
|
-
import { mkdirSync as
|
|
17458
|
+
import { mkdirSync as mkdirSync16 } from "node:fs";
|
|
17012
17459
|
import { resolve as resolve11 } from "node:path";
|
|
17013
17460
|
|
|
17014
17461
|
// ../agent-cli/src/commands/ollama.ts
|
|
@@ -17331,16 +17778,16 @@ function installPlan(id2, platform, have) {
|
|
|
17331
17778
|
}
|
|
17332
17779
|
|
|
17333
17780
|
// ../agent-cli/src/commands/onboard/state.ts
|
|
17334
|
-
import { existsSync as
|
|
17335
|
-
import { dirname as
|
|
17781
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync10, writeFileSync as writeFileSync15 } from "node:fs";
|
|
17782
|
+
import { dirname as dirname12, join as join23 } from "node:path";
|
|
17336
17783
|
var FEATURES = ["claude-code", "claude-api", "ollama", "voice", "sandbox"];
|
|
17337
17784
|
var emptyState = () => ({ version: 1, done: {}, features: [], restart: [] });
|
|
17338
17785
|
var statePath = (dataDir) => join23(dataDir, "onboarding.json");
|
|
17339
17786
|
function loadState(dataDir) {
|
|
17340
17787
|
const f = statePath(dataDir);
|
|
17341
|
-
if (!
|
|
17788
|
+
if (!existsSync21(f)) return emptyState();
|
|
17342
17789
|
try {
|
|
17343
|
-
const raw = JSON.parse(
|
|
17790
|
+
const raw = JSON.parse(readFileSync14(f, "utf8"));
|
|
17344
17791
|
const done = raw.done && typeof raw.done === "object" ? Object.fromEntries(Object.entries(raw.done).filter(([, v]) => typeof v === "string")) : {};
|
|
17345
17792
|
const features2 = Array.isArray(raw.features) ? raw.features.filter((x) => FEATURES.includes(x)) : [];
|
|
17346
17793
|
const restart = Array.isArray(raw.restart) ? raw.restart.filter((x) => typeof x === "string") : [];
|
|
@@ -17351,13 +17798,13 @@ function loadState(dataDir) {
|
|
|
17351
17798
|
}
|
|
17352
17799
|
function saveState(dataDir, state) {
|
|
17353
17800
|
const f = statePath(dataDir);
|
|
17354
|
-
|
|
17801
|
+
mkdirSync15(dirname12(f), { recursive: true });
|
|
17355
17802
|
const tmp = `${f}.tmp`;
|
|
17356
|
-
|
|
17803
|
+
writeFileSync15(tmp, JSON.stringify(state, null, 2));
|
|
17357
17804
|
renameSync4(tmp, f);
|
|
17358
17805
|
}
|
|
17359
17806
|
function resetState(dataDir) {
|
|
17360
|
-
|
|
17807
|
+
rmSync10(statePath(dataDir), { force: true });
|
|
17361
17808
|
}
|
|
17362
17809
|
|
|
17363
17810
|
// ../agent-cli/src/commands/onboard/ui.ts
|
|
@@ -17709,7 +18156,7 @@ async function workspace(c) {
|
|
|
17709
18156
|
const answer = await text2(c.ui, current ? "Workspace folder" : "Workspace folder (Enter to skip)", current ?? "");
|
|
17710
18157
|
if (answer) {
|
|
17711
18158
|
const abs = resolve11(answer);
|
|
17712
|
-
|
|
18159
|
+
mkdirSync16(abs, { recursive: true });
|
|
17713
18160
|
cfg.roots = [abs];
|
|
17714
18161
|
saveConfig(cfg);
|
|
17715
18162
|
io2.out(`Workspace: ${abs}`);
|
|
@@ -17879,19 +18326,54 @@ async function doctorCommand(io2, partial = {}) {
|
|
|
17879
18326
|
return requiredMissing ? 1 : 0;
|
|
17880
18327
|
}
|
|
17881
18328
|
|
|
18329
|
+
// ../agent-cli/src/commands/autostart.ts
|
|
18330
|
+
var USAGE3 = "Usage: friend autostart on|off|status";
|
|
18331
|
+
var real = (io2) => new Autostart(realAutostartDeps(io2.dataDir), currentSelf);
|
|
18332
|
+
async function autostartCommand(args, io2, autostart = real(io2)) {
|
|
18333
|
+
switch (args[0]) {
|
|
18334
|
+
case "on":
|
|
18335
|
+
case "off": {
|
|
18336
|
+
const r = await (args[0] === "on" ? autostart.enable() : autostart.disable());
|
|
18337
|
+
if (!r.ok) {
|
|
18338
|
+
io2.err(r.message);
|
|
18339
|
+
return 1;
|
|
18340
|
+
}
|
|
18341
|
+
io2.out(r.message);
|
|
18342
|
+
if (args[0] === "on") io2.out("Turn it off again with: friend autostart off");
|
|
18343
|
+
return 0;
|
|
18344
|
+
}
|
|
18345
|
+
case "status": {
|
|
18346
|
+
const s = await autostart.status();
|
|
18347
|
+
if (!s.supported) {
|
|
18348
|
+
io2.out("Starting Friend by itself is not supported on this kind of computer.");
|
|
18349
|
+
return 0;
|
|
18350
|
+
}
|
|
18351
|
+
io2.out(`Starts by itself when you sign in: ${s.enabled ? "yes" : "no"}${s.enabled ? ` (${s.kind})` : ""}`);
|
|
18352
|
+
if (!s.enabled) io2.out("Turn it on with: friend autostart on");
|
|
18353
|
+
return 0;
|
|
18354
|
+
}
|
|
18355
|
+
case "refresh":
|
|
18356
|
+
await autostart.refresh();
|
|
18357
|
+
return 0;
|
|
18358
|
+
default:
|
|
18359
|
+
io2.err(USAGE3);
|
|
18360
|
+
return 1;
|
|
18361
|
+
}
|
|
18362
|
+
}
|
|
18363
|
+
|
|
17882
18364
|
// ../agent-cli/src/commands/daemon.ts
|
|
17883
|
-
import { spawn as
|
|
17884
|
-
import { closeSync as closeSync2, existsSync as
|
|
18365
|
+
import { spawn as spawn11, spawnSync as spawnSync2 } from "node:child_process";
|
|
18366
|
+
import { closeSync as closeSync2, existsSync as existsSync22, mkdirSync as mkdirSync17, openSync as openSync2, readFileSync as readFileSync15, renameSync as renameSync5, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync16 } from "node:fs";
|
|
17885
18367
|
import { connect } from "node:net";
|
|
17886
18368
|
import { join as join24 } from "node:path";
|
|
17887
|
-
var
|
|
18369
|
+
var USAGE4 = "Usage: friend daemon start | stop | restart | status";
|
|
17888
18370
|
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
17889
18371
|
var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
|
|
17890
18372
|
var realDeps = {
|
|
17891
18373
|
launch(logFile) {
|
|
17892
18374
|
const fd = openSync2(logFile, "a");
|
|
17893
18375
|
try {
|
|
17894
|
-
const child =
|
|
18376
|
+
const child = spawn11(process.execPath, [process.argv[1], "start"], { detached: true, stdio: ["ignore", fd, fd], windowsHide: true, env: { ...process.env, FRIEND_DAEMON: "1" } });
|
|
17895
18377
|
child.unref();
|
|
17896
18378
|
return child.pid;
|
|
17897
18379
|
} finally {
|
|
@@ -17943,7 +18425,7 @@ var pidFile = (dataDir) => join24(dataDir, "friend.pid");
|
|
|
17943
18425
|
var logFileOf = (dataDir) => join24(dataDir, "friend.log");
|
|
17944
18426
|
function readPid(dataDir) {
|
|
17945
18427
|
try {
|
|
17946
|
-
const r = JSON.parse(
|
|
18428
|
+
const r = JSON.parse(readFileSync15(pidFile(dataDir), "utf8"));
|
|
17947
18429
|
return Number.isInteger(r.pid) && r.pid > 0 ? r : null;
|
|
17948
18430
|
} catch {
|
|
17949
18431
|
return null;
|
|
@@ -17952,7 +18434,7 @@ function readPid(dataDir) {
|
|
|
17952
18434
|
function running(dataDir, deps) {
|
|
17953
18435
|
const r = readPid(dataDir);
|
|
17954
18436
|
if (r && deps.alive(r.pid)) return r;
|
|
17955
|
-
if (r)
|
|
18437
|
+
if (r) rmSync11(pidFile(dataDir), { force: true });
|
|
17956
18438
|
return null;
|
|
17957
18439
|
}
|
|
17958
18440
|
function since(ms2) {
|
|
@@ -17974,10 +18456,10 @@ async function start(io2, deps) {
|
|
|
17974
18456
|
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).`);
|
|
17975
18457
|
return 1;
|
|
17976
18458
|
}
|
|
17977
|
-
|
|
18459
|
+
mkdirSync17(io2.dataDir, { recursive: true });
|
|
17978
18460
|
const log = logFileOf(io2.dataDir);
|
|
17979
18461
|
try {
|
|
17980
|
-
if (
|
|
18462
|
+
if (existsSync22(log) && statSync6(log).size > MAX_LOG_BYTES) renameSync5(log, `${log}.old`);
|
|
17981
18463
|
} catch {
|
|
17982
18464
|
}
|
|
17983
18465
|
const pid = deps.launch(log);
|
|
@@ -17985,7 +18467,7 @@ async function start(io2, deps) {
|
|
|
17985
18467
|
io2.err("Could not start Friend in the background.");
|
|
17986
18468
|
return 1;
|
|
17987
18469
|
}
|
|
17988
|
-
|
|
18470
|
+
writeFileSync16(pidFile(io2.dataDir), JSON.stringify({ pid, startedAt: deps.now() }));
|
|
17989
18471
|
for (let i = 0; i < 60; i++) {
|
|
17990
18472
|
if (await deps.portOpen(cfg.port, 300)) {
|
|
17991
18473
|
io2.out(`Friend is running in the background (pid ${pid}) at ${urls.local}`);
|
|
@@ -17996,7 +18478,7 @@ async function start(io2, deps) {
|
|
|
17996
18478
|
if (!deps.alive(pid)) break;
|
|
17997
18479
|
await sleep(250);
|
|
17998
18480
|
}
|
|
17999
|
-
|
|
18481
|
+
rmSync11(pidFile(io2.dataDir), { force: true });
|
|
18000
18482
|
io2.err(`Friend did not start. See the log: ${log}`);
|
|
18001
18483
|
return 1;
|
|
18002
18484
|
}
|
|
@@ -18011,7 +18493,7 @@ async function stop(io2, deps) {
|
|
|
18011
18493
|
io2.err(`Could not stop Friend (pid ${r.pid}). End that process yourself.`);
|
|
18012
18494
|
return 1;
|
|
18013
18495
|
}
|
|
18014
|
-
|
|
18496
|
+
rmSync11(pidFile(io2.dataDir), { force: true });
|
|
18015
18497
|
io2.out("Friend stopped.");
|
|
18016
18498
|
return 0;
|
|
18017
18499
|
}
|
|
@@ -18043,25 +18525,27 @@ async function daemonCommand(args, io2, deps = realDeps) {
|
|
|
18043
18525
|
case "status":
|
|
18044
18526
|
return status(io2, deps);
|
|
18045
18527
|
default:
|
|
18046
|
-
io2.err(
|
|
18528
|
+
io2.err(USAGE4);
|
|
18047
18529
|
return 1;
|
|
18048
18530
|
}
|
|
18049
18531
|
}
|
|
18050
18532
|
|
|
18051
18533
|
// ../agent-cli/src/commands/upgrade.ts
|
|
18052
18534
|
import crossSpawn5 from "cross-spawn";
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
|
|
18535
|
+
import { appendFileSync } from "node:fs";
|
|
18536
|
+
import { join as join25 } from "node:path";
|
|
18537
|
+
var PACKAGE2 = "friend-assistant";
|
|
18538
|
+
var real2 = {
|
|
18539
|
+
current: () => true ? "0.1.45" : "development",
|
|
18056
18540
|
latest: () => new Promise((resolve13) => {
|
|
18057
|
-
const child = crossSpawn5("npm", ["view",
|
|
18541
|
+
const child = crossSpawn5("npm", ["view", PACKAGE2, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
18058
18542
|
let out = "";
|
|
18059
18543
|
child.stdout?.on("data", (d) => out += d.toString());
|
|
18060
18544
|
child.on("error", () => resolve13(null));
|
|
18061
18545
|
child.on("close", (code) => resolve13(code === 0 && /^\d+\.\d+\.\d+/.test(out.trim()) ? out.trim().split(/\s+/)[0] : null));
|
|
18062
18546
|
}),
|
|
18063
18547
|
install: (version) => new Promise((resolve13) => {
|
|
18064
|
-
const child = crossSpawn5("npm", ["install", "-g", `${
|
|
18548
|
+
const child = crossSpawn5("npm", ["install", "-g", `${PACKAGE2}@${version}`, "--prefer-online"], { stdio: "inherit" });
|
|
18065
18549
|
child.on("error", () => resolve13(1));
|
|
18066
18550
|
child.on("close", (code) => resolve13(code ?? 1));
|
|
18067
18551
|
}),
|
|
@@ -18070,6 +18554,11 @@ var real = {
|
|
|
18070
18554
|
child.on("error", () => resolve13(1));
|
|
18071
18555
|
child.on("close", (code) => resolve13(code ?? 1));
|
|
18072
18556
|
}),
|
|
18557
|
+
refreshAutostart: () => new Promise((resolve13) => {
|
|
18558
|
+
const child = crossSpawn5("friend", ["autostart", "refresh"], { stdio: "ignore" });
|
|
18559
|
+
child.on("error", () => resolve13());
|
|
18560
|
+
child.on("close", () => resolve13());
|
|
18561
|
+
}),
|
|
18073
18562
|
daemon: realDeps
|
|
18074
18563
|
};
|
|
18075
18564
|
var newer = (a, b) => {
|
|
@@ -18078,12 +18567,26 @@ var newer = (a, b) => {
|
|
|
18078
18567
|
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0);
|
|
18079
18568
|
return false;
|
|
18080
18569
|
};
|
|
18081
|
-
async function upgradeCommand(args,
|
|
18570
|
+
async function upgradeCommand(args, io0, deps = real2) {
|
|
18082
18571
|
const force = args.includes("--force");
|
|
18083
|
-
|
|
18084
|
-
|
|
18572
|
+
const auto = args.includes("--auto");
|
|
18573
|
+
if (args.some((a) => a !== "--force" && a !== "--auto")) {
|
|
18574
|
+
io0.err("Usage: friend upgrade [--force] [--auto]");
|
|
18085
18575
|
return 1;
|
|
18086
18576
|
}
|
|
18577
|
+
const log = deps.log ?? ((line) => appendFileSync(join25(io0.dataDir, "update.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
18578
|
+
`));
|
|
18579
|
+
const io2 = auto ? {
|
|
18580
|
+
...io0,
|
|
18581
|
+
out: (l) => {
|
|
18582
|
+
io0.out(l);
|
|
18583
|
+
log(l);
|
|
18584
|
+
},
|
|
18585
|
+
err: (l) => {
|
|
18586
|
+
io0.err(l);
|
|
18587
|
+
log(l);
|
|
18588
|
+
}
|
|
18589
|
+
} : io0;
|
|
18087
18590
|
const current = deps.current();
|
|
18088
18591
|
io2.out(`Installed: ${current}`);
|
|
18089
18592
|
const latest = await deps.latest();
|
|
@@ -18106,7 +18609,7 @@ async function upgradeCommand(args, io2, deps = real) {
|
|
|
18106
18609
|
io2.err(`Friend is running in a terminal (port ${cfg.port} is in use). Press Ctrl+C there to stop it, then run: friend upgrade`);
|
|
18107
18610
|
return 1;
|
|
18108
18611
|
}
|
|
18109
|
-
io2.out(`Installing ${
|
|
18612
|
+
io2.out(`Installing ${PACKAGE2}@${latest}\u2026`);
|
|
18110
18613
|
const code = await deps.install(latest);
|
|
18111
18614
|
if (code !== 0) {
|
|
18112
18615
|
io2.err("The install failed. If it says EBUSY or EPERM, close whatever is using Friend (a terminal running it, a voice process) and run: friend upgrade");
|
|
@@ -18114,6 +18617,8 @@ async function upgradeCommand(args, io2, deps = real) {
|
|
|
18114
18617
|
return 1;
|
|
18115
18618
|
}
|
|
18116
18619
|
io2.out(`Upgraded ${current} \u2192 ${latest}.`);
|
|
18620
|
+
await deps.refreshAutostart?.().catch(() => {
|
|
18621
|
+
});
|
|
18117
18622
|
if (wasRunning) return deps.startAgain();
|
|
18118
18623
|
io2.out("Start it with: friend start (or in the background: friend daemon start)");
|
|
18119
18624
|
return 0;
|
|
@@ -18187,7 +18692,7 @@ async function claudeCommand(args, io2, deps = {}) {
|
|
|
18187
18692
|
}
|
|
18188
18693
|
|
|
18189
18694
|
// ../agent-cli/src/commands/setup.ts
|
|
18190
|
-
import { mkdirSync as
|
|
18695
|
+
import { mkdirSync as mkdirSync18 } from "node:fs";
|
|
18191
18696
|
import { resolve as resolve12 } from "node:path";
|
|
18192
18697
|
async function setupCommand3(io2) {
|
|
18193
18698
|
const db = openDb(io2.dataDir);
|
|
@@ -18213,7 +18718,7 @@ async function setupCommand3(io2) {
|
|
|
18213
18718
|
const cfg = loadConfig(io2.dataDir);
|
|
18214
18719
|
if (workspace2) {
|
|
18215
18720
|
const abs = resolve12(workspace2);
|
|
18216
|
-
|
|
18721
|
+
mkdirSync18(abs, { recursive: true });
|
|
18217
18722
|
cfg.roots = [abs];
|
|
18218
18723
|
}
|
|
18219
18724
|
if (apiKey) cfg.apiKey = apiKey;
|
|
@@ -18278,7 +18783,7 @@ async function statusCommand3(io2) {
|
|
|
18278
18783
|
}
|
|
18279
18784
|
|
|
18280
18785
|
// ../agent-cli/src/cli.ts
|
|
18281
|
-
var
|
|
18786
|
+
var USAGE5 = `Usage: friend <command>
|
|
18282
18787
|
|
|
18283
18788
|
onboard guided setup, step by step: checks this PC, installs what is missing, asks what you want
|
|
18284
18789
|
doctor check what this PC has and what is missing (changes nothing)
|
|
@@ -18288,6 +18793,7 @@ var USAGE4 = `Usage: friend <command>
|
|
|
18288
18793
|
start run the gateway and a local device node
|
|
18289
18794
|
daemon start|stop|restart|status run Friend in the background (no terminal needed), stop it, or see if it is running
|
|
18290
18795
|
upgrade [--force] stop the background Friend, install the newest version, start it again
|
|
18796
|
+
autostart on|off|status start Friend by itself when you sign in to this computer (Windows, macOS, Linux)
|
|
18291
18797
|
pair print a one-time code to connect another PC
|
|
18292
18798
|
node --url <ws-url> --code <code> [--roots a,b] [--name pc]
|
|
18293
18799
|
connect this PC to a Friend gateway
|
|
@@ -18320,6 +18826,8 @@ async function run(argv, io2) {
|
|
|
18320
18826
|
return await daemonCommand(rest, io2);
|
|
18321
18827
|
case "upgrade":
|
|
18322
18828
|
return await upgradeCommand(rest, io2);
|
|
18829
|
+
case "autostart":
|
|
18830
|
+
return await autostartCommand(rest, io2);
|
|
18323
18831
|
case "pair":
|
|
18324
18832
|
return await pairCommand(io2);
|
|
18325
18833
|
case "node":
|
|
@@ -18335,12 +18843,12 @@ async function run(argv, io2) {
|
|
|
18335
18843
|
case "help":
|
|
18336
18844
|
case "--help":
|
|
18337
18845
|
case "-h":
|
|
18338
|
-
io2.out(
|
|
18846
|
+
io2.out(USAGE5);
|
|
18339
18847
|
return 0;
|
|
18340
18848
|
case "version":
|
|
18341
18849
|
case "--version":
|
|
18342
18850
|
case "-v":
|
|
18343
|
-
io2.out(true ? "0.1.
|
|
18851
|
+
io2.out(true ? "0.1.45" : "development");
|
|
18344
18852
|
return 0;
|
|
18345
18853
|
case "lan":
|
|
18346
18854
|
return await lanCommand(rest, io2);
|
|
@@ -18359,7 +18867,7 @@ async function run(argv, io2) {
|
|
|
18359
18867
|
case "config":
|
|
18360
18868
|
return await configCommand(rest, io2);
|
|
18361
18869
|
default:
|
|
18362
|
-
io2.err(
|
|
18870
|
+
io2.err(USAGE5);
|
|
18363
18871
|
return 1;
|
|
18364
18872
|
}
|
|
18365
18873
|
} catch (e) {
|