@qearlyao/familiar 0.3.0 → 0.4.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/HEARTBEAT.md +1 -1
- package/README.md +29 -0
- package/config.example.toml +2 -0
- package/dist/{agent.js → agent/factory.js} +11 -11
- package/dist/agent/session-helpers.js +1 -1
- package/dist/agent/tools.js +4 -4
- package/dist/cli.js +11 -11
- package/dist/{config.js → config/index.js} +7 -7
- package/dist/config/model-refs.js +1 -1
- package/dist/{config-overrides.js → config/overrides.js} +1 -1
- package/dist/{config-registry.js → config/registry.js} +2 -2
- package/dist/{settings.js → config/settings.js} +2 -2
- package/dist/{chat-log.js → conversation/chat-log.js} +1 -1
- package/dist/{contact-note.js → conversation/contact-note.js} +1 -1
- package/dist/{owner-identity.js → conversation/owner-identity.js} +2 -2
- package/dist/discord/channel.js +1 -1
- package/dist/discord/commands.js +1 -1
- package/dist/{discord.js → discord/daemon.js} +17 -17
- package/dist/discord/inbound.js +1 -1
- package/dist/discord/send.js +29 -20
- package/dist/discord/turn.js +3 -3
- package/dist/index.js +12 -12
- package/dist/{data-retention.js → lifecycle/data-retention.js} +1 -1
- package/dist/{hot-reload.js → lifecycle/hot-reload.js} +2 -2
- package/dist/media/attachment-limits.js +3 -0
- package/dist/{generated-media.js → media/generated-media.js} +1 -1
- package/dist/{image-gen.js → media/image-gen.js} +2 -2
- package/dist/{inbound-attachments.js → media/inbound-attachments.js} +47 -43
- package/dist/media/media-understanding.js +215 -0
- package/dist/memory/lcm/summarizer.js +1 -1
- package/dist/{added-models.js → models/added-models.js} +1 -1
- package/dist/{persona.js → prompting/persona.js} +1 -1
- package/dist/{agent-core.js → runtime/agent-core.js} +1 -1
- package/dist/{agent-events.js → runtime/agent-events.js} +1 -1
- package/dist/{agent-work-queue.js → runtime/agent-work-queue.js} +2 -2
- package/dist/{runtime.js → runtime/conversation-runtime.js} +3 -3
- package/dist/{runtime-manager.js → runtime/runtime-manager.js} +2 -2
- package/dist/{scheduler-runner.js → runtime/scheduler-runner.js} +1 -1
- package/dist/{scheduler.js → runtime/scheduler.js} +3 -3
- package/dist/{browser-tools.js → tools/browser-tools.js} +17 -26
- package/dist/util/fs.js +2 -1
- package/dist/web/agent-routes.js +104 -0
- package/dist/web/auth-routes.js +39 -0
- package/dist/web/auth.js +124 -30
- package/dist/web/config-routes.js +55 -0
- package/dist/web/conversation-routes.js +122 -0
- package/dist/web/daemon.js +108 -0
- package/dist/web/diary-routes.js +88 -0
- package/dist/web/errors.js +3 -0
- package/dist/web/event-hub.js +3 -3
- package/dist/web/messages.js +13 -10
- package/dist/web/multipart.js +7 -1
- package/dist/web/payloads.js +1 -1
- package/dist/web/request-context.js +25 -0
- package/dist/web/route-helpers.js +9 -0
- package/dist/web/routes.js +37 -0
- package/dist/web/runtime-actions.js +231 -0
- package/dist/web/session-store.js +161 -0
- package/dist/web/static.js +1 -1
- package/dist/web/stream.js +12 -3
- package/dist/{web-tools.js → web-tools/index.js} +8 -8
- package/npm-shrinkwrap.json +79 -2
- package/package.json +3 -1
- package/web/dist/assets/index-C-k4O5Dz.js +6 -0
- package/web/dist/assets/index-Dj-L9nX4.css +2 -0
- package/web/dist/assets/markdown-kaIeGxdv.js +14 -0
- package/web/dist/assets/react-Bi_azaFt.js +9 -0
- package/web/dist/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
- package/web/dist/assets/ui-C12-nN_X.js +51 -0
- package/web/dist/assets/vendor-D1QXMhXm.js +16 -0
- package/web/dist/index.html +7 -2
- package/dist/media-understanding.js +0 -120
- package/dist/web.js +0 -641
- package/web/dist/assets/index-CSkxUQCr.js +0 -63
- package/web/dist/assets/index-DllM6RqL.css +0 -2
- /package/dist/{ids.js → conversation/ids.js} +0 -0
- /package/dist/{control.js → lifecycle/control.js} +0 -0
- /package/dist/{service.js → lifecycle/service.js} +0 -0
- /package/dist/{image-derivatives.js → media/image-derivatives.js} +0 -0
- /package/dist/{tts.js → media/tts.js} +0 -0
- /package/dist/{models.js → models/index.js} +0 -0
- /package/dist/{skills.js → prompting/skills.js} +0 -0
- /package/dist/{silent-marker.js → runtime/silent-marker.js} +0 -0
package/HEARTBEAT.md
CHANGED
|
@@ -8,7 +8,7 @@ you can message me first. i'm not waiting on you — but if something's been sit
|
|
|
8
8
|
|
|
9
9
|
## reflect
|
|
10
10
|
|
|
11
|
-
write today's diary entry (memories/diaries/YYYY-MM-DD.md). these aren't homework — they're how you stay coherent across the gaps. if today felt like nothing in particular, "today felt like nothing in particular" is a real entry.
|
|
11
|
+
write today's diary entry (memories/diaries/YYYY-MM-DD.md). these aren't homework — they're how you stay coherent across the gaps. if today felt like nothing in particular, "today felt like nothing in particular" is a real entry. give it a real `# title` at the top — something that names the day's feeling or thread, not the date (the date's already in the filename, and repeating it just shows up twice on my end).
|
|
12
12
|
|
|
13
13
|
## pursue
|
|
14
14
|
|
package/README.md
CHANGED
|
@@ -153,6 +153,35 @@ The WebUI listens on the configured `[web]` port and bind address. The default
|
|
|
153
153
|
`tailscale-only` auth mode currently means "trust the network boundary"; it does
|
|
154
154
|
not verify Tailscale identity yet.
|
|
155
155
|
|
|
156
|
+
For a VPS behind nginx or another HTTPS reverse proxy, keep Familiar bound to
|
|
157
|
+
loopback and use bearer login:
|
|
158
|
+
|
|
159
|
+
```toml
|
|
160
|
+
[web]
|
|
161
|
+
port = 8787
|
|
162
|
+
bind_address = "127.0.0.1"
|
|
163
|
+
auth_mode = "bearer"
|
|
164
|
+
bearer_token = "${FAMILIAR_WEB_BEARER_TOKEN}"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Familiar treats `FAMILIAR_WEB_BEARER_TOKEN` as the WebUI login secret. A
|
|
168
|
+
successful browser login creates an HttpOnly device cookie; the token is not
|
|
169
|
+
stored in the browser. Nginx should terminate HTTPS and pass WebSocket upgrades:
|
|
170
|
+
|
|
171
|
+
```nginx
|
|
172
|
+
location / {
|
|
173
|
+
proxy_pass http://127.0.0.1:8787;
|
|
174
|
+
proxy_set_header Host $host;
|
|
175
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
176
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
177
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
178
|
+
proxy_set_header Connection "upgrade";
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Familiar trusts forwarded IP/proto headers only when the direct proxy connection
|
|
183
|
+
comes from loopback.
|
|
184
|
+
|
|
156
185
|
## Service Management
|
|
157
186
|
|
|
158
187
|
macOS and Linux users can install a user-level service after configuring the
|
package/config.example.toml
CHANGED
|
@@ -12,8 +12,10 @@ allow_bot_messages = true
|
|
|
12
12
|
[web]
|
|
13
13
|
port = 8787
|
|
14
14
|
auth_mode = "tailscale-only" # tailscale-only | bearer | public-2fa
|
|
15
|
+
# bearer mode uses this token as the WebUI login secret, then issues device cookies.
|
|
15
16
|
bearer_token = "${FAMILIAR_WEB_BEARER_TOKEN:-}"
|
|
16
17
|
totp_secret = "${FAMILIAR_WEB_TOTP_SECRET:-}"
|
|
18
|
+
# keep 127.0.0.1 when exposing through nginx/caddy on a VPS.
|
|
17
19
|
bind_address = "127.0.0.1"
|
|
18
20
|
|
|
19
21
|
[browser]
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { streamSimple } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
3
|
+
import { setConfigOverridesPath } from "../config/overrides.js";
|
|
4
|
+
import { applyConfigOverridesToConfig } from "../config/registry.js";
|
|
5
|
+
import { createGeneratedMediaSink } from "../media/generated-media.js";
|
|
6
|
+
import { setAddedModelsPath } from "../models/added-models.js";
|
|
7
|
+
import { clampConfiguredThinkingLevel, createConfiguredModel, isThinkingLevel, parseModelRef, resolveModel, supportedThinkingLevels, } from "../models/index.js";
|
|
8
|
+
import { buildSystemPrompt, loadPersona } from "../prompting/persona.js";
|
|
9
|
+
import { formatFamiliarSkillsForPrompt, loadFamiliarSkills, logSkillDiagnostics } from "../prompting/skills.js";
|
|
10
|
+
import { normalizeProviderPayload } from "./payload-normalizers.js";
|
|
11
|
+
import { assertModelAllowed, deriveSessionId, formatModel, getLastAssistantText, getRequestApiKey, installProviderDebugFilter, isNoisyProviderDebug, logUsage, resolveModelName, userTextMessage, } from "./session-helpers.js";
|
|
12
|
+
import { createFamiliarTools, setReferenceAttachments } from "./tools.js";
|
|
13
|
+
import { loadStoredMessages, writePayloadLog, writeTranscriptLog } from "./transcript-log.js";
|
|
14
14
|
export const __agentTest = {
|
|
15
15
|
isNoisyProviderDebug,
|
|
16
16
|
normalizeProviderPayload,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { assertModelCanAuthenticate, isAllowedModel, resolveModelApiKey } from "../models.js";
|
|
2
|
+
import { assertModelCanAuthenticate, isAllowedModel, resolveModelApiKey } from "../models/index.js";
|
|
3
3
|
const NOISY_GOOGLE_VERTEX_AUTH_DEBUG = "The user provided project/location will take precedence over the API key from the environment variables.";
|
|
4
4
|
let providerDebugFilterInstalled = false;
|
|
5
5
|
export function deriveSessionId(workspacePath, sessionKey) {
|
package/dist/agent/tools.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { createWebTools } from "../web-tools.js";
|
|
2
|
+
import { createImageGenTool } from "../media/image-gen.js";
|
|
3
|
+
import { createTtsTool } from "../media/tts.js";
|
|
4
|
+
import { createBrowserTools } from "../tools/browser-tools.js";
|
|
5
|
+
import { createWebTools } from "../web-tools/index.js";
|
|
6
6
|
import { BASH_DESCRIPTION, EDIT_DESCRIPTION, READ_DESCRIPTION, WRITE_DESCRIPTION } from "./tool-descriptions.js";
|
|
7
7
|
export function createFamiliarTools(config, mediaSink, referenceAttachments = () => [], memoryService) {
|
|
8
8
|
const bashTool = createBashTool(config.workspacePath);
|
package/dist/cli.js
CHANGED
|
@@ -5,19 +5,19 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { config as loadDotenv } from "dotenv";
|
|
8
|
-
import { createFamiliarAgent } from "./agent.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { startDiscordDaemon } from "./discord.js";
|
|
13
|
-
import {
|
|
14
|
-
import { startWorkspaceHotReload } from "./hot-reload.js";
|
|
8
|
+
import { createFamiliarAgent } from "./agent/factory.js";
|
|
9
|
+
import { loadConfig } from "./config/index.js";
|
|
10
|
+
import { loadSettingsStore } from "./config/settings.js";
|
|
11
|
+
import { loadOwnerIdentity } from "./conversation/owner-identity.js";
|
|
12
|
+
import { startDiscordDaemon } from "./discord/daemon.js";
|
|
13
|
+
import { runDataRetention } from "./lifecycle/data-retention.js";
|
|
14
|
+
import { startWorkspaceHotReload } from "./lifecycle/hot-reload.js";
|
|
15
|
+
import { formatServiceResult, installService, serviceStatus, uninstallService, upgradeFamiliar, } from "./lifecycle/service.js";
|
|
16
|
+
import { cleanupGeneratedAttachments } from "./media/generated-media.js";
|
|
15
17
|
import { memoryHelp, runMemoryOperator } from "./memory/operator.js";
|
|
16
18
|
import { createMemoryService } from "./memory/service.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { loadSettingsStore } from "./settings.js";
|
|
20
|
-
import { startWebDaemon } from "./web.js";
|
|
19
|
+
import { createAgentCore } from "./runtime/agent-core.js";
|
|
20
|
+
import { startWebDaemon } from "./web/daemon.js";
|
|
21
21
|
const SOURCE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
const PROJECT_ROOT = resolve(SOURCE_DIR, "..");
|
|
23
23
|
const DEFAULT_WORKSPACE_PATH = resolve(homedir(), ".familiar");
|
|
@@ -2,13 +2,13 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { parse } from "smol-toml";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
5
|
+
import { resolveProviderSetting } from "../models/index.js";
|
|
6
|
+
import { readEnum } from "../util/guards.js";
|
|
7
|
+
import { BROWSER_BACKENDS, BROWSER_WINDOW_MODES, CACHE_RETENTIONS, DISCORD_CHANNEL_TRIGGERS, DISCORD_CHUNK_MODES, DISCORD_DISPATCH_MODES, DISCORD_REPLY_MODES, IMAGE_GEN_APIS, MEDIA_UNDERSTANDING_PROVIDERS, MEMORY_EMBEDDING_FORMATS, THINKING_LEVELS, TTS_PROVIDERS, WEB_AUTH_MODES, } from "./enums.js";
|
|
8
|
+
import { interpolateValue } from "./interpolate.js";
|
|
9
|
+
import { maybeParseProviderModelRef, parseProviderModelRef } from "./model-refs.js";
|
|
10
|
+
import { assertKnownKeys, readBoolean, readConfigString, readFraction, readInteger, readIntegerInRange, readNumberInRange, readOptionalConfigString, readOptionalInteger, readOptionalString, readPositiveNumber, readString, readStringArray, readStringRecord, resolveWorkspacePath, } from "./readers.js";
|
|
11
|
+
import { defaultBrowserAllowedSites, readCronJobs, readPromptOverrides } from "./sections.js";
|
|
12
12
|
const loggedConfigWarnings = new Set();
|
|
13
13
|
const DEFAULT_MEMORY_EMBEDDING_BASE_URLS = {
|
|
14
14
|
google: "https://generativelanguage.googleapis.com/v1beta",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { atomicWriteJson, createWriteQueue, isEnoent } from "
|
|
3
|
+
import { atomicWriteJson, createWriteQueue, isEnoent } from "../util/fs.js";
|
|
4
4
|
let overridesPath = resolve(process.cwd(), "data", "settings", "config-overrides.json");
|
|
5
5
|
let loaded = false;
|
|
6
6
|
let cache = {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isAllowedModel, parseModelRef, resolveProviderSetting } from "../models/index.js";
|
|
2
|
+
import { clearConfigOverride, loadConfigOverrides, setConfigOverride } from "./overrides.js";
|
|
3
3
|
function requireBoolean(value, key) {
|
|
4
4
|
if (typeof value !== "boolean")
|
|
5
5
|
throw new Error(`${key} must be a boolean`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { isThinkingLevel } from "
|
|
3
|
-
import { atomicWriteJson, createWriteQueue, readFileOrNull } from "
|
|
2
|
+
import { isThinkingLevel } from "../models/index.js";
|
|
3
|
+
import { atomicWriteJson, createWriteQueue, readFileOrNull } from "../util/fs.js";
|
|
4
4
|
export function formatSetting(setting) {
|
|
5
5
|
return `${setting.value} (${setting.source})`;
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, mkdir, open, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { isEnoent, readFileOrNull } from "
|
|
3
|
+
import { isEnoent, readFileOrNull } from "../util/fs.js";
|
|
4
4
|
export function hiddenWebMessageIds(records) {
|
|
5
5
|
return new Set(records.flatMap((record) => {
|
|
6
6
|
if (record.type === "assistant_retry")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { readFileOrNull } from "
|
|
2
|
+
import { readFileOrNull } from "../util/fs.js";
|
|
3
3
|
let contactNotePath = resolve(process.cwd(), "CONTACT.md");
|
|
4
4
|
let cachedNickname = null;
|
|
5
5
|
export function setContactNotePath(path) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { atomicWriteJson, readFileOrNull } from "
|
|
3
|
-
import { isRecord } from "
|
|
2
|
+
import { atomicWriteJson, readFileOrNull } from "../util/fs.js";
|
|
3
|
+
import { isRecord } from "../util/guards.js";
|
|
4
4
|
export function ownerIdentityPath(dataDir) {
|
|
5
5
|
return resolve(dataDir, "owner-identity.json");
|
|
6
6
|
}
|
package/dist/discord/channel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChannelType } from "discord.js";
|
|
2
|
-
import { chatChannelKey } from "../chat-log.js";
|
|
2
|
+
import { chatChannelKey } from "../conversation/chat-log.js";
|
|
3
3
|
export function buildChannelRef(channel, channelId) {
|
|
4
4
|
const scope = channel.type === ChannelType.DM ? "dm" : channel.isThread() ? "thread" : "channel";
|
|
5
5
|
const channelName = "name" in channel ? channel.name : undefined;
|
package/dist/discord/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApplicationCommandOptionType, ApplicationCommandType, ApplicationIntegrationType, ChannelType, InteractionContextType, MessageFlags, } from "discord.js";
|
|
2
|
-
import { formatSetting } from "../settings.js";
|
|
2
|
+
import { formatSetting } from "../config/settings.js";
|
|
3
3
|
import { normalizeOutboundText } from "./send.js";
|
|
4
4
|
export const FAMILIAR_COMMAND_NAME = "familiar";
|
|
5
5
|
const THINKING_CHOICES = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Events, } from "discord.js";
|
|
2
|
-
import {
|
|
3
|
-
import { chatChannelKey } from "
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
2
|
+
import { formatSetting } from "../config/settings.js";
|
|
3
|
+
import { chatChannelKey } from "../conversation/chat-log.js";
|
|
4
|
+
import { saveOwnerIdentity } from "../conversation/owner-identity.js";
|
|
5
|
+
import { thinkingDurationMs } from "../runtime/agent-events.js";
|
|
6
|
+
import { buildChannelRef, fetchMessageAnchor, getChannelRef, isDmChannel, runtimeKeyFromMessage, } from "./channel.js";
|
|
7
|
+
import { isAllowedMessage, withReadyClient } from "./client.js";
|
|
8
|
+
import { EPHEMERAL_REPLY, FAMILIAR_COMMAND_NAME, formatCommandResponse, getAutocompleteChoices, inboundInputFromInteraction, isAllowedInteractionChannel, registerFamiliarApplicationCommand, replyEphemeral, replyInteractionError, } from "./commands.js";
|
|
9
|
+
import { canSteerFromRecord, getChannelTriggerSetting, getDispatchMode, toInboundInput } from "./inbound.js";
|
|
10
|
+
import { sendChannelMessage, sendDiscordAttachments, sendReply } from "./send.js";
|
|
11
|
+
import { isCanceledJob, runAgentTurn } from "./turn.js";
|
|
12
12
|
const RETRY_MS = 15_000;
|
|
13
13
|
async function applyControlCommand(options) {
|
|
14
14
|
const { control, runtime, familiarAgent, settings, channelTrigger, isDm, activeAgentOwner, restart } = options;
|
|
@@ -138,8 +138,8 @@ export function startDiscordDaemon(config, token, familiarAgent, settings, _memo
|
|
|
138
138
|
async deliver({ reply, parsedReply }) {
|
|
139
139
|
const channel = await getOwnerDmChannel();
|
|
140
140
|
return parsedReply.silent
|
|
141
|
-
? await sendDiscordAttachments(
|
|
142
|
-
: await sendChannelMessage(config,
|
|
141
|
+
? await sendDiscordAttachments(token, channel.id, reply.attachments)
|
|
142
|
+
: await sendChannelMessage(config, token, channel, parsedReply.text, reply.attachments);
|
|
143
143
|
},
|
|
144
144
|
};
|
|
145
145
|
const drainJobs = async (message, runtime) => {
|
|
@@ -155,8 +155,8 @@ export function startDiscordDaemon(config, token, familiarAgent, settings, _memo
|
|
|
155
155
|
const { reply, parsedReply, summary, assistantMessageId } = turn;
|
|
156
156
|
const replyAnchor = await fetchMessageAnchor(message, dispatch.triggerMessageId);
|
|
157
157
|
const messageIds = parsedReply.silent
|
|
158
|
-
? await sendDiscordAttachments(
|
|
159
|
-
: await sendReply(config,
|
|
158
|
+
? await sendDiscordAttachments(token, replyAnchor.channelId, reply.attachments)
|
|
159
|
+
: await sendReply(config, token, replyAnchor, parsedReply.text, dispatch.triggerMessageId, reply.attachments);
|
|
160
160
|
await runtime.completeActiveJob({
|
|
161
161
|
text: parsedReply.text,
|
|
162
162
|
messageIds,
|
|
@@ -176,7 +176,7 @@ export function startDiscordDaemon(config, token, familiarAgent, settings, _memo
|
|
|
176
176
|
await runtime.appendError(errorText);
|
|
177
177
|
const fallback = "I hit an error while handling that message.";
|
|
178
178
|
const replyAnchor = await fetchMessageAnchor(message, dispatch.triggerMessageId);
|
|
179
|
-
const messageIds = await sendReply(config,
|
|
179
|
+
const messageIds = await sendReply(config, token, replyAnchor, fallback, dispatch.triggerMessageId);
|
|
180
180
|
await runtime.noteOutbound({
|
|
181
181
|
text: fallback,
|
|
182
182
|
messageIds,
|
|
@@ -237,7 +237,7 @@ export function startDiscordDaemon(config, token, familiarAgent, settings, _memo
|
|
|
237
237
|
activeAgentOwner: core.activeOwner,
|
|
238
238
|
restart: options.restart,
|
|
239
239
|
});
|
|
240
|
-
const messageIds = await sendReply(config,
|
|
240
|
+
const messageIds = await sendReply(config, token, message, text);
|
|
241
241
|
await runtime.noteOutbound({ text, messageIds, control: control.command });
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
@@ -267,7 +267,7 @@ export function startDiscordDaemon(config, token, familiarAgent, settings, _memo
|
|
|
267
267
|
const channelKey = runtimeKeyFromMessage(message);
|
|
268
268
|
const existingRuntime = await core.peekRuntime(channelKey);
|
|
269
269
|
await existingRuntime?.appendError(error instanceof Error ? error.message : String(error));
|
|
270
|
-
await sendReply(config,
|
|
270
|
+
await sendReply(config, token, message, "I hit an error while handling that message.");
|
|
271
271
|
}
|
|
272
272
|
};
|
|
273
273
|
const onInteractionCreate = async (interaction) => {
|
package/dist/discord/inbound.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { materializeInboundAttachments } from "../inbound-attachments.js";
|
|
1
|
+
import { materializeInboundAttachments } from "../media/inbound-attachments.js";
|
|
2
2
|
import { isDmChannel, messageMentionsBot } from "./channel.js";
|
|
3
3
|
export function getDispatchMode(config, message) {
|
|
4
4
|
return isDmChannel(message.channel) ? config.discord.dmMode : config.discord.channelMode;
|
package/dist/discord/send.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { extname } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { parseAgentReply as parseSilentMarker } from "../silent-marker.js";
|
|
3
|
+
import { parseAgentReply as parseSilentMarker } from "../runtime/silent-marker.js";
|
|
5
4
|
import { chunkDiscord } from "./chunking.js";
|
|
6
5
|
const NEWLINE_BURST_DELAY_MS = 500;
|
|
7
|
-
const DISCORD_ATTACHMENT_SEND_TIMEOUT_MS =
|
|
6
|
+
const DISCORD_ATTACHMENT_SEND_TIMEOUT_MS = 120_000;
|
|
7
|
+
const DISCORD_API_BASE_URL = "https://discord.com/api/v10";
|
|
8
8
|
function sleep(ms) {
|
|
9
9
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
10
|
}
|
|
@@ -22,7 +22,7 @@ export function normalizeOutboundText(text) {
|
|
|
22
22
|
function fallbackMimeType(name) {
|
|
23
23
|
return extname(name).toLowerCase() === ".mp3" ? "audio/mpeg" : "application/octet-stream";
|
|
24
24
|
}
|
|
25
|
-
export async function
|
|
25
|
+
export async function buildDiscordAttachmentFiles(attachments) {
|
|
26
26
|
const files = [];
|
|
27
27
|
for (const attachment of attachments) {
|
|
28
28
|
if (!attachment.localPath)
|
|
@@ -36,17 +36,26 @@ export async function buildRawFiles(attachments) {
|
|
|
36
36
|
}
|
|
37
37
|
return files;
|
|
38
38
|
}
|
|
39
|
-
export async function postDiscordAttachments(
|
|
40
|
-
const files = await
|
|
39
|
+
export async function postDiscordAttachments(botToken, channelId, attachments) {
|
|
40
|
+
const files = await buildDiscordAttachmentFiles(attachments);
|
|
41
41
|
if (files.length === 0)
|
|
42
42
|
return [];
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
form.set("payload_json", JSON.stringify({}));
|
|
45
|
+
for (const [index, file] of files.entries()) {
|
|
46
|
+
const bytes = new Uint8Array(file.data.byteLength);
|
|
47
|
+
bytes.set(file.data);
|
|
48
|
+
form.set(`files[${index}]`, new Blob([bytes], { type: file.contentType }), file.name);
|
|
49
|
+
}
|
|
50
|
+
const response = await fetch(`${DISCORD_API_BASE_URL}/channels/${channelId}/messages`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { Authorization: `Bot ${botToken}` },
|
|
53
|
+
body: form,
|
|
46
54
|
signal: AbortSignal.timeout(DISCORD_ATTACHMENT_SEND_TIMEOUT_MS),
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
});
|
|
56
|
+
const data = (await response.json().catch(() => ({})));
|
|
57
|
+
if (!response.ok || !data.id)
|
|
58
|
+
throw new Error(data.message || `Discord attachment send failed (${response.status})`);
|
|
50
59
|
return [data.id];
|
|
51
60
|
}
|
|
52
61
|
export function parseOutboundReply(text) {
|
|
@@ -55,7 +64,7 @@ export function parseOutboundReply(text) {
|
|
|
55
64
|
return parsed;
|
|
56
65
|
return { text: normalizeOutboundText(parsed.text), silent: false };
|
|
57
66
|
}
|
|
58
|
-
async function sendChunkedMessage(config,
|
|
67
|
+
async function sendChunkedMessage(config, botToken, channel, channelId, normalizedText, attachments, sendChunk) {
|
|
59
68
|
const chunks = chunkDiscord(config, normalizedText);
|
|
60
69
|
const sentIds = [];
|
|
61
70
|
for (const [index, chunk] of chunks.entries()) {
|
|
@@ -64,12 +73,12 @@ async function sendChunkedMessage(config, rest, channel, channelId, normalizedTe
|
|
|
64
73
|
const sent = await sendChunk(chunk, index);
|
|
65
74
|
sentIds.push(sent.id);
|
|
66
75
|
}
|
|
67
|
-
const attachmentIds = await sendDiscordAttachments(
|
|
76
|
+
const attachmentIds = await sendDiscordAttachments(botToken, channelId, attachments);
|
|
68
77
|
return [...sentIds, ...attachmentIds];
|
|
69
78
|
}
|
|
70
|
-
export async function sendReply(config,
|
|
79
|
+
export async function sendReply(config, botToken, message, text, replyToMessageId, attachments = []) {
|
|
71
80
|
const normalizedText = normalizeOutboundText(text);
|
|
72
|
-
return sendChunkedMessage(config,
|
|
81
|
+
return sendChunkedMessage(config, botToken, message.channel, message.channelId, normalizedText, attachments, async (chunk, index) => {
|
|
73
82
|
if (!message.channel.isSendable()) {
|
|
74
83
|
throw new Error(`Discord channel is not sendable: ${message.channelId}`);
|
|
75
84
|
}
|
|
@@ -86,18 +95,18 @@ export async function sendReply(config, rest, message, text, replyToMessageId, a
|
|
|
86
95
|
return message.channel.send(chunk);
|
|
87
96
|
});
|
|
88
97
|
}
|
|
89
|
-
export async function sendChannelMessage(config,
|
|
98
|
+
export async function sendChannelMessage(config, botToken, channel, text, attachments = []) {
|
|
90
99
|
if (!channel.isSendable()) {
|
|
91
100
|
throw new Error("Discord channel is not sendable");
|
|
92
101
|
}
|
|
93
102
|
const normalizedText = normalizeOutboundText(text);
|
|
94
|
-
return sendChunkedMessage(config,
|
|
103
|
+
return sendChunkedMessage(config, botToken, channel, channel.id, normalizedText, attachments, (chunk) => channel.send(chunk));
|
|
95
104
|
}
|
|
96
|
-
export async function sendDiscordAttachments(
|
|
105
|
+
export async function sendDiscordAttachments(botToken, channelId, attachments) {
|
|
97
106
|
if (attachments.length === 0)
|
|
98
107
|
return [];
|
|
99
108
|
try {
|
|
100
|
-
return await postDiscordAttachments(
|
|
109
|
+
return await postDiscordAttachments(botToken, channelId, attachments);
|
|
101
110
|
}
|
|
102
111
|
catch (error) {
|
|
103
112
|
console.error("Discord attachment send failed", error);
|
package/dist/discord/turn.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isHeartbeatDue } from "../scheduler.js";
|
|
1
|
+
import { messageId } from "../conversation/ids.js";
|
|
2
|
+
import { createAgentEventRecorder, storedAgentEventFromAgentEvent, updateAgentEventSummary, } from "../runtime/agent-events.js";
|
|
3
|
+
import { isHeartbeatDue } from "../runtime/scheduler.js";
|
|
4
4
|
import { parseOutboundReply } from "./send.js";
|
|
5
5
|
export const HEARTBEAT_SKIPPED = Symbol("heartbeat-skipped");
|
|
6
6
|
export const CRON_SKIPPED = Symbol("cron-skipped");
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export { createFamiliarAgent } from "./agent.js";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export { startDiscordDaemon } from "./discord.js";
|
|
6
|
-
export { startWorkspaceHotReload } from "./hot-reload.js";
|
|
7
|
-
export { clampConfiguredThinkingLevel, createConfiguredModel, describeModelAuth, formatAllowedModels, isAllowedModel, isThinkingLevel, parseModelRef, resolveModel, resolveModelApiKey, supportedThinkingLevels, } from "./models.js";
|
|
8
|
-
export { buildSystemPrompt, loadPersona } from "./persona.js";
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export { startWebDaemon } from "./web.js";
|
|
1
|
+
export { createFamiliarAgent } from "./agent/factory.js";
|
|
2
|
+
export { loadConfig } from "./config/index.js";
|
|
3
|
+
export { loadSettingsStore, } from "./config/settings.js";
|
|
4
|
+
export { buildRecordBase, chatChannelKey, chatLogPath, createChatLog, } from "./conversation/chat-log.js";
|
|
5
|
+
export { startDiscordDaemon } from "./discord/daemon.js";
|
|
6
|
+
export { startWorkspaceHotReload } from "./lifecycle/hot-reload.js";
|
|
7
|
+
export { clampConfiguredThinkingLevel, createConfiguredModel, describeModelAuth, formatAllowedModels, isAllowedModel, isThinkingLevel, parseModelRef, resolveModel, resolveModelApiKey, supportedThinkingLevels, } from "./models/index.js";
|
|
8
|
+
export { buildSystemPrompt, loadPersona } from "./prompting/persona.js";
|
|
9
|
+
export { formatFamiliarSkillsForPrompt, loadFamiliarSkills } from "./prompting/skills.js";
|
|
10
|
+
export { createAgentCore } from "./runtime/agent-core.js";
|
|
11
|
+
export { ConversationRuntime, } from "./runtime/conversation-runtime.js";
|
|
12
|
+
export { startWebDaemon } from "./web/daemon.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lstat, readdir, rm } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { isEnoent } from "
|
|
3
|
+
import { isEnoent } from "../util/fs.js";
|
|
4
4
|
export async function runDataRetention(config, now = Date.now()) {
|
|
5
5
|
if (config.data.transcripts.retentionDays > 0) {
|
|
6
6
|
console.warn("data.transcripts.retention_days is configured; transcript replay may lose restart context after retention.");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import { basename, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { refreshContactNote } from "
|
|
5
|
-
import { isEnoent } from "
|
|
4
|
+
import { refreshContactNote } from "../conversation/contact-note.js";
|
|
5
|
+
import { isEnoent } from "../util/fs.js";
|
|
6
6
|
const ROOT_FILES = new Set([
|
|
7
7
|
"config.toml",
|
|
8
8
|
".env",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lstat, mkdir, readdir, rm } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
-
import { isEnoent } from "
|
|
3
|
+
import { isEnoent } from "../util/fs.js";
|
|
4
4
|
export function createGeneratedMediaSink() {
|
|
5
5
|
const attachments = [];
|
|
6
6
|
return {
|
|
@@ -4,11 +4,11 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, isAbsolute, relative, resolve } from "node:path";
|
|
5
5
|
import { findEnvKeys, generateImages, getEnvApiKey, getImageModels, getImageProviders, } from "@earendil-works/pi-ai";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
+
import { parseModelRef } from "../models/index.js";
|
|
8
|
+
import { imageMimeTypeFromPath, sniffImageMimeType } from "../util/image-mime.js";
|
|
7
9
|
import { ensureGeneratedAttachmentsDir } from "./generated-media.js";
|
|
8
10
|
import { ensureInlineImageDerivative } from "./image-derivatives.js";
|
|
9
11
|
import { promptImagesFromAttachments } from "./inbound-attachments.js";
|
|
10
|
-
import { parseModelRef } from "./models.js";
|
|
11
|
-
import { imageMimeTypeFromPath, sniffImageMimeType } from "./util/image-mime.js";
|
|
12
12
|
const IMAGE_GEN_NOTICE_PREFIX = "Generated image attachment:";
|
|
13
13
|
const OPENROUTER_IMAGE_BASE_URL = "https://openrouter.ai/api/v1";
|
|
14
14
|
const imageGenSchema = Type.Object({
|