mercury-agent 0.4.23 → 0.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +5 -0
  2. package/docs/ARCHITECTURE.md +30 -0
  3. package/docs/DESIGN.md +42 -0
  4. package/docs/ROADMAP.md +41 -0
  5. package/docs/VISION.md +32 -0
  6. package/docs/archive/.gitkeep +0 -0
  7. package/docs/backlog/.gitkeep +0 -0
  8. package/docs/bugs/.gitkeep +0 -0
  9. package/docs/bugs/bwrap-privileged-linux-docker.md +140 -0
  10. package/docs/debug/major/.gitkeep +0 -0
  11. package/docs/debug/minor/.gitkeep +0 -0
  12. package/docs/debug/moderate/.gitkeep +0 -0
  13. package/docs/html-slides/.gitkeep +0 -0
  14. package/docs/ideas/.gitkeep +0 -0
  15. package/docs/in-progress/.gitkeep +0 -0
  16. package/docs/notes/.gitkeep +0 -0
  17. package/docs/pending-updates/.gitkeep +0 -0
  18. package/docs/runbooks/publish-checklist.md +46 -0
  19. package/docs/templates/TEMPLATE-AUTOPILOT-CONFIG.yaml +35 -0
  20. package/docs/templates/TEMPLATE-BUG.md +71 -0
  21. package/docs/templates/TEMPLATE-CI.yml +25 -0
  22. package/docs/templates/TEMPLATE-DECISIONS.md +11 -0
  23. package/docs/templates/TEMPLATE-FEATURE.md +127 -0
  24. package/docs/templates/TEMPLATE-GOAL.md +31 -0
  25. package/docs/templates/TEMPLATE-IDEA.md +31 -0
  26. package/docs/templates/TEMPLATE-NOTE.md +27 -0
  27. package/docs/templates/TEMPLATE-ROADMAP.md +21 -0
  28. package/examples/extensions/pinchtab/index.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/adapters/whatsapp.ts +635 -635
  31. package/src/agent/container-entry.ts +1021 -1021
  32. package/src/agent/container-runner.ts +18 -12
  33. package/src/agent/model-capabilities.ts +231 -231
  34. package/src/bridges/discord.ts +178 -178
  35. package/src/bridges/slack.ts +179 -179
  36. package/src/bridges/teams.ts +162 -162
  37. package/src/bridges/telegram.ts +579 -579
  38. package/src/cli/mercury.ts +2587 -2585
  39. package/src/cli/whatsapp-auth.ts +263 -263
  40. package/src/config.ts +316 -316
  41. package/src/core/commands.ts +110 -110
  42. package/src/core/permissions.ts +196 -196
  43. package/src/core/router.ts +211 -204
  44. package/src/core/routes/chat.ts +175 -175
  45. package/src/core/routes/dashboard.ts +2493 -2493
  46. package/src/core/routes/messages.ts +37 -37
  47. package/src/core/routes/mutes.ts +95 -95
  48. package/src/core/routes/roles.ts +135 -135
  49. package/src/core/runtime.ts +1508 -1508
  50. package/src/core/task-scheduler.ts +139 -139
  51. package/src/extensions/catalog.ts +117 -117
  52. package/src/extensions/hooks.ts +161 -161
  53. package/src/extensions/installer.ts +306 -306
  54. package/src/extensions/loader.ts +271 -271
  55. package/src/extensions/permission-guard.ts +52 -52
  56. package/src/server.ts +391 -391
  57. package/src/storage/db.ts +1684 -1687
  58. package/src/storage/pi-auth.ts +95 -95
  59. package/src/tts/azure.ts +52 -52
  60. package/src/tts/synthesize.ts +133 -133
@@ -978,18 +978,24 @@ export class AgentContainerRunner {
978
978
  "CONTAINER_RUNTIME=runsc",
979
979
  );
980
980
  } else if (this.config.containerBwrapDockerCompat || isDockerDesktop()) {
981
- logger.info("Enabling bwrap Docker compat (seccomp/apparmor/SYS_ADMIN)", {
982
- configFlag: this.config.containerBwrapDockerCompat,
983
- dockerDesktop: isDockerDesktop(),
984
- });
985
- args.push(
986
- "--security-opt",
987
- "seccomp=unconfined",
988
- "--security-opt",
989
- "apparmor=unconfined",
990
- "--cap-add",
991
- "SYS_ADMIN",
992
- );
981
+ if (this.config.containerBwrapDockerCompat) {
982
+ logger.info(
983
+ "Enabling bwrap Docker compat with --privileged (containerBwrapDockerCompat=true)",
984
+ );
985
+ args.push("--privileged");
986
+ } else {
987
+ logger.info(
988
+ "Enabling bwrap Docker compat (seccomp/apparmor/SYS_ADMIN) for Docker Desktop",
989
+ );
990
+ args.push(
991
+ "--security-opt",
992
+ "seccomp=unconfined",
993
+ "--security-opt",
994
+ "apparmor=unconfined",
995
+ "--cap-add",
996
+ "SYS_ADMIN",
997
+ );
998
+ }
993
999
  }
994
1000
 
995
1001
  for (const { key, value } of envPairs) {
@@ -1,231 +1,231 @@
1
- /**
2
- * Model capability flags — used to adapt prompts, pi tool flags, and skill installation.
3
- */
4
-
5
- import { existsSync, readFileSync } from "node:fs";
6
- import path from "node:path";
7
- import { getModels, type KnownProvider } from "@earendil-works/pi-ai";
8
- import { parse as parseYaml } from "yaml";
9
- import { z } from "zod";
10
- import type { ModelLeg } from "../config.js";
11
- import {
12
- DEFAULT_CAPABILITIES,
13
- type ModelCapabilities,
14
- type ModelCapabilityKey,
15
- } from "./model-capabilities-core.js";
16
-
17
- export type {
18
- ModelCapabilities,
19
- ModelCapabilityKey,
20
- } from "./model-capabilities-core.js";
21
- export { DEFAULT_CAPABILITIES } from "./model-capabilities-core.js";
22
-
23
- export type CapabilityResolveSource = "env" | "yaml" | "builtin" | "default";
24
-
25
- export type ResolvedModelCapabilities = {
26
- capabilities: ModelCapabilities;
27
- source: CapabilityResolveSource;
28
- };
29
-
30
- function mergePartialCapabilities(
31
- partial: Partial<Record<ModelCapabilityKey, unknown>>,
32
- ): ModelCapabilities {
33
- const out = { ...DEFAULT_CAPABILITIES };
34
- for (const k of Object.keys(partial) as ModelCapabilityKey[]) {
35
- const v = partial[k];
36
- if (typeof v === "boolean") out[k] = v;
37
- }
38
- return out;
39
- }
40
-
41
- const partialCapsSchema = z.object({
42
- tools: z.boolean().optional(),
43
- vision: z.boolean().optional(),
44
- audio_input: z.boolean().optional(),
45
- audio_output: z.boolean().optional(),
46
- extended_thinking: z.boolean().optional(),
47
- });
48
-
49
- const yamlFileSchema = z.object({
50
- models: z.record(z.string(), partialCapsSchema),
51
- });
52
-
53
- export type UserModelCapabilitiesMap = Map<string, ModelCapabilities>;
54
-
55
- /** Path to optional user overrides: `<dataDir>/model-capabilities.yaml` */
56
- export function modelCapabilitiesYamlPath(dataDirAbsolute: string): string {
57
- return path.join(dataDirAbsolute, "model-capabilities.yaml");
58
- }
59
-
60
- /**
61
- * Load `.mercury/model-capabilities.yaml` (under resolved data dir).
62
- * Returns null if missing or invalid (callers may log).
63
- */
64
- export function loadUserModelCapabilitiesMap(
65
- dataDirAbsolute: string,
66
- ): UserModelCapabilitiesMap | null {
67
- const yamlPath = modelCapabilitiesYamlPath(dataDirAbsolute);
68
- if (!existsSync(yamlPath)) return null;
69
- try {
70
- const raw = readFileSync(yamlPath, "utf8");
71
- const doc = parseYaml(raw) as unknown;
72
- const parsed = yamlFileSchema.safeParse(doc);
73
- if (!parsed.success) return null;
74
- const map = new Map<string, ModelCapabilities>();
75
- for (const [modelId, partial] of Object.entries(parsed.data.models)) {
76
- map.set(modelId.trim(), mergePartialCapabilities(partial));
77
- }
78
- return map;
79
- } catch {
80
- return null;
81
- }
82
- }
83
-
84
- /**
85
- * Look up capabilities for a model using pi's MODELS registry as the source of truth.
86
- * Derives: vision (image in input), audio_input (audio in input), extended_thinking (reasoning).
87
- * tools defaults to true — pi has no tools field; all pi-known models support tool use.
88
- * audio_output is not tracked by pi; always false.
89
- */
90
- function matchBuiltinCapabilities(
91
- provider: string,
92
- modelId: string,
93
- ): ModelCapabilities | null {
94
- // Cast to KnownProvider — getModels returns [] for unrecognised providers at runtime
95
- const model = getModels(provider as KnownProvider).find(
96
- (m) => m.id === modelId,
97
- );
98
- if (!model) return null;
99
- return {
100
- tools: true,
101
- vision: model.input.includes("image"),
102
- audio_input: false, // pi types input as ("text"|"image")[]; no audio models exist yet
103
- audio_output: false,
104
- extended_thinking: model.reasoning,
105
- };
106
- }
107
-
108
- /** Parse MERCURY_MODEL_CAPABILITIES JSON; returns null if unset or invalid. */
109
- export function parseModelCapabilitiesEnv(
110
- raw: string | undefined,
111
- ): ModelCapabilities | null {
112
- const trimmed = raw?.trim();
113
- if (!trimmed) return null;
114
- try {
115
- const json = JSON.parse(trimmed) as unknown;
116
- const r = partialCapsSchema.safeParse(json);
117
- if (!r.success) return null;
118
- return mergePartialCapabilities(r.data);
119
- } catch {
120
- return null;
121
- }
122
- }
123
-
124
- /**
125
- * Resolve capabilities for one model id (single leg).
126
- * Priority: env global override → YAML exact model key → pi MODELS lookup → default.
127
- */
128
- export function resolveModelCapabilitiesWithSource(
129
- modelId: string,
130
- provider: string,
131
- userMap: UserModelCapabilitiesMap | null,
132
- envCaps: ModelCapabilities | null,
133
- ): ResolvedModelCapabilities {
134
- if (envCaps) {
135
- return { capabilities: envCaps, source: "env" };
136
- }
137
-
138
- const exact = userMap?.get(modelId.trim());
139
- if (exact) {
140
- return { capabilities: exact, source: "yaml" };
141
- }
142
-
143
- const builtin = matchBuiltinCapabilities(provider, modelId);
144
- if (builtin) {
145
- return { capabilities: builtin, source: "builtin" };
146
- }
147
-
148
- return { capabilities: { ...DEFAULT_CAPABILITIES }, source: "default" };
149
- }
150
-
151
- export function resolveModelCapabilities(
152
- modelId: string,
153
- provider: string,
154
- userMap: UserModelCapabilitiesMap | null,
155
- envCaps: ModelCapabilities | null,
156
- ): ModelCapabilities {
157
- return resolveModelCapabilitiesWithSource(modelId, provider, userMap, envCaps)
158
- .capabilities;
159
- }
160
-
161
- /** Capabilities for each leg in the model chain (same length as `chain`). */
162
- export function resolveModelChainCapabilities(
163
- chain: ModelLeg[],
164
- dataDirAbsolute: string,
165
- envCaps: ModelCapabilities | null,
166
- ): {
167
- chainCaps: ModelCapabilities[];
168
- userMap: UserModelCapabilitiesMap | null;
169
- } {
170
- const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
171
- const chainCaps = chain.map((leg) =>
172
- resolveModelCapabilities(leg.model, leg.provider, userMap, envCaps),
173
- );
174
- return { chainCaps, userMap };
175
- }
176
-
177
- export function chainSupportsRequirements(
178
- requires: ModelCapabilityKey[],
179
- chainCaps: ModelCapabilities[],
180
- ): boolean {
181
- if (requires.length === 0) return true;
182
- return chainCaps.some((caps) => requires.every((key) => caps[key] === true));
183
- }
184
-
185
- /** Log warnings for models that fell back to defaults (once per distinct model id). */
186
- export function logUnknownModelCapabilityWarnings(
187
- chain: ModelLeg[],
188
- dataDirAbsolute: string,
189
- envCaps: ModelCapabilities | null,
190
- log: { warn: (msg: string, obj?: Record<string, unknown>) => void },
191
- ): void {
192
- if (envCaps) return;
193
- const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
194
- const seen = new Set<string>();
195
-
196
- for (const leg of chain) {
197
- const id = leg.model.trim();
198
- if (seen.has(id)) continue;
199
- seen.add(id);
200
-
201
- const { source } = resolveModelCapabilitiesWithSource(
202
- id,
203
- leg.provider,
204
- userMap,
205
- null,
206
- );
207
- if (source === "default") {
208
- log.warn(
209
- `Model "${id}" not in built-in capability map and not in model-capabilities.yaml; assuming default capabilities (tools=true, vision=false). Set MERCURY_MODEL_CAPABILITIES or add .mercury/model-capabilities.yaml to override.`,
210
- { model: id },
211
- );
212
- }
213
- }
214
- }
215
-
216
- export function logExtensionCapabilityMismatches(
217
- extensions: Array<{ name: string; requires?: ModelCapabilityKey[] }>,
218
- chainCaps: ModelCapabilities[],
219
- log: { warn: (msg: string, obj?: Record<string, unknown>) => void },
220
- ): void {
221
- for (const ext of extensions) {
222
- const req = ext.requires;
223
- if (!req?.length) continue;
224
- if (!chainSupportsRequirements(req, chainCaps)) {
225
- log.warn(
226
- `Extension "${ext.name}" requires ${req.join(", ")} but no model leg in MERCURY_MODEL_CHAIN supports it; extension skills will not be installed.`,
227
- { extension: ext.name, requires: req },
228
- );
229
- }
230
- }
231
- }
1
+ /**
2
+ * Model capability flags — used to adapt prompts, pi tool flags, and skill installation.
3
+ */
4
+
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { getModels, type KnownProvider } from "@earendil-works/pi-ai";
8
+ import { parse as parseYaml } from "yaml";
9
+ import { z } from "zod";
10
+ import type { ModelLeg } from "../config.js";
11
+ import {
12
+ DEFAULT_CAPABILITIES,
13
+ type ModelCapabilities,
14
+ type ModelCapabilityKey,
15
+ } from "./model-capabilities-core.js";
16
+
17
+ export type {
18
+ ModelCapabilities,
19
+ ModelCapabilityKey,
20
+ } from "./model-capabilities-core.js";
21
+ export { DEFAULT_CAPABILITIES } from "./model-capabilities-core.js";
22
+
23
+ export type CapabilityResolveSource = "env" | "yaml" | "builtin" | "default";
24
+
25
+ export type ResolvedModelCapabilities = {
26
+ capabilities: ModelCapabilities;
27
+ source: CapabilityResolveSource;
28
+ };
29
+
30
+ function mergePartialCapabilities(
31
+ partial: Partial<Record<ModelCapabilityKey, unknown>>,
32
+ ): ModelCapabilities {
33
+ const out = { ...DEFAULT_CAPABILITIES };
34
+ for (const k of Object.keys(partial) as ModelCapabilityKey[]) {
35
+ const v = partial[k];
36
+ if (typeof v === "boolean") out[k] = v;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ const partialCapsSchema = z.object({
42
+ tools: z.boolean().optional(),
43
+ vision: z.boolean().optional(),
44
+ audio_input: z.boolean().optional(),
45
+ audio_output: z.boolean().optional(),
46
+ extended_thinking: z.boolean().optional(),
47
+ });
48
+
49
+ const yamlFileSchema = z.object({
50
+ models: z.record(z.string(), partialCapsSchema),
51
+ });
52
+
53
+ export type UserModelCapabilitiesMap = Map<string, ModelCapabilities>;
54
+
55
+ /** Path to optional user overrides: `<dataDir>/model-capabilities.yaml` */
56
+ export function modelCapabilitiesYamlPath(dataDirAbsolute: string): string {
57
+ return path.join(dataDirAbsolute, "model-capabilities.yaml");
58
+ }
59
+
60
+ /**
61
+ * Load `.mercury/model-capabilities.yaml` (under resolved data dir).
62
+ * Returns null if missing or invalid (callers may log).
63
+ */
64
+ export function loadUserModelCapabilitiesMap(
65
+ dataDirAbsolute: string,
66
+ ): UserModelCapabilitiesMap | null {
67
+ const yamlPath = modelCapabilitiesYamlPath(dataDirAbsolute);
68
+ if (!existsSync(yamlPath)) return null;
69
+ try {
70
+ const raw = readFileSync(yamlPath, "utf8");
71
+ const doc = parseYaml(raw) as unknown;
72
+ const parsed = yamlFileSchema.safeParse(doc);
73
+ if (!parsed.success) return null;
74
+ const map = new Map<string, ModelCapabilities>();
75
+ for (const [modelId, partial] of Object.entries(parsed.data.models)) {
76
+ map.set(modelId.trim(), mergePartialCapabilities(partial));
77
+ }
78
+ return map;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Look up capabilities for a model using pi's MODELS registry as the source of truth.
86
+ * Derives: vision (image in input), audio_input (audio in input), extended_thinking (reasoning).
87
+ * tools defaults to true — pi has no tools field; all pi-known models support tool use.
88
+ * audio_output is not tracked by pi; always false.
89
+ */
90
+ function matchBuiltinCapabilities(
91
+ provider: string,
92
+ modelId: string,
93
+ ): ModelCapabilities | null {
94
+ // Cast to KnownProvider — getModels returns [] for unrecognised providers at runtime
95
+ const model = getModels(provider as KnownProvider).find(
96
+ (m) => m.id === modelId,
97
+ );
98
+ if (!model) return null;
99
+ return {
100
+ tools: true,
101
+ vision: model.input.includes("image"),
102
+ audio_input: false, // pi types input as ("text"|"image")[]; no audio models exist yet
103
+ audio_output: false,
104
+ extended_thinking: model.reasoning,
105
+ };
106
+ }
107
+
108
+ /** Parse MERCURY_MODEL_CAPABILITIES JSON; returns null if unset or invalid. */
109
+ export function parseModelCapabilitiesEnv(
110
+ raw: string | undefined,
111
+ ): ModelCapabilities | null {
112
+ const trimmed = raw?.trim();
113
+ if (!trimmed) return null;
114
+ try {
115
+ const json = JSON.parse(trimmed) as unknown;
116
+ const r = partialCapsSchema.safeParse(json);
117
+ if (!r.success) return null;
118
+ return mergePartialCapabilities(r.data);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Resolve capabilities for one model id (single leg).
126
+ * Priority: env global override → YAML exact model key → pi MODELS lookup → default.
127
+ */
128
+ export function resolveModelCapabilitiesWithSource(
129
+ modelId: string,
130
+ provider: string,
131
+ userMap: UserModelCapabilitiesMap | null,
132
+ envCaps: ModelCapabilities | null,
133
+ ): ResolvedModelCapabilities {
134
+ if (envCaps) {
135
+ return { capabilities: envCaps, source: "env" };
136
+ }
137
+
138
+ const exact = userMap?.get(modelId.trim());
139
+ if (exact) {
140
+ return { capabilities: exact, source: "yaml" };
141
+ }
142
+
143
+ const builtin = matchBuiltinCapabilities(provider, modelId);
144
+ if (builtin) {
145
+ return { capabilities: builtin, source: "builtin" };
146
+ }
147
+
148
+ return { capabilities: { ...DEFAULT_CAPABILITIES }, source: "default" };
149
+ }
150
+
151
+ export function resolveModelCapabilities(
152
+ modelId: string,
153
+ provider: string,
154
+ userMap: UserModelCapabilitiesMap | null,
155
+ envCaps: ModelCapabilities | null,
156
+ ): ModelCapabilities {
157
+ return resolveModelCapabilitiesWithSource(modelId, provider, userMap, envCaps)
158
+ .capabilities;
159
+ }
160
+
161
+ /** Capabilities for each leg in the model chain (same length as `chain`). */
162
+ export function resolveModelChainCapabilities(
163
+ chain: ModelLeg[],
164
+ dataDirAbsolute: string,
165
+ envCaps: ModelCapabilities | null,
166
+ ): {
167
+ chainCaps: ModelCapabilities[];
168
+ userMap: UserModelCapabilitiesMap | null;
169
+ } {
170
+ const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
171
+ const chainCaps = chain.map((leg) =>
172
+ resolveModelCapabilities(leg.model, leg.provider, userMap, envCaps),
173
+ );
174
+ return { chainCaps, userMap };
175
+ }
176
+
177
+ export function chainSupportsRequirements(
178
+ requires: ModelCapabilityKey[],
179
+ chainCaps: ModelCapabilities[],
180
+ ): boolean {
181
+ if (requires.length === 0) return true;
182
+ return chainCaps.some((caps) => requires.every((key) => caps[key] === true));
183
+ }
184
+
185
+ /** Log warnings for models that fell back to defaults (once per distinct model id). */
186
+ export function logUnknownModelCapabilityWarnings(
187
+ chain: ModelLeg[],
188
+ dataDirAbsolute: string,
189
+ envCaps: ModelCapabilities | null,
190
+ log: { warn: (msg: string, obj?: Record<string, unknown>) => void },
191
+ ): void {
192
+ if (envCaps) return;
193
+ const userMap = loadUserModelCapabilitiesMap(dataDirAbsolute);
194
+ const seen = new Set<string>();
195
+
196
+ for (const leg of chain) {
197
+ const id = leg.model.trim();
198
+ if (seen.has(id)) continue;
199
+ seen.add(id);
200
+
201
+ const { source } = resolveModelCapabilitiesWithSource(
202
+ id,
203
+ leg.provider,
204
+ userMap,
205
+ null,
206
+ );
207
+ if (source === "default") {
208
+ log.warn(
209
+ `Model "${id}" not in built-in capability map and not in model-capabilities.yaml; assuming default capabilities (tools=true, vision=false). Set MERCURY_MODEL_CAPABILITIES or add .mercury/model-capabilities.yaml to override.`,
210
+ { model: id },
211
+ );
212
+ }
213
+ }
214
+ }
215
+
216
+ export function logExtensionCapabilityMismatches(
217
+ extensions: Array<{ name: string; requires?: ModelCapabilityKey[] }>,
218
+ chainCaps: ModelCapabilities[],
219
+ log: { warn: (msg: string, obj?: Record<string, unknown>) => void },
220
+ ): void {
221
+ for (const ext of extensions) {
222
+ const req = ext.requires;
223
+ if (!req?.length) continue;
224
+ if (!chainSupportsRequirements(req, chainCaps)) {
225
+ log.warn(
226
+ `Extension "${ext.name}" requires ${req.join(", ")} but no model leg in MERCURY_MODEL_CHAIN supports it; extension skills will not be installed.`,
227
+ { extension: ext.name, requires: req },
228
+ );
229
+ }
230
+ }
231
+ }