@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/CONTACT.md +2 -0
- package/config.example.toml +7 -13
- package/dist/added-models.js +80 -0
- package/dist/agent.js +54 -8
- package/dist/browser-tools.js +100 -30
- package/dist/cli.js +1 -0
- package/dist/config-overrides.js +68 -0
- package/dist/config-registry.js +289 -0
- package/dist/config.js +23 -133
- package/dist/contact-note.js +41 -0
- package/dist/discord.js +17 -7
- package/dist/hot-reload.js +26 -2
- package/dist/models.js +3 -2
- package/dist/persona.js +1 -0
- package/dist/web-tools.js +1 -3
- package/dist/web.js +202 -26
- package/package.json +5 -4
- package/skills/memes/SKILL.md +238 -0
- package/web/dist/assets/index-CUvbIJKO.js +60 -0
- package/web/dist/assets/index-CcQ13VAY.css +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-ClgkMgaq.css +0 -2
- package/web/dist/assets/index-Cu2QquuR.js +0 -59
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
|
-
|
|
551
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
44
|
-
"@earendil-works/pi-ai": "^0.
|
|
45
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
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
|