@getfamiliar/shared 0.1.13 → 0.1.16
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/build/ChatInput.d.ts +42 -0
- package/build/ChatInput.js +45 -0
- package/build/ChatMessage.d.ts +11 -0
- package/build/ChatMessage.js +2 -1
- package/build/ChatMessageBus.d.ts +11 -1
- package/build/ChatMessageBus.js +26 -0
- package/build/DockerTools.d.ts +119 -0
- package/build/DockerTools.js +175 -0
- package/build/Plugin.d.ts +103 -1
- package/build/PromptTemplate.d.ts +105 -0
- package/build/PromptTemplate.js +194 -0
- package/build/Schema.d.ts +1 -1
- package/build/Schema.js +23 -0
- package/build/Scratch.d.ts +36 -0
- package/build/Scratch.js +29 -0
- package/build/WebAuth.d.ts +166 -0
- package/build/WebAuth.js +18 -0
- package/build/WebMenu.d.ts +54 -0
- package/build/WebMenu.js +23 -0
- package/build/WebPlugin.d.ts +137 -0
- package/build/WebPlugin.js +13 -0
- package/build/WebServerDefaults.d.ts +9 -0
- package/build/WebServerDefaults.js +9 -0
- package/build/WebStaticAssets.d.ts +72 -0
- package/build/WebStaticAssets.js +204 -0
- package/build/index.d.ts +10 -1
- package/build/index.js +10 -0
- package/package.json +2 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing of one line a user typed into a chat (cli-chat, web-chat):
|
|
3
|
+
* a builtin command of the chat client, a direct handler call
|
|
4
|
+
* (`/topic/sub …`) or a plain message.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Result of {@link parseChatInput}.
|
|
8
|
+
*
|
|
9
|
+
* A `handler` arm covers both the plain message (no topic — the chat
|
|
10
|
+
* plugin's own topic applies) and the `/topic/sub/handler …` direct call,
|
|
11
|
+
* whose path is the handler's topic.
|
|
12
|
+
*/
|
|
13
|
+
export type ParsedChatInput<Builtin extends string = string> = {
|
|
14
|
+
readonly kind: "handler";
|
|
15
|
+
readonly topic?: string;
|
|
16
|
+
readonly prompt: string;
|
|
17
|
+
/**
|
|
18
|
+
* Trimmed raw input line, before slash-command parsing. For direct
|
|
19
|
+
* calls this is what the chat persists as the `role='user'`
|
|
20
|
+
* chatmessage (`NewEvent.userChatMessage`), so later chat turns see
|
|
21
|
+
* exactly what the user typed. For plain messages it equals `prompt`.
|
|
22
|
+
*/
|
|
23
|
+
readonly rawInput: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "builtin";
|
|
26
|
+
readonly command: Builtin;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Parse one raw input line.
|
|
30
|
+
*
|
|
31
|
+
* Rules:
|
|
32
|
+
* - A line whose first word is one of `builtins` → builtin.
|
|
33
|
+
* - `/a/b … rest` (any other `/` path) → handler call with topic `a:b`;
|
|
34
|
+
* anything after the first space is the prompt.
|
|
35
|
+
* - Anything else (and a bare `/`) → plain message, the trimmed line is
|
|
36
|
+
* the prompt.
|
|
37
|
+
*
|
|
38
|
+
* @param raw The typed line.
|
|
39
|
+
* @param builtins The chat client's own commands (e.g. `["/exit", "/clear"]`).
|
|
40
|
+
* @returns The parsed input.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseChatInput<Builtin extends string>(raw: string, builtins: readonly Builtin[]): ParsedChatInput<Builtin>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing of one line a user typed into a chat (cli-chat, web-chat):
|
|
3
|
+
* a builtin command of the chat client, a direct handler call
|
|
4
|
+
* (`/topic/sub …`) or a plain message.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Parse one raw input line.
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - A line whose first word is one of `builtins` → builtin.
|
|
11
|
+
* - `/a/b … rest` (any other `/` path) → handler call with topic `a:b`;
|
|
12
|
+
* anything after the first space is the prompt.
|
|
13
|
+
* - Anything else (and a bare `/`) → plain message, the trimmed line is
|
|
14
|
+
* the prompt.
|
|
15
|
+
*
|
|
16
|
+
* @param raw The typed line.
|
|
17
|
+
* @param builtins The chat client's own commands (e.g. `["/exit", "/clear"]`).
|
|
18
|
+
* @returns The parsed input.
|
|
19
|
+
*/
|
|
20
|
+
export function parseChatInput(raw, builtins) {
|
|
21
|
+
const line = raw.trim();
|
|
22
|
+
if (!line.startsWith("/")) {
|
|
23
|
+
return { kind: "handler", prompt: line, rawInput: line };
|
|
24
|
+
}
|
|
25
|
+
const spaceIdx = line.indexOf(" ");
|
|
26
|
+
const commandPart = spaceIdx === -1 ? line : line.slice(0, spaceIdx);
|
|
27
|
+
const messagePart = spaceIdx === -1 ? "" : line.slice(spaceIdx + 1).trim();
|
|
28
|
+
if (builtins.includes(commandPart)) {
|
|
29
|
+
return { kind: "builtin", command: commandPart };
|
|
30
|
+
}
|
|
31
|
+
// Empty segments (trailing `/` or `//`) produce no topic words.
|
|
32
|
+
const segments = commandPart
|
|
33
|
+
.slice(1)
|
|
34
|
+
.split("/")
|
|
35
|
+
.filter((segment) => segment.length > 0);
|
|
36
|
+
if (segments.length === 0) {
|
|
37
|
+
return { kind: "handler", prompt: line, rawInput: line };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
kind: "handler",
|
|
41
|
+
topic: segments.join(":"),
|
|
42
|
+
prompt: messagePart,
|
|
43
|
+
rawInput: line,
|
|
44
|
+
};
|
|
45
|
+
}
|
package/build/ChatMessage.d.ts
CHANGED
|
@@ -41,6 +41,17 @@ export interface NewChatMessage {
|
|
|
41
41
|
readonly role: ChatRole;
|
|
42
42
|
readonly textContent: string;
|
|
43
43
|
}
|
|
44
|
+
/** Largest page {@link ChatHistoryQuery.limit} accepts. */
|
|
45
|
+
export declare const MAX_CHAT_HISTORY_PAGE = 200;
|
|
46
|
+
/** Query for one page of a channel's chat history (`ctx.chat.listMessages`). */
|
|
47
|
+
export interface ChatHistoryQuery {
|
|
48
|
+
/** Channel id (matched against the parent event's `preferred_chat_channel_id`). */
|
|
49
|
+
readonly channelId: string;
|
|
50
|
+
/** Only messages with a smaller id (cursor for paging backwards); omit for the newest page. */
|
|
51
|
+
readonly beforeId?: string;
|
|
52
|
+
/** Page size, 1 … {@link MAX_CHAT_HISTORY_PAGE}. */
|
|
53
|
+
readonly limit: number;
|
|
54
|
+
}
|
|
44
55
|
/** Filter for {@link import("./ChatMessageBus").ChatMessageBus.subscribe}. */
|
|
45
56
|
export interface ChatFilter {
|
|
46
57
|
/**
|
package/build/ChatMessage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChatFilter, ChatMessage, NewChatMessage } from "./ChatMessage.js";
|
|
1
|
+
import type { ChatFilter, ChatHistoryQuery, ChatMessage, NewChatMessage } from "./ChatMessage.js";
|
|
2
2
|
import type { Logger } from "./logging/Logger.js";
|
|
3
3
|
import type { PostgresConnection } from "./PostgresConnection.js";
|
|
4
4
|
/**
|
|
@@ -68,6 +68,16 @@ export declare class ChatMessageBus {
|
|
|
68
68
|
* thread to attach to).
|
|
69
69
|
*/
|
|
70
70
|
fetchHistoryForEvent(eventId: string): Promise<ChatMessage[]>;
|
|
71
|
+
/**
|
|
72
|
+
* One page of a channel's chat history: the newest `limit` messages
|
|
73
|
+
* on `channelId` (joined via `events.preferred_chat_channel_id`)
|
|
74
|
+
* older than `beforeId`, returned oldest first. Page backwards by
|
|
75
|
+
* passing the first returned id as the next `beforeId`.
|
|
76
|
+
*
|
|
77
|
+
* @param query Channel, optional cursor and page size.
|
|
78
|
+
* @returns Up to `limit` messages in chronological order.
|
|
79
|
+
*/
|
|
80
|
+
listByChannel(query: ChatHistoryQuery): Promise<ChatMessage[]>;
|
|
71
81
|
/**
|
|
72
82
|
* Subscribe to chat messages matching `filter`. Returns an
|
|
73
83
|
* unsubscribe function.
|
package/build/ChatMessageBus.js
CHANGED
|
@@ -85,6 +85,32 @@ export class ChatMessageBus {
|
|
|
85
85
|
ORDER BY cm.created_at ASC, cm.id ASC`, [eventId]);
|
|
86
86
|
return result.rows.map(mapRow);
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* One page of a channel's chat history: the newest `limit` messages
|
|
90
|
+
* on `channelId` (joined via `events.preferred_chat_channel_id`)
|
|
91
|
+
* older than `beforeId`, returned oldest first. Page backwards by
|
|
92
|
+
* passing the first returned id as the next `beforeId`.
|
|
93
|
+
*
|
|
94
|
+
* @param query Channel, optional cursor and page size.
|
|
95
|
+
* @returns Up to `limit` messages in chronological order.
|
|
96
|
+
*/
|
|
97
|
+
async listByChannel(query) {
|
|
98
|
+
const values = [query.channelId, query.limit];
|
|
99
|
+
const cursor = query.beforeId === undefined ? "" : "AND cm.id < $3";
|
|
100
|
+
if (query.beforeId !== undefined) {
|
|
101
|
+
values.push(query.beforeId);
|
|
102
|
+
}
|
|
103
|
+
const result = await this.connection.getPool().query(`SELECT * FROM (
|
|
104
|
+
SELECT cm.*
|
|
105
|
+
FROM chatmessages cm
|
|
106
|
+
JOIN events e ON e.id = cm.event_id
|
|
107
|
+
WHERE e.preferred_chat_channel_id = $1 ${cursor}
|
|
108
|
+
ORDER BY cm.id DESC
|
|
109
|
+
LIMIT $2
|
|
110
|
+
) page
|
|
111
|
+
ORDER BY page.id ASC`, values);
|
|
112
|
+
return result.rows.map(mapRow);
|
|
113
|
+
}
|
|
88
114
|
/**
|
|
89
115
|
* Subscribe to chat messages matching `filter`. Returns an
|
|
90
116
|
* unsubscribe function.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared bridge network all Familiar containers join. Owned
|
|
3
|
+
* by the daemon (see `commands/Start.ts`); created once via
|
|
4
|
+
* {@link ensureNetwork} before any container is started.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SHARED_NETWORK_NAME = "familiar-net";
|
|
7
|
+
/**
|
|
8
|
+
* Internal (egress-less) bridge network the locked-down agent container
|
|
9
|
+
* joins. Created with `--internal` so the agent has no route to the
|
|
10
|
+
* internet; it reaches postgres (dual-homed onto this net) and the host
|
|
11
|
+
* bastion (via the `familiar-bastion-bridge` socat sidecar, also on this
|
|
12
|
+
* net) by container name. Owned by the daemon, created via
|
|
13
|
+
* {@link ensureNetwork} with `{ internal: true }`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ISOLATED_NETWORK_NAME = "familiar-isolated";
|
|
16
|
+
/** Options shared by {@link dockerExec}. */
|
|
17
|
+
export interface DockerExecOptions {
|
|
18
|
+
/** If true, non-zero exit codes resolve instead of throwing. */
|
|
19
|
+
readonly allowFailure?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Output of {@link dockerCapture}. */
|
|
22
|
+
export interface DockerCaptureResult {
|
|
23
|
+
readonly code: number | null;
|
|
24
|
+
readonly stdout: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Run a docker CLI command and discard its stdio. Resolves on exit
|
|
28
|
+
* code 0 (or always when `allowFailure` is set); rejects otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export declare function dockerExec(args: readonly string[], options?: DockerExecOptions): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Run a docker CLI command and capture stdout. Never throws on non-zero
|
|
33
|
+
* exit; callers inspect the returned `code`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function dockerCapture(args: readonly string[]): Promise<DockerCaptureResult>;
|
|
36
|
+
/** Whether a container with the given name is currently running. */
|
|
37
|
+
export declare function isContainerRunning(name: string): Promise<boolean>;
|
|
38
|
+
/** Whether a Docker network with the given name already exists. */
|
|
39
|
+
export declare function isNetworkPresent(name: string): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a Docker image with the given reference is present in the
|
|
42
|
+
* local image store. Used by the pull-mode image lifecycle to avoid
|
|
43
|
+
* re-pulling a version-pinned image that's already local.
|
|
44
|
+
*
|
|
45
|
+
* @param ref Image reference (tag or registry-qualified `name:tag`).
|
|
46
|
+
* @returns true if `docker image inspect` succeeds.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isImagePresent(ref: string): Promise<boolean>;
|
|
49
|
+
/** Options for {@link buildNetworkCreateArgs} / {@link ensureNetwork}. */
|
|
50
|
+
export interface NetworkCreateOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Create the network with `--internal` so containers on it have no
|
|
53
|
+
* egress to the host or the internet — only to other containers on
|
|
54
|
+
* the same network. Used for the agent's `familiar-isolated` net.
|
|
55
|
+
*/
|
|
56
|
+
readonly internal?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the `docker network create` argument vector. Pure (no side
|
|
60
|
+
* effects) so it can be unit-tested without a daemon.
|
|
61
|
+
*
|
|
62
|
+
* @param name Network name to create.
|
|
63
|
+
* @param options Network creation flags (e.g. `internal`).
|
|
64
|
+
* @returns The full docker CLI argv, e.g. `["network", "create", "--internal", "familiar-isolated"]`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildNetworkCreateArgs(name: string, options?: NetworkCreateOptions): string[];
|
|
67
|
+
/**
|
|
68
|
+
* Create the named bridge network if it doesn't already exist. Idempotent.
|
|
69
|
+
*
|
|
70
|
+
* @param name Network name to ensure.
|
|
71
|
+
* @param options Network creation flags applied only on first creation
|
|
72
|
+
* (an existing network is left as-is).
|
|
73
|
+
*/
|
|
74
|
+
export declare function ensureNetwork(name: string, options?: NetworkCreateOptions): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Attach an already-running container to an additional network by name.
|
|
77
|
+
* Callers recreate their container on every `start()` (via
|
|
78
|
+
* {@link removeContainer} first), so the attachment is always fresh —
|
|
79
|
+
* this throws on failure rather than swallowing it, so a genuine
|
|
80
|
+
* problem (missing network, missing container) surfaces instead of
|
|
81
|
+
* silently leaving the container unable to reach its peer.
|
|
82
|
+
*
|
|
83
|
+
* @param network Network to connect the container to.
|
|
84
|
+
* @param container Name of the running container to attach.
|
|
85
|
+
* @throws If the docker command fails (network/container missing, etc.).
|
|
86
|
+
*/
|
|
87
|
+
export declare function connectNetwork(network: string, container: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Force-remove a container by name. Default `allowFailure: true` because
|
|
90
|
+
* "remove if it exists" is the common idempotent-cleanup intent.
|
|
91
|
+
*/
|
|
92
|
+
export declare function removeContainer(name: string, options?: DockerExecOptions): Promise<void>;
|
|
93
|
+
/** Send `docker stop` to a container. Ignores already-stopped/missing containers. */
|
|
94
|
+
export declare function stopContainer(name: string): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Run a docker CLI command with the parent process's stdin/stdout/
|
|
97
|
+
* stderr inherited, blocking until the child exits. Used by
|
|
98
|
+
* subcommands that hand the user's terminal to a foreground docker
|
|
99
|
+
* process (`familiar psql`'s psql shell, `familiar mcp call`'s
|
|
100
|
+
* one-shot CLI invocations against an MCP runtime container).
|
|
101
|
+
*
|
|
102
|
+
* Returns the child's exit status — `null`-on-signal collapses to
|
|
103
|
+
* 1 so the caller's `process.exit(...)` always sees a usable
|
|
104
|
+
* number. A failure to launch docker itself (e.g. binary missing)
|
|
105
|
+
* surfaces as a thrown {@link Error} the caller can format.
|
|
106
|
+
*
|
|
107
|
+
* @throws If the docker binary couldn't be spawned at all.
|
|
108
|
+
*/
|
|
109
|
+
export declare function dockerInteractive(args: readonly string[]): number;
|
|
110
|
+
/**
|
|
111
|
+
* On Linux, `host.docker.internal` isn't built-in and must be mapped to
|
|
112
|
+
* the host gateway explicitly. macOS and Windows resolve it natively.
|
|
113
|
+
*
|
|
114
|
+
* Used by sidecars that dial a host-side listener (the bastion bridge,
|
|
115
|
+
* the tailscale plugin's serve proxy to the webserver).
|
|
116
|
+
*
|
|
117
|
+
* @returns Extra `docker run` args, or an empty array on macOS/Windows.
|
|
118
|
+
*/
|
|
119
|
+
export declare function hostGatewayArgs(): string[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Shared bridge network all Familiar containers join. Owned
|
|
5
|
+
* by the daemon (see `commands/Start.ts`); created once via
|
|
6
|
+
* {@link ensureNetwork} before any container is started.
|
|
7
|
+
*/
|
|
8
|
+
export const SHARED_NETWORK_NAME = "familiar-net";
|
|
9
|
+
/**
|
|
10
|
+
* Internal (egress-less) bridge network the locked-down agent container
|
|
11
|
+
* joins. Created with `--internal` so the agent has no route to the
|
|
12
|
+
* internet; it reaches postgres (dual-homed onto this net) and the host
|
|
13
|
+
* bastion (via the `familiar-bastion-bridge` socat sidecar, also on this
|
|
14
|
+
* net) by container name. Owned by the daemon, created via
|
|
15
|
+
* {@link ensureNetwork} with `{ internal: true }`.
|
|
16
|
+
*/
|
|
17
|
+
export const ISOLATED_NETWORK_NAME = "familiar-isolated";
|
|
18
|
+
/**
|
|
19
|
+
* Run a docker CLI command and discard its stdio. Resolves on exit
|
|
20
|
+
* code 0 (or always when `allowFailure` is set); rejects otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export function dockerExec(args, options = {}) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const proc = spawn("docker", [...args], { stdio: ["ignore", "ignore", "pipe"] });
|
|
25
|
+
let stderr = "";
|
|
26
|
+
proc.stderr.on("data", (chunk) => {
|
|
27
|
+
stderr += chunk.toString();
|
|
28
|
+
});
|
|
29
|
+
proc.on("close", (code) => {
|
|
30
|
+
if (code === 0 || options.allowFailure) {
|
|
31
|
+
resolve();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const trimmed = stderr.trim();
|
|
35
|
+
const suffix = trimmed.length > 0 ? `: ${trimmed}` : "";
|
|
36
|
+
reject(new Error(`docker ${args.join(" ")} exited with code ${code}${suffix}`));
|
|
37
|
+
});
|
|
38
|
+
proc.on("error", reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run a docker CLI command and capture stdout. Never throws on non-zero
|
|
43
|
+
* exit; callers inspect the returned `code`.
|
|
44
|
+
*/
|
|
45
|
+
export function dockerCapture(args) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const proc = spawn("docker", [...args], { stdio: ["ignore", "pipe", "ignore"] });
|
|
48
|
+
let stdout = "";
|
|
49
|
+
proc.stdout.on("data", (chunk) => {
|
|
50
|
+
stdout += chunk.toString();
|
|
51
|
+
});
|
|
52
|
+
proc.on("close", (code) => {
|
|
53
|
+
resolve({ code, stdout });
|
|
54
|
+
});
|
|
55
|
+
proc.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Whether a container with the given name is currently running. */
|
|
59
|
+
export async function isContainerRunning(name) {
|
|
60
|
+
const { code, stdout } = await dockerCapture([
|
|
61
|
+
"container",
|
|
62
|
+
"inspect",
|
|
63
|
+
"-f",
|
|
64
|
+
"{{.State.Running}}",
|
|
65
|
+
name,
|
|
66
|
+
]);
|
|
67
|
+
return code === 0 && stdout.trim() === "true";
|
|
68
|
+
}
|
|
69
|
+
/** Whether a Docker network with the given name already exists. */
|
|
70
|
+
export async function isNetworkPresent(name) {
|
|
71
|
+
const { code } = await dockerCapture(["network", "inspect", name]);
|
|
72
|
+
return code === 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a Docker image with the given reference is present in the
|
|
76
|
+
* local image store. Used by the pull-mode image lifecycle to avoid
|
|
77
|
+
* re-pulling a version-pinned image that's already local.
|
|
78
|
+
*
|
|
79
|
+
* @param ref Image reference (tag or registry-qualified `name:tag`).
|
|
80
|
+
* @returns true if `docker image inspect` succeeds.
|
|
81
|
+
*/
|
|
82
|
+
export async function isImagePresent(ref) {
|
|
83
|
+
const { code } = await dockerCapture(["image", "inspect", ref]);
|
|
84
|
+
return code === 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build the `docker network create` argument vector. Pure (no side
|
|
88
|
+
* effects) so it can be unit-tested without a daemon.
|
|
89
|
+
*
|
|
90
|
+
* @param name Network name to create.
|
|
91
|
+
* @param options Network creation flags (e.g. `internal`).
|
|
92
|
+
* @returns The full docker CLI argv, e.g. `["network", "create", "--internal", "familiar-isolated"]`.
|
|
93
|
+
*/
|
|
94
|
+
export function buildNetworkCreateArgs(name, options = {}) {
|
|
95
|
+
const args = ["network", "create"];
|
|
96
|
+
if (options.internal) {
|
|
97
|
+
args.push("--internal");
|
|
98
|
+
}
|
|
99
|
+
args.push(name);
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create the named bridge network if it doesn't already exist. Idempotent.
|
|
104
|
+
*
|
|
105
|
+
* @param name Network name to ensure.
|
|
106
|
+
* @param options Network creation flags applied only on first creation
|
|
107
|
+
* (an existing network is left as-is).
|
|
108
|
+
*/
|
|
109
|
+
export async function ensureNetwork(name, options = {}) {
|
|
110
|
+
if (!(await isNetworkPresent(name))) {
|
|
111
|
+
await dockerExec(buildNetworkCreateArgs(name, options));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Attach an already-running container to an additional network by name.
|
|
116
|
+
* Callers recreate their container on every `start()` (via
|
|
117
|
+
* {@link removeContainer} first), so the attachment is always fresh —
|
|
118
|
+
* this throws on failure rather than swallowing it, so a genuine
|
|
119
|
+
* problem (missing network, missing container) surfaces instead of
|
|
120
|
+
* silently leaving the container unable to reach its peer.
|
|
121
|
+
*
|
|
122
|
+
* @param network Network to connect the container to.
|
|
123
|
+
* @param container Name of the running container to attach.
|
|
124
|
+
* @throws If the docker command fails (network/container missing, etc.).
|
|
125
|
+
*/
|
|
126
|
+
export function connectNetwork(network, container) {
|
|
127
|
+
return dockerExec(["network", "connect", network, container]);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Force-remove a container by name. Default `allowFailure: true` because
|
|
131
|
+
* "remove if it exists" is the common idempotent-cleanup intent.
|
|
132
|
+
*/
|
|
133
|
+
export function removeContainer(name, options = { allowFailure: true }) {
|
|
134
|
+
return dockerExec(["rm", "-f", name], options);
|
|
135
|
+
}
|
|
136
|
+
/** Send `docker stop` to a container. Ignores already-stopped/missing containers. */
|
|
137
|
+
export function stopContainer(name) {
|
|
138
|
+
return dockerExec(["stop", name], { allowFailure: true });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Run a docker CLI command with the parent process's stdin/stdout/
|
|
142
|
+
* stderr inherited, blocking until the child exits. Used by
|
|
143
|
+
* subcommands that hand the user's terminal to a foreground docker
|
|
144
|
+
* process (`familiar psql`'s psql shell, `familiar mcp call`'s
|
|
145
|
+
* one-shot CLI invocations against an MCP runtime container).
|
|
146
|
+
*
|
|
147
|
+
* Returns the child's exit status — `null`-on-signal collapses to
|
|
148
|
+
* 1 so the caller's `process.exit(...)` always sees a usable
|
|
149
|
+
* number. A failure to launch docker itself (e.g. binary missing)
|
|
150
|
+
* surfaces as a thrown {@link Error} the caller can format.
|
|
151
|
+
*
|
|
152
|
+
* @throws If the docker binary couldn't be spawned at all.
|
|
153
|
+
*/
|
|
154
|
+
export function dockerInteractive(args) {
|
|
155
|
+
const result = spawnSync("docker", [...args], { stdio: "inherit" });
|
|
156
|
+
if (result.error) {
|
|
157
|
+
throw new Error(`failed to launch docker: ${result.error.message}`);
|
|
158
|
+
}
|
|
159
|
+
return result.status ?? 1;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* On Linux, `host.docker.internal` isn't built-in and must be mapped to
|
|
163
|
+
* the host gateway explicitly. macOS and Windows resolve it natively.
|
|
164
|
+
*
|
|
165
|
+
* Used by sidecars that dial a host-side listener (the bastion bridge,
|
|
166
|
+
* the tailscale plugin's serve proxy to the webserver).
|
|
167
|
+
*
|
|
168
|
+
* @returns Extra `docker run` args, or an empty array on macOS/Windows.
|
|
169
|
+
*/
|
|
170
|
+
export function hostGatewayArgs() {
|
|
171
|
+
if (os.platform() === "linux") {
|
|
172
|
+
return ["--add-host=host.docker.internal:host-gateway"];
|
|
173
|
+
}
|
|
174
|
+
return [];
|
|
175
|
+
}
|
package/build/Plugin.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
2
2
|
import type { CommandDef } from "citty";
|
|
3
3
|
import type { AgentRunRow } from "./AgentRun.js";
|
|
4
4
|
import type { CalendarApi } from "./Calendar.js";
|
|
5
|
-
import type { ChatFilter } from "./ChatMessage.js";
|
|
5
|
+
import type { ChatFilter, ChatHistoryQuery, ChatMessage } from "./ChatMessage.js";
|
|
6
6
|
import type { ChatHandler, ChatUnsubscribe } from "./ChatMessageBus.js";
|
|
7
7
|
import type { ConfigService } from "./Config.js";
|
|
8
8
|
import type { EventFile, EventRow, NewEvent } from "./Event.js";
|
|
@@ -10,10 +10,14 @@ import type { Logger } from "./logging/Logger.js";
|
|
|
10
10
|
import type { MailApi } from "./Mail.js";
|
|
11
11
|
import type { MailStyleTemplate } from "./MailStyleTemplate.js";
|
|
12
12
|
import type { ModelMetaData, ModelProviderDescriptor } from "./ModelMetaData.js";
|
|
13
|
+
import type { ScratchFile } from "./Scratch.js";
|
|
13
14
|
import type { StepResultRow } from "./StepResult.js";
|
|
14
15
|
import type { StorageApi } from "./Storage.js";
|
|
15
16
|
import type { ToolLevel } from "./ToolLevel.js";
|
|
16
17
|
import type { ToolRunContext } from "./ToolRunner.js";
|
|
18
|
+
import type { WebApi, WebUi } from "./WebAuth.js";
|
|
19
|
+
import type { WebMenuContribution } from "./WebMenu.js";
|
|
20
|
+
import type { WebPluginUi } from "./WebPlugin.js";
|
|
17
21
|
import type { WorkspaceWatcherApi } from "./WorkspaceFile.js";
|
|
18
22
|
export type { ChatHandler, Client as McpClient };
|
|
19
23
|
/**
|
|
@@ -234,6 +238,25 @@ export interface HostContext {
|
|
|
234
238
|
* can deadlock the input-event watcher's claim.
|
|
235
239
|
*/
|
|
236
240
|
appendAssistantMessage(eventId: string, text: string): Promise<void>;
|
|
241
|
+
/**
|
|
242
|
+
* Insert a chatmessage of either role attached to `eventId`, for
|
|
243
|
+
* recording a conversation that happened outside the agent — e.g.
|
|
244
|
+
* the transcript of a voice call. Channel is resolved via the
|
|
245
|
+
* event's `preferred_chat_channel_id`. The same rule as for
|
|
246
|
+
* {@link appendAssistantMessage} applies: only for events that
|
|
247
|
+
* are already settled — typically a record-only event emitted
|
|
248
|
+
* with `state: "done"`, which no handler ever runs for.
|
|
249
|
+
*/
|
|
250
|
+
appendMessage(eventId: string, role: "user" | "assistant", text: string): Promise<void>;
|
|
251
|
+
/**
|
|
252
|
+
* Read one page of a channel's history (both roles), oldest first:
|
|
253
|
+
* the newest `limit` messages before `beforeId`. For chat UIs that
|
|
254
|
+
* show scrollback; the same per-channel history the model sees for
|
|
255
|
+
* that channel's chat events.
|
|
256
|
+
*
|
|
257
|
+
* @throws {Error} When `limit` is outside 1 … `MAX_CHAT_HISTORY_PAGE`.
|
|
258
|
+
*/
|
|
259
|
+
listMessages(query: ChatHistoryQuery): Promise<readonly ChatMessage[]>;
|
|
237
260
|
};
|
|
238
261
|
/**
|
|
239
262
|
* Per-event scratch directory operations. Each event the bus
|
|
@@ -262,6 +285,23 @@ export interface HostContext {
|
|
|
262
285
|
* caller is responsible for unique names.
|
|
263
286
|
*/
|
|
264
287
|
addFiles(eventId: string, files: readonly EventFile[]): Promise<readonly string[]>;
|
|
288
|
+
/**
|
|
289
|
+
* The regular files directly in `/scratch/<eventId>/` (no
|
|
290
|
+
* subdirectories), hidden ones included — filter them with
|
|
291
|
+
* `isHiddenScratchFile` before showing files to users. Sorted
|
|
292
|
+
* by name; empty when the folder does not exist.
|
|
293
|
+
*
|
|
294
|
+
* @throws {Error} When `eventId` is not a numeric event id.
|
|
295
|
+
*/
|
|
296
|
+
listFiles(eventId: string): Promise<readonly ScratchFile[]>;
|
|
297
|
+
/**
|
|
298
|
+
* Host path of `/scratch/<eventId>/<name>`, for reading or
|
|
299
|
+
* streaming the file. The file may not exist.
|
|
300
|
+
*
|
|
301
|
+
* @throws {Error} When `eventId` is not a numeric event id or
|
|
302
|
+
* `name` is not a plain basename (same rules as `addFiles`).
|
|
303
|
+
*/
|
|
304
|
+
resolveFile(eventId: string, name: string): string;
|
|
265
305
|
};
|
|
266
306
|
/**
|
|
267
307
|
* The pino-style {@link Logger} for the plugin. Use `.info` for the
|
|
@@ -271,6 +311,14 @@ export interface HostContext {
|
|
|
271
311
|
* plugin doing anything.
|
|
272
312
|
*/
|
|
273
313
|
readonly logger: Logger;
|
|
314
|
+
/**
|
|
315
|
+
* Whether the daemon runs in dev mode (`FAMILIAR_DEV=1`, set by the
|
|
316
|
+
* monorepo's `npm run dev` launcher). Lets plugins enable developer
|
|
317
|
+
* conveniences such as the web UI's hot module reloading.
|
|
318
|
+
*/
|
|
319
|
+
readonly isDevMode: boolean;
|
|
320
|
+
/** Version of the running familiar host (the `@getfamiliar/host` package version, e.g. `0.4.2`). */
|
|
321
|
+
readonly familiarVersion: string;
|
|
274
322
|
/**
|
|
275
323
|
* Absolute host path of the project's `data/` directory.
|
|
276
324
|
*
|
|
@@ -399,6 +447,14 @@ export interface HostContext {
|
|
|
399
447
|
* which maps the user's configured mounts onto providers.
|
|
400
448
|
*/
|
|
401
449
|
readonly storage: StorageApi;
|
|
450
|
+
/**
|
|
451
|
+
* Core webserver hooks. A plugin that fronts the webserver with its
|
|
452
|
+
* own reverse proxy registers it during **`prepare()`** via
|
|
453
|
+
* `ctx.web.registerTrustedProxy({ secret, userHeader })`, the same
|
|
454
|
+
* contract a user-configured `webserver.auth.trustedProxies` entry
|
|
455
|
+
* follows (see {@link WebApi}).
|
|
456
|
+
*/
|
|
457
|
+
readonly web: WebApi;
|
|
402
458
|
/**
|
|
403
459
|
* Read the per-mailbox style template the user (or the
|
|
404
460
|
* extract-style handler) wrote at `data/mail/templates/<mailbox>/<name>.json`.
|
|
@@ -694,6 +750,52 @@ export interface PluginHostManifest {
|
|
|
694
750
|
* call.
|
|
695
751
|
*/
|
|
696
752
|
tools?(ctx: HostContext): readonly PluginTool[];
|
|
753
|
+
/**
|
|
754
|
+
* Provide the web UI: return its root controller, a Hono app mounted
|
|
755
|
+
* at `/` of the core webserver, and optionally a renderer for the
|
|
756
|
+
* login-flow pages ({@link WebUi.renderAuthPage}). Called after `start()` resolves (at
|
|
757
|
+
* boot and after every restart), so closures over `start`-time state
|
|
758
|
+
* (e.g. a dev server) are safe; a restart swaps the controller
|
|
759
|
+
* without rebinding the socket.
|
|
760
|
+
*
|
|
761
|
+
* Authentication stays in the core: every request reaching the
|
|
762
|
+
* controller passed the auth middleware, `c.get("principal")` is the
|
|
763
|
+
* signed-in user. The core routes `/login`, `/login/:token` and
|
|
764
|
+
* `/logout` take precedence over the controller's routes; the core
|
|
765
|
+
* decides what they do and only lets the UI render their markup.
|
|
766
|
+
*
|
|
767
|
+
* When several plugins provide a UI, `webserver.uiPlugin` picks one;
|
|
768
|
+
* without any, the webserver serves a plain page asking to install
|
|
769
|
+
* one (e.g. `@getfamiliar/plugin-web-ui`).
|
|
770
|
+
*
|
|
771
|
+
* Synchronous, side-effect-free: just build and return the app.
|
|
772
|
+
*/
|
|
773
|
+
webUi?(ctx: HostContext): WebUi;
|
|
774
|
+
/**
|
|
775
|
+
* Contribute items to the web UI's sidebar menu: direct links or
|
|
776
|
+
* collapsible groups with sub-entries (icon, label, routes), plus a
|
|
777
|
+
* weight positioning this plugin's items among the others. Called
|
|
778
|
+
* after `start()` (at boot and after every restart); a disabled
|
|
779
|
+
* plugin contributes nothing. The core validates the items (lucide
|
|
780
|
+
* icon id, non-empty labels, routes starting with `/`) and disables
|
|
781
|
+
* the plugin in phase `webMenu` when they are malformed.
|
|
782
|
+
*
|
|
783
|
+
* Synchronous, side-effect-free: derive the items from constants and
|
|
784
|
+
* `ctx.config`.
|
|
785
|
+
*/
|
|
786
|
+
webMenu?(ctx: HostContext): WebMenuContribution;
|
|
787
|
+
/**
|
|
788
|
+
* Serve HTTP routes and, optionally, a page in the web UI: return a
|
|
789
|
+
* controller (mounted at `/plugins/<plugin-id>/` behind the core
|
|
790
|
+
* authentication, concrete routes only, no catch-all) and an optional
|
|
791
|
+
* page module the web UI loads for browser routes under
|
|
792
|
+
* `/plugins/<plugin-id>` (see {@link WebPluginUi}). Called after
|
|
793
|
+
* `start()` (at boot and after every restart); a throw disables the
|
|
794
|
+
* plugin in phase `webPlugin`.
|
|
795
|
+
*
|
|
796
|
+
* Synchronous, side-effect-free: just build and return.
|
|
797
|
+
*/
|
|
798
|
+
webPlugin?(ctx: HostContext): WebPluginUi;
|
|
697
799
|
/**
|
|
698
800
|
* Supply {@link ModelMetaData} for a model the default (models.dev)
|
|
699
801
|
* database does not cover. The host calls this on every plugin that
|