nova-audio-agent 0.1.2 → 0.2.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 CHANGED
@@ -28,7 +28,7 @@ In the desktop settings, configure your [DashScope](https://platform.qianwenai.c
28
28
  API key for the default Qwen realtime voice service and your
29
29
  [Tavily](https://docs.tavily.com) API key for web search. For coding tasks, set up
30
30
  a logged-in Codex executable; see the
31
- [setup guide](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/getting-started.md).
31
+ [setup guide](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/en/getting-started.md).
32
32
  Allow microphone access when prompted, then launch Nova:
33
33
 
34
34
  ```bash
@@ -66,14 +66,14 @@ for your installed version.
66
66
 
67
67
  ## Installation details
68
68
 
69
- The CLI downloads the matching `v0.1.1` desktop release from
70
- [GitHub Releases](https://github.com/deepnovacore/NovaAudioAgent/releases/tag/v0.1.1)
69
+ The CLI downloads the matching `v0.2.0` desktop release from
70
+ [GitHub Releases](https://github.com/deepnovacore/NovaAudioAgent/releases/tag/v0.2.0)
71
71
  into `~/.nova-audio-agent/cli/releases/` and verifies its published SHA-256 digest
72
72
  before launching it. It reuses the desktop client's encrypted settings store;
73
73
  the CLI does not read or print secret values.
74
74
 
75
75
  Documentation-only npm updates may have a newer package version while retaining
76
- the same desktop release. `novaaudio --version` continues to report `0.1.1`.
76
+ the same desktop release. `novaaudio --version` continues to report `0.2.0`.
77
77
 
78
78
  This release supports macOS arm64 and Windows x64. Linux and Intel Mac desktop
79
79
  downloads are not included. The desktop application is currently unsigned, so
@@ -81,9 +81,9 @@ macOS Gatekeeper or Windows SmartScreen may display a security warning.
81
81
 
82
82
  ## Learn more
83
83
 
84
- - [Getting started and integrations](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/getting-started.md)
85
- - [Runtime architecture](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/architecture.md)
86
- - [Design: when should a proactive voice agent speak?](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/blog/2026-08-proactive-voice-agent-design-space.md)
84
+ - [Getting started and integrations](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/en/getting-started.md)
85
+ - [Runtime architecture](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/en/architecture.md)
86
+ - [Design: when should a proactive voice agent speak?](https://github.com/deepnovacore/NovaAudioAgent/blob/main/docs/en/blog/2026-08-proactive-voice-agent-design-space.md)
87
87
  - [Report an issue](https://github.com/deepnovacore/NovaAudioAgent/issues)
88
88
  - [Build from source and contribute](https://github.com/deepnovacore/NovaAudioAgent/blob/main/CONTRIBUTING.md)
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nova-audio-agent",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Install and launch the Nova Audio Agent desktop client from the command line.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,13 +3,13 @@
3
3
  "repository": "deepnovacore/NovaAudioAgent",
4
4
  "targets": {
5
5
  "darwin-arm64": {
6
- "artifact": "nova-audio-agent-0.1.1-macos-arm64-app.zip",
7
- "executable": "Nova Audio Agent Ambient Orb.app/Contents/MacOS/Nova Audio Agent Ambient Orb",
6
+ "artifact": "nova-audio-agent-0.2.0-macos-arm64-app.zip",
7
+ "executable": "Nova Audio Agent Desktop.app/Contents/MacOS/Nova Audio Agent Desktop",
8
8
  "archive": "zip"
9
9
  },
10
10
  "win32-x64": {
11
- "artifact": "nova-audio-agent-0.1.1-windows-x64-portable.zip",
12
- "executable": "Nova Audio Agent Ambient Orb.exe",
11
+ "artifact": "nova-audio-agent-0.2.0-windows-x64-portable.zip",
12
+ "executable": "Nova Audio Agent Desktop.exe",
13
13
  "archive": "zip"
14
14
  }
15
15
  }
@@ -0,0 +1,266 @@
1
+ // Generated from runtime/src/config/capability-registry.ts; run node runtime/scripts/check-capabilities.mjs --write.
2
+ /** One dependency-free registry parser, also emitted into the standalone CLI by check-capabilities.mjs. */
3
+ import { readFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ export const DEFAULT_CAPABILITIES_PATH = '~/.nova-audio-agent/capabilities.json';
7
+ export const BAILIAN_SEARCH_MCP_URL = 'https://dashscope.aliyuncs.com/api/v1/mcps/EnhancedSearch/mcp';
8
+ export const BAILIAN_SEARCH_MCP_TOOL = 'search_pro';
9
+ export const DEFAULT_FRONTBRAIN_TOOL_BUDGET = 24;
10
+ export const MCP_NON_AUTH_HEADERS = ['accept', 'content-type', 'user-agent'];
11
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
12
+ const SERVER_NAME = /^[a-z][a-z0-9_]{0,31}$/u;
13
+ const MAX_CONFIG_BYTES = 256 * 1024;
14
+ export class CapabilityConfigurationError extends Error {
15
+ reason;
16
+ code = 'invalid_capabilities_configuration';
17
+ constructor(reason) {
18
+ super(`invalid capabilities configuration: ${reason}`);
19
+ this.reason = reason;
20
+ this.name = 'CapabilityConfigurationError';
21
+ }
22
+ }
23
+ function environmentOverride(environment, name) {
24
+ const value = environment[name]?.trim();
25
+ return value === '' ? undefined : value;
26
+ }
27
+ function omittedDefault(value, fallback) { return value === undefined ? fallback : value; }
28
+ function invalid(field) { throw new CapabilityConfigurationError(field); }
29
+ function object(value, field, keys) {
30
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
31
+ invalid(field);
32
+ const record = value;
33
+ if (keys !== undefined && Object.keys(record).some(key => !keys.includes(key)))
34
+ invalid(field);
35
+ return record;
36
+ }
37
+ function bool(value, fallback, field) {
38
+ if (value === undefined)
39
+ return fallback;
40
+ if (typeof value !== 'boolean')
41
+ invalid(field);
42
+ return value;
43
+ }
44
+ function string(value, field, max = 8192) {
45
+ if (typeof value !== 'string' || value.trim() === '' || value.length > max || /[\u0000\r\n]/u.test(value))
46
+ invalid(field);
47
+ return value;
48
+ }
49
+ function integer(value, fallback, max, field) {
50
+ if (value === undefined)
51
+ return fallback;
52
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > max)
53
+ invalid(field);
54
+ return value;
55
+ }
56
+ function stringMap(value, field) {
57
+ const entries = Object.entries(object(omittedDefault(value, {}), field));
58
+ if (entries.length > 64)
59
+ invalid(field);
60
+ return Object.fromEntries(entries.map(([key, val]) => {
61
+ if (!/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/u.test(key))
62
+ invalid(field);
63
+ if (typeof val !== 'string' || val.length > 8192 || /[\u0000\r\n]/u.test(val))
64
+ invalid(field);
65
+ return [key, val];
66
+ }));
67
+ }
68
+ export function interpolateCapabilityValue(value, environment) {
69
+ const resolved = value.replace(/\$\{([^}]+)\}/gu, (_match, name) => {
70
+ if (!ENV_NAME.test(name))
71
+ invalid('invalid_environment_reference');
72
+ const replacement = environment[name];
73
+ if (replacement === undefined || replacement.trim() === '')
74
+ invalid(`missing_environment:${name}`);
75
+ if (/[\u0000\r\n]/u.test(replacement))
76
+ invalid(`invalid_environment:${name}`);
77
+ return replacement;
78
+ });
79
+ if (resolved.length > 8192)
80
+ invalid('interpolated_value_too_large');
81
+ return resolved;
82
+ }
83
+ function interpolateMap(value, environment) {
84
+ return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, interpolateCapabilityValue(val, environment)]));
85
+ }
86
+ export function validateMcpEndpoint(value, headers = {}) {
87
+ let url;
88
+ try {
89
+ url = new URL(value);
90
+ }
91
+ catch {
92
+ invalid('invalid_mcp_endpoint');
93
+ }
94
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
95
+ // Unknown custom headers may themselves be credentials. Only known non-auth metadata is safe on HTTP.
96
+ const hasAuth = Object.keys(headers).some(key => !MCP_NON_AUTH_HEADERS.some(name => name === key.toLowerCase()));
97
+ if (url.username || url.password || url.hash
98
+ || (url.protocol !== 'https:' && (url.protocol !== 'http:' || !loopback || hasAuth)))
99
+ invalid('insecure_mcp_endpoint');
100
+ }
101
+ function moduleConfig(value, field, keys) {
102
+ return object(omittedDefault(value, {}), field, ['enabled', ...keys]);
103
+ }
104
+ export function parseCapabilityRegistry(input, environment = {}) {
105
+ const document = object(input, 'document', ['version', 'modules', 'mcpServers', 'frontbrainToolBudget']);
106
+ if (document.version !== 1)
107
+ invalid('version');
108
+ const modules = object(omittedDefault(document.modules, {}), 'modules', ['search', 'camera', 'coding', 'knowledge']);
109
+ const search = moduleConfig(modules.search, 'modules.search', ['provider', 'mcp', 'tavily']);
110
+ const camera = moduleConfig(modules.camera, 'modules.camera', []);
111
+ const coding = moduleConfig(modules.coding, 'modules.coding', []);
112
+ const knowledge = moduleConfig(modules.knowledge, 'modules.knowledge', ['exposeToCodex']);
113
+ const enabled = bool(search.enabled, true, 'modules.search.enabled');
114
+ const configuredProvider = omittedDefault(search.provider, 'tavily');
115
+ if (configuredProvider !== 'tavily' && configuredProvider !== 'mcp')
116
+ invalid('modules.search.provider');
117
+ const provider = environmentOverride(environment, 'NOVA_AUDIO_AGENT_SEARCH_PROVIDER') ?? configuredProvider;
118
+ if (provider !== 'tavily' && provider !== 'mcp')
119
+ invalid('NOVA_AUDIO_AGENT_SEARCH_PROVIDER');
120
+ const overrides = [];
121
+ if (environment.NOVA_AUDIO_AGENT_SEARCH_PROVIDER?.trim())
122
+ overrides.push('NOVA_AUDIO_AGENT_SEARCH_PROVIDER');
123
+ let cameraEnabled = bool(camera.enabled, true, 'modules.camera.enabled');
124
+ const cameraOverride = environment.NOVA_AUDIO_AGENT_CAMERA_MODULE_ENABLED?.trim().toLowerCase();
125
+ if (cameraOverride) {
126
+ if (!['true', 'false', '1', '0', 'yes', 'no', 'on', 'off'].includes(cameraOverride))
127
+ invalid('NOVA_AUDIO_AGENT_CAMERA_MODULE_ENABLED');
128
+ cameraEnabled = ['true', '1', 'yes', 'on'].includes(cameraOverride);
129
+ overrides.push('NOVA_AUDIO_AGENT_CAMERA_MODULE_ENABLED');
130
+ }
131
+ const tavily = object(omittedDefault(search.tavily, {}), 'modules.search.tavily', ['apiKeyEnv']);
132
+ const apiKeyEnv = string(omittedDefault(tavily.apiKeyEnv, 'TAVILY_API_KEY'), 'modules.search.tavily.apiKeyEnv', 128);
133
+ if (!ENV_NAME.test(apiKeyEnv))
134
+ invalid('modules.search.tavily.apiKeyEnv');
135
+ let mcp;
136
+ if (search.mcp !== undefined || (enabled && provider === 'mcp')) {
137
+ const config = object(omittedDefault(search.mcp, {}), 'modules.search.mcp', ['url', 'tool', 'headers', 'timeoutMs', 'maxResultBytes']);
138
+ const urlOverride = environmentOverride(environment, 'NOVA_AUDIO_AGENT_SEARCH_MCP_URL');
139
+ const toolOverride = environmentOverride(environment, 'NOVA_AUDIO_AGENT_SEARCH_MCP_TOOL');
140
+ const preset = !urlOverride && config.url === undefined;
141
+ const configuredUrl = string(omittedDefault(config.url, BAILIAN_SEARCH_MCP_URL), 'modules.search.mcp.url');
142
+ const rawUrl = string(urlOverride ?? configuredUrl, 'modules.search.mcp.url');
143
+ const configuredTool = string(omittedDefault(config.tool, preset ? BAILIAN_SEARCH_MCP_TOOL : 'web_search'), 'modules.search.mcp.tool', 256);
144
+ const tool = string(toolOverride ?? configuredTool, 'modules.search.mcp.tool', 256);
145
+ const rawHeaders = stringMap(omittedDefault(config.headers, preset ? { authorization: 'Bearer ${DASHSCOPE_API_KEY}' } : {}), 'modules.search.mcp.headers');
146
+ const headers = enabled && provider === 'mcp' ? interpolateMap(rawHeaders, environment) : rawHeaders;
147
+ const url = enabled && provider === 'mcp' ? interpolateCapabilityValue(rawUrl, environment) : rawUrl;
148
+ if (enabled && provider === 'mcp')
149
+ validateMcpEndpoint(url, headers);
150
+ mcp = { url, tool, headers,
151
+ timeoutMs: integer(config.timeoutMs, 8000, 60000, 'modules.search.mcp.timeoutMs'),
152
+ maxResultBytes: integer(config.maxResultBytes, 262144, 1048576, 'modules.search.mcp.maxResultBytes') };
153
+ if (urlOverride)
154
+ overrides.push('NOVA_AUDIO_AGENT_SEARCH_MCP_URL');
155
+ if (toolOverride)
156
+ overrides.push('NOVA_AUDIO_AGENT_SEARCH_MCP_TOOL');
157
+ }
158
+ const servers = object(omittedDefault(document.mcpServers, {}), 'mcpServers');
159
+ if (Object.keys(servers).length > 8)
160
+ invalid('mcpServers:max_8');
161
+ const mcpServers = Object.create(null);
162
+ const serverStatuses = [];
163
+ for (const [index, [name, value]] of Object.entries(servers).entries()) {
164
+ const safeName = SERVER_NAME.test(name) ? name : `invalid_server_${index + 1}`;
165
+ try {
166
+ if (!SERVER_NAME.test(name))
167
+ invalid('invalid_server_name');
168
+ if (name === 'nova_camera' || name === 'nova_knowledge')
169
+ invalid('reserved_server_name');
170
+ const server = parseServer(value, environment);
171
+ mcpServers[name] = server;
172
+ serverStatuses.push({ name, status: server.enabled ? 'configured' : 'disabled' });
173
+ }
174
+ catch (error) {
175
+ serverStatuses.push({ name: safeName, status: 'failed', reason: error instanceof CapabilityConfigurationError ? error.reason : 'invalid_server' });
176
+ }
177
+ }
178
+ return { version: 1, modules: {
179
+ search: { enabled, provider, tavily: { apiKeyEnv, ...(environment[apiKeyEnv] === undefined ? {} : { apiKey: environment[apiKeyEnv] }) }, ...(mcp === undefined ? {} : { mcp }) },
180
+ camera: { enabled: cameraEnabled }, coding: { enabled: bool(coding.enabled, true, 'modules.coding.enabled') },
181
+ knowledge: { enabled: bool(knowledge.enabled, false, 'modules.knowledge.enabled'), exposeToCodex: bool(knowledge.exposeToCodex, false, 'modules.knowledge.exposeToCodex') },
182
+ }, mcpServers, serverStatuses, overrides,
183
+ frontbrainToolBudget: integer(document.frontbrainToolBudget, DEFAULT_FRONTBRAIN_TOOL_BUDGET, 256, 'frontbrainToolBudget') };
184
+ }
185
+ function parseServer(value, environment) {
186
+ const config = object(value, 'server', ['enabled', 'transport', 'url', 'headers', 'command', 'args', 'env', 'tools', 'exposeTo']);
187
+ const enabled = bool(config.enabled, true, 'server.enabled');
188
+ const transport = config.transport;
189
+ if (transport !== 'streamable-http' && transport !== 'stdio')
190
+ invalid('server.transport');
191
+ const exposure = object(omittedDefault(config.exposeTo, {}), 'server.exposeTo', ['frontbrain', 'codex']);
192
+ const exposeTo = { frontbrain: bool(exposure.frontbrain, false, 'server.exposeTo.frontbrain'), codex: bool(exposure.codex, true, 'server.exposeTo.codex') };
193
+ const rawTools = object(omittedDefault(config.tools, {}), 'server.tools');
194
+ if (Object.keys(rawTools).length > 32)
195
+ invalid('server.tools:max_32');
196
+ const tools = Object.fromEntries(Object.entries(rawTools).map(([name, value]) => {
197
+ string(name, 'server.tool_name', 256);
198
+ const tool = object(value, 'server.tool', ['enabled', 'timeoutMs', 'maxResultBytes', 'maxCallsPerTurn']);
199
+ return [name, { enabled: bool(tool.enabled, false, 'server.tool.enabled'),
200
+ timeoutMs: integer(tool.timeoutMs, 8000, 60000, 'server.tool.timeoutMs'),
201
+ maxResultBytes: integer(tool.maxResultBytes, 32768, 1048576, 'server.tool.maxResultBytes'),
202
+ maxCallsPerTurn: integer(tool.maxCallsPerTurn, 2, 32, 'server.tool.maxCallsPerTurn') }];
203
+ }));
204
+ if (transport === 'streamable-http') {
205
+ if (config.command !== undefined || config.args !== undefined || config.env !== undefined)
206
+ invalid('server.transport_fields');
207
+ const rawHeaders = stringMap(config.headers, 'server.headers');
208
+ const rawUrl = string(config.url, 'server.url');
209
+ const headers = enabled ? interpolateMap(rawHeaders, environment) : rawHeaders;
210
+ const url = enabled ? interpolateCapabilityValue(rawUrl, environment) : rawUrl;
211
+ if (enabled)
212
+ validateMcpEndpoint(url, headers);
213
+ return { enabled, transport, url, urlInterpolated: /\$\{[A-Za-z_][A-Za-z0-9_]*\}/u.test(rawUrl), headers, tools, exposeTo };
214
+ }
215
+ if (config.url !== undefined || config.headers !== undefined)
216
+ invalid('server.transport_fields');
217
+ const command = string(config.command, 'server.command');
218
+ const args = omittedDefault(config.args, []);
219
+ if (!Array.isArray(args) || args.length > 64 || args.some(arg => typeof arg !== 'string' || arg.length > 8192 || arg.includes('\0')))
220
+ invalid('server.args');
221
+ const env = stringMap(config.env, 'server.env');
222
+ if (Object.keys(env).some(key => !ENV_NAME.test(key)))
223
+ invalid('server.env');
224
+ return { enabled, transport, command, args: args, env: enabled ? interpolateMap(env, environment) : env, tools, exposeTo };
225
+ }
226
+ export function loadCapabilityRegistry(options = {}) {
227
+ const environment = options.environment ?? process.env;
228
+ const explicitPath = (options.path === '' ? undefined : options.path) ?? environmentOverride(environment, 'NOVA_AUDIO_AGENT_CAPABILITIES_CONFIG');
229
+ const path = explicitPath ?? DEFAULT_CAPABILITIES_PATH;
230
+ const explicit = explicitPath !== undefined;
231
+ const resolved = path.startsWith('~/') ? join(options.home ?? homedir(), path.slice(2)) : path;
232
+ let input;
233
+ try {
234
+ const bytes = readFileSync(resolved);
235
+ if (bytes.byteLength > MAX_CONFIG_BYTES)
236
+ invalid('file_too_large');
237
+ input = JSON.parse(bytes.toString('utf8'));
238
+ }
239
+ catch (error) {
240
+ if (!explicit && error.code === 'ENOENT')
241
+ input = { version: 1 };
242
+ else
243
+ throw error instanceof CapabilityConfigurationError ? error : new CapabilityConfigurationError('file_unreadable_or_invalid_json');
244
+ }
245
+ return parseCapabilityRegistry(input, environment);
246
+ }
247
+ export function capabilityStatus(registry, toolCount = null) {
248
+ return { modules: {
249
+ search: { enabled: registry.modules.search.enabled, provider: registry.modules.search.provider },
250
+ camera: registry.modules.camera, coding: registry.modules.coding, knowledge: registry.modules.knowledge,
251
+ }, servers: registry.serverStatuses, overrides: registry.overrides, toolCount, toolBudget: registry.frontbrainToolBudget };
252
+ }
253
+ export function inspectCapabilities(options = {}) {
254
+ try {
255
+ const registry = loadCapabilityRegistry(options);
256
+ const environment = options.environment ?? process.env;
257
+ const missingTavily = registry.modules.search.enabled && registry.modules.search.provider === 'tavily'
258
+ && !environment[registry.modules.search.tavily.apiKeyEnv]?.trim();
259
+ return { ok: !missingTavily && registry.serverStatuses.every(server => server.status !== 'failed'),
260
+ ...capabilityStatus(registry),
261
+ ...(missingTavily ? { reason: `missing_environment:${registry.modules.search.tavily.apiKeyEnv}` } : {}) };
262
+ }
263
+ catch (error) {
264
+ return { ok: false, reason: error instanceof CapabilityConfigurationError ? error.reason : 'invalid_configuration' };
265
+ }
266
+ }
package/src/command.mjs CHANGED
@@ -37,7 +37,17 @@ export async function main(argv, {
37
37
  stdout.write(`Configured keys: ${report.configuredSecretKeys.length === 0 ? 'none' : report.configuredSecretKeys.join(', ')}\n`)
38
38
  stdout.write(`Codex: ${report.codexPresent ? 'found' : 'missing'}\n`)
39
39
  }
40
- return report.supported ? 0 : 1
40
+ if (report.capabilities !== undefined) {
41
+ const status = report.capabilities
42
+ stdout.write(`Capabilities: ${status.ok ? 'valid' : 'needs attention'}${status.reason ? ` (${status.reason})` : ''}\n`)
43
+ if (status.modules) {
44
+ for (const [name, module] of Object.entries(status.modules)) stdout.write(` ${name}: ${module.enabled ? 'enabled' : 'disabled'}${module.provider ? ` (${module.provider})` : ''}\n`)
45
+ stdout.write(` FrontBrain budget: ${status.toolBudget}; exact count requires runtime composition\n`)
46
+ for (const server of status.servers) stdout.write(` MCP ${server.name}: ${server.status}${server.reason ? ` (${server.reason})` : ''}\n`)
47
+ for (const name of status.overrides) stdout.write(` Override: ${name}\n`)
48
+ }
49
+ }
50
+ return report.supported && report.capabilities?.ok !== false ? 0 : 1
41
51
  }
42
52
  if (command !== 'start' && command !== 'config') {
43
53
  stdout.write(`${HELP_TEXT}\n`)
package/src/runtime.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import {inspectCapabilities} from './capability-registry.mjs'
1
2
  import { createHash, randomUUID, timingSafeEqual } from 'node:crypto'
2
3
  import { spawn, spawnSync } from 'node:child_process'
3
4
  import {
@@ -430,8 +431,16 @@ export async function inspectDoctor({
430
431
  const executable = resolve(root, target.executable)
431
432
  const settings = desktopSettingsPath({platform, home, environment})
432
433
  let secretKeys = []
434
+ let capabilitiesConfigPath
433
435
  try {
434
436
  const document = JSON.parse(await readFile(settings, 'utf8'))
437
+ // Match settings-store v4 migration/string validation, then backendLaunchSpec's nonempty saved-path override.
438
+ const acceptsV4Fields = document?.version === undefined || (typeof document.version === 'number' && document.version >= 4)
439
+ const candidate = document?.capabilitiesConfigPath
440
+ if (acceptsV4Fields && typeof candidate === 'string' && !/[\u0000-\u001f\u007f]/u.test(candidate)) {
441
+ const normalized = candidate.trim()
442
+ if (normalized && [...normalized].length <= 32768) capabilitiesConfigPath = normalized
443
+ }
435
444
  if (document?.secrets && typeof document.secrets === 'object' && !Array.isArray(document.secrets)) {
436
445
  secretKeys = Object.keys(document.secrets).sort()
437
446
  }
@@ -443,5 +452,11 @@ export async function inspectDoctor({
443
452
  settingsPresent: await access(settings).then(() => true, () => false),
444
453
  configuredSecretKeys: Object.freeze(secretKeys),
445
454
  codexPresent: findCodex(platform),
455
+ capabilities: inspectCapabilities({
456
+ environment: capabilitiesConfigPath === undefined ? environment : {
457
+ ...environment, NOVA_AUDIO_AGENT_CAPABILITIES_CONFIG: capabilitiesConfigPath,
458
+ },
459
+ ...(home === undefined ? {} : {home}),
460
+ }),
446
461
  })
447
462
  }
package/src/target.mjs CHANGED
@@ -1,18 +1,18 @@
1
1
  import { homedir } from 'node:os'
2
2
  import { join } from 'node:path'
3
3
 
4
- export const PRODUCT_VERSION = '0.1.1'
4
+ export const PRODUCT_VERSION = '0.2.0'
5
5
  export const RELEASE_REPOSITORY = 'deepnovacore/NovaAudioAgent'
6
6
 
7
7
  const DEFINITIONS = Object.freeze({
8
8
  'darwin-arm64': Object.freeze({
9
9
  artifact: `nova-audio-agent-${PRODUCT_VERSION}-macos-arm64-app.zip`,
10
- executable: 'Nova Audio Agent Ambient Orb.app/Contents/MacOS/Nova Audio Agent Ambient Orb',
10
+ executable: 'Nova Audio Agent Desktop.app/Contents/MacOS/Nova Audio Agent Desktop',
11
11
  archive: 'zip',
12
12
  }),
13
13
  'win32-x64': Object.freeze({
14
14
  artifact: `nova-audio-agent-${PRODUCT_VERSION}-windows-x64-portable.zip`,
15
- executable: 'Nova Audio Agent Ambient Orb.exe',
15
+ executable: 'Nova Audio Agent Desktop.exe',
16
16
  archive: 'zip',
17
17
  }),
18
18
  })
@@ -43,6 +43,7 @@ export function desktopSettingsPath({
43
43
  home = homedir(),
44
44
  environment = process.env,
45
45
  } = {}) {
46
+ // Stable storage identity shared with the renamed desktop (including encrypted settings).
46
47
  const product = 'Nova Audio Agent Ambient Orb'
47
48
  if (platform === 'darwin') {
48
49
  return join(home, 'Library', 'Application Support', product, 'ambient-orb-settings.json')