@qearlyao/familiar 0.2.0 → 0.2.2
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 +10 -0
- package/dist/browser-tools.js +30 -4
- package/dist/discord.js +23 -2
- package/dist/memory/lcm/context-transformer.js +14 -3
- package/dist/memory/lcm/context.js +15 -4
- package/package.json +1 -1
- package/web/dist/assets/index-BPZQbZh5.js +61 -0
- package/web/dist/familiar.svg +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CUvbIJKO.js +0 -60
package/README.md
CHANGED
|
@@ -8,6 +8,16 @@ optional real-browser control in one workspace.
|
|
|
8
8
|
This project is still early. The current release is meant for trusted friends who
|
|
9
9
|
are comfortable editing a config file and running a long-lived Node process.
|
|
10
10
|
|
|
11
|
+
## Credits
|
|
12
|
+
|
|
13
|
+
Familiar builds on the [pi](https://github.com/earendil-works/pi)
|
|
14
|
+
stack, including `@earendil-works/pi-ai`, `@earendil-works/pi-agent-core`, and
|
|
15
|
+
`@earendil-works/pi-coding-agent`.
|
|
16
|
+
|
|
17
|
+
It also borrows ideas and structure from
|
|
18
|
+
[lossless-claw](https://github.com/Martian-Engineering/lossless-claw) and
|
|
19
|
+
[pi-lcm-memory](https://github.com/sharkone/pi-lcm-memory).
|
|
20
|
+
|
|
11
21
|
## Requirements
|
|
12
22
|
|
|
13
23
|
- Node.js 22 or newer. Node.js 24 LTS is recommended and is the primary tested runtime.
|
package/dist/browser-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { stat } from "node:fs/promises";
|
|
4
|
+
import { platform } from "node:os";
|
|
4
5
|
import { basename, extname, resolve } from "node:path";
|
|
5
6
|
import { Type } from "typebox";
|
|
6
7
|
import { ensureBrowserScreenshotsDir } from "./generated-media.js";
|
|
@@ -102,12 +103,36 @@ const browserSchema = Type.Object({
|
|
|
102
103
|
description: "Site-command positional arguments, in OpenCLI usage order, such as twitter post text.",
|
|
103
104
|
})),
|
|
104
105
|
}, { additionalProperties: false });
|
|
106
|
+
function quoteWindowsShellArg(value) {
|
|
107
|
+
const escaped = value
|
|
108
|
+
.replace(/%/g, "%%")
|
|
109
|
+
.replace(/(\\*)"/g, '$1$1\\"')
|
|
110
|
+
.replace(/(\\+)$/g, "$1$1");
|
|
111
|
+
return `"${escaped}"`;
|
|
112
|
+
}
|
|
113
|
+
function buildSpawnInvocation(spec, currentPlatform = platform(), comSpec = process.env.ComSpec ?? "cmd.exe") {
|
|
114
|
+
const options = {
|
|
115
|
+
stdio: [spec.stdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
116
|
+
env: spec.env,
|
|
117
|
+
};
|
|
118
|
+
if (currentPlatform !== "win32")
|
|
119
|
+
return { command: spec.command, args: spec.args, options };
|
|
120
|
+
const commandLine = [spec.command, ...spec.args].map(quoteWindowsShellArg).join(" ");
|
|
121
|
+
return {
|
|
122
|
+
command: comSpec,
|
|
123
|
+
// cmd.exe strips one outer quote pair from the /c string. Wrap the whole
|
|
124
|
+
// already-quoted command so .cmd shims with spaced paths still receive argv.
|
|
125
|
+
args: ["/d", "/s", "/c", `"${commandLine}"`],
|
|
126
|
+
options: {
|
|
127
|
+
...options,
|
|
128
|
+
windowsVerbatimArguments: true,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
105
132
|
function defaultBrowserRunner() {
|
|
106
133
|
return (spec, options) => new Promise((resolvePromise, reject) => {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
env: spec.env,
|
|
110
|
-
});
|
|
134
|
+
const invocation = buildSpawnInvocation(spec);
|
|
135
|
+
const child = spawn(invocation.command, invocation.args, invocation.options);
|
|
111
136
|
const timeout = setTimeout(() => {
|
|
112
137
|
child.kill("SIGTERM");
|
|
113
138
|
reject(new Error(`Browser command timed out after ${options.timeoutMs}ms.`));
|
|
@@ -685,6 +710,7 @@ export function createBrowserTools(config, mediaSink, runner = defaultBrowserRun
|
|
|
685
710
|
];
|
|
686
711
|
}
|
|
687
712
|
export const __browserToolsTest = {
|
|
713
|
+
buildSpawnInvocation,
|
|
688
714
|
buildHarnessSpec,
|
|
689
715
|
buildPageArgs,
|
|
690
716
|
buildRunSpec,
|
package/dist/discord.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { once } from "node:events";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { ApplicationCommandOptionType, ApplicationCommandType, ApplicationIntegrationType, ChannelType, Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, Partials, } from "discord.js";
|
|
4
5
|
import { createAgentEventRecorder, storedAgentEventFromAgentEvent, thinkingDurationMs, updateAgentEventSummary, } from "./agent-events.js";
|
|
5
6
|
import { chatChannelKey, createChatLog } from "./chat-log.js";
|
|
@@ -326,6 +327,26 @@ async function delayBetweenBurstChunks(config, channel) {
|
|
|
326
327
|
function normalizeOutboundText(text) {
|
|
327
328
|
return text.trim() || "(empty response)";
|
|
328
329
|
}
|
|
330
|
+
async function discordAttachmentPayload(attachment) {
|
|
331
|
+
if (!attachment.localPath)
|
|
332
|
+
return undefined;
|
|
333
|
+
return {
|
|
334
|
+
attachment: await readFile(attachment.localPath),
|
|
335
|
+
name: attachment.name,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
async function discordAttachmentPayloads(attachments) {
|
|
339
|
+
const payloads = [];
|
|
340
|
+
for (const attachment of attachments) {
|
|
341
|
+
const payload = await discordAttachmentPayload(attachment);
|
|
342
|
+
if (payload)
|
|
343
|
+
payloads.push(payload);
|
|
344
|
+
}
|
|
345
|
+
return payloads;
|
|
346
|
+
}
|
|
347
|
+
export const __test = {
|
|
348
|
+
discordAttachmentPayloads,
|
|
349
|
+
};
|
|
329
350
|
function parseAgentReply(text) {
|
|
330
351
|
const normalized = text.replace(/\r\n/g, "\n").trim();
|
|
331
352
|
if (normalized === SILENT_RESPONSE_MARKER) {
|
|
@@ -344,7 +365,7 @@ async function sendReply(config, message, text, replyToMessageId, attachments =
|
|
|
344
365
|
for (const [index, chunk] of chunks.entries()) {
|
|
345
366
|
if (index > 0)
|
|
346
367
|
await delayBetweenBurstChunks(config, message.channel);
|
|
347
|
-
const files = index === 0 ?
|
|
368
|
+
const files = index === 0 ? await discordAttachmentPayloads(attachments) : [];
|
|
348
369
|
let sent;
|
|
349
370
|
if (index === 0 && config.discord.replyMode === "reply") {
|
|
350
371
|
try {
|
|
@@ -379,7 +400,7 @@ async function sendChannelMessage(config, channel, text, attachments = []) {
|
|
|
379
400
|
for (const [index, chunk] of chunks.entries()) {
|
|
380
401
|
if (index > 0)
|
|
381
402
|
await delayBetweenBurstChunks(config, channel);
|
|
382
|
-
const files = index === 0 ?
|
|
403
|
+
const files = index === 0 ? await discordAttachmentPayloads(attachments) : [];
|
|
383
404
|
const sent = await channel.send(files.length > 0 ? { content: chunk, files } : chunk);
|
|
384
405
|
sentIds.push(sent.id);
|
|
385
406
|
}
|
|
@@ -599,8 +599,13 @@ function lcmRecordPartsFromAgentMessage(message) {
|
|
|
599
599
|
return [];
|
|
600
600
|
const parts = [];
|
|
601
601
|
for (const item of content) {
|
|
602
|
-
if (item.type === "text")
|
|
603
|
-
parts.push({
|
|
602
|
+
if (item.type === "text") {
|
|
603
|
+
parts.push({
|
|
604
|
+
kind: "text",
|
|
605
|
+
text: item.text,
|
|
606
|
+
...(item.textSignature ? { signature: item.textSignature } : {}),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
604
609
|
else if (item.type === "thinking") {
|
|
605
610
|
parts.push({
|
|
606
611
|
kind: "thinking",
|
|
@@ -609,7 +614,13 @@ function lcmRecordPartsFromAgentMessage(message) {
|
|
|
609
614
|
});
|
|
610
615
|
}
|
|
611
616
|
else if (item.type === "toolCall") {
|
|
612
|
-
parts.push({
|
|
617
|
+
parts.push({
|
|
618
|
+
kind: "tool_call",
|
|
619
|
+
toolCallId: item.id,
|
|
620
|
+
toolName: item.name,
|
|
621
|
+
arguments: item.arguments,
|
|
622
|
+
...(item.thoughtSignature ? { signature: item.thoughtSignature } : {}),
|
|
623
|
+
});
|
|
613
624
|
}
|
|
614
625
|
else if (item.type === "image") {
|
|
615
626
|
parts.push({ kind: "text", text: `[image: ${item.mimeType}]` });
|
|
@@ -114,13 +114,18 @@ function estimateUserMessageTokens(message) {
|
|
|
114
114
|
function estimateAssistantMessageTokens(message) {
|
|
115
115
|
let tokens = MESSAGE_OVERHEAD_TOKENS;
|
|
116
116
|
for (const block of message.content) {
|
|
117
|
-
if (block.type === "text")
|
|
117
|
+
if (block.type === "text") {
|
|
118
118
|
tokens += estimateTextTokens(block.text);
|
|
119
|
-
|
|
119
|
+
tokens += estimateTextTokens(block.textSignature ?? "");
|
|
120
|
+
}
|
|
121
|
+
else if (block.type === "thinking") {
|
|
120
122
|
tokens += estimateTextTokens(block.thinking);
|
|
123
|
+
tokens += estimateTextTokens(block.thinkingSignature ?? "");
|
|
124
|
+
}
|
|
121
125
|
else if (block.type === "toolCall") {
|
|
122
126
|
tokens += estimateTextTokens(block.name);
|
|
123
127
|
tokens += estimateJsonTokens(block.arguments);
|
|
128
|
+
tokens += estimateTextTokens(block.thoughtSignature ?? "");
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
return tokens;
|
|
@@ -319,8 +324,13 @@ function structuredLcmRecordToAgentMessage(record, timestamp) {
|
|
|
319
324
|
function structuredAssistantContent(parts) {
|
|
320
325
|
const content = [];
|
|
321
326
|
for (const part of parts) {
|
|
322
|
-
if (part.kind === "text" && part.text)
|
|
323
|
-
content.push({
|
|
327
|
+
if (part.kind === "text" && part.text) {
|
|
328
|
+
content.push({
|
|
329
|
+
type: "text",
|
|
330
|
+
text: part.text,
|
|
331
|
+
...(part.signature ? { textSignature: part.signature } : {}),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
324
334
|
else if (part.kind === "thinking" && part.text) {
|
|
325
335
|
content.push({
|
|
326
336
|
type: "thinking",
|
|
@@ -334,6 +344,7 @@ function structuredAssistantContent(parts) {
|
|
|
334
344
|
id: part.toolCallId,
|
|
335
345
|
name: part.toolName,
|
|
336
346
|
arguments: normalizeToolArguments(part.arguments),
|
|
347
|
+
...(part.signature ? { thoughtSignature: part.signature } : {}),
|
|
337
348
|
});
|
|
338
349
|
}
|
|
339
350
|
}
|