@lobu/cli 6.0.1 → 7.0.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/README.md +20 -27
- package/dist/bundled-skills/lobu/SKILL.md +11 -11
- package/dist/commands/_lib/apply/apply-cmd.d.ts +38 -0
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +574 -40
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +180 -1
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +308 -28
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +703 -89
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +61 -3
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +382 -92
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/apply/prompt.d.ts +6 -0
- package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
- package/dist/commands/_lib/apply/prompt.js +16 -0
- package/dist/commands/_lib/apply/prompt.js.map +1 -1
- package/dist/commands/_lib/apply/render.d.ts +9 -0
- package/dist/commands/_lib/apply/render.d.ts.map +1 -1
- package/dist/commands/_lib/apply/render.js +80 -3
- package/dist/commands/_lib/apply/render.js.map +1 -1
- package/dist/commands/agent.d.ts +7 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +65 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts +12 -9
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +125 -57
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/dev.d.ts +23 -7
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +197 -49
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +136 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts +8 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +72 -6
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts +22 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +355 -182
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +28 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +14 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/mcp.d.ts +2 -2
- package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
- package/dist/commands/memory/_lib/mcp.js +24 -12
- package/dist/commands/memory/_lib/mcp.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
- package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
- package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/schema.d.ts +29 -2
- package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
- package/dist/commands/memory/_lib/schema.js +121 -5
- package/dist/commands/memory/_lib/schema.js.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.js +46 -24
- package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
- package/dist/commands/memory/run.d.ts.map +1 -1
- package/dist/commands/memory/run.js +2 -2
- package/dist/commands/memory/run.js.map +1 -1
- package/dist/commands/org.d.ts +4 -0
- package/dist/commands/org.d.ts.map +1 -1
- package/dist/commands/org.js +10 -0
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/platforms/platform-prompts.d.ts +0 -1
- package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
- package/dist/commands/platforms/platform-prompts.js +54 -8
- package/dist/commands/platforms/platform-prompts.js.map +1 -1
- package/dist/commands/telemetry.d.ts +10 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +68 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/token.d.ts +9 -0
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +54 -0
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connectors/README.md +534 -0
- package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
- package/dist/connectors/apple_health.ts +138 -0
- package/dist/connectors/apple_screen_time.ts +82 -0
- package/dist/connectors/browser-scraper-utils.ts +246 -0
- package/dist/connectors/capterra.ts +277 -0
- package/dist/connectors/g2.ts +290 -0
- package/dist/connectors/github.ts +1530 -0
- package/dist/connectors/glassdoor.ts +295 -0
- package/dist/connectors/gmaps.ts +197 -0
- package/dist/connectors/google_calendar.ts +641 -0
- package/dist/connectors/google_gmail.ts +754 -0
- package/dist/connectors/google_photos.ts +776 -0
- package/dist/connectors/google_play.ts +349 -0
- package/dist/connectors/hackernews.ts +471 -0
- package/dist/connectors/index.ts +28 -0
- package/dist/connectors/ios_appstore.ts +226 -0
- package/dist/connectors/linkedin.ts +494 -0
- package/dist/connectors/local_directory.ts +91 -0
- package/dist/connectors/microsoft_outlook.ts +410 -0
- package/dist/connectors/producthunt.ts +471 -0
- package/dist/connectors/reddit.ts +600 -0
- package/dist/connectors/revolut.ts +572 -0
- package/dist/connectors/rss.ts +448 -0
- package/dist/connectors/spotify.ts +590 -0
- package/dist/connectors/trustpilot.ts +203 -0
- package/dist/connectors/website.ts +629 -0
- package/dist/connectors/whatsapp.ts +1081 -0
- package/dist/connectors/whatsapp_local.ts +125 -0
- package/dist/connectors/x.ts +536 -0
- package/dist/connectors/youtube.ts +666 -0
- package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
- package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
- package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
- package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
- package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
- package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
- package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
- package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
- package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
- package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
- package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
- package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
- package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
- package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
- package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
- package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
- package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
- package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
- package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
- package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
- package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
- package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
- package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
- package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
- package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
- package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
- package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
- package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
- package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
- package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
- package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
- package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
- package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
- package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
- package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
- package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
- package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
- package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
- package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
- package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
- package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
- package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
- package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
- package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
- package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
- package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
- package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
- package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
- package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
- package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
- package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
- package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
- package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
- package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
- package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
- package/dist/eval/types.d.ts +2 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +210 -132
- package/dist/index.js.map +1 -1
- package/dist/internal/api-client.d.ts +4 -8
- package/dist/internal/api-client.d.ts.map +1 -1
- package/dist/internal/api-client.js +1 -1
- package/dist/internal/api-client.js.map +1 -1
- package/dist/internal/context.js +2 -2
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +6 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/gateway-url.d.ts +14 -0
- package/dist/internal/gateway-url.d.ts.map +1 -1
- package/dist/internal/gateway-url.js +19 -0
- package/dist/internal/gateway-url.js.map +1 -1
- package/dist/internal/index.d.ts +3 -4
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +3 -3
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/oauth.d.ts +6 -5
- package/dist/internal/oauth.d.ts.map +1 -1
- package/dist/internal/oauth.js +2 -2
- package/dist/internal/project-link.d.ts +10 -0
- package/dist/internal/project-link.d.ts.map +1 -0
- package/dist/internal/project-link.js +48 -0
- package/dist/internal/project-link.js.map +1 -0
- package/dist/providers.json +2 -2
- package/dist/server.bundle.mjs +31654 -30866
- package/dist/start-local.bundle.mjs +74409 -0
- package/dist/templates/README.md.tmpl +10 -11
- package/dist/templates/TESTING.md.tmpl +9 -9
- package/package.json +15 -13
- package/dist/__tests__/chat.integration.test.d.ts +0 -2
- package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
- package/dist/__tests__/chat.integration.test.js +0 -337
- package/dist/__tests__/chat.integration.test.js.map +0 -1
- package/dist/__tests__/dev.test.d.ts +0 -2
- package/dist/__tests__/dev.test.d.ts.map +0 -1
- package/dist/__tests__/dev.test.js +0 -25
- package/dist/__tests__/dev.test.js.map +0 -1
- package/dist/__tests__/init-memory.test.d.ts +0 -2
- package/dist/__tests__/init-memory.test.d.ts.map +0 -1
- package/dist/__tests__/init-memory.test.js +0 -45
- package/dist/__tests__/init-memory.test.js.map +0 -1
- package/dist/__tests__/token.test.d.ts +0 -2
- package/dist/__tests__/token.test.d.ts.map +0 -1
- package/dist/__tests__/token.test.js +0 -52
- package/dist/__tests__/token.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
- package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
- package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
- package/dist/commands/apply.d.ts +0 -3
- package/dist/commands/apply.d.ts.map +0 -1
- package/dist/commands/apply.js +0 -5
- package/dist/commands/apply.js.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
- package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
- package/dist/internal/__tests__/api-client.test.d.ts +0 -2
- package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
- package/dist/internal/__tests__/api-client.test.js +0 -95
- package/dist/internal/__tests__/api-client.test.js.map +0 -1
- package/dist/internal/__tests__/context.test.d.ts +0 -2
- package/dist/internal/__tests__/context.test.d.ts.map +0 -1
- package/dist/internal/__tests__/context.test.js +0 -77
- package/dist/internal/__tests__/context.test.js.map +0 -1
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
+
import Ajv from "ajv";
|
|
4
|
+
import addFormats from "ajv-formats";
|
|
3
5
|
import { parse as parseToml } from "smol-toml";
|
|
4
6
|
import { ValidationError } from "../../memory/_lib/errors.js";
|
|
7
|
+
import { expandModelDefinition, parseModelYamlFile, validateModel, } from "../../memory/_lib/schema.js";
|
|
5
8
|
import { CONFIG_FILENAME, isLoadError, loadConfig, } from "../../../config/loader.js";
|
|
9
|
+
import { CronExpressionParser } from "cron-parser";
|
|
10
|
+
// ── Connector slug / schedule validators (round-2) ─────────────────────────
|
|
11
|
+
// Mirror packages/server/src/utils/connections.ts CONNECTION_SLUG_PATTERN and
|
|
12
|
+
// the server's validateSchedule (packages/server/src/utils/cron.ts) so the CLI
|
|
13
|
+
// fails loud *before* any mutation instead of getting a server 4xx.
|
|
14
|
+
const CONNECTION_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
15
|
+
// auth_profiles slugs are sanitized server-side; require canonical form so the
|
|
16
|
+
// diff key matches what is stored (server cap is 80 chars).
|
|
17
|
+
const AUTH_PROFILE_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,79}$/;
|
|
18
|
+
const MIN_CRON_INTERVAL_MS = 60_000;
|
|
19
|
+
function cronError(schedule) {
|
|
20
|
+
try {
|
|
21
|
+
const it = CronExpressionParser.parse(schedule);
|
|
22
|
+
const first = it.next().toDate();
|
|
23
|
+
const second = it.next().toDate();
|
|
24
|
+
if (second.getTime() - first.getTime() < MIN_CRON_INTERVAL_MS) {
|
|
25
|
+
return `schedule "${schedule}" is too frequent (minimum interval is 1 minute)`;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
return `invalid cron expression "${schedule}" — ${err instanceof Error ? err.message : String(err)}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
6
33
|
// ── Stable platform IDs (mirror of file-loader.ts) ─────────────────────────
|
|
7
34
|
//
|
|
8
35
|
// keep in sync with packages/server/src/gateway/config/file-loader.ts
|
|
@@ -25,55 +52,48 @@ function asEnvRef(value) {
|
|
|
25
52
|
const match = ENV_REF.exec(value.trim());
|
|
26
53
|
return match?.[1] ?? null;
|
|
27
54
|
}
|
|
55
|
+
/** Visit every string leaf in `value` (recursing arrays + plain objects). */
|
|
56
|
+
function walkStrings(value, visit) {
|
|
57
|
+
if (typeof value === "string") {
|
|
58
|
+
visit(value);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
for (const item of value)
|
|
63
|
+
walkStrings(item, visit);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
for (const v of Object.values(value)) {
|
|
68
|
+
walkStrings(v, visit);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Add every `$ENV` reference found among the string leaves of `value`. */
|
|
73
|
+
function collectEnvRefsFrom(value, out) {
|
|
74
|
+
walkStrings(value, (s) => {
|
|
75
|
+
const ref = asEnvRef(s);
|
|
76
|
+
if (ref)
|
|
77
|
+
out.add(ref);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
28
80
|
function collectEnvRefs(config, out) {
|
|
29
81
|
for (const agentConfig of Object.values(config.agents)) {
|
|
30
82
|
for (const provider of agentConfig.providers) {
|
|
31
|
-
if (provider.key)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
if (provider.secret_ref) {
|
|
37
|
-
const ref = asEnvRef(provider.secret_ref);
|
|
38
|
-
if (ref)
|
|
39
|
-
out.add(ref);
|
|
40
|
-
}
|
|
83
|
+
if (provider.key)
|
|
84
|
+
collectEnvRefsFrom(provider.key, out);
|
|
85
|
+
if (provider.secret_ref)
|
|
86
|
+
collectEnvRefsFrom(provider.secret_ref, out);
|
|
41
87
|
}
|
|
42
88
|
for (const platform of agentConfig.platforms) {
|
|
43
|
-
|
|
44
|
-
const ref = asEnvRef(value);
|
|
45
|
-
if (ref)
|
|
46
|
-
out.add(ref);
|
|
47
|
-
}
|
|
89
|
+
collectEnvRefsFrom(platform.config, out);
|
|
48
90
|
}
|
|
49
91
|
if (agentConfig.skills.mcp) {
|
|
50
92
|
for (const mcp of Object.values(agentConfig.skills.mcp)) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
out.add(ref);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (mcp.env) {
|
|
59
|
-
for (const v of Object.values(mcp.env)) {
|
|
60
|
-
const ref = asEnvRef(v);
|
|
61
|
-
if (ref)
|
|
62
|
-
out.add(ref);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (mcp.oauth) {
|
|
66
|
-
if (mcp.oauth.client_id) {
|
|
67
|
-
const ref = asEnvRef(mcp.oauth.client_id);
|
|
68
|
-
if (ref)
|
|
69
|
-
out.add(ref);
|
|
70
|
-
}
|
|
71
|
-
if (mcp.oauth.client_secret) {
|
|
72
|
-
const ref = asEnvRef(mcp.oauth.client_secret);
|
|
73
|
-
if (ref)
|
|
74
|
-
out.add(ref);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
93
|
+
collectEnvRefsFrom(mcp.headers, out);
|
|
94
|
+
collectEnvRefsFrom(mcp.env, out);
|
|
95
|
+
collectEnvRefsFrom(mcp.oauth?.client_id, out);
|
|
96
|
+
collectEnvRefsFrom(mcp.oauth?.client_secret, out);
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
}
|
|
@@ -426,6 +446,17 @@ function buildPlatforms(agentId, agentConfig, env) {
|
|
|
426
446
|
};
|
|
427
447
|
if (platform.name)
|
|
428
448
|
desired.name = platform.name;
|
|
449
|
+
if (platform.channels && platform.channels.length > 0) {
|
|
450
|
+
if (platform.type !== "slack") {
|
|
451
|
+
throw new ValidationError(`agent "${agentId}" platform "${platform.type}": \`channels\` is only supported for Slack`);
|
|
452
|
+
}
|
|
453
|
+
for (const entry of platform.channels) {
|
|
454
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(entry.trim())) {
|
|
455
|
+
throw new ValidationError(`agent "${agentId}" Slack \`channels\` entry "${entry}" must be in "<teamId>/<channelId>" form (e.g. "T0ABCDEF/C0123ABCD")`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
desired.channels = platform.channels.map((e) => e.trim());
|
|
459
|
+
}
|
|
429
460
|
out.push(desired);
|
|
430
461
|
}
|
|
431
462
|
return out;
|
|
@@ -435,25 +466,61 @@ function isRecord(value) {
|
|
|
435
466
|
}
|
|
436
467
|
function parseEntityType(raw) {
|
|
437
468
|
if (!isRecord(raw) || typeof raw.slug !== "string") {
|
|
438
|
-
throw new ValidationError(`
|
|
469
|
+
throw new ValidationError(`model-bundle "entities" entries must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
|
|
439
470
|
}
|
|
440
471
|
const out = { slug: raw.slug };
|
|
441
472
|
if (typeof raw.name === "string")
|
|
442
473
|
out.name = raw.name;
|
|
443
474
|
if (typeof raw.description === "string")
|
|
444
475
|
out.description = raw.description;
|
|
445
|
-
if (
|
|
446
|
-
|
|
476
|
+
if (isRecord(raw.metadata_schema)) {
|
|
477
|
+
if (Array.isArray(raw.metadata_schema.required)) {
|
|
478
|
+
out.required = raw.metadata_schema.required.filter((v) => typeof v === "string");
|
|
479
|
+
}
|
|
480
|
+
if (isRecord(raw.metadata_schema.properties)) {
|
|
481
|
+
out.properties = raw.metadata_schema.properties;
|
|
482
|
+
}
|
|
447
483
|
}
|
|
448
|
-
if (isRecord(raw.properties))
|
|
449
|
-
out.properties = raw.properties;
|
|
450
484
|
if (isRecord(raw.metadata))
|
|
451
485
|
out.metadata = raw.metadata;
|
|
452
486
|
return out;
|
|
453
487
|
}
|
|
488
|
+
function parseWatcher(raw) {
|
|
489
|
+
if (!isRecord(raw) || typeof raw.slug !== "string") {
|
|
490
|
+
throw new ValidationError(`watcher model files must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
|
|
491
|
+
}
|
|
492
|
+
if (typeof raw.prompt !== "string" || !raw.prompt.trim()) {
|
|
493
|
+
throw new ValidationError(`watcher "${raw.slug}" is missing a "prompt" string`);
|
|
494
|
+
}
|
|
495
|
+
if (typeof raw.agent !== "string" || !raw.agent.trim()) {
|
|
496
|
+
throw new ValidationError(`watcher "${raw.slug}" is missing an "agent" field — every watcher must name the agent that owns it (e.g. \`agent: my-agent\`, matching an \`[agents.<id>]\` block in lobu.toml)`);
|
|
497
|
+
}
|
|
498
|
+
const extractionSchema = isRecord(raw.extraction_schema)
|
|
499
|
+
? raw.extraction_schema
|
|
500
|
+
: {};
|
|
501
|
+
const out = {
|
|
502
|
+
slug: raw.slug,
|
|
503
|
+
agent: raw.agent,
|
|
504
|
+
prompt: raw.prompt,
|
|
505
|
+
extractionSchema,
|
|
506
|
+
};
|
|
507
|
+
if (typeof raw.name === "string")
|
|
508
|
+
out.name = raw.name;
|
|
509
|
+
if (typeof raw.description === "string")
|
|
510
|
+
out.description = raw.description;
|
|
511
|
+
if (typeof raw.schedule === "string")
|
|
512
|
+
out.schedule = raw.schedule;
|
|
513
|
+
if (Array.isArray(raw.sources)) {
|
|
514
|
+
out.sources = raw.sources
|
|
515
|
+
.filter(isRecord)
|
|
516
|
+
.filter((s) => typeof s.name === "string" && typeof s.query === "string")
|
|
517
|
+
.map((s) => ({ name: s.name, query: s.query }));
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
454
521
|
function parseRelationshipType(raw) {
|
|
455
522
|
if (!isRecord(raw) || typeof raw.slug !== "string") {
|
|
456
|
-
throw new ValidationError(`
|
|
523
|
+
throw new ValidationError(`model-bundle "relationships" entries must be objects with a "slug" string field; got ${JSON.stringify(raw)}`);
|
|
457
524
|
}
|
|
458
525
|
const out = { slug: raw.slug };
|
|
459
526
|
if (typeof raw.name === "string")
|
|
@@ -471,57 +538,78 @@ function parseRelationshipType(raw) {
|
|
|
471
538
|
return out;
|
|
472
539
|
}
|
|
473
540
|
/**
|
|
474
|
-
* Read memory schema files referenced by `[memory
|
|
475
|
-
* file
|
|
476
|
-
* `
|
|
477
|
-
*
|
|
478
|
-
*
|
|
541
|
+
* Read memory schema files referenced by `[memory].models`. Every nested YAML
|
|
542
|
+
* file must be a dbt-style `version: 2` bundle with top-level `entities`,
|
|
543
|
+
* `relationships`, and `watchers` arrays; multi-document YAML streams are
|
|
544
|
+
* supported. `lobu apply` syncs entity types, relationship types, and watchers
|
|
545
|
+
* from these files; watcher sync is create-only (drift ignored).
|
|
479
546
|
*/
|
|
480
|
-
async function
|
|
481
|
-
const empty = {
|
|
482
|
-
|
|
483
|
-
|
|
547
|
+
async function loadMemoryModels(config, projectRoot) {
|
|
548
|
+
const empty = {
|
|
549
|
+
entityTypes: [],
|
|
550
|
+
relationshipTypes: [],
|
|
551
|
+
watchers: [],
|
|
552
|
+
};
|
|
553
|
+
const mem = config.memory;
|
|
554
|
+
if (!mem || mem.enabled === false)
|
|
484
555
|
return empty;
|
|
485
|
-
const inline = config.memory;
|
|
486
|
-
if (inline?.schema) {
|
|
487
|
-
const entityTypesRaw = Array.isArray(inline.schema.entity_types)
|
|
488
|
-
? inline.schema.entity_types
|
|
489
|
-
: [];
|
|
490
|
-
const relTypesRaw = Array.isArray(inline.schema.relationship_types)
|
|
491
|
-
? inline.schema.relationship_types
|
|
492
|
-
: [];
|
|
493
|
-
return {
|
|
494
|
-
entityTypes: entityTypesRaw.map(parseEntityType),
|
|
495
|
-
relationshipTypes: relTypesRaw.map(parseRelationshipType),
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
556
|
// Models directory (matches seed-cmd's resolution rules).
|
|
499
|
-
const modelsRel =
|
|
557
|
+
const modelsRel = mem.models?.trim() || "./models";
|
|
500
558
|
const modelsPath = resolve(projectRoot, modelsRel);
|
|
501
559
|
const { existsSync, readdirSync, readFileSync } = await import("node:fs");
|
|
502
|
-
const { parse: parseYaml } = await import("yaml");
|
|
503
560
|
if (!existsSync(modelsPath))
|
|
504
561
|
return empty;
|
|
505
562
|
const entityTypes = [];
|
|
506
563
|
const relationshipTypes = [];
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
564
|
+
const watchers = [];
|
|
565
|
+
const readModelFiles = (dir, prefix = "") => {
|
|
566
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
567
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
568
|
+
.flatMap((entry) => {
|
|
569
|
+
const relPath = prefix ? join(prefix, entry.name) : entry.name;
|
|
570
|
+
const fullPath = join(dir, entry.name);
|
|
571
|
+
if (entry.isDirectory())
|
|
572
|
+
return readModelFiles(fullPath, relPath);
|
|
573
|
+
if (entry.isFile() &&
|
|
574
|
+
(entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
|
|
575
|
+
return [relPath];
|
|
576
|
+
}
|
|
577
|
+
return [];
|
|
578
|
+
});
|
|
579
|
+
};
|
|
580
|
+
const errors = [];
|
|
581
|
+
for (const file of readModelFiles(modelsPath)) {
|
|
511
582
|
const raw = readFileSync(join(modelsPath, file), "utf-8");
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
583
|
+
const { documents, errors: parseErrors } = parseModelYamlFile(raw, file);
|
|
584
|
+
errors.push(...parseErrors);
|
|
585
|
+
for (const { data: document, file: documentFile } of documents) {
|
|
586
|
+
const expanded = expandModelDefinition(document, documentFile);
|
|
587
|
+
errors.push(...expanded.errors);
|
|
588
|
+
for (const model of expanded.models) {
|
|
589
|
+
const modelErrors = validateModel(model.data, model.file);
|
|
590
|
+
if (modelErrors.length > 0) {
|
|
591
|
+
errors.push(...modelErrors);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (model.modelType === "entity") {
|
|
595
|
+
entityTypes.push(parseEntityType(model.data));
|
|
596
|
+
}
|
|
597
|
+
else if (model.modelType === "relationship") {
|
|
598
|
+
relationshipTypes.push(parseRelationshipType(model.data));
|
|
599
|
+
}
|
|
600
|
+
else if (model.modelType === "watcher") {
|
|
601
|
+
watchers.push(parseWatcher(model.data));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
521
604
|
}
|
|
522
|
-
// watcher files are out of scope for v1 apply
|
|
523
605
|
}
|
|
524
|
-
|
|
606
|
+
if (errors.length > 0) {
|
|
607
|
+
const detail = errors
|
|
608
|
+
.map((e) => `${e.file}: ${e.field} — ${e.message}`)
|
|
609
|
+
.join("\n ");
|
|
610
|
+
throw new ValidationError(`Model validation failed\n ${detail}`);
|
|
611
|
+
}
|
|
612
|
+
return { entityTypes, relationshipTypes, watchers };
|
|
525
613
|
}
|
|
526
614
|
/**
|
|
527
615
|
* The Zod schema strips unknown keys, so we re-parse the raw TOML to surface
|
|
@@ -552,10 +640,513 @@ async function rejectUnsupportedAgentShapes(cwd) {
|
|
|
552
640
|
continue;
|
|
553
641
|
const watchers = agentConfig.watchers;
|
|
554
642
|
if (Array.isArray(watchers) && watchers.length > 0) {
|
|
555
|
-
throw new ValidationError(`agent "${agentId}" declares [[agents.${agentId}.watchers]]
|
|
643
|
+
throw new ValidationError(`agent "${agentId}" declares [[agents.${agentId}.watchers]] in lobu.toml — watchers live in a \`[memory].models\` YAML bundle (the same file as your entity types), each with an \`agent: ${agentId}\` field pointing back here. Move the watcher there.`);
|
|
556
644
|
}
|
|
557
645
|
}
|
|
558
646
|
}
|
|
647
|
+
// ── Connectors (data-source connectors) ───────────────────────────────────
|
|
648
|
+
const AUTH_PROFILE_KINDS = new Set([
|
|
649
|
+
"env",
|
|
650
|
+
"oauth_app",
|
|
651
|
+
"oauth_account",
|
|
652
|
+
"browser_session",
|
|
653
|
+
]);
|
|
654
|
+
function asString(value) {
|
|
655
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
656
|
+
}
|
|
657
|
+
function parseConnectionDoc(raw, file) {
|
|
658
|
+
const slug = asString(raw.slug);
|
|
659
|
+
if (!slug) {
|
|
660
|
+
throw new ValidationError(`${file}: \`type: connection\` doc is missing a "slug" string`);
|
|
661
|
+
}
|
|
662
|
+
if (!CONNECTION_SLUG_PATTERN.test(slug)) {
|
|
663
|
+
throw new ValidationError(`${file}: connection slug "${slug}" must match /^[a-z0-9][a-z0-9-]{0,62}$/ (lowercase letters/digits/hyphens, no leading hyphen, ≤63 chars)`);
|
|
664
|
+
}
|
|
665
|
+
const connector = asString(raw.connector);
|
|
666
|
+
if (!connector) {
|
|
667
|
+
throw new ValidationError(`${file}: connection "${slug}" is missing a "connector" key`);
|
|
668
|
+
}
|
|
669
|
+
const out = {
|
|
670
|
+
slug,
|
|
671
|
+
connector,
|
|
672
|
+
feeds: [],
|
|
673
|
+
sourceFile: file,
|
|
674
|
+
};
|
|
675
|
+
const name = asString(raw.name);
|
|
676
|
+
if (name)
|
|
677
|
+
out.name = name;
|
|
678
|
+
const auth = asString(raw.auth);
|
|
679
|
+
if (auth)
|
|
680
|
+
out.authProfileSlug = auth;
|
|
681
|
+
const appAuth = asString(raw.app_auth);
|
|
682
|
+
if (appAuth)
|
|
683
|
+
out.appAuthProfileSlug = appAuth;
|
|
684
|
+
if (raw.config !== undefined) {
|
|
685
|
+
if (!isRecord(raw.config)) {
|
|
686
|
+
throw new ValidationError(`${file}: connection "${slug}" \`config\` must be an object`);
|
|
687
|
+
}
|
|
688
|
+
out.config = raw.config;
|
|
689
|
+
}
|
|
690
|
+
if (raw.feeds !== undefined) {
|
|
691
|
+
if (!Array.isArray(raw.feeds)) {
|
|
692
|
+
throw new ValidationError(`${file}: connection "${slug}" \`feeds\` must be an array`);
|
|
693
|
+
}
|
|
694
|
+
const seen = new Set();
|
|
695
|
+
for (const entry of raw.feeds) {
|
|
696
|
+
if (!isRecord(entry)) {
|
|
697
|
+
throw new ValidationError(`${file}: connection "${slug}" feed entries must be objects`);
|
|
698
|
+
}
|
|
699
|
+
const feedKey = asString(entry.feed);
|
|
700
|
+
if (!feedKey) {
|
|
701
|
+
throw new ValidationError(`${file}: connection "${slug}" feed entry is missing a "feed" key`);
|
|
702
|
+
}
|
|
703
|
+
if (seen.has(feedKey)) {
|
|
704
|
+
throw new ValidationError(`${file}: connection "${slug}" declares feed "${feedKey}" twice`);
|
|
705
|
+
}
|
|
706
|
+
seen.add(feedKey);
|
|
707
|
+
const feed = { feedKey };
|
|
708
|
+
const feedName = asString(entry.name);
|
|
709
|
+
if (feedName)
|
|
710
|
+
feed.name = feedName;
|
|
711
|
+
const schedule = asString(entry.schedule);
|
|
712
|
+
if (schedule) {
|
|
713
|
+
const err = cronError(schedule);
|
|
714
|
+
if (err) {
|
|
715
|
+
throw new ValidationError(`${file}: connection "${slug}" feed "${feedKey}" ${err}`);
|
|
716
|
+
}
|
|
717
|
+
feed.schedule = schedule;
|
|
718
|
+
}
|
|
719
|
+
if (entry.config !== undefined) {
|
|
720
|
+
if (!isRecord(entry.config)) {
|
|
721
|
+
throw new ValidationError(`${file}: connection "${slug}" feed "${feedKey}" \`config\` must be an object`);
|
|
722
|
+
}
|
|
723
|
+
feed.config = entry.config;
|
|
724
|
+
}
|
|
725
|
+
out.feeds.push(feed);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return out;
|
|
729
|
+
}
|
|
730
|
+
function parseAuthProfileDoc(raw, file) {
|
|
731
|
+
const slug = asString(raw.slug);
|
|
732
|
+
if (!slug) {
|
|
733
|
+
throw new ValidationError(`${file}: \`type: auth_profile\` doc is missing a "slug" string`);
|
|
734
|
+
}
|
|
735
|
+
if (!AUTH_PROFILE_SLUG_PATTERN.test(slug)) {
|
|
736
|
+
throw new ValidationError(`${file}: auth_profile slug "${slug}" must match /^[a-z0-9][a-z0-9-]{0,79}$/ (lowercase letters/digits/hyphens, no leading hyphen, ≤80 chars)`);
|
|
737
|
+
}
|
|
738
|
+
const connector = asString(raw.connector);
|
|
739
|
+
if (!connector) {
|
|
740
|
+
throw new ValidationError(`${file}: auth_profile "${slug}" is missing a "connector" key`);
|
|
741
|
+
}
|
|
742
|
+
const kind = asString(raw.kind);
|
|
743
|
+
if (!kind || !AUTH_PROFILE_KINDS.has(kind)) {
|
|
744
|
+
throw new ValidationError(`${file}: auth_profile "${slug}" \`kind\` must be one of env|oauth_app|oauth_account|browser_session (got ${JSON.stringify(raw.kind)})`);
|
|
745
|
+
}
|
|
746
|
+
const out = {
|
|
747
|
+
slug,
|
|
748
|
+
connector,
|
|
749
|
+
kind: kind,
|
|
750
|
+
sourceFile: file,
|
|
751
|
+
};
|
|
752
|
+
const name = asString(raw.name);
|
|
753
|
+
if (name)
|
|
754
|
+
out.name = name;
|
|
755
|
+
if (raw.credentials !== undefined) {
|
|
756
|
+
if (!isRecord(raw.credentials)) {
|
|
757
|
+
throw new ValidationError(`${file}: auth_profile "${slug}" \`credentials\` must be an object`);
|
|
758
|
+
}
|
|
759
|
+
const creds = {};
|
|
760
|
+
for (const [k, v] of Object.entries(raw.credentials)) {
|
|
761
|
+
if (typeof v !== "string") {
|
|
762
|
+
throw new ValidationError(`${file}: auth_profile "${slug}" credential "${k}" must be a string (use $ENV for secrets)`);
|
|
763
|
+
}
|
|
764
|
+
creds[k] = v;
|
|
765
|
+
}
|
|
766
|
+
if (kind === "oauth_account" || kind === "browser_session") {
|
|
767
|
+
if (Object.keys(creds).length > 0) {
|
|
768
|
+
throw new ValidationError(`${file}: auth_profile "${slug}" has \`kind: ${kind}\` — credentials must not be set; \`lobu apply\` never writes interactive-auth tokens (complete auth via the connect URL).`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
out.credentials = creds;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
777
|
+
function parseConnectorDoc(raw, file) {
|
|
778
|
+
const key = asString(raw.key);
|
|
779
|
+
if (!key) {
|
|
780
|
+
throw new ValidationError(`${file}: \`type: connector\` doc is missing a "key" string`);
|
|
781
|
+
}
|
|
782
|
+
const sourcePath = asString(raw.source_path);
|
|
783
|
+
const sourceUrl = asString(raw.source_url);
|
|
784
|
+
if (!!sourcePath === !!sourceUrl) {
|
|
785
|
+
throw new ValidationError(`${file}: connector "${key}" must declare exactly one of \`source_path\` or \`source_url\``);
|
|
786
|
+
}
|
|
787
|
+
if (sourceUrl) {
|
|
788
|
+
let parsed;
|
|
789
|
+
try {
|
|
790
|
+
parsed = new URL(sourceUrl);
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
throw new ValidationError(`${file}: connector "${key}" source_url is not a valid URL: ${sourceUrl}`);
|
|
794
|
+
}
|
|
795
|
+
if (parsed.protocol !== "https:") {
|
|
796
|
+
throw new ValidationError(`${file}: connector "${key}" source_url must use https (got ${parsed.protocol}//)`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
key,
|
|
801
|
+
...(sourcePath ? { sourcePath } : {}),
|
|
802
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
const EMPTY_CONNECTORS = {
|
|
806
|
+
definitions: [],
|
|
807
|
+
authProfiles: [],
|
|
808
|
+
connections: [],
|
|
809
|
+
};
|
|
810
|
+
/**
|
|
811
|
+
* Load the `[memory].connectors` directory:
|
|
812
|
+
* - every `*.connector.ts` is auto-discovered as a connector definition
|
|
813
|
+
* (raw source pushed to the server, which compiles + extracts the key)
|
|
814
|
+
* - `*.yaml` files are multi-doc (`---`-separated); each doc carries
|
|
815
|
+
* `version: 1` and a `type:` of `connection`, `auth_profile`, or `connector`
|
|
816
|
+
*
|
|
817
|
+
* `connector:` config validation against the connector's `optionsSchema` /
|
|
818
|
+
* feed `configSchema` / `authSchema` happens later (in `apply-cmd`) once the
|
|
819
|
+
* remote connector-definition catalog is available — the CLI never compiles
|
|
820
|
+
* connectors locally.
|
|
821
|
+
*/
|
|
822
|
+
async function loadConnectors(config, projectRoot, env, envRefs) {
|
|
823
|
+
const mem = config.memory;
|
|
824
|
+
if (!mem || mem.enabled === false)
|
|
825
|
+
return EMPTY_CONNECTORS;
|
|
826
|
+
const dirRel = mem.connectors?.trim() || "./connectors";
|
|
827
|
+
const dirPath = resolve(projectRoot, dirRel);
|
|
828
|
+
let entries;
|
|
829
|
+
try {
|
|
830
|
+
entries = (await readdir(dirPath)).sort();
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return EMPTY_CONNECTORS;
|
|
834
|
+
}
|
|
835
|
+
const { parseAllDocuments } = await import("yaml");
|
|
836
|
+
const definitionsByKey = new Map();
|
|
837
|
+
// Keys explicitly declared by a `type: connector` doc (vs auto-discovered
|
|
838
|
+
// from a `*.connector.ts` filename). A given connector key may be declared by
|
|
839
|
+
// at most one such doc — even two docs pointing at the same `source_path`.
|
|
840
|
+
const connectorDocKeyDeclaredBy = new Map();
|
|
841
|
+
// `.connector.ts` files keyed by their *absolute path* — we don't know the
|
|
842
|
+
// connector key until the server compiles them. `type: connector` docs with
|
|
843
|
+
// `source_path:` that point at one of these files just dedupe to the file.
|
|
844
|
+
const tsFileDefinitions = new Map();
|
|
845
|
+
const authProfiles = new Map();
|
|
846
|
+
const connections = new Map();
|
|
847
|
+
for (const entry of entries) {
|
|
848
|
+
const entryPath = join(dirPath, entry);
|
|
849
|
+
let entryStat;
|
|
850
|
+
try {
|
|
851
|
+
entryStat = await stat(entryPath);
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (!entryStat.isFile())
|
|
857
|
+
continue;
|
|
858
|
+
// Auto-discovered local connector definition.
|
|
859
|
+
if (entry.endsWith(".connector.ts")) {
|
|
860
|
+
const sourceCode = await readFile(entryPath, "utf-8");
|
|
861
|
+
tsFileDefinitions.set(entryPath, {
|
|
862
|
+
key: null,
|
|
863
|
+
sourcePath: entryPath,
|
|
864
|
+
sourceCode,
|
|
865
|
+
sourceFile: `${dirRel}/${entry}`,
|
|
866
|
+
});
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml"))
|
|
870
|
+
continue;
|
|
871
|
+
const rel = `${dirRel}/${entry}`;
|
|
872
|
+
const raw = await readFile(entryPath, "utf-8");
|
|
873
|
+
let docs;
|
|
874
|
+
try {
|
|
875
|
+
docs = parseAllDocuments(raw)
|
|
876
|
+
.map((doc) => doc.toJSON())
|
|
877
|
+
.filter((doc) => doc !== null && doc !== undefined);
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
throw new ValidationError(`${rel}: failed to parse YAML — ${err instanceof Error ? err.message : String(err)}`);
|
|
881
|
+
}
|
|
882
|
+
for (const doc of docs) {
|
|
883
|
+
if (!isRecord(doc)) {
|
|
884
|
+
throw new ValidationError(`${rel}: each connectors doc must be a mapping with \`version\` and \`type\``);
|
|
885
|
+
}
|
|
886
|
+
const type = asString(doc.type);
|
|
887
|
+
if (!type) {
|
|
888
|
+
throw new ValidationError(`${rel}: connectors doc is missing a "type" (connection|auth_profile|connector)`);
|
|
889
|
+
}
|
|
890
|
+
if (doc.version !== undefined && doc.version !== 1) {
|
|
891
|
+
throw new ValidationError(`${rel}: unsupported connectors doc version ${JSON.stringify(doc.version)} (expected 1)`);
|
|
892
|
+
}
|
|
893
|
+
if (type === "connection") {
|
|
894
|
+
const conn = parseConnectionDoc(doc, rel);
|
|
895
|
+
if (connections.has(conn.slug)) {
|
|
896
|
+
throw new ValidationError(`${rel}: duplicate connection slug "${conn.slug}"`);
|
|
897
|
+
}
|
|
898
|
+
connections.set(conn.slug, conn);
|
|
899
|
+
}
|
|
900
|
+
else if (type === "auth_profile") {
|
|
901
|
+
const profile = parseAuthProfileDoc(doc, rel);
|
|
902
|
+
if (authProfiles.has(profile.slug)) {
|
|
903
|
+
throw new ValidationError(`${rel}: duplicate auth_profile slug "${profile.slug}"`);
|
|
904
|
+
}
|
|
905
|
+
if (profile.credentials) {
|
|
906
|
+
// Expand `$ENV` refs in-place (collect them too, so the apply
|
|
907
|
+
// secrets gate fails loud) — never push literal `$NAME` strings.
|
|
908
|
+
const resolved = {};
|
|
909
|
+
for (const [k, v] of Object.entries(profile.credentials)) {
|
|
910
|
+
const ref = asEnvRef(v);
|
|
911
|
+
if (!ref) {
|
|
912
|
+
resolved[k] = v;
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
envRefs.add(ref);
|
|
916
|
+
const value = env[ref];
|
|
917
|
+
if (value === undefined || value === "") {
|
|
918
|
+
throw new ValidationError(`${rel}: auth_profile "${profile.slug}" credential "${k}" references $${ref}, but it is unset or empty in the apply environment`);
|
|
919
|
+
}
|
|
920
|
+
resolved[k] = value;
|
|
921
|
+
}
|
|
922
|
+
profile.credentials = resolved;
|
|
923
|
+
}
|
|
924
|
+
authProfiles.set(profile.slug, profile);
|
|
925
|
+
}
|
|
926
|
+
else if (type === "connector") {
|
|
927
|
+
const parsed = parseConnectorDoc(doc, rel);
|
|
928
|
+
const priorDoc = connectorDocKeyDeclaredBy.get(parsed.key);
|
|
929
|
+
if (priorDoc) {
|
|
930
|
+
throw new ValidationError(`connector key "${parsed.key}" is declared by two \`type: connector\` docs — ${priorDoc} and ${rel}; keys must be unique`);
|
|
931
|
+
}
|
|
932
|
+
connectorDocKeyDeclaredBy.set(parsed.key, rel);
|
|
933
|
+
if (parsed.sourceUrl) {
|
|
934
|
+
const prior = definitionsByKey.get(parsed.key);
|
|
935
|
+
if (prior) {
|
|
936
|
+
throw new ValidationError(`connector key "${parsed.key}" is declared twice — in ${prior.sourceFile} and ${rel}; keys must be unique`);
|
|
937
|
+
}
|
|
938
|
+
const priorTs = [...tsFileDefinitions.values()].find((d) => d.key === parsed.key);
|
|
939
|
+
if (priorTs) {
|
|
940
|
+
throw new ValidationError(`connector key "${parsed.key}" is declared twice — in ${priorTs.sourceFile} and ${rel}; keys must be unique`);
|
|
941
|
+
}
|
|
942
|
+
definitionsByKey.set(parsed.key, {
|
|
943
|
+
key: parsed.key,
|
|
944
|
+
sourceUrl: parsed.sourceUrl,
|
|
945
|
+
sourceFile: rel,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
else if (parsed.sourcePath) {
|
|
949
|
+
// `source_path` is resolved relative to the manifest YAML file's
|
|
950
|
+
// directory (the connectors/ dir), matching the watcher-classifier
|
|
951
|
+
// `source_path` convention.
|
|
952
|
+
const abs = resolve(dirPath, parsed.sourcePath);
|
|
953
|
+
// The declared key must not collide with another connector definition.
|
|
954
|
+
const keyClash = definitionsByKey.get(parsed.key) ??
|
|
955
|
+
[...tsFileDefinitions.entries()].find(([p, d]) => d.key === parsed.key && p !== abs)?.[1];
|
|
956
|
+
if (keyClash) {
|
|
957
|
+
throw new ValidationError(`connector key "${parsed.key}" is declared twice — in ${keyClash.sourceFile} and ${rel}; keys must be unique`);
|
|
958
|
+
}
|
|
959
|
+
if (tsFileDefinitions.has(abs)) {
|
|
960
|
+
// Already auto-discovered as a `*.connector.ts` file; the
|
|
961
|
+
// `type: connector` doc just declares its key for clearer output.
|
|
962
|
+
const existing = tsFileDefinitions.get(abs);
|
|
963
|
+
if (existing) {
|
|
964
|
+
if (existing.key !== null && existing.key !== parsed.key) {
|
|
965
|
+
throw new ValidationError(`${existing.sourceFile} declares connector key "${existing.key}" but ${rel} declares "${parsed.key}" for the same file — they must agree`);
|
|
966
|
+
}
|
|
967
|
+
existing.key = parsed.key;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
let sourceCode;
|
|
972
|
+
try {
|
|
973
|
+
sourceCode = await readFile(abs, "utf-8");
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
throw new ValidationError(`${rel}: connector "${parsed.key}" \`source_path\` ${parsed.sourcePath} does not exist`);
|
|
977
|
+
}
|
|
978
|
+
tsFileDefinitions.set(abs, {
|
|
979
|
+
key: parsed.key,
|
|
980
|
+
sourcePath: abs,
|
|
981
|
+
sourceCode,
|
|
982
|
+
sourceFile: rel,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
throw new ValidationError(`${rel}: unknown connectors doc type "${type}" (expected connection|auth_profile|connector)`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const allDefs = [...definitionsByKey.values(), ...tsFileDefinitions.values()];
|
|
993
|
+
const seenKeys = new Map();
|
|
994
|
+
for (const def of allDefs) {
|
|
995
|
+
if (def.key === null)
|
|
996
|
+
continue;
|
|
997
|
+
const prior = seenKeys.get(def.key);
|
|
998
|
+
if (prior) {
|
|
999
|
+
throw new ValidationError(`connector key "${def.key}" is declared twice — in ${prior} and ${def.sourceFile}; keys must be unique`);
|
|
1000
|
+
}
|
|
1001
|
+
seenKeys.set(def.key, def.sourceFile);
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
definitions: allDefs.sort((a, b) => (a.key ?? a.sourceFile).localeCompare(b.key ?? b.sourceFile)),
|
|
1005
|
+
authProfiles: [...authProfiles.values()].sort((a, b) => a.slug.localeCompare(b.slug)),
|
|
1006
|
+
connections: [...connections.values()].sort((a, b) => a.slug.localeCompare(b.slug)),
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function schemaFromAuthMethods(authSchema) {
|
|
1010
|
+
const kinds = new Set();
|
|
1011
|
+
if (!authSchema || typeof authSchema !== "object")
|
|
1012
|
+
return kinds;
|
|
1013
|
+
const methods = authSchema.methods;
|
|
1014
|
+
if (!Array.isArray(methods))
|
|
1015
|
+
return kinds;
|
|
1016
|
+
for (const method of methods) {
|
|
1017
|
+
if (!isRecord(method))
|
|
1018
|
+
continue;
|
|
1019
|
+
const t = asString(method.type);
|
|
1020
|
+
// ConnectorAuthMethod `type` ∈ env_keys | oauth | browser | interactive | none
|
|
1021
|
+
if (t === "env_keys")
|
|
1022
|
+
kinds.add("env");
|
|
1023
|
+
else if (t === "oauth") {
|
|
1024
|
+
kinds.add("oauth_app");
|
|
1025
|
+
kinds.add("oauth_account");
|
|
1026
|
+
}
|
|
1027
|
+
else if (t === "browser" || t === "interactive") {
|
|
1028
|
+
kinds.add("browser_session");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return kinds;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Build per-connector validation schemas from a connector definition. Accepts
|
|
1035
|
+
* either a typed `ConnectorDefinition` (from `@lobu/connector-sdk`) or the
|
|
1036
|
+
* snake_cased shape the server's `manage_connections list_connector_definitions`
|
|
1037
|
+
* returns (`options_schema`, `feeds_schema`, `auth_schema`).
|
|
1038
|
+
*/
|
|
1039
|
+
export function resolveConnectorSchemas(def) {
|
|
1040
|
+
const optionsSchema = ("optionsSchema" in def ? def.optionsSchema : undefined) ??
|
|
1041
|
+
("options_schema" in def ? (def.options_schema ?? undefined) : undefined) ??
|
|
1042
|
+
undefined;
|
|
1043
|
+
const feedsRaw = ("feeds" in def ? def.feeds : undefined) ??
|
|
1044
|
+
("feeds_schema" in def ? (def.feeds_schema ?? undefined) : undefined) ??
|
|
1045
|
+
undefined;
|
|
1046
|
+
const authSchema = ("authSchema" in def ? def.authSchema : undefined) ??
|
|
1047
|
+
("auth_schema" in def ? (def.auth_schema ?? undefined) : undefined) ??
|
|
1048
|
+
undefined;
|
|
1049
|
+
const feedKeys = new Set();
|
|
1050
|
+
const feedConfigSchemas = new Map();
|
|
1051
|
+
if (feedsRaw && typeof feedsRaw === "object") {
|
|
1052
|
+
for (const [feedKey, feedDef] of Object.entries(feedsRaw)) {
|
|
1053
|
+
if (!feedDef || typeof feedDef !== "object")
|
|
1054
|
+
continue;
|
|
1055
|
+
feedKeys.add(feedKey);
|
|
1056
|
+
const cfg = feedDef.configSchema;
|
|
1057
|
+
if (cfg && typeof cfg === "object") {
|
|
1058
|
+
feedConfigSchemas.set(feedKey, cfg);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
...(optionsSchema ? { optionsSchema } : {}),
|
|
1064
|
+
feedKeys,
|
|
1065
|
+
feedConfigSchemas,
|
|
1066
|
+
authKinds: schemaFromAuthMethods(authSchema),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
let sharedAjv = null;
|
|
1070
|
+
function getAjv() {
|
|
1071
|
+
if (!sharedAjv) {
|
|
1072
|
+
sharedAjv = new Ajv({ allErrors: true, strict: false });
|
|
1073
|
+
addFormats(sharedAjv);
|
|
1074
|
+
}
|
|
1075
|
+
return sharedAjv;
|
|
1076
|
+
}
|
|
1077
|
+
function validateAgainstSchema(schema, value, context) {
|
|
1078
|
+
const ajv = getAjv();
|
|
1079
|
+
let validate;
|
|
1080
|
+
try {
|
|
1081
|
+
validate = ajv.compile(schema);
|
|
1082
|
+
}
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
// A malformed connector schema is the connector author's problem, not the
|
|
1085
|
+
// operator's — surface it but don't block the whole apply on it.
|
|
1086
|
+
throw new ValidationError(`${context}: connector declares an invalid JSON schema — ${err instanceof Error ? err.message : String(err)}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (!validate(value ?? {})) {
|
|
1089
|
+
const detail = (validate.errors ?? [])
|
|
1090
|
+
.map((e) => `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim())
|
|
1091
|
+
.join("; ");
|
|
1092
|
+
throw new ValidationError(`${context}: ${detail || "does not match the connector schema"}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Validate a single connection (+ its feeds) and its referenced auth-profile
|
|
1097
|
+
* kinds against a resolved connector schema. Pass `null` to skip schema
|
|
1098
|
+
* checks (e.g. a connector that only exists as a local `.ts` not yet
|
|
1099
|
+
* compiled by the server) — structural checks have already run at load time.
|
|
1100
|
+
*/
|
|
1101
|
+
export function validateConnectionAgainstConnector(connection, authProfiles, schemas) {
|
|
1102
|
+
// Validate against `{}` when config is omitted too — that surfaces missing
|
|
1103
|
+
// required keys instead of letting an empty config slip through.
|
|
1104
|
+
if (schemas?.optionsSchema) {
|
|
1105
|
+
validateAgainstSchema(schemas.optionsSchema, connection.config ?? {}, `${connection.sourceFile}: connection "${connection.slug}" config`);
|
|
1106
|
+
}
|
|
1107
|
+
for (const feed of connection.feeds) {
|
|
1108
|
+
if (!schemas)
|
|
1109
|
+
continue;
|
|
1110
|
+
if (schemas.feedKeys.size > 0 && !schemas.feedKeys.has(feed.feedKey)) {
|
|
1111
|
+
throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" references unknown feed "${feed.feedKey}" for connector "${connection.connector}" (known feeds: ${[...schemas.feedKeys].sort().join(", ") || "(none)"})`);
|
|
1112
|
+
}
|
|
1113
|
+
const feedSchema = schemas.feedConfigSchemas.get(feed.feedKey);
|
|
1114
|
+
if (feedSchema) {
|
|
1115
|
+
validateAgainstSchema(feedSchema, feed.config ?? {}, `${connection.sourceFile}: connection "${connection.slug}" feed "${feed.feedKey}" config`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// `auth:` must reference a runtime/account profile (never `oauth_app`);
|
|
1119
|
+
// `app_auth:` must reference an `oauth_app` profile.
|
|
1120
|
+
if (connection.authProfileSlug) {
|
|
1121
|
+
const profile = requireAuthProfile(connection, authProfiles, connection.authProfileSlug);
|
|
1122
|
+
if (profile.kind === "oauth_app") {
|
|
1123
|
+
throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" \`auth\` references auth profile "${connection.authProfileSlug}" of kind \`oauth_app\` — use \`app_auth\` for OAuth-app credentials and \`auth\` for the account/runtime profile`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (connection.appAuthProfileSlug) {
|
|
1127
|
+
const profile = requireAuthProfile(connection, authProfiles, connection.appAuthProfileSlug);
|
|
1128
|
+
if (profile.kind !== "oauth_app") {
|
|
1129
|
+
throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" \`app_auth\` must reference an \`oauth_app\` auth profile (got \`${profile.kind}\`)`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function requireAuthProfile(connection, authProfiles, slugRef) {
|
|
1134
|
+
const profile = authProfiles.get(slugRef);
|
|
1135
|
+
if (!profile) {
|
|
1136
|
+
throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" references auth profile "${slugRef}" which is not declared in any \`type: auth_profile\` doc`);
|
|
1137
|
+
}
|
|
1138
|
+
if (profile.connector !== connection.connector) {
|
|
1139
|
+
throw new ValidationError(`${connection.sourceFile}: connection "${connection.slug}" references auth profile "${slugRef}" for connector "${profile.connector}", but the connection uses connector "${connection.connector}"`);
|
|
1140
|
+
}
|
|
1141
|
+
return profile;
|
|
1142
|
+
}
|
|
1143
|
+
export function validateAuthProfileAgainstConnector(profile, schemas) {
|
|
1144
|
+
if (!schemas)
|
|
1145
|
+
return;
|
|
1146
|
+
if (schemas.authKinds.size > 0 && !schemas.authKinds.has(profile.kind)) {
|
|
1147
|
+
throw new ValidationError(`${profile.sourceFile}: auth_profile "${profile.slug}" uses \`kind: ${profile.kind}\`, but connector "${profile.connector}" supports: ${[...schemas.authKinds].sort().join(", ") || "(none)"}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
559
1150
|
export async function loadDesiredState(opts) {
|
|
560
1151
|
const result = await loadConfig(opts.cwd);
|
|
561
1152
|
if (isLoadError(result)) {
|
|
@@ -587,11 +1178,34 @@ export async function loadDesiredState(opts) {
|
|
|
587
1178
|
metadata.description = agentConfig.description;
|
|
588
1179
|
agents.push({ metadata, settings, platforms });
|
|
589
1180
|
}
|
|
590
|
-
const
|
|
1181
|
+
const { entityTypes, relationshipTypes, watchers } = await loadMemoryModels(config, opts.cwd);
|
|
1182
|
+
for (const watcher of watchers) {
|
|
1183
|
+
if (!config.agents[watcher.agent]) {
|
|
1184
|
+
throw new ValidationError(`watcher "${watcher.slug}" names agent "${watcher.agent}", but there is no \`[agents.${watcher.agent}]\` block in lobu.toml`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const connectors = opts.only
|
|
1188
|
+
? { definitions: [], authProfiles: [], connections: [] }
|
|
1189
|
+
: await loadConnectors(config, opts.cwd, env, requiredSecrets);
|
|
1190
|
+
const memory = config.memory && config.memory.enabled !== false
|
|
1191
|
+
? {
|
|
1192
|
+
...(config.memory.org ? { org: config.memory.org } : {}),
|
|
1193
|
+
...(config.memory.organization_id
|
|
1194
|
+
? { organizationId: config.memory.organization_id }
|
|
1195
|
+
: {}),
|
|
1196
|
+
...(config.memory.name ? { name: config.memory.name } : {}),
|
|
1197
|
+
...(config.memory.description
|
|
1198
|
+
? { description: config.memory.description }
|
|
1199
|
+
: {}),
|
|
1200
|
+
}
|
|
1201
|
+
: undefined;
|
|
591
1202
|
return {
|
|
592
1203
|
state: {
|
|
593
1204
|
agents,
|
|
594
|
-
|
|
1205
|
+
...(memory ? { memory } : {}),
|
|
1206
|
+
memorySchema: { entityTypes, relationshipTypes },
|
|
1207
|
+
watchers,
|
|
1208
|
+
connectors,
|
|
595
1209
|
requiredSecrets: [...requiredSecrets].sort(),
|
|
596
1210
|
},
|
|
597
1211
|
configPath,
|