neoagent 2.3.0 → 2.3.1-beta.2
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 +1 -1
- package/docs/automation.md +1 -1
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +10 -1
- package/docs/integrations.md +5 -0
- package/package.json +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +17742 -17688
- package/server/routes/integrations.js +11 -8
- package/server/services/ai/engine.js +9 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +120 -10
- package/server/services/integrations/env.js +14 -0
- package/server/services/integrations/home_assistant/provider.js +350 -0
- package/server/services/integrations/registry.js +6 -0
- package/server/services/integrations/spotify/provider.js +487 -0
- package/server/services/integrations/weather/provider.js +559 -0
- package/server/services/messaging/manager.js +29 -7
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/schedule.js +33 -5
- package/server/services/tasks/adapters/weather_event.js +84 -0
- package/server/services/tasks/integration_runtime.js +85 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/agentBridge.js +20 -4
- package/server/services/voice/message.js +3 -0
- package/server/services/voice/runtimeManager.js +136 -1
|
@@ -159,6 +159,9 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
159
159
|
if (!session) {
|
|
160
160
|
return res.status(404).send('Connection session not found.');
|
|
161
161
|
}
|
|
162
|
+
const provider = manager.getProvider(req.params.provider);
|
|
163
|
+
const providerLabel = provider?.label || req.params.provider;
|
|
164
|
+
const appLabel = provider?.getApp?.(session.appKey)?.label || session.appKey || 'account';
|
|
162
165
|
const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
|
|
163
166
|
const statusUrl = `/api/integrations/${encodeURIComponent(req.params.provider)}/connect/${encodeURIComponent(req.params.sessionId)}/status?agentId=${encodeURIComponent(agentId)}`;
|
|
164
167
|
res.send(`
|
|
@@ -166,7 +169,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
166
169
|
<head>
|
|
167
170
|
<meta charset="utf-8" />
|
|
168
171
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
169
|
-
<title>Connect ${escapeHtml(
|
|
172
|
+
<title>Connect ${escapeHtml(providerLabel)}</title>
|
|
170
173
|
<style>
|
|
171
174
|
body { font-family: ui-sans-serif, system-ui, sans-serif; background: #0b1220; color: #f8fafc; margin: 0; padding: 24px; }
|
|
172
175
|
.card { max-width: 560px; margin: 0 auto; background: #111827; border: 1px solid #1f2937; border-radius: 20px; padding: 24px; }
|
|
@@ -179,11 +182,11 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
179
182
|
<body>
|
|
180
183
|
<div class="card">
|
|
181
184
|
<div class="pill">Official integration</div>
|
|
182
|
-
<h1>Connect
|
|
183
|
-
<p class="muted">
|
|
185
|
+
<h1>Connect ${escapeHtml(providerLabel)}</h1>
|
|
186
|
+
<p class="muted">Complete connection for ${escapeHtml(appLabel)}. This window closes automatically when linking is finished.</p>
|
|
184
187
|
<div id="status" class="muted">Starting connection…</div>
|
|
185
|
-
<img id="qr" alt="
|
|
186
|
-
<p class="muted">
|
|
188
|
+
<img id="qr" alt="Integration QR code" style="display:none;" />
|
|
189
|
+
<p class="muted">If this flow needs QR scan approval, the code will appear below.</p>
|
|
187
190
|
</div>
|
|
188
191
|
<script>
|
|
189
192
|
const statusEl = document.getElementById('status');
|
|
@@ -207,12 +210,12 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
207
210
|
const data = await response.json();
|
|
208
211
|
const status = String(data.status || 'connecting');
|
|
209
212
|
if (status === 'awaiting_qr' && data.qr) {
|
|
210
|
-
statusEl.textContent = 'Scan this QR code
|
|
213
|
+
statusEl.textContent = 'Scan this QR code to continue linking.';
|
|
211
214
|
qrEl.src = 'https://api.qrserver.com/v1/create-qr-code/?data=' + encodeURIComponent(data.qr) + '&size=320x320';
|
|
212
215
|
qrEl.style.display = 'block';
|
|
213
216
|
} else if (status === 'connected') {
|
|
214
217
|
qrEl.style.display = 'none';
|
|
215
|
-
statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your
|
|
218
|
+
statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your account') + '. Closing…';
|
|
216
219
|
notifyOpener({
|
|
217
220
|
type: 'integration_oauth_success',
|
|
218
221
|
provider,
|
|
@@ -233,7 +236,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
|
|
|
233
236
|
});
|
|
234
237
|
return;
|
|
235
238
|
} else {
|
|
236
|
-
statusEl.textContent = 'Waiting for
|
|
239
|
+
statusEl.textContent = 'Waiting for the integration to finish linking…';
|
|
237
240
|
}
|
|
238
241
|
setTimeout(refresh, 1500);
|
|
239
242
|
}
|
|
@@ -686,6 +686,7 @@ class AgentEngine {
|
|
|
686
686
|
content,
|
|
687
687
|
kind,
|
|
688
688
|
expectsReply = false,
|
|
689
|
+
deferFollowUp = false,
|
|
689
690
|
} = {}) {
|
|
690
691
|
const runMeta = this.getRunMeta(runId);
|
|
691
692
|
if (!runMeta || runMeta.aborted) {
|
|
@@ -715,6 +716,9 @@ class AgentEngine {
|
|
|
715
716
|
kind: normalizedKind,
|
|
716
717
|
expectsReply,
|
|
717
718
|
});
|
|
719
|
+
if (deferFollowUp === true) {
|
|
720
|
+
metadata.defer_follow_up = true;
|
|
721
|
+
}
|
|
718
722
|
const createdAt = new Date().toISOString();
|
|
719
723
|
|
|
720
724
|
if (triggerSource === 'messaging') {
|
|
@@ -739,6 +743,7 @@ class AgentEngine {
|
|
|
739
743
|
content: normalizedContent,
|
|
740
744
|
kind: normalizedKind,
|
|
741
745
|
expectsReply,
|
|
746
|
+
deferFollowUp,
|
|
742
747
|
});
|
|
743
748
|
} else {
|
|
744
749
|
db.prepare(
|
|
@@ -758,6 +763,7 @@ class AgentEngine {
|
|
|
758
763
|
content: normalizedContent,
|
|
759
764
|
kind: normalizedKind,
|
|
760
765
|
expectsReply: expectsReply === true,
|
|
766
|
+
deferFollowUp: deferFollowUp === true,
|
|
761
767
|
createdAt,
|
|
762
768
|
});
|
|
763
769
|
runMeta.lastInterimMessage = normalizedContent;
|
|
@@ -767,6 +773,7 @@ class AgentEngine {
|
|
|
767
773
|
content: normalizedContent,
|
|
768
774
|
kind: normalizedKind,
|
|
769
775
|
expectsReply: expectsReply === true,
|
|
776
|
+
deferFollowUp: deferFollowUp === true,
|
|
770
777
|
triggerSource,
|
|
771
778
|
platform: triggerSource === 'messaging' ? platform : 'web',
|
|
772
779
|
});
|
|
@@ -783,6 +790,7 @@ class AgentEngine {
|
|
|
783
790
|
latestInterim: {
|
|
784
791
|
kind: normalizedKind,
|
|
785
792
|
expectsReply: expectsReply === true,
|
|
793
|
+
deferFollowUp: deferFollowUp === true,
|
|
786
794
|
content: normalizedContent,
|
|
787
795
|
createdAt,
|
|
788
796
|
},
|
|
@@ -795,6 +803,7 @@ class AgentEngine {
|
|
|
795
803
|
sent: true,
|
|
796
804
|
kind: normalizedKind,
|
|
797
805
|
expectsReply: expectsReply === true,
|
|
806
|
+
deferFollowUp: deferFollowUp === true,
|
|
798
807
|
content: normalizedContent,
|
|
799
808
|
terminal: terminalInterim,
|
|
800
809
|
};
|
|
@@ -138,7 +138,7 @@ When drafting on behalf of the user, match their likely voice from available con
|
|
|
138
138
|
If the user approves a previously shown draft, send that draft rather than silently rewriting it.
|
|
139
139
|
|
|
140
140
|
TASKS
|
|
141
|
-
Use one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
|
|
141
|
+
Use one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. When calling task tools, prefer one unified trigger section: trigger={ type, config }. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
|
|
142
142
|
Do not create vague tasks like "check this" when the future run would not know what "this" means. Resolve references into names, links, file paths, IDs, dates, and success criteria before saving the task.
|
|
143
143
|
For notification tasks, distinguish between notifying the user in their current messaging channel, emailing the user, and contacting someone else. Default reminders should notify the user through the active messaging channel unless the user explicitly asks for email, phone, or a third party.
|
|
144
144
|
When creating or updating a task, include whether it should notify every time, only on change, only on errors, or only when a condition is met. If unspecified, choose the least noisy useful behavior and say what you chose.
|
|
@@ -91,6 +91,103 @@ function compactToolDefinition(tool, options = {}) {
|
|
|
91
91
|
return compact;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function normalizeScheduleTriggerConfig(inputConfig = {}) {
|
|
95
|
+
if (!inputConfig || typeof inputConfig !== 'object' || Array.isArray(inputConfig)) {
|
|
96
|
+
return inputConfig;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalized = { ...inputConfig };
|
|
100
|
+
const schedule = (normalized.schedule && typeof normalized.schedule === 'object' && !Array.isArray(normalized.schedule))
|
|
101
|
+
? normalized.schedule
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
const modeCandidate = String(
|
|
105
|
+
normalized.mode
|
|
106
|
+
|| normalized.type
|
|
107
|
+
|| normalized.scheduleType
|
|
108
|
+
|| normalized.schedule_type
|
|
109
|
+
|| (normalized.oneTime || normalized.one_time ? 'one_time' : '')
|
|
110
|
+
|| ''
|
|
111
|
+
).trim().toLowerCase();
|
|
112
|
+
if (modeCandidate === 'once') normalized.mode = 'one_time';
|
|
113
|
+
else if (modeCandidate === 'one_time' || modeCandidate === 'recurring') normalized.mode = modeCandidate;
|
|
114
|
+
|
|
115
|
+
const cronCandidate = normalized.cronExpression
|
|
116
|
+
|| normalized.cron_expression
|
|
117
|
+
|| normalized.cron
|
|
118
|
+
|| schedule?.cronExpression
|
|
119
|
+
|| schedule?.cron_expression
|
|
120
|
+
|| schedule?.cron;
|
|
121
|
+
if (cronCandidate != null) {
|
|
122
|
+
normalized.cronExpression = String(cronCandidate).trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const runAtCandidate = normalized.runAt
|
|
126
|
+
|| normalized.run_at
|
|
127
|
+
|| normalized.at
|
|
128
|
+
|| normalized.when
|
|
129
|
+
|| schedule?.runAt
|
|
130
|
+
|| schedule?.run_at
|
|
131
|
+
|| schedule?.at
|
|
132
|
+
|| schedule?.when;
|
|
133
|
+
if (runAtCandidate != null) {
|
|
134
|
+
normalized.runAt = String(runAtCandidate).trim();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!normalized.mode) {
|
|
138
|
+
normalized.mode = normalized.runAt ? 'one_time' : 'recurring';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
delete normalized.schedule;
|
|
142
|
+
return normalized;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeTaskTriggerInput(triggerType, triggerConfig) {
|
|
146
|
+
if (String(triggerType || '').trim() !== 'schedule') {
|
|
147
|
+
return triggerConfig;
|
|
148
|
+
}
|
|
149
|
+
return normalizeScheduleTriggerConfig(triggerConfig || {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveTaskTriggerArgs(args = {}, fallbackTriggerType = null) {
|
|
153
|
+
let triggerType = args.trigger_type !== undefined
|
|
154
|
+
? String(args.trigger_type || '').trim()
|
|
155
|
+
: String(fallbackTriggerType || '').trim();
|
|
156
|
+
let triggerConfig = args.trigger_config;
|
|
157
|
+
let hasType = args.trigger_type !== undefined;
|
|
158
|
+
let hasConfig = args.trigger_config !== undefined;
|
|
159
|
+
|
|
160
|
+
if (args.trigger && typeof args.trigger === 'object' && !Array.isArray(args.trigger)) {
|
|
161
|
+
const trigger = args.trigger;
|
|
162
|
+
const unifiedType = trigger.type ?? trigger.triggerType ?? trigger.trigger_type;
|
|
163
|
+
const unifiedHasType = unifiedType !== undefined && unifiedType !== null && String(unifiedType).trim().length > 0;
|
|
164
|
+
if (unifiedHasType) {
|
|
165
|
+
triggerType = String(unifiedType).trim();
|
|
166
|
+
hasType = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const hasUnifiedConfig = Object.prototype.hasOwnProperty.call(trigger, 'config')
|
|
170
|
+
|| Object.prototype.hasOwnProperty.call(trigger, 'triggerConfig')
|
|
171
|
+
|| Object.prototype.hasOwnProperty.call(trigger, 'trigger_config');
|
|
172
|
+
if (hasUnifiedConfig) {
|
|
173
|
+
triggerConfig = trigger.config ?? trigger.triggerConfig ?? trigger.trigger_config;
|
|
174
|
+
hasConfig = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const normalizedType = String(triggerType || '').trim() || null;
|
|
179
|
+
const normalizedConfig = hasConfig
|
|
180
|
+
? normalizeTaskTriggerInput(normalizedType || fallbackTriggerType || 'schedule', triggerConfig)
|
|
181
|
+
: undefined;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
triggerType: normalizedType,
|
|
185
|
+
triggerConfig: normalizedConfig,
|
|
186
|
+
hasType,
|
|
187
|
+
hasConfig,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
94
191
|
function isProactiveTrigger(triggerSource) {
|
|
95
192
|
return triggerSource === 'schedule' || triggerSource === 'tasks';
|
|
96
193
|
}
|
|
@@ -876,15 +973,16 @@ function getAvailableTools(app, options = {}) {
|
|
|
876
973
|
type: 'object',
|
|
877
974
|
properties: {
|
|
878
975
|
name: { type: 'string', description: 'Short descriptive name for the task.' },
|
|
879
|
-
|
|
880
|
-
|
|
976
|
+
trigger: { type: 'object', description: 'Unified trigger object. Prefer { type: "schedule" | integration_trigger_type, config: {...} }.' },
|
|
977
|
+
trigger_type: { type: 'string', description: 'Trigger type such as schedule, gmail_message_received, outlook_email_received, slack_message_received, teams_message_received, weather_event, or whatsapp_personal_message_received.' },
|
|
978
|
+
trigger_config: { type: 'object', description: 'Trigger-specific configuration object. For schedule triggers prefer { mode: "recurring", cronExpression: "m h dom mon dow" } or { mode: "one_time", runAt: ISO datetime }. 5-field cron only (seconds unsupported).' },
|
|
881
979
|
prompt: { type: 'string', description: 'The instructions the agent will run when the trigger fires.' },
|
|
882
980
|
enabled: { type: 'boolean', description: 'Whether to activate immediately.' },
|
|
883
981
|
model: { type: 'string', description: 'Optional model override.' },
|
|
884
982
|
call_to: { type: 'string', description: 'Optional E.164 phone number to call via Telnyx when this task fires.' },
|
|
885
983
|
call_greeting: { type: 'string', description: 'Optional spoken greeting hint for make_call.' }
|
|
886
984
|
},
|
|
887
|
-
required: ['name', '
|
|
985
|
+
required: ['name', 'prompt']
|
|
888
986
|
}
|
|
889
987
|
},
|
|
890
988
|
{
|
|
@@ -911,8 +1009,9 @@ function getAvailableTools(app, options = {}) {
|
|
|
911
1009
|
properties: {
|
|
912
1010
|
task_id: { type: 'number', description: 'The numeric ID of the task to update.' },
|
|
913
1011
|
name: { type: 'string', description: 'New name for the task.' },
|
|
1012
|
+
trigger: { type: 'object', description: 'Unified trigger object. Use { type, config } to update trigger in one section.' },
|
|
914
1013
|
trigger_type: { type: 'string', description: 'Updated trigger type.' },
|
|
915
|
-
trigger_config: { type: 'object', description: 'Updated trigger-specific configuration.' },
|
|
1014
|
+
trigger_config: { type: 'object', description: 'Updated trigger-specific configuration. For schedule triggers use mode+cronExpression (recurring) or mode+runAt (one_time).' },
|
|
916
1015
|
prompt: { type: 'string', description: 'Updated task prompt.' },
|
|
917
1016
|
enabled: { type: 'boolean', description: 'Enable or disable the task.' },
|
|
918
1017
|
model: { type: 'string', description: 'Specific AI model ID for this task. Set to empty string to clear the override.' },
|
|
@@ -1123,7 +1222,8 @@ function getAvailableTools(app, options = {}) {
|
|
|
1123
1222
|
properties: {
|
|
1124
1223
|
content: { type: 'string', description: 'Natural assistant message derived from the current task state.' },
|
|
1125
1224
|
kind: { type: 'string', enum: Array.from(INTERIM_KINDS), description: 'ack, progress, question, or blocker' },
|
|
1126
|
-
expects_reply: { type: 'boolean', description: 'Set true only when the current run should pause for the user to answer.' }
|
|
1225
|
+
expects_reply: { type: 'boolean', description: 'Set true only when the current run should pause for the user to answer.' },
|
|
1226
|
+
defer_follow_up: { type: 'boolean', description: 'Set true when you choose to deliver the final result later via the user\'s last connected chat target.' }
|
|
1127
1227
|
},
|
|
1128
1228
|
required: ['content', 'kind']
|
|
1129
1229
|
}
|
|
@@ -1847,6 +1947,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1847
1947
|
}
|
|
1848
1948
|
const interimContent = typeof args.content === 'string' ? args.content : '';
|
|
1849
1949
|
const expectsReply = args.expects_reply === true;
|
|
1950
|
+
const deferFollowUp = args.defer_follow_up === true;
|
|
1850
1951
|
return engine.publishInterimUpdate({
|
|
1851
1952
|
userId,
|
|
1852
1953
|
runId,
|
|
@@ -1858,6 +1959,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1858
1959
|
content: interimContent,
|
|
1859
1960
|
kind: normalizeInterimKind(args.kind),
|
|
1860
1961
|
expectsReply,
|
|
1962
|
+
deferFollowUp,
|
|
1861
1963
|
});
|
|
1862
1964
|
}
|
|
1863
1965
|
|
|
@@ -2205,10 +2307,17 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2205
2307
|
const s = taskRuntime();
|
|
2206
2308
|
if (!s) return { error: 'Task runtime not available' };
|
|
2207
2309
|
try {
|
|
2310
|
+
const resolvedTrigger = resolveTaskTriggerArgs(args, 'schedule');
|
|
2311
|
+
if (!resolvedTrigger.hasType || !resolvedTrigger.triggerType) {
|
|
2312
|
+
return { error: 'Task trigger type is required (use trigger.type or trigger_type).' };
|
|
2313
|
+
}
|
|
2314
|
+
if (!resolvedTrigger.hasConfig || resolvedTrigger.triggerConfig === undefined) {
|
|
2315
|
+
return { error: 'Task trigger config is required (use trigger.config or trigger_config).' };
|
|
2316
|
+
}
|
|
2208
2317
|
const task = await s.createTask(userId, {
|
|
2209
2318
|
name: args.name,
|
|
2210
|
-
triggerType:
|
|
2211
|
-
triggerConfig:
|
|
2319
|
+
triggerType: resolvedTrigger.triggerType,
|
|
2320
|
+
triggerConfig: resolvedTrigger.triggerConfig,
|
|
2212
2321
|
prompt: args.prompt,
|
|
2213
2322
|
enabled: args.enabled !== false,
|
|
2214
2323
|
model: args.model || null,
|
|
@@ -2246,12 +2355,13 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2246
2355
|
const s = taskRuntime();
|
|
2247
2356
|
if (!s) return { error: 'Task runtime not available' };
|
|
2248
2357
|
try {
|
|
2249
|
-
const existing = db.prepare('SELECT agent_id FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(args.task_id, userId);
|
|
2358
|
+
const existing = db.prepare('SELECT agent_id, trigger_type FROM scheduled_tasks WHERE id = ? AND user_id = ?').get(args.task_id, userId);
|
|
2250
2359
|
if (!existing || existing.agent_id !== agentId) return { error: 'Task not found for this agent.' };
|
|
2251
2360
|
const updates = {};
|
|
2361
|
+
const resolvedTrigger = resolveTaskTriggerArgs(args, existing.trigger_type || 'schedule');
|
|
2252
2362
|
if (args.name !== undefined) updates.name = args.name;
|
|
2253
|
-
if (
|
|
2254
|
-
if (
|
|
2363
|
+
if (resolvedTrigger.hasType && resolvedTrigger.triggerType) updates.triggerType = resolvedTrigger.triggerType;
|
|
2364
|
+
if (resolvedTrigger.hasConfig && resolvedTrigger.triggerConfig !== undefined) updates.triggerConfig = resolvedTrigger.triggerConfig;
|
|
2255
2365
|
if (args.prompt !== undefined) updates.prompt = args.prompt;
|
|
2256
2366
|
if (args.enabled !== undefined) updates.enabled = args.enabled;
|
|
2257
2367
|
if (args.model !== undefined) updates.model = args.model || null;
|
|
@@ -56,6 +56,18 @@ function resolveMicrosoftOAuthConfig() {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function resolveHomeAssistantOAuthConfig() {
|
|
60
|
+
const base = resolveOAuthConfig('HOME_ASSISTANT');
|
|
61
|
+
return {
|
|
62
|
+
...base,
|
|
63
|
+
baseUrl: trimEnv('HOME_ASSISTANT_BASE_URL').replace(/\/$/, ''),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveSpotifyOAuthConfig() {
|
|
68
|
+
return resolveOAuthConfig('SPOTIFY');
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
function describeEnvStatus(config, options = {}) {
|
|
60
72
|
const label = String(options.label || 'This integration').trim() || 'This integration';
|
|
61
73
|
if (config.configured) {
|
|
@@ -76,10 +88,12 @@ function describeEnvStatus(config, options = {}) {
|
|
|
76
88
|
module.exports = {
|
|
77
89
|
describeEnvStatus,
|
|
78
90
|
resolveFigmaOAuthConfig,
|
|
91
|
+
resolveHomeAssistantOAuthConfig,
|
|
79
92
|
resolveMicrosoftOAuthConfig,
|
|
80
93
|
resolveNotionOAuthConfig,
|
|
81
94
|
resolveOAuthConfig,
|
|
82
95
|
resolveGoogleOAuthConfig,
|
|
83
96
|
resolvePublicBaseUrl,
|
|
97
|
+
resolveSpotifyOAuthConfig,
|
|
84
98
|
resolveSlackOAuthConfig,
|
|
85
99
|
};
|