@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 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.
@@ -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 child = spawn(spec.command, spec.args, {
108
- stdio: [spec.stdin ? "pipe" : "ignore", "pipe", "pipe"],
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 ? attachments.flatMap((attachment) => (attachment.localPath ? [attachment.localPath] : [])) : [];
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 ? attachments.flatMap((attachment) => (attachment.localPath ? [attachment.localPath] : [])) : [];
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({ kind: "text", text: item.text });
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({ kind: "tool_call", toolCallId: item.id, toolName: item.name, arguments: item.arguments });
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
- else if (block.type === "thinking")
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({ type: "text", text: part.text });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qearlyao/familiar",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {