@qearlyao/familiar 0.4.2 → 0.4.5
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 +2 -1
- package/dist/conversation/contact-note.js +3 -0
- package/dist/lifecycle/service.js +165 -1
- package/dist/web/daemon.js +2 -0
- package/dist/web/file-routes.js +134 -0
- package/dist/web/http.js +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/web/dist/assets/index-BRRpS8Nf.js +8 -0
- package/web/dist/assets/index-CTbDswwP.css +2 -0
- package/web/dist/assets/ui-Cl8N5bkD.js +51 -0
- package/web/dist/index.html +4 -7
- package/web/dist/assets/index-BBeSYXbS.js +0 -8
- package/web/dist/assets/index-DEa6w1xC.css +0 -2
- package/web/dist/assets/ui-C12-nN_X.js +0 -51
package/README.md
CHANGED
|
@@ -199,7 +199,8 @@ familiar uninstall-service
|
|
|
199
199
|
macOS uses `launchd`; Linux uses user `systemd`. Windows users should run
|
|
200
200
|
`familiar run` in a foreground terminal for now. The short
|
|
201
201
|
`start`/`stop`/`restart` commands control the installed user service. Service
|
|
202
|
-
logs are written under `<workspace>/logs
|
|
202
|
+
logs are written under `<workspace>/logs`; service installs configure weekly log
|
|
203
|
+
rotation on macOS and on Linux when `logrotate` is available.
|
|
203
204
|
|
|
204
205
|
Upgrade the global npm package and append missing workspace defaults with:
|
|
205
206
|
|
|
@@ -26,6 +26,9 @@ export function parseContactNickname(raw, fallback) {
|
|
|
26
26
|
export async function refreshContactNote() {
|
|
27
27
|
cachedNickname = parseContactNickname(await loadContactNote(), "");
|
|
28
28
|
}
|
|
29
|
+
export function applyContactNoteContent(raw) {
|
|
30
|
+
cachedNickname = parseContactNickname(raw, "");
|
|
31
|
+
}
|
|
29
32
|
export function getContactNickname(fallback) {
|
|
30
33
|
return cachedNickname || fallback;
|
|
31
34
|
}
|
|
@@ -6,16 +6,26 @@ import { dirname, resolve } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
const execFileAsync = promisify(execFile);
|
|
8
8
|
const SERVICE_LABEL = "com.qearlyao.familiar";
|
|
9
|
+
const LOG_ROTATION_LABEL = `${SERVICE_LABEL}.log-rotation`;
|
|
9
10
|
const SYSTEMD_SERVICE = "familiar.service";
|
|
11
|
+
const SYSTEMD_LOGROTATE_SERVICE = "familiar-logrotate.service";
|
|
12
|
+
const SYSTEMD_LOGROTATE_TIMER = "familiar-logrotate.timer";
|
|
10
13
|
function servicePaths(workspacePath, input) {
|
|
11
14
|
const logDir = input.resolvePath(workspacePath, "logs");
|
|
15
|
+
const systemdUserDir = input.resolvePath(input.homeDir, ".config", "systemd", "user");
|
|
12
16
|
return {
|
|
13
17
|
servicePath: input.platform === "darwin"
|
|
14
18
|
? input.resolvePath(input.homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`)
|
|
15
|
-
: input.resolvePath(
|
|
19
|
+
: input.resolvePath(systemdUserDir, SYSTEMD_SERVICE),
|
|
16
20
|
logDir,
|
|
17
21
|
stdoutPath: input.resolvePath(logDir, "familiar.out.log"),
|
|
18
22
|
stderrPath: input.resolvePath(logDir, "familiar.err.log"),
|
|
23
|
+
logrotateConfigPath: input.resolvePath(logDir, "familiar.logrotate.conf"),
|
|
24
|
+
logrotateStatePath: input.resolvePath(logDir, "familiar.logrotate.state"),
|
|
25
|
+
logRotationScriptPath: input.resolvePath(logDir, "familiar-rotate-logs.sh"),
|
|
26
|
+
logRotationLaunchdPath: input.resolvePath(input.homeDir, "Library", "LaunchAgents", `${LOG_ROTATION_LABEL}.plist`),
|
|
27
|
+
logrotateServicePath: input.resolvePath(systemdUserDir, SYSTEMD_LOGROTATE_SERVICE),
|
|
28
|
+
logrotateTimerPath: input.resolvePath(systemdUserDir, SYSTEMD_LOGROTATE_TIMER),
|
|
19
29
|
};
|
|
20
30
|
}
|
|
21
31
|
function buildSpec(workspacePath, options = {}) {
|
|
@@ -60,6 +70,12 @@ function systemdQuote(value) {
|
|
|
60
70
|
return value;
|
|
61
71
|
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
|
|
62
72
|
}
|
|
73
|
+
function shellQuote(value) {
|
|
74
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
75
|
+
}
|
|
76
|
+
function logrotateQuote(value) {
|
|
77
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
78
|
+
}
|
|
63
79
|
function launchdPlist(spec) {
|
|
64
80
|
const args = [spec.nodePath, spec.cliPath, "run", spec.workspacePath]
|
|
65
81
|
.map((value) => `\t\t<string>${xmlEscape(value)}</string>`)
|
|
@@ -97,6 +113,26 @@ ${args}
|
|
|
97
113
|
</plist>
|
|
98
114
|
`;
|
|
99
115
|
}
|
|
116
|
+
function launchdLogRotationPlist(spec) {
|
|
117
|
+
const args = ["/bin/sh", spec.paths.logRotationScriptPath, spec.paths.stdoutPath, spec.paths.stderrPath]
|
|
118
|
+
.map((value) => `\t\t<string>${xmlEscape(value)}</string>`)
|
|
119
|
+
.join("\n");
|
|
120
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
121
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
122
|
+
<plist version="1.0">
|
|
123
|
+
<dict>
|
|
124
|
+
\t<key>Label</key>
|
|
125
|
+
\t<string>${LOG_ROTATION_LABEL}</string>
|
|
126
|
+
\t<key>ProgramArguments</key>
|
|
127
|
+
\t<array>
|
|
128
|
+
${args}
|
|
129
|
+
\t</array>
|
|
130
|
+
\t<key>StartInterval</key>
|
|
131
|
+
\t<integer>604800</integer>
|
|
132
|
+
</dict>
|
|
133
|
+
</plist>
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
100
136
|
function systemdUnit(spec) {
|
|
101
137
|
const execStart = [spec.nodePath, spec.cliPath, "run", spec.workspacePath].map(systemdQuote).join(" ");
|
|
102
138
|
return `[Unit]
|
|
@@ -122,6 +158,62 @@ StandardError=append:${spec.paths.stderrPath}
|
|
|
122
158
|
WantedBy=default.target
|
|
123
159
|
`;
|
|
124
160
|
}
|
|
161
|
+
function logRotationScript() {
|
|
162
|
+
return `set -eu
|
|
163
|
+
|
|
164
|
+
for log_path in "$@"; do
|
|
165
|
+
\t[ -s "$log_path" ] || continue
|
|
166
|
+
\trm -f "$log_path.8.gz"
|
|
167
|
+
\tindex=7
|
|
168
|
+
\twhile [ "$index" -ge 1 ]; do
|
|
169
|
+
\t\tnext=$((index + 1))
|
|
170
|
+
\t\tif [ -f "$log_path.$index.gz" ]; then
|
|
171
|
+
\t\t\tmv "$log_path.$index.gz" "$log_path.$next.gz"
|
|
172
|
+
\t\tfi
|
|
173
|
+
\t\tindex=$((index - 1))
|
|
174
|
+
\tdone
|
|
175
|
+
\tcp "$log_path" "$log_path.1"
|
|
176
|
+
\t: > "$log_path"
|
|
177
|
+
\tgzip -f "$log_path.1"
|
|
178
|
+
done
|
|
179
|
+
`;
|
|
180
|
+
}
|
|
181
|
+
function logrotateConfig(spec) {
|
|
182
|
+
return `${[spec.paths.stdoutPath, spec.paths.stderrPath].map(logrotateQuote).join(" ")} {
|
|
183
|
+
weekly
|
|
184
|
+
rotate 8
|
|
185
|
+
missingok
|
|
186
|
+
notifempty
|
|
187
|
+
copytruncate
|
|
188
|
+
compress
|
|
189
|
+
delaycompress
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
function logrotateService(spec, logrotatePath) {
|
|
194
|
+
const execStart = [logrotatePath, "-s", spec.paths.logrotateStatePath, spec.paths.logrotateConfigPath]
|
|
195
|
+
.map(systemdQuote)
|
|
196
|
+
.join(" ");
|
|
197
|
+
return `[Unit]
|
|
198
|
+
Description=Rotate Familiar service logs
|
|
199
|
+
|
|
200
|
+
[Service]
|
|
201
|
+
Type=oneshot
|
|
202
|
+
ExecStart=${execStart}
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
function logrotateTimer() {
|
|
206
|
+
return `[Unit]
|
|
207
|
+
Description=Rotate Familiar service logs weekly
|
|
208
|
+
|
|
209
|
+
[Timer]
|
|
210
|
+
OnCalendar=weekly
|
|
211
|
+
Persistent=true
|
|
212
|
+
|
|
213
|
+
[Install]
|
|
214
|
+
WantedBy=timers.target
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
125
217
|
async function commandExists(command) {
|
|
126
218
|
try {
|
|
127
219
|
await execFileAsync(command, ["--version"]);
|
|
@@ -134,6 +226,18 @@ async function commandExists(command) {
|
|
|
134
226
|
async function hasCommand(command, options) {
|
|
135
227
|
return options.commandExists ? options.commandExists(command) : commandExists(command);
|
|
136
228
|
}
|
|
229
|
+
async function commandPath(command, options) {
|
|
230
|
+
if (!(await hasCommand(command, options)))
|
|
231
|
+
return undefined;
|
|
232
|
+
try {
|
|
233
|
+
const output = await capture("sh", ["-c", `command -v ${shellQuote(command)}`], options);
|
|
234
|
+
const [path] = output.trim().split(/\r?\n/);
|
|
235
|
+
return path || undefined;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
137
241
|
async function run(command, args, options) {
|
|
138
242
|
if (options.runCommand) {
|
|
139
243
|
await options.runCommand(command, args);
|
|
@@ -195,6 +299,57 @@ function serviceDetails(spec) {
|
|
|
195
299
|
`stderr: ${spec.paths.stderrPath}`,
|
|
196
300
|
];
|
|
197
301
|
}
|
|
302
|
+
async function prepareLogRotation(spec, options) {
|
|
303
|
+
if (spec.platform === "darwin") {
|
|
304
|
+
await writeFile(spec.paths.logRotationScriptPath, logRotationScript(), "utf8");
|
|
305
|
+
await writeFile(spec.paths.logRotationLaunchdPath, launchdLogRotationPlist(spec), "utf8");
|
|
306
|
+
return { detail: `log_rotation: ${spec.paths.logRotationLaunchdPath}`, installed: true };
|
|
307
|
+
}
|
|
308
|
+
if (spec.platform !== "linux")
|
|
309
|
+
return undefined;
|
|
310
|
+
const logrotatePath = await commandPath("logrotate", options);
|
|
311
|
+
if (!logrotatePath) {
|
|
312
|
+
return {
|
|
313
|
+
detail: "logrotate: unavailable; install logrotate and rerun familiar install-service",
|
|
314
|
+
installed: false,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
await writeFile(spec.paths.logrotateConfigPath, logrotateConfig(spec), "utf8");
|
|
318
|
+
await writeFile(spec.paths.logrotateServicePath, logrotateService(spec, logrotatePath), "utf8");
|
|
319
|
+
await writeFile(spec.paths.logrotateTimerPath, logrotateTimer(), "utf8");
|
|
320
|
+
return { detail: `logrotate: ${spec.paths.logrotateConfigPath}`, installed: true };
|
|
321
|
+
}
|
|
322
|
+
async function enableLogRotation(spec, result, options) {
|
|
323
|
+
if (!result?.installed)
|
|
324
|
+
return;
|
|
325
|
+
if (spec.platform === "darwin") {
|
|
326
|
+
await runOptional("launchctl", ["bootout", guiDomain(options), spec.paths.logRotationLaunchdPath], options);
|
|
327
|
+
await run("launchctl", ["bootstrap", guiDomain(options), spec.paths.logRotationLaunchdPath], options);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (spec.platform === "linux") {
|
|
331
|
+
await run("systemctl", ["--user", "enable", "--now", SYSTEMD_LOGROTATE_TIMER], options);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function removeLogRotation(spec, options) {
|
|
335
|
+
if (spec.platform === "darwin") {
|
|
336
|
+
await runOptional("launchctl", ["bootout", guiDomain(options), spec.paths.logRotationLaunchdPath], options);
|
|
337
|
+
}
|
|
338
|
+
else if (spec.platform === "linux" && (await hasCommand("systemctl", options))) {
|
|
339
|
+
await runOptional("systemctl", ["--user", "disable", "--now", SYSTEMD_LOGROTATE_TIMER], options);
|
|
340
|
+
}
|
|
341
|
+
for (const path of [
|
|
342
|
+
spec.paths.logRotationLaunchdPath,
|
|
343
|
+
spec.paths.logRotationScriptPath,
|
|
344
|
+
spec.paths.logrotateServicePath,
|
|
345
|
+
spec.paths.logrotateTimerPath,
|
|
346
|
+
spec.paths.logrotateConfigPath,
|
|
347
|
+
spec.paths.logrotateStatePath,
|
|
348
|
+
]) {
|
|
349
|
+
if (existsSync(path))
|
|
350
|
+
await rm(path);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
198
353
|
function serviceControlTitle(action) {
|
|
199
354
|
if (action === "start")
|
|
200
355
|
return "Familiar service started.";
|
|
@@ -246,10 +401,12 @@ export async function installService(workspacePath, options = {}) {
|
|
|
246
401
|
await mkdir(spec.paths.logDir, { recursive: true });
|
|
247
402
|
const serviceText = spec.platform === "darwin" ? launchdPlist(spec) : systemdUnit(spec);
|
|
248
403
|
await writeFile(spec.paths.servicePath, serviceText, "utf8");
|
|
404
|
+
const logRotation = await prepareLogRotation(spec, options);
|
|
249
405
|
if (spec.platform === "darwin") {
|
|
250
406
|
await runOptional("launchctl", ["bootout", guiDomain(options), spec.paths.servicePath], options);
|
|
251
407
|
await run("launchctl", ["bootstrap", guiDomain(options), spec.paths.servicePath], options);
|
|
252
408
|
await run("launchctl", ["kickstart", "-k", `${guiDomain(options)}/${SERVICE_LABEL}`], options);
|
|
409
|
+
await enableLogRotation(spec, logRotation, options);
|
|
253
410
|
}
|
|
254
411
|
else {
|
|
255
412
|
if (!(await hasCommand("systemctl", options))) {
|
|
@@ -257,8 +414,11 @@ export async function installService(workspacePath, options = {}) {
|
|
|
257
414
|
}
|
|
258
415
|
await run("systemctl", ["--user", "daemon-reload"], options);
|
|
259
416
|
await run("systemctl", ["--user", "enable", "--now", SYSTEMD_SERVICE], options);
|
|
417
|
+
await enableLogRotation(spec, logRotation, options);
|
|
260
418
|
}
|
|
261
419
|
const details = serviceDetails(spec);
|
|
420
|
+
if (logRotation)
|
|
421
|
+
details.push(logRotation.detail);
|
|
262
422
|
const pathWarning = versionManagedPathWarning(spec);
|
|
263
423
|
if (pathWarning)
|
|
264
424
|
details.push(pathWarning);
|
|
@@ -282,11 +442,13 @@ export async function uninstallService(workspacePath, options = {}) {
|
|
|
282
442
|
return unsupported(spec.platform);
|
|
283
443
|
if (spec.platform === "darwin") {
|
|
284
444
|
await runOptional("launchctl", ["bootout", guiDomain(options), spec.paths.servicePath], options);
|
|
445
|
+
await removeLogRotation(spec, options);
|
|
285
446
|
}
|
|
286
447
|
else {
|
|
287
448
|
if (await hasCommand("systemctl", options)) {
|
|
288
449
|
await runOptional("systemctl", ["--user", "disable", "--now", SYSTEMD_SERVICE], options);
|
|
289
450
|
}
|
|
451
|
+
await removeLogRotation(spec, options);
|
|
290
452
|
}
|
|
291
453
|
if (existsSync(spec.paths.servicePath))
|
|
292
454
|
await rm(spec.paths.servicePath);
|
|
@@ -345,8 +507,10 @@ export const __serviceTest = {
|
|
|
345
507
|
SYSTEMD_SERVICE,
|
|
346
508
|
buildSpec,
|
|
347
509
|
launchdPlist,
|
|
510
|
+
launchdLogRotationPlist,
|
|
348
511
|
systemdUnit,
|
|
349
512
|
systemdQuote,
|
|
513
|
+
logRotationScript,
|
|
350
514
|
xmlEscape,
|
|
351
515
|
versionManagedPathWarning,
|
|
352
516
|
};
|
package/dist/web/daemon.js
CHANGED
|
@@ -9,6 +9,7 @@ import { registerWebConfigRoutes } from "./config-routes.js";
|
|
|
9
9
|
import { registerWebConversationRoutes } from "./conversation-routes.js";
|
|
10
10
|
import { registerWebDiaryRoutes } from "./diary-routes.js";
|
|
11
11
|
import { createWebEventHub } from "./event-hub.js";
|
|
12
|
+
import { registerWebFileRoutes } from "./file-routes.js";
|
|
12
13
|
import { HttpError, sendText } from "./http.js";
|
|
13
14
|
import { createWebRouteRegistry } from "./routes.js";
|
|
14
15
|
import { createWebRuntimeActions } from "./runtime-actions.js";
|
|
@@ -69,6 +70,7 @@ export async function startWebDaemon(config, familiarAgent, agentCore, options =
|
|
|
69
70
|
});
|
|
70
71
|
registerWebConfigRoutes(route, config, agentCore);
|
|
71
72
|
registerWebDiaryRoutes(route, config);
|
|
73
|
+
registerWebFileRoutes(route, config);
|
|
72
74
|
await subscribeKnownRuntimes();
|
|
73
75
|
const server = createServer((request, response) => {
|
|
74
76
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { applyContactNoteContent, setContactNotePath } from "../conversation/contact-note.js";
|
|
4
|
+
import { isEnoent } from "../util/fs.js";
|
|
5
|
+
import { isRecord } from "../util/guards.js";
|
|
6
|
+
import { HttpError, readJsonBody, sendJson } from "./http.js";
|
|
7
|
+
const WEB_FILE_DEFINITIONS = [
|
|
8
|
+
{
|
|
9
|
+
id: "soul",
|
|
10
|
+
name: "SOUL.md",
|
|
11
|
+
title: "soul",
|
|
12
|
+
description: "who your companion is trying to be",
|
|
13
|
+
path: (config) => config.persona.soul,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "user",
|
|
17
|
+
name: "USER.md",
|
|
18
|
+
title: "you",
|
|
19
|
+
description: "what they should remember about you",
|
|
20
|
+
path: (config) => config.persona.user,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "memory",
|
|
24
|
+
name: "MEMORY.md",
|
|
25
|
+
title: "memory",
|
|
26
|
+
description: "the long thread that should stay close",
|
|
27
|
+
path: (config) => config.persona.memory,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "heartbeat",
|
|
31
|
+
name: "HEARTBEAT.md",
|
|
32
|
+
title: "heartbeat",
|
|
33
|
+
description: "what they do when the room gets quiet",
|
|
34
|
+
path: (config) => resolve(config.workspacePath, "HEARTBEAT.md"),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "contact",
|
|
38
|
+
name: "CONTACT.md",
|
|
39
|
+
title: "contact",
|
|
40
|
+
description: "the name you keep for yourself here",
|
|
41
|
+
path: (config) => config.persona.contact,
|
|
42
|
+
afterWrite: (path, content) => {
|
|
43
|
+
setContactNotePath(path);
|
|
44
|
+
applyContactNoteContent(content);
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
const WEB_FILES = WEB_FILE_DEFINITIONS;
|
|
49
|
+
const MAX_WEB_FILE_BODY_BYTES = 1024 * 1024;
|
|
50
|
+
export function registerWebFileRoutes(route, config) {
|
|
51
|
+
route("GET", "/api/web/files", async (_request, response) => {
|
|
52
|
+
sendJson(response, 200, { files: await listWebFiles(config) });
|
|
53
|
+
});
|
|
54
|
+
route("GET", "/api/web/file", async (_request, response, url) => {
|
|
55
|
+
const id = url.searchParams.get("id") ?? "";
|
|
56
|
+
sendJson(response, 200, { file: await readWebFile(config, id) });
|
|
57
|
+
});
|
|
58
|
+
route("PUT", "/api/web/file", async (request, response) => {
|
|
59
|
+
const body = await readJsonBody(request, MAX_WEB_FILE_BODY_BYTES);
|
|
60
|
+
const { id, content } = fileUpdateFromBody(body);
|
|
61
|
+
sendJson(response, 200, { file: await writeWebFile(config, id, content) });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function listWebFiles(config) {
|
|
65
|
+
return Promise.all(WEB_FILES.map((definition) => readFileSummary(config, definition)));
|
|
66
|
+
}
|
|
67
|
+
export async function readWebFile(config, rawId) {
|
|
68
|
+
const definition = webFileDefinition(rawId);
|
|
69
|
+
const path = webFilePath(config, definition);
|
|
70
|
+
const summary = await readFileSummary(config, definition);
|
|
71
|
+
let content = "";
|
|
72
|
+
try {
|
|
73
|
+
content = await readFile(path, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (!isEnoent(error))
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
return { ...summary, content };
|
|
80
|
+
}
|
|
81
|
+
export async function writeWebFile(config, rawId, content) {
|
|
82
|
+
const definition = webFileDefinition(rawId);
|
|
83
|
+
const path = webFilePath(config, definition);
|
|
84
|
+
await mkdir(dirname(path), { recursive: true });
|
|
85
|
+
await writeFile(path, content, "utf8");
|
|
86
|
+
definition.afterWrite?.(path, content);
|
|
87
|
+
return readWebFile(config, definition.id);
|
|
88
|
+
}
|
|
89
|
+
async function readFileSummary(config, definition) {
|
|
90
|
+
const path = webFilePath(config, definition);
|
|
91
|
+
try {
|
|
92
|
+
const fileStat = await stat(path);
|
|
93
|
+
if (!fileStat.isFile())
|
|
94
|
+
throw new HttpError(404, `${definition.name} is not a file`);
|
|
95
|
+
return {
|
|
96
|
+
id: definition.id,
|
|
97
|
+
name: definition.name,
|
|
98
|
+
title: definition.title,
|
|
99
|
+
description: definition.description,
|
|
100
|
+
mtimeMs: Math.floor(fileStat.mtimeMs),
|
|
101
|
+
sizeBytes: fileStat.size,
|
|
102
|
+
exists: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!isEnoent(error))
|
|
107
|
+
throw error;
|
|
108
|
+
return {
|
|
109
|
+
id: definition.id,
|
|
110
|
+
name: definition.name,
|
|
111
|
+
title: definition.title,
|
|
112
|
+
description: definition.description,
|
|
113
|
+
mtimeMs: null,
|
|
114
|
+
sizeBytes: 0,
|
|
115
|
+
exists: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function fileUpdateFromBody(body) {
|
|
120
|
+
if (!isRecord(body) || typeof body.id !== "string")
|
|
121
|
+
throw new HttpError(400, "file id is required");
|
|
122
|
+
if (typeof body.content !== "string")
|
|
123
|
+
throw new HttpError(400, "content is required");
|
|
124
|
+
return { id: body.id, content: body.content };
|
|
125
|
+
}
|
|
126
|
+
function webFileDefinition(rawId) {
|
|
127
|
+
const definition = WEB_FILES.find((candidate) => candidate.id === rawId);
|
|
128
|
+
if (!definition)
|
|
129
|
+
throw new HttpError(400, `unknown file: ${rawId}`);
|
|
130
|
+
return definition;
|
|
131
|
+
}
|
|
132
|
+
function webFilePath(config, definition) {
|
|
133
|
+
return resolve(definition.path(config));
|
|
134
|
+
}
|
package/dist/web/http.js
CHANGED
|
@@ -21,13 +21,13 @@ export function sendText(response, status, text) {
|
|
|
21
21
|
response.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
22
22
|
response.end(text);
|
|
23
23
|
}
|
|
24
|
-
export async function readJsonBody(request) {
|
|
24
|
+
export async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) {
|
|
25
25
|
const chunks = [];
|
|
26
26
|
let total = 0;
|
|
27
27
|
for await (const chunk of request) {
|
|
28
28
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
29
29
|
total += buffer.length;
|
|
30
|
-
if (total >
|
|
30
|
+
if (total > maxBytes)
|
|
31
31
|
throw new HttpError(413, "Request body too large");
|
|
32
32
|
chunks.push(buffer);
|
|
33
33
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qearlyao/familiar",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@qearlyao/familiar",
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.5",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@earendil-works/pi-agent-core": "0.78.0",
|