@makerstufferupper/flypi 0.0.0-stage → 0.1.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -2
  3. package/assets/screenshot.png +0 -0
  4. package/bin/flypi.mjs +28 -0
  5. package/dist/ui/assets/index-CG1CuGxu.js +105 -0
  6. package/dist/ui/assets/index-qaKjH5fH.css +1 -0
  7. package/dist/ui/flypi-logo-dark.jpeg +0 -0
  8. package/dist/ui/flypi-logo-light.jpeg +0 -0
  9. package/dist/ui/index.html +17 -0
  10. package/package.json +71 -5
  11. package/scripts/benchmark-nli.ts +273 -0
  12. package/scripts/flypi.mjs +303 -0
  13. package/scripts/probe-turn-boundary.ts +97 -0
  14. package/scripts/probe.ts +119 -0
  15. package/scripts/record-transcript.ts +63 -0
  16. package/scripts/reembed-memory.ts +41 -0
  17. package/scripts/release.mjs +35 -0
  18. package/scripts/secret.ts +24 -0
  19. package/scripts/serve-daemon.ts +101 -0
  20. package/scripts/serve-memory.ts +78 -0
  21. package/scripts/serve.ts +89 -0
  22. package/scripts/smoke-memory.mjs +239 -0
  23. package/scripts/smoke-secrets.mjs +153 -0
  24. package/scripts/smoke.mjs +59 -0
  25. package/scripts/start.mjs +294 -0
  26. package/scripts/turn-boundary.ts +52 -0
  27. package/scripts/update.ts +92 -0
  28. package/scripts/warm-models.ts +41 -0
  29. package/src/agent/config-files.ts +529 -0
  30. package/src/agent/mcp-signin.ts +137 -0
  31. package/src/agent/mcp-status.ts +168 -0
  32. package/src/agent/onboarding.ts +74 -0
  33. package/src/agent/packages.ts +150 -0
  34. package/src/agent/paths.ts +80 -0
  35. package/src/agent/permission-gate.ts +70 -0
  36. package/src/agent/pi-mcp.ts +97 -0
  37. package/src/agent/runtime.ts +347 -0
  38. package/src/daemon/index.ts +442 -0
  39. package/src/daemon/jobs.ts +202 -0
  40. package/src/daemon/update.ts +248 -0
  41. package/src/memory/capture.ts +109 -0
  42. package/src/memory/config.ts +90 -0
  43. package/src/memory/consolidate.ts +218 -0
  44. package/src/memory/db.ts +393 -0
  45. package/src/memory/embed.ts +154 -0
  46. package/src/memory/judge.ts +167 -0
  47. package/src/memory/mcp.ts +496 -0
  48. package/src/memory/media.ts +135 -0
  49. package/src/memory/progress.ts +39 -0
  50. package/src/memory/recall.ts +528 -0
  51. package/src/memory/review.ts +86 -0
  52. package/src/memory/store.ts +1034 -0
  53. package/src/memory/wiki.ts +140 -0
  54. package/src/secrets/cli.ts +248 -0
  55. package/src/secrets/config.ts +133 -0
  56. package/src/secrets/crypto.ts +180 -0
  57. package/src/secrets/extension.ts +253 -0
  58. package/src/secrets/redact.ts +132 -0
  59. package/src/secrets/vault.ts +396 -0
  60. package/src/server/event-log.ts +49 -0
  61. package/src/server/extension-ui.ts +118 -0
  62. package/src/server/index.ts +398 -0
  63. package/src/server/memory-api.ts +158 -0
  64. package/src/server/protocol.ts +947 -0
  65. package/src/server/run-store.ts +136 -0
  66. package/src/server/secrets-api.ts +88 -0
  67. package/src/server/session-registry.ts +593 -0
  68. package/src/shared/attachments.ts +40 -0
  69. package/src/shared/cache-probe.ts +47 -0
  70. package/src/shared/markdown.ts +17 -0
  71. package/src/shared/metrics.ts +212 -0
  72. package/src/shared/sqlite.ts +74 -0
  73. package/src/shared/transcript.ts +404 -0
  74. package/src/shared/turn-boundary.ts +98 -0
  75. package/src/shared/views.ts +31 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The media a memory can carry — an image or a recording — plus decoding far enough for the
3
+ * embedding model to see it.
4
+ *
5
+ * The bytes are the memory; the path is where it came from. Both are kept: a file can be deleted or
6
+ * moved, and a media row that lost its file still has its bytes and its vector (docs/memory.md,
7
+ * multimodal memory).
8
+ */
9
+ import { readFile } from "node:fs/promises";
10
+ import { extname } from "node:path";
11
+
12
+ export type MediaKind = "image" | "audio";
13
+
14
+ export interface MemoryMedia {
15
+ kind: MediaKind;
16
+ /** Provenance, and what the image decoder reads. */
17
+ path: string;
18
+ mime: string;
19
+ bytes: Uint8Array;
20
+ }
21
+
22
+ /** What EG2's audio encoder is fed: mono, 16 kHz, samples in [-1, 1]. */
23
+ export const AUDIO_RATE = 16_000;
24
+
25
+ const IMAGE_MIME: Record<string, string> = {
26
+ ".png": "image/png",
27
+ ".jpg": "image/jpeg",
28
+ ".jpeg": "image/jpeg",
29
+ ".webp": "image/webp",
30
+ ".gif": "image/gif",
31
+ };
32
+ const AUDIO_MIME: Record<string, string> = { ".wav": "audio/wav" };
33
+
34
+ /**
35
+ * Read a media file and say what it is. Only WAV is accepted for audio: mp3/m4a would need a decoder
36
+ * of our own or an ffmpeg subprocess, and every recorder on this machine can write WAV.
37
+ *
38
+ * shortcut: container == extension, when a caller needs mime sniffing, read the file's magic bytes.
39
+ */
40
+ export async function readMedia(path: string): Promise<MemoryMedia> {
41
+ const extension = extname(path).toLowerCase();
42
+ const mime = IMAGE_MIME[extension] ?? AUDIO_MIME[extension];
43
+ if (!mime) {
44
+ throw new Error(`unsupported media ${extension || path}: images are png/jpeg/webp/gif, audio is wav (re-encode the recording to wav)`);
45
+ }
46
+ const bytes = new Uint8Array(await readFile(path));
47
+ if (bytes.length === 0) throw new Error(`${path} is empty`);
48
+ return { kind: extension in AUDIO_MIME ? "audio" : "image", path, mime, bytes };
49
+ }
50
+
51
+ export interface DecodedAudio {
52
+ samples: Float32Array;
53
+ seconds: number;
54
+ }
55
+
56
+ /**
57
+ * Decode a WAV into mono 16 kHz floats: PCM 8/16/24/32 and IEEE float, any channel count downmixed,
58
+ * any rate resampled. The container's own metadata is trusted, and a file that does not parse says
59
+ * so — a half-decoded voice note would be indexed as a different one.
60
+ */
61
+ export function decodeWav(bytes: Uint8Array): DecodedAudio {
62
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
63
+ const ascii = (at: number) => String.fromCharCode(view.getUint8(at), view.getUint8(at + 1), view.getUint8(at + 2), view.getUint8(at + 3));
64
+ if (bytes.length < 44 || ascii(0) !== "RIFF" || ascii(8) !== "WAVE") {
65
+ throw new Error("not a wav file (no RIFF/WAVE header)");
66
+ }
67
+
68
+ let format: { code: number; channels: number; rate: number; bits: number } | undefined;
69
+ let at = 12;
70
+ while (at + 8 <= bytes.length) {
71
+ const id = ascii(at);
72
+ const size = view.getUint32(at + 4, true);
73
+ const body = at + 8;
74
+ if (id === "fmt ") {
75
+ format = {
76
+ code: view.getUint16(body, true),
77
+ channels: view.getUint16(body + 2, true),
78
+ rate: view.getUint32(body + 4, true),
79
+ bits: view.getUint16(body + 14, true),
80
+ };
81
+ } else if (id === "data" && format) {
82
+ const perSample = format.bits / 8;
83
+ const width = perSample * format.channels; // bytes per frame, all channels
84
+ const declared = Math.floor(size / width);
85
+ const frames = Math.floor((bytes.length - body) / width);
86
+ // A recording cut off mid-write is a different memory than the whole one, so it is refused
87
+ // rather than remembered short.
88
+ if (frames < declared) throw new Error(`wav is truncated: ${frames} of ${declared} samples are there`);
89
+ const mono = new Float32Array(frames);
90
+ for (let frame = 0; frame < frames; frame += 1) {
91
+ let sum = 0;
92
+ for (let channel = 0; channel < format.channels; channel += 1) {
93
+ const offset = body + frame * width + channel * perSample;
94
+ sum += sampleAt(view, offset, format);
95
+ }
96
+ mono[frame] = sum / format.channels;
97
+ }
98
+ return { samples: resample(mono, format.rate, AUDIO_RATE), seconds: mono.length / format.rate };
99
+ }
100
+ at += 8 + size + (size % 2);
101
+ }
102
+ throw new Error("wav file has no data chunk");
103
+ }
104
+
105
+ function sampleAt(view: DataView, offset: number, format: { code: number; bits: number }): number {
106
+ const { code, bits } = format;
107
+ if (code === 3) {
108
+ if (bits === 32) return view.getFloat32(offset, true);
109
+ if (bits === 64) return view.getFloat64(offset, true);
110
+ }
111
+ if (code === 1) {
112
+ if (bits === 8) return (view.getUint8(offset) - 128) / 128; // 8-bit PCM is unsigned
113
+ if (bits === 16) return view.getInt16(offset, true) / 32768;
114
+ if (bits === 24) {
115
+ const packed = view.getUint8(offset) | (view.getUint8(offset + 1) << 8) | (view.getUint8(offset + 2) << 16);
116
+ return (packed << 8) / 2147483648; // << 8 sign-extends a 24-bit sample
117
+ }
118
+ if (bits === 32) return view.getInt32(offset, true) / 2147483648;
119
+ }
120
+ throw new Error(`wav encoding not supported (format ${code}, ${bits}-bit)`);
121
+ }
122
+
123
+ /** Linear interpolation: good enough for a 16 kHz mel spectrogram, and it keeps this dependency-free. */
124
+ function resample(samples: Float32Array, from: number, to: number): Float32Array {
125
+ if (from === to || samples.length === 0) return samples;
126
+ const ratio = to / from;
127
+ const out = new Float32Array(Math.max(0, Math.round(samples.length * ratio)));
128
+ for (let i = 0; i < out.length; i += 1) {
129
+ const at = i / ratio;
130
+ const low = Math.floor(at);
131
+ const high = Math.min(low + 1, samples.length - 1);
132
+ out[i] = samples[low]! + (samples[high]! - samples[low]!) * (at - low);
133
+ }
134
+ return out;
135
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Model-download progress (docs/architecture.md Phase 7). A first run fetches ~1 GB of ONNX weights before
3
+ * the daemon is listening, which looks exactly like a hang from the outside — so every loader takes a
4
+ * callback, and this turns what transformers.js reports into one readable line.
5
+ */
6
+
7
+ /** transformers.js's `progress_callback` payload. `AutoModel.from_pretrained` wraps a plain function, so the
8
+ * aggregate `progress_total` events (all files of the model together) arrive without any wrapping here. */
9
+ export interface ModelProgress {
10
+ status: string;
11
+ name?: string;
12
+ file?: string;
13
+ progress?: number;
14
+ loaded?: number;
15
+ total?: number;
16
+ }
17
+
18
+ const mb = (bytes: number) => Math.round(bytes / 1024 / 1024);
19
+
20
+ /**
21
+ * A callback that logs one line per model: at most twice a second, never going backwards, and always the
22
+ * final 100%. The per-file events are ignored on purpose — a model is several files, and interleaving them
23
+ * in a detached daemon's log is unreadable. Weights already on disk report 100% at once, which is the
24
+ * honest answer for "nothing to download".
25
+ */
26
+ export function modelProgress(model: string, log: (line: string) => void = console.log): (progress: ModelProgress) => void {
27
+ let shown = -1;
28
+ let at = 0;
29
+ return (progress) => {
30
+ if (progress.status !== "progress_total" || typeof progress.progress !== "number") return;
31
+ const percent = Math.floor(progress.progress);
32
+ if (percent <= shown) return;
33
+ const now = Date.now();
34
+ if (percent < 100 && now - at < 500) return;
35
+ shown = percent;
36
+ at = now;
37
+ log(`flypi: downloading ${model}: ${mb(progress.loaded ?? 0)}/${mb(progress.total ?? 0)} MB (${percent}%)`);
38
+ };
39
+ }