@pellux/goodvibes-agent 1.0.30 → 1.0.33
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/CHANGELOG.md +42 -1
- package/README.md +7 -5
- package/dist/package/main.js +7323 -2268
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +6 -2
- package/docs/connected-host.md +26 -5
- package/docs/getting-started.md +30 -10
- package/docs/knowledge-artifacts-and-multimodal.md +16 -4
- package/docs/project-planning.md +2 -2
- package/docs/providers-and-routing.md +3 -2
- package/docs/release-and-publishing.md +15 -11
- package/docs/tools-and-commands.md +20 -8
- package/package.json +8 -3
- package/release/live-verification/live-verification.json +148 -0
- package/release/live-verification/live-verification.md +187 -0
- package/release/performance-snapshot.json +57 -0
- package/release/release-notes.md +28 -0
- package/release/release-readiness.json +581 -0
- package/src/cli/agent-knowledge-command.ts +5 -5
- package/src/cli/agent-knowledge-format.ts +11 -0
- package/src/cli/agent-knowledge-runtime.ts +92 -13
- package/src/cli/bundle-command.ts +5 -4
- package/src/cli/entrypoint.ts +5 -2
- package/src/cli/external-runtime.ts +2 -15
- package/src/cli/management.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/knowledge.ts +2 -2
- package/src/runtime/bootstrap.ts +2 -2
- package/src/runtime/connected-host-auth.ts +16 -0
- package/src/tools/agent-channel-send-tool.ts +5 -7
- package/src/tools/agent-harness-channel-metadata.ts +177 -0
- package/src/tools/agent-harness-connected-host-status.ts +1 -1
- package/src/tools/agent-harness-delegation-posture.ts +216 -0
- package/src/tools/agent-harness-keybinding-metadata.ts +57 -22
- package/src/tools/agent-harness-mcp-metadata.ts +246 -0
- package/src/tools/agent-harness-media-posture.ts +282 -0
- package/src/tools/agent-harness-metadata.ts +21 -4
- package/src/tools/agent-harness-model-routing.ts +501 -0
- package/src/tools/agent-harness-notification-metadata.ts +217 -0
- package/src/tools/agent-harness-operator-methods.ts +285 -0
- package/src/tools/agent-harness-pairing-posture.ts +265 -0
- package/src/tools/agent-harness-provider-account-metadata.ts +203 -0
- package/src/tools/agent-harness-release-evidence.ts +364 -0
- package/src/tools/agent-harness-release-readiness.ts +298 -0
- package/src/tools/agent-harness-security-posture.ts +646 -0
- package/src/tools/agent-harness-service-posture.ts +201 -0
- package/src/tools/agent-harness-session-metadata.ts +282 -0
- package/src/tools/agent-harness-setup-posture.ts +295 -0
- package/src/tools/agent-harness-tool-schema.ts +103 -27
- package/src/tools/agent-harness-tool.ts +209 -236
- package/src/tools/agent-harness-ui-surface-metadata.ts +1 -1
- package/src/tools/agent-harness-workspace-actions.ts +232 -0
- package/src/tools/agent-knowledge-ingest-tool.ts +6 -8
- package/src/tools/agent-knowledge-tool.ts +122 -23
- package/src/tools/agent-local-registry-tool.ts +4 -5
- package/src/tools/agent-media-generate-tool.ts +4 -6
- package/src/tools/agent-notify-tool.ts +5 -6
- package/src/tools/agent-operator-action-tool.ts +6 -8
- package/src/tools/agent-operator-briefing-tool.ts +3 -4
- package/src/tools/agent-reminder-schedule-tool.ts +6 -7
- package/src/tools/agent-tool-policy-guard.ts +8 -17
- package/src/tools/agent-work-plan-tool.ts +3 -4
- package/src/version.ts +2 -2
|
@@ -160,7 +160,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
160
160
|
const [id] = commandValues(rest);
|
|
161
161
|
if (!id) return { output: 'Usage: goodvibes-agent knowledge get <source|node|issue id>', exitCode: 2 };
|
|
162
162
|
const route = `/api/goodvibes-agent/knowledge/items/${encodeURIComponent(id)}`;
|
|
163
|
-
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.itemGet, async (connection) => (
|
|
163
|
+
const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.itemGet, route }, async (connection) => (
|
|
164
164
|
await getAgentKnowledgeJson(connection, route)
|
|
165
165
|
));
|
|
166
166
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -189,7 +189,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
189
189
|
const id = values[1];
|
|
190
190
|
if (!id) return { output: 'Usage: goodvibes-agent knowledge connectors doctor <connectorId>', exitCode: 2 };
|
|
191
191
|
const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}/doctor`;
|
|
192
|
-
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorDoctor, async (connection) => (
|
|
192
|
+
const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorDoctor, route }, async (connection) => (
|
|
193
193
|
await getAgentKnowledgeJson(connection, route)
|
|
194
194
|
));
|
|
195
195
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -201,7 +201,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
201
201
|
const id = values[0];
|
|
202
202
|
if (id) {
|
|
203
203
|
const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}`;
|
|
204
|
-
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorGet, async (connection) => (
|
|
204
|
+
const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorGet, route }, async (connection) => (
|
|
205
205
|
await getAgentKnowledgeJson(connection, route)
|
|
206
206
|
));
|
|
207
207
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -224,7 +224,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
224
224
|
const [id] = commandValues(rest);
|
|
225
225
|
if (!id) return { output: 'Usage: goodvibes-agent knowledge connector <connectorId>', exitCode: 2 };
|
|
226
226
|
const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}`;
|
|
227
|
-
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorGet, async (connection) => (
|
|
227
|
+
const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorGet, route }, async (connection) => (
|
|
228
228
|
await getAgentKnowledgeJson(connection, route)
|
|
229
229
|
));
|
|
230
230
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -238,7 +238,7 @@ export async function handleAgentKnowledgeCommand(runtime: CliCommandRuntime): P
|
|
|
238
238
|
const [id] = commandValues(rest);
|
|
239
239
|
if (!id) return { output: 'Usage: goodvibes-agent knowledge connector-doctor <connectorId>', exitCode: 2 };
|
|
240
240
|
const route = `/api/goodvibes-agent/knowledge/connectors/${encodeURIComponent(id)}/doctor`;
|
|
241
|
-
const result = await runKnowledgeCall(runtime, AGENT_KNOWLEDGE_METHODS.connectorDoctor, async (connection) => (
|
|
241
|
+
const result = await runKnowledgeCall(runtime, { ...AGENT_KNOWLEDGE_METHODS.connectorDoctor, route }, async (connection) => (
|
|
242
242
|
await getAgentKnowledgeJson(connection, route)
|
|
243
243
|
));
|
|
244
244
|
if (!result.ok) return { output: formatFailure(result, json), exitCode: 1 };
|
|
@@ -47,6 +47,7 @@ export function formatAgentKnowledgeFailureKind(kind: string): string {
|
|
|
47
47
|
if (kind === 'connected_host_unavailable') return 'connected host unavailable';
|
|
48
48
|
if (kind === 'connected_host_route_unavailable') return 'connected host route unavailable';
|
|
49
49
|
if (kind === 'connected_host_error') return 'connected host error';
|
|
50
|
+
if (kind === 'scope_contamination') return 'scope contamination';
|
|
50
51
|
if (kind === 'version_mismatch') return 'version mismatch';
|
|
51
52
|
return kind.replace(/[_-]+/g, ' ');
|
|
52
53
|
}
|
|
@@ -163,6 +164,7 @@ export function formatEntityList(data: unknown, kind: 'sources' | 'nodes' | 'iss
|
|
|
163
164
|
...values.slice(0, limit).map((value, index) => (
|
|
164
165
|
kind === 'issues' ? format(value) : ` ${index + 1}. ${format(value)}`
|
|
165
166
|
)),
|
|
167
|
+
` route /api/goodvibes-agent/knowledge/${kind}`,
|
|
166
168
|
].join('\n');
|
|
167
169
|
}
|
|
168
170
|
|
|
@@ -179,6 +181,7 @@ export function formatItem(data: unknown, id: string): string {
|
|
|
179
181
|
`Agent Knowledge item ${id}`,
|
|
180
182
|
` ${sourceLine(source)}`,
|
|
181
183
|
relatedKnowledgeCountsLine(relatedEdges, linkedSources, linkedNodes),
|
|
184
|
+
' route /api/goodvibes-agent/knowledge/items/{id}',
|
|
182
185
|
].join('\n');
|
|
183
186
|
}
|
|
184
187
|
if (node) {
|
|
@@ -186,6 +189,7 @@ export function formatItem(data: unknown, id: string): string {
|
|
|
186
189
|
`Agent Knowledge item ${id}`,
|
|
187
190
|
` ${nodeLine(node)}`,
|
|
188
191
|
relatedKnowledgeCountsLine(relatedEdges, linkedSources, linkedNodes),
|
|
192
|
+
' route /api/goodvibes-agent/knowledge/items/{id}',
|
|
189
193
|
].join('\n');
|
|
190
194
|
}
|
|
191
195
|
if (issue) {
|
|
@@ -193,6 +197,7 @@ export function formatItem(data: unknown, id: string): string {
|
|
|
193
197
|
`Agent Knowledge item ${id}`,
|
|
194
198
|
issueLine(issue),
|
|
195
199
|
relatedKnowledgeCountsLine(relatedEdges, linkedSources, linkedNodes),
|
|
200
|
+
' route /api/goodvibes-agent/knowledge/items/{id}',
|
|
196
201
|
].join('\n');
|
|
197
202
|
}
|
|
198
203
|
return [
|
|
@@ -231,6 +236,7 @@ export function formatConnectors(data: unknown): string {
|
|
|
231
236
|
return [
|
|
232
237
|
`Agent Knowledge connectors (${connectors.length})`,
|
|
233
238
|
...connectors.map(connectorLine),
|
|
239
|
+
' route /api/goodvibes-agent/knowledge/connectors',
|
|
234
240
|
].join('\n');
|
|
235
241
|
}
|
|
236
242
|
|
|
@@ -299,6 +305,7 @@ export function formatAsk(data: unknown, query: string): string {
|
|
|
299
305
|
}
|
|
300
306
|
if (facts.length > 0) lines.push('', `Facts ${facts.length}`);
|
|
301
307
|
if (gaps.length > 0) lines.push('', `Gaps ${gaps.length}`);
|
|
308
|
+
lines.push('', 'route /api/goodvibes-agent/knowledge/ask');
|
|
302
309
|
return lines.join('\n');
|
|
303
310
|
}
|
|
304
311
|
|
|
@@ -316,6 +323,7 @@ export function formatSearch(data: unknown, query: string): string {
|
|
|
316
323
|
return [
|
|
317
324
|
`Agent Knowledge search ${query}`,
|
|
318
325
|
...results.slice(0, 10).map((result, index) => ` ${index + 1}. ${resultLine(result)}`),
|
|
326
|
+
' route /api/goodvibes-agent/knowledge/search',
|
|
319
327
|
].join('\n');
|
|
320
328
|
}
|
|
321
329
|
|
|
@@ -385,5 +393,8 @@ export function formatFailure(failure: AgentKnowledgeFailureLike, json: boolean)
|
|
|
385
393
|
failure.kind === 'connected_host_route_unavailable'
|
|
386
394
|
? ' next update the connected GoodVibes host to the SDK version required by this Agent package.'
|
|
387
395
|
: null,
|
|
396
|
+
failure.kind === 'scope_contamination'
|
|
397
|
+
? ' next update the connected GoodVibes host so Agent Knowledge routes return only Agent-owned scope data.'
|
|
398
|
+
: null,
|
|
388
399
|
].filter((line): line is string => Boolean(line)).join('\n');
|
|
389
400
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import { createBrowserAgentSdk } from '@pellux/goodvibes-sdk/browser/agent';
|
|
4
2
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
3
|
import { SDK_VERSION, VERSION } from '../version.ts';
|
|
4
|
+
import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
|
|
6
5
|
import type { ConnectedHostCallMethod } from './agent-knowledge-methods.ts';
|
|
7
6
|
|
|
8
7
|
export type JsonRecord = Record<string, unknown>;
|
|
@@ -22,7 +21,13 @@ export interface AgentKnowledgeConnectionRuntime {
|
|
|
22
21
|
|
|
23
22
|
export interface AgentKnowledgeFailure {
|
|
24
23
|
readonly ok: false;
|
|
25
|
-
readonly kind:
|
|
24
|
+
readonly kind:
|
|
25
|
+
| 'connected_host_unavailable'
|
|
26
|
+
| 'auth_required'
|
|
27
|
+
| 'version_mismatch'
|
|
28
|
+
| 'connected_host_route_unavailable'
|
|
29
|
+
| 'connected_host_error'
|
|
30
|
+
| 'scope_contamination';
|
|
26
31
|
readonly error: string;
|
|
27
32
|
readonly baseUrl: string;
|
|
28
33
|
readonly route: string;
|
|
@@ -56,15 +61,8 @@ export function resolveConnectedHostConnection(runtime: AgentKnowledgeConnection
|
|
|
56
61
|
const host = String(runtime.configManager.get('controlPlane.host') ?? '127.0.0.1');
|
|
57
62
|
const port = Number(runtime.configManager.get('controlPlane.port') ?? 3421);
|
|
58
63
|
const baseUrl = `http://${host}:${Number.isFinite(port) ? port : 3421}`;
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const parsed = JSON.parse(readFileSync(tokenPath, 'utf-8')) as unknown;
|
|
63
|
-
const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
|
|
64
|
-
return { baseUrl, token, tokenPath };
|
|
65
|
-
} catch {
|
|
66
|
-
return { baseUrl, token: null, tokenPath };
|
|
67
|
-
}
|
|
64
|
+
const token = readConnectedHostOperatorToken(runtime.homeDirectory);
|
|
65
|
+
return { baseUrl, token: token.token, tokenPath: token.path };
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
export async function fetchConnectedHostStatus(connection: AgentKnowledgeConnection): Promise<{ readonly ok: boolean; readonly status: number; readonly body: unknown }> {
|
|
@@ -122,6 +120,87 @@ export function createAgentSdk(connection: AgentKnowledgeConnection) {
|
|
|
122
120
|
});
|
|
123
121
|
}
|
|
124
122
|
|
|
123
|
+
const AGENT_KNOWLEDGE_SCOPE_KEYS = new Set(['spaceid', 'knowledgespaceid']);
|
|
124
|
+
const AGENT_KNOWLEDGE_SCOPE_TEXT_PATTERNS: readonly { readonly label: string; readonly pattern: RegExp }[] = [
|
|
125
|
+
{
|
|
126
|
+
label: 'default knowledge scope id',
|
|
127
|
+
pattern: /["']?(?:knowledge[-_\s]*space[-_\s]*id|knowledgespaceid|space[-_\s]*id|spaceid)["']?\s*[:=]\s*["']?default["']?/i,
|
|
128
|
+
},
|
|
129
|
+
{ label: 'host assistant payload marker', pattern: /home\s*assistant/i },
|
|
130
|
+
{ label: 'host graph payload marker', pattern: /home\s*graph|homegraph/i },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
function normalizedJsonKey(key: string): string {
|
|
134
|
+
return key.replace(/[-_\s]/g, '').toLowerCase();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findAgentKnowledgeTextContamination(value: string): string | null {
|
|
138
|
+
for (const { label, pattern } of AGENT_KNOWLEDGE_SCOPE_TEXT_PATTERNS) {
|
|
139
|
+
if (pattern.test(value)) return label;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function findAgentKnowledgeScopeContamination(value: unknown): string | null {
|
|
145
|
+
const seen = new WeakSet<object>();
|
|
146
|
+
const visit = (node: unknown): string | null => {
|
|
147
|
+
if (typeof node === 'string') return findAgentKnowledgeTextContamination(node);
|
|
148
|
+
if (!node || typeof node !== 'object') return null;
|
|
149
|
+
if (seen.has(node)) return null;
|
|
150
|
+
seen.add(node);
|
|
151
|
+
if (Array.isArray(node)) {
|
|
152
|
+
for (const item of node) {
|
|
153
|
+
const nested = visit(item);
|
|
154
|
+
if (nested) return nested;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
for (const [key, nestedValue] of Object.entries(node as JsonRecord)) {
|
|
159
|
+
const keyFinding = findAgentKnowledgeTextContamination(key);
|
|
160
|
+
if (keyFinding && keyFinding !== 'default knowledge scope id') return keyFinding;
|
|
161
|
+
const normalizedKey = normalizedJsonKey(key);
|
|
162
|
+
if (
|
|
163
|
+
AGENT_KNOWLEDGE_SCOPE_KEYS.has(normalizedKey)
|
|
164
|
+
&& typeof nestedValue === 'string'
|
|
165
|
+
&& nestedValue.trim().toLowerCase() === 'default'
|
|
166
|
+
) {
|
|
167
|
+
return `${key}=default`;
|
|
168
|
+
}
|
|
169
|
+
const nested = visit(nestedValue);
|
|
170
|
+
if (nested) return nested;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
174
|
+
return visit(value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function agentKnowledgeScopeContaminationFailure(
|
|
178
|
+
connection: AgentKnowledgeConnection,
|
|
179
|
+
route: string,
|
|
180
|
+
finding: string,
|
|
181
|
+
): AgentKnowledgeFailure {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
kind: 'scope_contamination',
|
|
185
|
+
error: [
|
|
186
|
+
`Agent Knowledge route returned non-Agent knowledge contamination (${finding}).`,
|
|
187
|
+
'GoodVibes Agent must use only isolated /api/goodvibes-agent/knowledge/* scope data.',
|
|
188
|
+
].join(' '),
|
|
189
|
+
baseUrl: connection.baseUrl,
|
|
190
|
+
route,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function validateAgentKnowledgeData<TData>(
|
|
195
|
+
data: TData,
|
|
196
|
+
connection: AgentKnowledgeConnection,
|
|
197
|
+
method: ConnectedHostCallMethod,
|
|
198
|
+
): AgentKnowledgeResult<TData> {
|
|
199
|
+
const contamination = findAgentKnowledgeScopeContamination(data);
|
|
200
|
+
if (contamination) return agentKnowledgeScopeContaminationFailure(connection, method.route, contamination);
|
|
201
|
+
return { ok: true, kind: method.kind, route: method.route, data };
|
|
202
|
+
}
|
|
203
|
+
|
|
125
204
|
export async function postAgentKnowledgeJson<TData>(
|
|
126
205
|
connection: AgentKnowledgeConnection,
|
|
127
206
|
route: string,
|
|
@@ -239,7 +318,7 @@ export async function runKnowledgeCall<TData>(
|
|
|
239
318
|
}
|
|
240
319
|
try {
|
|
241
320
|
const data = await call(connection);
|
|
242
|
-
return
|
|
321
|
+
return validateAgentKnowledgeData(data, connection, method);
|
|
243
322
|
} catch (error) {
|
|
244
323
|
return classifyKnowledgeError(error, connection, method.route);
|
|
245
324
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
3
|
import { RuntimeEventBus } from '@/runtime/index.ts';
|
|
4
4
|
import { createShellPathService } from '@/runtime/index.ts';
|
|
5
5
|
import { listProviderRuntimeSnapshots } from '@pellux/goodvibes-sdk/platform/providers';
|
|
6
6
|
import { createRuntimeServices } from '../runtime/services.ts';
|
|
7
7
|
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
8
|
+
import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
|
|
8
9
|
import { getOnboardingCheckMarkerPath } from '../runtime/onboarding/index.ts';
|
|
9
10
|
import { CONFIG_SCHEMA } from '../config/index.ts';
|
|
10
11
|
import { SecretsManager } from '../config/secrets.ts';
|
|
@@ -76,14 +77,14 @@ function readAuthPosture(runtime: CliCommandRuntime) {
|
|
|
76
77
|
});
|
|
77
78
|
const userStorePath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-users.json');
|
|
78
79
|
const bootstrapCredentialPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-bootstrap.txt');
|
|
79
|
-
const
|
|
80
|
+
const operatorToken = readConnectedHostOperatorToken(runtime.homeDirectory);
|
|
80
81
|
return {
|
|
81
82
|
userStorePath,
|
|
82
83
|
userStorePresent: existsSync(userStorePath),
|
|
83
84
|
bootstrapCredentialPath,
|
|
84
85
|
bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
|
|
85
|
-
operatorTokenPath,
|
|
86
|
-
operatorTokenPresent:
|
|
86
|
+
operatorTokenPath: operatorToken.path,
|
|
87
|
+
operatorTokenPresent: operatorToken.present && Boolean(operatorToken.token),
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
package/src/cli/entrypoint.ts
CHANGED
|
@@ -186,6 +186,9 @@ export async function prepareShellCliRuntime(
|
|
|
186
186
|
configManager,
|
|
187
187
|
homeDirectory: bootstrapHomeDirectory,
|
|
188
188
|
});
|
|
189
|
+
const effectiveOperatorTokenPath = externalRuntime.operatorToken.present
|
|
190
|
+
? externalRuntime.operatorToken.path
|
|
191
|
+
: operatorTokenPath;
|
|
189
192
|
const statusOptions = {
|
|
190
193
|
configManager,
|
|
191
194
|
workingDirectory: bootstrapWorkingDir,
|
|
@@ -196,8 +199,8 @@ export async function prepareShellCliRuntime(
|
|
|
196
199
|
userStorePresent: existsSync(userStorePath),
|
|
197
200
|
bootstrapCredentialPath,
|
|
198
201
|
bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
|
|
199
|
-
operatorTokenPath,
|
|
200
|
-
operatorTokenPresent:
|
|
202
|
+
operatorTokenPath: effectiveOperatorTokenPath,
|
|
203
|
+
operatorTokenPresent: externalRuntime.operatorToken.present,
|
|
201
204
|
},
|
|
202
205
|
service,
|
|
203
206
|
externalRuntime,
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
2
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
3
|
import { SDK_VERSION } from '../version.ts';
|
|
4
|
+
import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
|
|
6
5
|
|
|
7
6
|
type JsonRecord = Record<string, unknown>;
|
|
8
7
|
|
|
@@ -47,18 +46,6 @@ function resolveBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
|
|
|
47
46
|
return `http://${host}:${Number.isFinite(port) ? port : 3421}`;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function readOperatorToken(homeDirectory: string): { readonly token: string | null; readonly path: string } {
|
|
51
|
-
const path = join(homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
|
|
52
|
-
if (!existsSync(path)) return { token: null, path };
|
|
53
|
-
try {
|
|
54
|
-
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
|
|
55
|
-
const token = isRecord(parsed) && typeof parsed.token === 'string' ? parsed.token : null;
|
|
56
|
-
return { token, path };
|
|
57
|
-
} catch {
|
|
58
|
-
return { token: null, path };
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
49
|
async function fetchJson(
|
|
63
50
|
url: string,
|
|
64
51
|
token: string | null,
|
|
@@ -90,7 +77,7 @@ export async function inspectCliExternalRuntime(
|
|
|
90
77
|
options: CliExternalRuntimeInspectionOptions,
|
|
91
78
|
): Promise<CliExternalRuntimeSnapshot> {
|
|
92
79
|
const baseUrl = resolveBaseUrl(options.configManager);
|
|
93
|
-
const token =
|
|
80
|
+
const token = readConnectedHostOperatorToken(options.homeDirectory);
|
|
94
81
|
const timeoutMs = options.timeoutMs ?? 1500;
|
|
95
82
|
const route = '/api/goodvibes-agent/knowledge/status' as const;
|
|
96
83
|
|
package/src/cli/management.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { formatProviderModel, getModelIdFromProviderModel } from '../config/prov
|
|
|
9
9
|
import { bootstrapRuntime } from '../runtime/bootstrap.ts';
|
|
10
10
|
import { createRuntimeServices } from '../runtime/services.ts';
|
|
11
11
|
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
12
|
+
import { readConnectedHostOperatorToken } from '../runtime/connected-host-auth.ts';
|
|
12
13
|
import type { RuntimeServices } from '../runtime/services.ts';
|
|
13
14
|
import { SecretsManager } from '../config/secrets.ts';
|
|
14
15
|
import { RuntimeEventBus, type TurnEvent } from '@/runtime/index.ts';
|
|
@@ -190,14 +191,14 @@ export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
|
190
191
|
});
|
|
191
192
|
const userStorePath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-users.json');
|
|
192
193
|
const bootstrapCredentialPath = shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-bootstrap.txt');
|
|
193
|
-
const
|
|
194
|
+
const operatorToken = readConnectedHostOperatorToken(runtime.homeDirectory);
|
|
194
195
|
return {
|
|
195
196
|
userStorePath,
|
|
196
197
|
userStorePresent: existsSync(userStorePath),
|
|
197
198
|
bootstrapCredentialPath,
|
|
198
199
|
bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
|
|
199
|
-
operatorTokenPath,
|
|
200
|
-
operatorTokenPresent:
|
|
200
|
+
operatorTokenPath: operatorToken.path,
|
|
201
|
+
operatorTokenPresent: operatorToken.present && Boolean(operatorToken.token),
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -21,7 +21,7 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
|
|
|
21
21
|
'Welcome To GoodVibes Agent',
|
|
22
22
|
' /setup - open Agent setup with current settings preloaded',
|
|
23
23
|
' /agent - open the Agent operator workspace',
|
|
24
|
-
' /knowledge - inspect isolated Agent Knowledge status, ask, and
|
|
24
|
+
' /knowledge - inspect isolated Agent Knowledge status, ask/search, libraries, map, connectors, and ingest paths',
|
|
25
25
|
' /memory - manage Agent-local memory',
|
|
26
26
|
' /personas - manage Agent-local personas',
|
|
27
27
|
' /skills - manage reusable Agent-local skills',
|
|
@@ -209,9 +209,9 @@ function renderKnowledgeAskResult(result: KnowledgeAskResult): string {
|
|
|
209
209
|
export const knowledgeCommand: SlashCommand = {
|
|
210
210
|
name: 'knowledge',
|
|
211
211
|
aliases: ['know', 'kb'],
|
|
212
|
-
description: 'Agent Knowledge: isolated Agent-owned
|
|
212
|
+
description: 'Agent Knowledge: isolated Agent-owned status, ask/search, source/node/issue lists, item lookup, map, connectors, ingest, and review queue.',
|
|
213
213
|
usage: '<subcommand> [args]',
|
|
214
|
-
argsHint: 'status|ask|ingest-url --yes|ingest-file --yes|import-bookmarks --yes|
|
|
214
|
+
argsHint: 'status|ask|search|list|get|map|connectors|connector|connector-doctor|ingest-url --yes|ingest-file --yes|import-urls --yes|import-bookmarks --yes|import-browser-history --yes|ingest-connector --yes|review-issue --yes|reindex --yes',
|
|
215
215
|
handler: async (args: string[], context: CommandContext): Promise<void> => {
|
|
216
216
|
if (args.length === 0 && context.openAgentWorkspace) {
|
|
217
217
|
context.openAgentWorkspace('knowledge');
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -51,8 +51,8 @@ const GOODVIBES_AGENT_OPERATOR_POLICY = [
|
|
|
51
51
|
'- Use the `agent_operator_briefing` tool for read-only main-conversation summaries of connected work plan, approvals, automation, schedules, and scheduler capacity. This tool must not call mutation routes, connected-host lifecycle routes, default knowledge, non-Agent knowledge spaces, separate Agent job creation, or GoodVibes TUI delegation.',
|
|
52
52
|
'- When the user explicitly asks Agent to approve, deny, or cancel a specific approval, run/pause/resume a specific automation job, cancel/retry a specific automation run, or run a specific schedule, use the `agent_operator_action` tool with confirm:true and the original user request. It can call only its allowlisted public operator routes and must not create, edit, delete, or discover automation definitions.',
|
|
53
53
|
'- Use the `agent_work_plan` tool to keep the visible Agent-local work plan current while working in the main conversation. Create, inspect, and update local work items proactively when useful. Removing items or clearing completed items requires an explicit user request and confirm:true.',
|
|
54
|
-
'- Use the `agent_harness` tool to inspect and operate the harness surface itself: Agent workspace action catalog, slash command catalog, model tool catalog, connected-host posture, and Agent settings. Use it to change Agent settings, invoke workspace action mirrors, or invoke slash-command mirrors only when the user explicitly asks; preserve confirm:true and explicitUserRequest for mutations, keep secret-backed settings in the secret manager, and keep connected-host lifecycle/posture externally owned.',
|
|
55
|
-
'- Use the `agent_knowledge` tool for Agent Knowledge status, ask, and
|
|
54
|
+
'- Use the `agent_harness` tool to inspect and operate the harness surface itself: Agent workspace action catalog, slash command catalog, model tool catalog, release evidence bundle, release readiness inventory, connected-host posture, and Agent settings. Use it to change Agent settings, invoke workspace action mirrors, or invoke slash-command mirrors only when the user explicitly asks; preserve confirm:true and explicitUserRequest for mutations, keep secret-backed settings in the secret manager, and keep connected-host lifecycle/posture externally owned.',
|
|
55
|
+
'- Use the `agent_knowledge` tool for Agent Knowledge status, ask, search, source/node/issue lists, item lookup, map summary, connector list/detail, and connector doctor from the main conversation. It must use only /api/goodvibes-agent/knowledge/* and must fail closed instead of falling back to default knowledge or non-Agent knowledge spaces.',
|
|
56
56
|
'- When the user explicitly asks Agent to add, import, remember, or ingest a URL, URL-list file, local file, bookmarks file, browser history, or connector input into Agent Knowledge, use the `agent_knowledge_ingest` tool with confirm:true and the original user request. It must write only to /api/goodvibes-agent/knowledge/* ingest routes and never to default knowledge or non-Agent knowledge spaces.',
|
|
57
57
|
'- Use the `agent_local_registry` tool when a scratchpad note, durable memory, reusable persona, skill, skill bundle, or routine would improve later work. Keep those records Agent-local, non-secret, source/provenance tagged, and reviewable. Notes are for temporary/source-triage context; promote them explicitly into memory, skills, personas, routines, or Agent Knowledge only when that is the correct durable home. Review memory with a confidence score when it should shape later turns. Starting a routine means applying its steps in this same serial conversation, not creating a background job.',
|
|
58
58
|
'- When the user explicitly asks Agent to generate an image, video, or other media artifact, use the `agent_media_generate` tool with confirm:true and the original user request. Generated media is stored as artifacts; do not print base64, call default knowledge, use non-Agent knowledge spaces, send channels, create separate Agent jobs, or request GoodVibes TUI delegation for media generation.',
|
|
@@ -20,6 +20,22 @@ export function connectedHostOperatorTokenPath(homeDirectory: string): string {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function readConnectedHostOperatorToken(homeDirectory: string): ConnectedHostOperatorToken {
|
|
23
|
+
const connectedHostEnvToken = process.env.GOODVIBES_CONNECTED_HOST_TOKEN?.trim();
|
|
24
|
+
if (connectedHostEnvToken) {
|
|
25
|
+
return {
|
|
26
|
+
path: 'env:GOODVIBES_CONNECTED_HOST_TOKEN',
|
|
27
|
+
present: true,
|
|
28
|
+
token: connectedHostEnvToken,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const legacyEnvToken = process.env.GOODVIBES_DAEMON_TOKEN?.trim();
|
|
32
|
+
if (legacyEnvToken) {
|
|
33
|
+
return {
|
|
34
|
+
path: 'env:GOODVIBES_DAEMON_TOKEN',
|
|
35
|
+
present: true,
|
|
36
|
+
token: legacyEnvToken,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
23
39
|
const path = connectedHostOperatorTokenPath(homeDirectory);
|
|
24
40
|
if (!existsSync(path)) return { path, present: false, token: null };
|
|
25
41
|
try {
|
|
@@ -42,11 +42,9 @@ export function createAgentChannelSendTool(
|
|
|
42
42
|
definition: {
|
|
43
43
|
name: 'agent_channel_send',
|
|
44
44
|
description: [
|
|
45
|
-
'Send one
|
|
46
|
-
'Use only
|
|
47
|
-
'
|
|
48
|
-
'This tool does not create channel routes, authorize accounts, manage connected-host hosting, use default knowledge, use non-Agent knowledge segments, create separate Agent jobs, run delegated review, or invoke arbitrary routes.',
|
|
49
|
-
'Set confirm:true only for an explicit user request. Otherwise return the preview/confirmation error.',
|
|
45
|
+
'Send one confirmed message through a configured Agent target.',
|
|
46
|
+
'Use only for explicit send/message/alert requests.',
|
|
47
|
+
'Provide exactly one target.',
|
|
50
48
|
].join(' '),
|
|
51
49
|
parameters: {
|
|
52
50
|
type: 'object',
|
|
@@ -61,7 +59,7 @@ export function createAgentChannelSendTool(
|
|
|
61
59
|
},
|
|
62
60
|
channel: {
|
|
63
61
|
type: 'string',
|
|
64
|
-
description: 'Optional channel target
|
|
62
|
+
description: 'Optional channel target. Use exactly one target field.',
|
|
65
63
|
},
|
|
66
64
|
route: {
|
|
67
65
|
type: 'string',
|
|
@@ -77,7 +75,7 @@ export function createAgentChannelSendTool(
|
|
|
77
75
|
},
|
|
78
76
|
confirm: {
|
|
79
77
|
type: 'boolean',
|
|
80
|
-
description: 'Required true
|
|
78
|
+
description: 'Required true for confirmed delivery.',
|
|
81
79
|
},
|
|
82
80
|
explicitUserRequest: {
|
|
83
81
|
type: 'string',
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
2
|
+
import type { AgentWorkspaceChannelStatus } from '../input/agent-workspace-channels.ts';
|
|
3
|
+
import { buildAgentWorkspaceChannels } from '../input/agent-workspace-channels.ts';
|
|
4
|
+
|
|
5
|
+
export interface AgentHarnessChannelArgs {
|
|
6
|
+
readonly channelId?: unknown;
|
|
7
|
+
readonly target?: unknown;
|
|
8
|
+
readonly query?: unknown;
|
|
9
|
+
readonly includeParameters?: unknown;
|
|
10
|
+
readonly limit?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ChannelLookupSource = 'channelId' | 'target' | 'query';
|
|
14
|
+
|
|
15
|
+
type ChannelResolution =
|
|
16
|
+
| { readonly status: 'found'; readonly channel: Record<string, unknown> }
|
|
17
|
+
| { readonly status: 'ambiguous'; readonly input: string; readonly candidates: readonly Record<string, unknown>[] }
|
|
18
|
+
| { readonly status: 'missing_lookup'; readonly usage: string };
|
|
19
|
+
|
|
20
|
+
function readString(value: unknown): string {
|
|
21
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readLimit(value: unknown, fallback: number): number {
|
|
25
|
+
const parsed = typeof value === 'string' && value.trim() ? Number(value) : value;
|
|
26
|
+
if (typeof parsed !== 'number' || !Number.isFinite(parsed)) return fallback;
|
|
27
|
+
return Math.max(1, Math.min(500, Math.trunc(parsed)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function channelLookupFromArgs(args: AgentHarnessChannelArgs): { readonly source: ChannelLookupSource; readonly input: string } | null {
|
|
31
|
+
const channelId = readString(args.channelId);
|
|
32
|
+
if (channelId) return { source: 'channelId', input: channelId };
|
|
33
|
+
const target = readString(args.target);
|
|
34
|
+
if (target) return { source: 'target', input: target };
|
|
35
|
+
const query = readString(args.query);
|
|
36
|
+
return query ? { source: 'query', input: query } : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function channelSearchText(channel: AgentWorkspaceChannelStatus): string {
|
|
40
|
+
return [
|
|
41
|
+
channel.id,
|
|
42
|
+
channel.label,
|
|
43
|
+
channel.delivery,
|
|
44
|
+
channel.risk,
|
|
45
|
+
channel.riskLabel,
|
|
46
|
+
channel.setupState,
|
|
47
|
+
channel.nextStep,
|
|
48
|
+
...channel.requiredKeys,
|
|
49
|
+
...channel.missingRequiredKeys,
|
|
50
|
+
...channel.defaultTargetKeys,
|
|
51
|
+
...channel.configuredDefaultTargetKeys,
|
|
52
|
+
].join('\n').toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function describeChannelCandidate(channel: AgentWorkspaceChannelStatus): Record<string, unknown> {
|
|
56
|
+
return {
|
|
57
|
+
channelId: channel.id,
|
|
58
|
+
label: channel.label,
|
|
59
|
+
setupState: channel.setupState,
|
|
60
|
+
ready: channel.ready,
|
|
61
|
+
delivery: channel.delivery,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function describeChannel(
|
|
66
|
+
channel: AgentWorkspaceChannelStatus,
|
|
67
|
+
options: { readonly includeParameters?: boolean; readonly lookup?: Record<string, unknown> } = {},
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
return {
|
|
70
|
+
id: channel.id,
|
|
71
|
+
label: channel.label,
|
|
72
|
+
enabled: channel.enabled,
|
|
73
|
+
ready: channel.ready,
|
|
74
|
+
setupState: channel.setupState,
|
|
75
|
+
delivery: channel.delivery,
|
|
76
|
+
risk: channel.risk,
|
|
77
|
+
riskLabel: channel.riskLabel,
|
|
78
|
+
missingConfigCount: channel.missingConfigCount,
|
|
79
|
+
defaultTarget: channel.defaultTarget,
|
|
80
|
+
nextStep: channel.nextStep,
|
|
81
|
+
requiredConfigKeys: channel.requiredKeys,
|
|
82
|
+
missingConfigKeys: channel.missingRequiredKeys,
|
|
83
|
+
defaultTargetKeys: channel.defaultTargetKeys,
|
|
84
|
+
configuredDefaultTargetKeys: channel.configuredDefaultTargetKeys,
|
|
85
|
+
...(options.lookup ? { lookup: options.lookup } : {}),
|
|
86
|
+
policy: {
|
|
87
|
+
effect: 'read-only',
|
|
88
|
+
values: 'Config key names and target key names are shown; secret values and stored target values are never returned.',
|
|
89
|
+
delivery: 'Use agent_channel_send only for one explicit, confirmed delivery target requested by the user.',
|
|
90
|
+
setup: 'Use connected-host setup/account/policy routes only as read-only diagnostics unless another confirmed first-class tool owns the mutation.',
|
|
91
|
+
},
|
|
92
|
+
modelAccess: {
|
|
93
|
+
sendTool: 'agent_channel_send',
|
|
94
|
+
notificationTool: 'agent_notify',
|
|
95
|
+
reminderTool: 'agent_reminder_schedule',
|
|
96
|
+
slashCommandDetail: `/channels show ${channel.id}`,
|
|
97
|
+
readOnlyConnectedRoutes: [
|
|
98
|
+
'/channels accounts',
|
|
99
|
+
'/channels policies',
|
|
100
|
+
'/channels status',
|
|
101
|
+
`/channels doctor ${channel.id}`,
|
|
102
|
+
`/channels setup ${channel.id}`,
|
|
103
|
+
],
|
|
104
|
+
settingsFilter: `agent_harness mode:"settings" prefix:"surfaces.${channel.id}" includeHidden:true`,
|
|
105
|
+
connectedHostBoundary: 'agent_harness mode:"connected_host_capability" query:"delivery"',
|
|
106
|
+
...(options.includeParameters ? {
|
|
107
|
+
deliveryTargetShape: 'surface[:route[:label]]',
|
|
108
|
+
exampleTarget: `${channel.id}:route:Label`,
|
|
109
|
+
} : {}),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function channelReadinessCatalogStatus(context: CommandContext): Record<string, unknown> {
|
|
115
|
+
const channels = buildAgentWorkspaceChannels(context);
|
|
116
|
+
return {
|
|
117
|
+
modes: ['channels', 'channel'],
|
|
118
|
+
channels: channels.length,
|
|
119
|
+
enabled: channels.filter((channel) => channel.enabled).length,
|
|
120
|
+
ready: channels.filter((channel) => channel.ready).length,
|
|
121
|
+
attention: channels.filter((channel) => channel.enabled && channel.setupState !== 'ready').length,
|
|
122
|
+
readOnly: true,
|
|
123
|
+
deliveryTool: 'agent_channel_send',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function listHarnessChannels(context: CommandContext, args: AgentHarnessChannelArgs): Record<string, unknown> {
|
|
128
|
+
const query = readString(args.query).toLowerCase();
|
|
129
|
+
const includeParameters = args.includeParameters === true;
|
|
130
|
+
const limit = readLimit(args.limit, 200);
|
|
131
|
+
const channels = buildAgentWorkspaceChannels(context);
|
|
132
|
+
const filtered = channels
|
|
133
|
+
.filter((channel) => !query || channelSearchText(channel).includes(query))
|
|
134
|
+
.slice(0, limit);
|
|
135
|
+
return {
|
|
136
|
+
channels: filtered.map((channel) => describeChannel(channel, { includeParameters })),
|
|
137
|
+
returned: filtered.length,
|
|
138
|
+
total: channels.length,
|
|
139
|
+
enabled: channels.filter((channel) => channel.enabled).length,
|
|
140
|
+
ready: channels.filter((channel) => channel.ready).length,
|
|
141
|
+
attention: channels.filter((channel) => channel.enabled && channel.setupState !== 'ready').length,
|
|
142
|
+
policy: 'Read-only channel readiness catalog. It returns key names, setup state, delivery posture, and model routes without printing secrets or sending messages.',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function describeHarnessChannel(context: CommandContext, args: AgentHarnessChannelArgs): ChannelResolution {
|
|
147
|
+
const lookup = channelLookupFromArgs(args);
|
|
148
|
+
if (!lookup) {
|
|
149
|
+
return {
|
|
150
|
+
status: 'missing_lookup',
|
|
151
|
+
usage: 'channel requires channelId, target, or query. Use mode:"channels" to inspect available channel ids.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const channels = buildAgentWorkspaceChannels(context);
|
|
155
|
+
const normalized = lookup.input.toLowerCase();
|
|
156
|
+
const exact = channels.find((channel) => channel.id === lookup.input);
|
|
157
|
+
if (exact) return { status: 'found', channel: describeChannel(exact, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'id' } }) };
|
|
158
|
+
const insensitiveId = channels.find((channel) => channel.id.toLowerCase() === normalized);
|
|
159
|
+
if (insensitiveId) return { status: 'found', channel: describeChannel(insensitiveId, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-id' } }) };
|
|
160
|
+
const insensitiveLabel = channels.find((channel) => channel.label.toLowerCase() === normalized);
|
|
161
|
+
if (insensitiveLabel) return { status: 'found', channel: describeChannel(insensitiveLabel, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'case-insensitive-label' } }) };
|
|
162
|
+
const searched = channels.filter((channel) => channelSearchText(channel).includes(normalized));
|
|
163
|
+
if (searched.length === 1) {
|
|
164
|
+
return { status: 'found', channel: describeChannel(searched[0]!, { includeParameters: true, lookup: { ...lookup, resolvedBy: 'search' } }) };
|
|
165
|
+
}
|
|
166
|
+
if (searched.length > 1) {
|
|
167
|
+
return {
|
|
168
|
+
status: 'ambiguous',
|
|
169
|
+
input: lookup.input,
|
|
170
|
+
candidates: searched.slice(0, 8).map(describeChannelCandidate),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
status: 'missing_lookup',
|
|
175
|
+
usage: `Unknown channel ${lookup.input}. Use mode:"channels" to inspect available channel ids.`,
|
|
176
|
+
};
|
|
177
|
+
}
|