@qearlyao/familiar 0.2.0 → 0.2.1

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,34 @@ 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
+ args: ["/d", "/s", "/c", commandLine],
124
+ options: {
125
+ ...options,
126
+ windowsVerbatimArguments: true,
127
+ },
128
+ };
129
+ }
105
130
  function defaultBrowserRunner() {
106
131
  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
- });
132
+ const invocation = buildSpawnInvocation(spec);
133
+ const child = spawn(invocation.command, invocation.args, invocation.options);
111
134
  const timeout = setTimeout(() => {
112
135
  child.kill("SIGTERM");
113
136
  reject(new Error(`Browser command timed out after ${options.timeoutMs}ms.`));
@@ -685,6 +708,7 @@ export function createBrowserTools(config, mediaSink, runner = defaultBrowserRun
685
708
  ];
686
709
  }
687
710
  export const __browserToolsTest = {
711
+ buildSpawnInvocation,
688
712
  buildHarnessSpec,
689
713
  buildPageArgs,
690
714
  buildRunSpec,
@@ -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.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {