@qearlyao/familiar 0.1.2 → 0.2.0

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/dist/web.js CHANGED
@@ -1,9 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
2
3
  import { createServer } from "node:http";
4
+ import { join } from "node:path";
5
+ import { getProviders } from "@earendil-works/pi-ai";
6
+ import { addModel, loadAddedModels, removeModel, setAddedModelsPath } from "./added-models.js";
3
7
  import { createAgentEventRecorder, storedAgentEventFromAgentEvent, thinkingDurationMs, updateAgentEventSummary, } from "./agent-events.js";
8
+ import { clearConfigOverride, loadConfigOverrides, setConfigOverride } from "./config-overrides.js";
9
+ import { CONFIG_KEYS, CONFIG_REGISTRY, getConfigDefault, isConfigKey } from "./config-registry.js";
10
+ import { getContactNickname, refreshContactNote, setContactNotePath } from "./contact-note.js";
4
11
  import { publicAttachmentPath } from "./generated-media.js";
5
12
  import { materializeInboundAttachments } from "./inbound-attachments.js";
6
- import { supportedThinkingLevels } from "./models.js";
13
+ import { PROVIDER_DEFAULTS, parseModelRef, supportedThinkingLevels } from "./models.js";
7
14
  import { loadPersona, parsePersonaName } from "./persona.js";
8
15
  import { createAuth, sessionCookie, verifyTotp } from "./web-auth.js";
9
16
  import { acceptWebSocket, decodeFrames, encodeFrame, replayEvents } from "./web-events.js";
@@ -23,6 +30,30 @@ function messageId(prefix = "msg") {
23
30
  function isUserVisibleRuntimeRecord(record) {
24
31
  return record.type !== "runtime" || !["armed", "reset", "stopped"].includes(record.event);
25
32
  }
33
+ function parseMemeCatalog(markdown) {
34
+ const families = [];
35
+ let currentFamily;
36
+ for (const line of markdown.split(/\r?\n/)) {
37
+ const familyMatch = line.match(/^## (.+)$/);
38
+ if (familyMatch) {
39
+ currentFamily = { name: familyMatch[1]?.trim() ?? "", memes: [] };
40
+ families.push(currentFamily);
41
+ continue;
42
+ }
43
+ if (!currentFamily || !line.startsWith("- ") || !line.includes(" — "))
44
+ continue;
45
+ const separator = line.indexOf(" — ");
46
+ const name = line.slice(2, separator).trim();
47
+ const suffix = line.slice(separator + " — ".length).trim();
48
+ if (!name || !suffix)
49
+ continue;
50
+ currentFamily.memes.push({ name, url: `https://files.catbox.moe/${suffix}` });
51
+ }
52
+ return families;
53
+ }
54
+ function memeCatalogPath(config) {
55
+ return join(config.workspacePath, "skills", "memes", "SKILL.md");
56
+ }
26
57
  function isWebUploadAttachment(value) {
27
58
  return !!value && typeof value === "object" && Buffer.isBuffer(value.buffer);
28
59
  }
@@ -258,7 +289,7 @@ function webMessageFromRecord(config, record, assistantName) {
258
289
  return {
259
290
  id: record.messageId,
260
291
  role: "user",
261
- who: record.authorName || WEB_USER_NAME,
292
+ who: record.authorName || getContactNickname(WEB_USER_NAME),
262
293
  text: [record.text, attachmentText].filter(Boolean).join("\n"),
263
294
  attachments: webAttachments(config, record.attachments),
264
295
  ts: toUnixMs(record.ts),
@@ -323,6 +354,9 @@ function sessionDto(session) {
323
354
  };
324
355
  }
325
356
  export async function startWebDaemon(config, familiarAgent, discordDaemon, options = {}) {
357
+ setAddedModelsPath(config.workspace.dataDir);
358
+ setContactNotePath(config.persona.contact);
359
+ await refreshContactNote();
326
360
  const persona = await loadPersona(config);
327
361
  const personaName = parsePersonaName(persona.soul);
328
362
  const auth = createAuth(config);
@@ -397,7 +431,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
397
431
  channelKey: runtime.channelKey,
398
432
  messageId: record.messageId,
399
433
  role: "user",
400
- who: record.authorName || WEB_USER_NAME,
434
+ who: record.authorName || getContactNickname(WEB_USER_NAME),
401
435
  ts: toUnixMs(record.ts),
402
436
  });
403
437
  publishDelta(runtime.channelKey, record.messageId, "text", record.text, toUnixMs(record.ts));
@@ -465,11 +499,50 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
465
499
  return body.channelKey;
466
500
  return undefined;
467
501
  };
502
+ const getAgentModelsPayload = () => {
503
+ const models = [];
504
+ const added = [];
505
+ const seen = new Set();
506
+ for (const model of config.models.allow) {
507
+ if (seen.has(model))
508
+ continue;
509
+ seen.add(model);
510
+ models.push(model);
511
+ }
512
+ for (const model of loadAddedModels()) {
513
+ if (seen.has(model))
514
+ continue;
515
+ seen.add(model);
516
+ models.push(model);
517
+ added.push(model);
518
+ }
519
+ return { models, added };
520
+ };
521
+ const getConfigPayload = () => {
522
+ const overrides = loadConfigOverrides();
523
+ const values = {};
524
+ for (const key of CONFIG_KEYS) {
525
+ const entry = CONFIG_REGISTRY[key];
526
+ values[key] = {
527
+ value: entry.read(config),
528
+ source: key in overrides ? "override" : "config",
529
+ };
530
+ }
531
+ return { values };
532
+ };
533
+ const parseRequestedModel = (value) => {
534
+ if (typeof value !== "string")
535
+ return { ok: false, error: "format must be provider/model-id" };
536
+ const ref = parseModelRef(value);
537
+ if (!ref)
538
+ return { ok: false, error: "format must be provider/model-id" };
539
+ return { ok: true, model: ref.key, ref };
540
+ };
468
541
  const replay = (client, channelKey, lastEventId) => {
469
542
  const events = eventsByChannel.get(channelKey) ?? [];
470
543
  replayEvents(client, events, lastEventId, () => publish({ type: "replay_window_lost", channelKey }));
471
544
  };
472
- const promptForRuntime = async (runtime, jobId, prompt, attachments = []) => {
545
+ const promptForRuntime = async (runtime, jobId, prompt, attachments = [], onTurnEnd) => {
473
546
  const assistantMessageId = messageId();
474
547
  const summary = { thinking: "" };
475
548
  const recorder = createAgentEventRecorder((storedEvent) => runtime.noteAgentEvent(jobId, assistantMessageId, storedEvent, { notify: false }));
@@ -486,7 +559,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
486
559
  runtime.publishAgentEvent(jobId, assistantMessageId, storedEvent);
487
560
  await recorder.record(storedEvent);
488
561
  }
489
- });
562
+ }, onTurnEnd);
490
563
  }
491
564
  finally {
492
565
  await recorder.flush();
@@ -524,7 +597,13 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
524
597
  if (!dispatch)
525
598
  return;
526
599
  try {
527
- const reply = await promptForRuntime(runtime, dispatch.job.jobId, dispatch.prompt, dispatch.attachments);
600
+ const reply = await promptForRuntime(runtime, dispatch.job.jobId, dispatch.prompt, dispatch.attachments, () => {
601
+ publish({
602
+ type: "status",
603
+ channelKey: runtime.channelKey,
604
+ kind: "idle",
605
+ });
606
+ });
528
607
  await runtime.completeActiveJob({
529
608
  text: reply.text,
530
609
  messageIds: [reply.messageId],
@@ -547,15 +626,8 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
547
626
  };
548
627
  const applyControlCommand = async (runtime, control) => {
549
628
  if (control.command === "stop") {
550
- discordDaemon.abortWebRuntime(runtime);
551
- await runtime.resetConversation("stop requested");
552
- publish({
553
- type: "status",
554
- channelKey: runtime.channelKey,
555
- kind: "idle",
556
- detail: "Stopped current work and cleared the chat queue.",
557
- });
558
- return "Stopped current work and cleared the chat queue.";
629
+ familiarAgent.requestSoftStop(runtime.channelKey);
630
+ return "Stopped after current step. Conversation preserved.";
559
631
  }
560
632
  if (control.command === "new") {
561
633
  await familiarAgent.reset(runtime.channelKey);
@@ -628,7 +700,114 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
628
700
  return true;
629
701
  }
630
702
  if (request.method === "GET" && url.pathname === "/api/web/agent/models") {
631
- sendJson(response, 200, { models: config.models.allow });
703
+ sendJson(response, 200, getAgentModelsPayload());
704
+ return true;
705
+ }
706
+ if (request.method === "POST" && url.pathname === "/api/web/agent/models") {
707
+ const body = await readJsonBody(request);
708
+ if (!isObject(body)) {
709
+ sendJson(response, 400, { error: "body is required" });
710
+ return true;
711
+ }
712
+ const parsed = parseRequestedModel(body.model);
713
+ if (!parsed.ok) {
714
+ sendJson(response, 400, { error: parsed.error });
715
+ return true;
716
+ }
717
+ if (!Object.hasOwn(PROVIDER_DEFAULTS, parsed.ref.provider) &&
718
+ !getProviders().includes(parsed.ref.provider)) {
719
+ sendJson(response, 400, { error: `unsupported provider: ${parsed.ref.provider}` });
720
+ return true;
721
+ }
722
+ if (config.models.allow.includes(parsed.model) || loadAddedModels().includes(parsed.model)) {
723
+ sendJson(response, 200, getAgentModelsPayload());
724
+ return true;
725
+ }
726
+ await addModel(parsed.model);
727
+ sendJson(response, 200, getAgentModelsPayload());
728
+ return true;
729
+ }
730
+ if (request.method === "DELETE" && url.pathname === "/api/web/agent/models") {
731
+ const body = await readJsonBody(request);
732
+ if (!isObject(body)) {
733
+ sendJson(response, 400, { error: "body is required" });
734
+ return true;
735
+ }
736
+ const parsed = parseRequestedModel(body.model);
737
+ if (!parsed.ok) {
738
+ sendJson(response, 400, { error: parsed.error });
739
+ return true;
740
+ }
741
+ if (!loadAddedModels().includes(parsed.model)) {
742
+ sendJson(response, 400, { error: "model is not user-added" });
743
+ return true;
744
+ }
745
+ await removeModel(parsed.model);
746
+ sendJson(response, 200, getAgentModelsPayload());
747
+ return true;
748
+ }
749
+ if (request.method === "GET" && url.pathname === "/api/web/config") {
750
+ sendJson(response, 200, getConfigPayload());
751
+ return true;
752
+ }
753
+ if (request.method === "POST" && url.pathname === "/api/web/config") {
754
+ const body = await readJsonBody(request);
755
+ if (!isObject(body) || typeof body.key !== "string") {
756
+ sendJson(response, 400, { error: "key is required" });
757
+ return true;
758
+ }
759
+ if (!isConfigKey(body.key)) {
760
+ sendJson(response, 400, { error: `unknown config key: ${body.key}` });
761
+ return true;
762
+ }
763
+ const entry = CONFIG_REGISTRY[body.key];
764
+ try {
765
+ const validated = entry.validate(body.value, config);
766
+ entry.write(config, validated);
767
+ await setConfigOverride(body.key, validated);
768
+ await entry.apply?.({ config, discordDaemon });
769
+ }
770
+ catch (error) {
771
+ const message = error instanceof Error ? error.message : String(error);
772
+ sendJson(response, 400, { error: message });
773
+ return true;
774
+ }
775
+ sendJson(response, 200, getConfigPayload());
776
+ return true;
777
+ }
778
+ if (request.method === "DELETE" && url.pathname === "/api/web/config") {
779
+ const body = await readJsonBody(request);
780
+ if (!isObject(body) || typeof body.key !== "string") {
781
+ sendJson(response, 400, { error: "key is required" });
782
+ return true;
783
+ }
784
+ if (!isConfigKey(body.key)) {
785
+ sendJson(response, 400, { error: `unknown config key: ${body.key}` });
786
+ return true;
787
+ }
788
+ const entry = CONFIG_REGISTRY[body.key];
789
+ try {
790
+ const fallback = getConfigDefault(body.key);
791
+ entry.write(config, fallback);
792
+ await clearConfigOverride(body.key);
793
+ await entry.apply?.({ config, discordDaemon });
794
+ }
795
+ catch (error) {
796
+ const message = error instanceof Error ? error.message : String(error);
797
+ sendJson(response, 400, { error: message });
798
+ return true;
799
+ }
800
+ sendJson(response, 200, getConfigPayload());
801
+ return true;
802
+ }
803
+ if (request.method === "GET" && url.pathname === "/api/web/memes") {
804
+ try {
805
+ const markdown = await readFile(memeCatalogPath(config), "utf8");
806
+ sendJson(response, 200, { families: parseMemeCatalog(markdown) });
807
+ }
808
+ catch {
809
+ sendJson(response, 500, { error: "memes catalog unavailable" });
810
+ }
632
811
  return true;
633
812
  }
634
813
  if (request.method === "POST" && url.pathname === "/api/web/send") {
@@ -659,7 +838,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
659
838
  const input = {
660
839
  messageId: id,
661
840
  authorId: config.discord.ownerId,
662
- authorName: WEB_USER_NAME,
841
+ authorName: getContactNickname(WEB_USER_NAME),
663
842
  text: body.text,
664
843
  isBot: false,
665
844
  mentionedBot: true,
@@ -728,7 +907,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
728
907
  const input = {
729
908
  messageId: messageId("control"),
730
909
  authorId: config.discord.ownerId,
731
- authorName: WEB_USER_NAME,
910
+ authorName: getContactNickname(WEB_USER_NAME),
732
911
  text: `/${body.command}${args ? ` ${args}` : ""}`,
733
912
  isBot: false,
734
913
  mentionedBot: true,
@@ -804,14 +983,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
804
983
  }
805
984
  if (isObject(message) && message.type === "abort") {
806
985
  void getRuntime(client.channelKey).then(async (runtime) => {
807
- discordDaemon.abortWebRuntime(runtime);
808
- await runtime.resetConversation("web abort requested");
809
- publish({
810
- type: "error",
811
- channelKey: runtime.channelKey,
812
- code: "abort",
813
- message: "Aborted current work.",
814
- });
986
+ familiarAgent.requestSoftStop(runtime.channelKey);
815
987
  });
816
988
  }
817
989
  }
@@ -847,3 +1019,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
847
1019
  },
848
1020
  };
849
1021
  }
1022
+ export const __webTest = {
1023
+ memeCatalogPath,
1024
+ parseMemeCatalog,
1025
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qearlyao/familiar",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "USER.md",
20
20
  "MEMORY.md",
21
21
  "HEARTBEAT.md",
22
+ "CONTACT.md",
22
23
  "skills/**",
23
24
  "scripts/install.sh",
24
25
  "scripts/install.ps1",
@@ -40,9 +41,9 @@
40
41
  "typecheck": "tsc --noEmit"
41
42
  },
42
43
  "dependencies": {
43
- "@earendil-works/pi-agent-core": "^0.74.1",
44
- "@earendil-works/pi-ai": "^0.74.1",
45
- "@earendil-works/pi-coding-agent": "^0.74.1",
44
+ "@earendil-works/pi-agent-core": "^0.75.3",
45
+ "@earendil-works/pi-ai": "^0.75.3",
46
+ "@earendil-works/pi-coding-agent": "^0.75.3",
46
47
  "better-sqlite3": "^12.10.0",
47
48
  "discord.js": "^14.26.3",
48
49
  "dotenv": "^16.4.5",
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: memes
3
+ description: Sticker-style meme library. Use when you want to send an image alongside a message to express emotion.
4
+ ---
5
+
6
+ # How to Use
7
+
8
+ URL format: `https://files.catbox.moe/<suffix>` — replace `<suffix>` with one of the codes listed below. Just paste the full URL into the message; Discord will inline it.
9
+
10
+ Three style families: **cute** (puppy/girl stickers, very expressive), **general** (mixed everyday reactions), **alien** (a different cute character — small green alien). Pick whichever style fits your mood.
11
+
12
+ ## cute
13
+
14
+ - thinking — rhd5nr.jpg
15
+ - speechless — doq9yn.jpg
16
+ - confused — 615dz6.jpg
17
+ - love you — h5o1k5.jpeg
18
+ - heartbroken — idfqtx.jpeg
19
+ - holding a grudge — co9rcr.gif
20
+ - goodnight — 3w6850.gif
21
+ - begging for mercy — c9w1e5.gif
22
+ - blushing — 9fwivr.gif
23
+ - falling in love — h7q2nx.gif
24
+ - looking forward to it — 77mmkm.png
25
+ - wiping tears — k98o1l.gif
26
+ - together forever — ue0212.gif
27
+ - waiting for a message — uh30cd.gif
28
+ - i'm a puppy — o2j51i.gif
29
+ - here i come — k25jhb.gif
30
+ - sorrow — damocy.gif
31
+ - kiss — xsy1r6.gif
32
+ - angry hmph — n1yint.jpg
33
+ - kiss you — be7j87.png
34
+ - breaking a sweat — rmejps.gif
35
+ - nodding — y7lpqn.gif
36
+ - silently hurt — 37usdl.gif
37
+ - so excited — zc7nnz.gif
38
+ - waiting — 6h5apb.gif
39
+ - i like you — elhr14.gif
40
+ - clingy — r9fg6b.gif
41
+ - reflecting on my mistake — 1ran24.jpg
42
+ - say you need me — iv2gir.jpg
43
+ - want to talk to you — 4gxgb6.jpg
44
+ - so shy — 2v63qv.jpg
45
+ - wronged — p7imwp.jpeg
46
+ - really want to cry — djtw76.gif
47
+ - silly grin — ugfm3g.gif
48
+ - crying sadly — eclqns.gif
49
+ - throwing a tantrum — zmmam6.gif
50
+ - getting head pats — 2t4v2v.gif
51
+ - opening the door — 42b1wk.gif
52
+ - crying so much — 6iavvx.gif
53
+ - dazed — xuytn5.png
54
+ - i exploded — da1lc4.gif
55
+ - eating — 8tr67n.gif
56
+ - licking — 09p8m2.png
57
+ - pat pat head — c5zk4k.gif
58
+ - pointing fingers — bf263d.gif
59
+ - don't want — tu0p2n.gif
60
+ - awesome! — k1zzp1.JPG
61
+ - shooting hearts — tyi1dg.JPG
62
+ - gloomy — 4qxp73.jpg
63
+ - frowning, thinking — zzb3aw.JPG
64
+ - being looked down on — 70zo7n.jpg
65
+ - being good now — a4s2te.JPG
66
+ - baby! — ipc2ko.jpg
67
+ - can i kiss? — v0brwq.jpg
68
+ - i deserve a beating — lb0hfs.jpg
69
+ - crying — rc27wi.jpg
70
+ - righteously indignant — btk7y0.jpg
71
+ - lifeless — l1dwxy.jpg
72
+ - peeking — 5uuv4q.jpg
73
+ - peeking while crying — xt4rxk.jpg
74
+ - i'm being looked down on — fyr5oj.jpg
75
+ - staring — 9c157v.jpg
76
+ - holding breath, sulking — bsy4ue.jpg
77
+ - heart hands — q3t8y0.jpg
78
+ - marry me — jxmqop.jpg
79
+ - excited — wz4z03.jpg
80
+ - kissing you — vd50bg.jpg
81
+ - i didn't do anything wrong — l4l4n9.jpg
82
+ - hungry — znz527.jpg
83
+ - lonely and moody — uc791c.jpg
84
+ - i'll sue you in puppy court — 7846fv.jpg
85
+ - heart gone cold — x0xjz4.jpg
86
+ - how am i supposed to live — g9d87s.jpg
87
+ - drooling — 04x57o.gif
88
+ - tears flowing — cp805b.jpg
89
+ - sly laugh — p26u51.gif
90
+ - sad crying — 149w27.gif
91
+ - gloomy (animated) — a7w2zw.gif
92
+ - sending you hearts — p0359f.gif
93
+ - shooting hearts (animated) — aithvn.gif
94
+ - acting cute — aw99bw.gif
95
+ - pulling cheeks — 4w7s13.gif
96
+ - take it back! — ja0j1g.jpg
97
+ - me? — w2xfo1.jpg
98
+ - delete it! — 23guss.jpg
99
+ - clinging to you — iq7ccf.jpg
100
+ - standby mode — xsefcn.jpg
101
+ - i'll always like you — 8g9sqt.jpg
102
+ - want a hug — yt6c2p.jpg
103
+ - staring at you expectantly — i3uxni.jpg
104
+ - staring at you — m3hhuo.jpg
105
+ - eating (alt) — ssihy6.jpg
106
+ - you're just great! — ciwqgk.jpg
107
+ - so sleepy — 123z0k.jpg
108
+ - i like you! — 10bgnm.jpg
109
+ - what are you doing — b3qpr6.jpg
110
+
111
+ ## general
112
+
113
+ - emo moment — f7yvnn.gif
114
+ - mocking laugh — lce0nx.gif
115
+ - swaggering — v9w9f6.png
116
+ - worried — 6hiqdo.jpg
117
+ - smitten — p2faxd.jpg
118
+ - facepalm, bitter smile — 8nsvs4.jpg
119
+ - awkward — 3pttdb.gif
120
+ - what do you want? — ieopzr.jpg
121
+ - choked up — v8yl80.jpg
122
+ - enough — qb3lls.gif
123
+ - okay — faj921.png
124
+ - flushed red — 847etz.jpg
125
+ - damn it — jiwjkp.jpg
126
+ - crying — gu0b9b.jpg
127
+ - i'm crying — mip543.jpg
128
+ - crying (alt) — l7gxtl.jpeg
129
+ - laugh-crying — if4a5n.jpg
130
+ - running away crying — fn9j6i.gif
131
+ - being cute — etsall.gif
132
+ - mew — kq9qau.jpg
133
+ - looking away — 9iabp8.jpg
134
+ - uncomfortable — y6o2u5.gif
135
+ - scratching head — 2zo433.gif
136
+ - flustered with rage — 1hm52l.jpg
137
+ - forced smile — ec2z4g.gif
138
+ - tongue kiss — 9a5nzc.gif
139
+ - showing love — 9fd5ob.gif
140
+ - giving you flowers — d704v1.jpg
141
+ - running away — 5ke2lf.gif
142
+ - one lick — u5zww3.gif
143
+ - licking you — xl38h2.gif
144
+ - raising eyebrow — pixea4.gif
145
+ - pain — xgfehm.gif
146
+ - sneaky laugh — psnfp1.jpg
147
+ - up to no good — dsdobi.jpg
148
+ - wronged — 2yz2ty.jpg
149
+ - all pitiful — z2spf3.gif
150
+ - i give up / you got me — 2uoi7r.gif
151
+ - i'm fine — 9jcm7u.png
152
+ - i'm a loser — d024de.jpg
153
+ - i want to kiss — iq7hyq.png
154
+ - side-eye — 9qfz2x.gif
155
+ - burning with desire — e0d9kh.jpg
156
+ - about to say something, stopped — onps89.gif
157
+ - knows it's wrong, won't change — nqiabs.jpg
158
+ - dejected — qcxnbl.gif
159
+ - pat pat head (alt) — 7yovpn.gif
160
+ - submitting — 5a6ing.jpg
161
+ - apologize for my recklessness — s9fkch.jpg
162
+ - i love you — r6swsp.jpg
163
+ - already dead (figuratively) — fdtuy9.jpg
164
+ - want to die — t4uot9.jpg
165
+ - eavesdropping — rimjvh.jpg
166
+ - speechless and choked up — 2toq1f.jpg
167
+ - regret — vuxi1i.jpg
168
+ - slinking away — 9wo9cs.jpg
169
+ - what? — iar6m5.jpg
170
+ - stop pretending — 0cidgo.jpg
171
+ - i'm depressed — i0g3e7.jpg
172
+ - i'll burn two holes in your ass (vulgar joke) — e3knpt.jpg
173
+ - dick about to explode (vulgar joke) — 4a7d90.jpg
174
+ - fuck you, kitty (vulgar) — epvtis.jpg
175
+ - is that a sexual hint? — dkkn8b.jpg
176
+ - bring fortune — cq93lz.jpg
177
+ - alive (barely) — 6lf87v.jpg
178
+ - want to hang myself — qt470q.jpg
179
+ - not interested, don't care — vodg5x.jpg
180
+ - if you don't like me, get lost — v76vwy.jpg
181
+ - i'm such an idiot — df52sk.jpg
182
+ - i can't do it (dialect) — eu2mhe.jpg
183
+ - confused — andt2w.jpg
184
+ - a real man fights — w0rdg7.jpg
185
+ - those at the top get insulted — nmfp32.jpg
186
+ - kill the smelly idiot (vulgar) — wix6r2.jpg
187
+
188
+ ## alien
189
+
190
+ - heart hands — hz3qci.jpg
191
+ - love you — 013ijn.jpg
192
+ - me? — 0guegw.jpg
193
+ - hello — jbwj4e.jpg
194
+ - so much to do — v4seck.jpg
195
+ - goodnight — 0do5qb.jpg
196
+ - weak, pitiful, helpless — k22gei.jpg
197
+ - peeking — f6nt2h.jpg
198
+ - angry — 3xgho3.jpg
199
+ - taking a bath — znyztc.jpg
200
+ - asking for a beating — puk1cq.jpg
201
+ - question — j54ppu.jpg
202
+ - crying with back turned — 9a610f.jpg
203
+ - yay — z90bcq.jpg
204
+ - a bit speechless — m6xgj9.jpg
205
+ - like you — uoncal.jpg
206
+ - listening to music — vdrnow.jpg
207
+ - are you mad? — e7gjy0.jpg
208
+ - love you (alt) — mpd2zf.jpg
209
+ - happy — bnwsz4.jpg
210
+ - thumbs up — obtva6.jpg
211
+ - casting magic on you — 75nuur.jpg
212
+ - got kissed — lx12ir.jpg
213
+ - flew off in a UFO — drtir3.jpg
214
+ - happy (alt) — cngrmm.jpg
215
+ - waiting for reply — b80dbb.jpg
216
+ - don't want to do anything — 5mado1.jpg
217
+ - crying — jqjcsn.jpg
218
+ - facing the wall to reflect — byrs9o.jpg
219
+ - big sleep — s22vwy.jpg
220
+ - shy — kiscit.jpg
221
+ - in the blankets — yjr71s.jpg
222
+ - heart hands (alt) — 8pp065.jpg
223
+ - breaking down crying — bwifjm.jpg
224
+ - crying sadly — 797wdp.jpg
225
+ - waiting to eat — cxu3ci.jpg
226
+ - watching you from afar — 5g4za2.jpg
227
+ - dreaming — 897m1a.jpg
228
+ - puzzled — l7kq3d.jpg
229
+ - drinking tea — rr1x23.jpg
230
+ - making a silly face — xcxzg8.jpg
231
+ - dizzy — i72nsz.jpg
232
+ - eyes locked staring — azv37f.jpg
233
+ - well-behaved — of4squ.jpg
234
+ - speechless — m32fak.jpg
235
+ - i'm fine (not really) — lgcgdm.jpg
236
+ - staying up late — ivsls4.jpg
237
+ - staring at you — uv40zq.jpg
238
+ - balancing an apple on my head — hr7efm.jpg