@ours.network/install 0.14.1 → 0.15.1
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 +8 -6
- package/install.mjs +125 -134
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,12 +49,14 @@ dependency on the things it installs): an ASCII banner, tasteful colour (degrade
|
|
|
49
49
|
`3051`, reserved for the Telegram connector). Applied once, then the stack is built with it.
|
|
50
50
|
3. **Four consent gates**, each paced with a clean `✓ … No problems.` line + an explicit
|
|
51
51
|
**Continue?** — never a start-twice-then-ask, never a silent failure:
|
|
52
|
-
- **1/4 ours core (the daemon)** — write config → install/start ONCE
|
|
53
|
-
re-run it reuses the running config (no re-ask) and only updates when
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
- **1/4 ours core (the daemon)** — write config → optional voice setup → install/start ONCE
|
|
53
|
+
→ boot service. On a re-run it reuses the running config (no re-ask) and only updates when
|
|
54
|
+
you say yes. Complete voice setup is kept without prompting. Missing/incomplete setup is
|
|
55
|
+
offered before the first start or pending update restart, then delegated to the canonical
|
|
56
|
+
`ours-mcp voice-setup` provider selector and hidden API-key prompt. Accepted setup owns the
|
|
57
|
+
one restart/readiness transaction; declining or already-ready setup preserves the normal
|
|
58
|
+
core lifecycle. The secret is written atomically to mode-`0600` config; a failed daemon
|
|
59
|
+
reload rolls back.
|
|
58
60
|
- **2/4 harness plugins** — the installer **drives the plugin CLIs itself**
|
|
59
61
|
(`claude plugin marketplace add …` + `claude plugin install ours@ours.network`;
|
|
60
62
|
`codex plugin marketplace add …` + `codex plugin add ours@ours-codex-marketplace`). Choosing
|
package/install.mjs
CHANGED
|
@@ -22,14 +22,14 @@ import { readFileSync, existsSync } from 'node:fs';
|
|
|
22
22
|
import { homedir, userInfo, platform as osPlatform, release as osRelease } from 'node:os';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { banner, heading, ok, info, warn, c, box, withSpinner, openTty, makeWriter, closeSync } from './lib/ui.mjs';
|
|
25
|
-
import { askLine,
|
|
25
|
+
import { askLine, askYesNo, isCancel } from './lib/prompt.mjs';
|
|
26
26
|
import {
|
|
27
27
|
suggestPort, parsePort, validateBroker, mergeConfig, parseVersion, parseStatus,
|
|
28
28
|
detectPlatform, classifyHarnessProbe, buildHandoffPrompt,
|
|
29
|
-
voiceSetupStatus,
|
|
29
|
+
voiceSetupStatus,
|
|
30
30
|
DEFAULT_PORT, resolveChannel, pkgSpec,
|
|
31
31
|
} from './lib/logic.mjs';
|
|
32
|
-
import { atomicWriteConfig
|
|
32
|
+
import { atomicWriteConfig } from './lib/config.mjs';
|
|
33
33
|
|
|
34
34
|
const NPM = process.env.OURS_NPM || 'npm';
|
|
35
35
|
// Release channel: OURS_CHANNEL=nightly installs @nightly for mcp/tg-connector/plugin
|
|
@@ -88,7 +88,12 @@ async function actSpin(label, desc, fn) {
|
|
|
88
88
|
// --- daemon probes (always safe to run — read-only) --------------------------------------------
|
|
89
89
|
const daemonVersionLine = () => (run('ours-mcp', ['--version'], { capture: true }).out.split('\n')[0] || '').trim();
|
|
90
90
|
const daemonStatusText = () => run('ours-mcp', ['status'], { capture: true }).out;
|
|
91
|
-
|
|
91
|
+
function daemonLifecycleState() {
|
|
92
|
+
const status = run('ours-mcp', ['status'], { capture: true });
|
|
93
|
+
if (!status.ok) return 'stopped';
|
|
94
|
+
return /^\s*pid:\s*\d+/m.test(status.out) ? 'managed' : 'external';
|
|
95
|
+
}
|
|
96
|
+
const daemonRunning = () => daemonLifecycleState() !== 'stopped';
|
|
92
97
|
const globalVersion = (pkg) => {
|
|
93
98
|
const ls = run(NPM, ['ls', '-g', pkg], { capture: true }).out;
|
|
94
99
|
const m = ls.match(new RegExp(pkg.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') + '@([0-9][0-9.]*)'));
|
|
@@ -331,8 +336,88 @@ async function main() {
|
|
|
331
336
|
const summary = [];
|
|
332
337
|
const record = (row) => summary.push(row);
|
|
333
338
|
|
|
339
|
+
// Voice setup is part of the core lifecycle, not a follow-up after it. On a
|
|
340
|
+
// fresh install this runs after the CLI is installed/configured but before
|
|
341
|
+
// the first daemon start. On an update it runs before any pending restart;
|
|
342
|
+
// a successful canonical voice-setup owns that one restart/readiness check.
|
|
343
|
+
// Declining, headless skipping, or an already-complete setup leaves the
|
|
344
|
+
// caller's normal start/restart lifecycle untouched.
|
|
345
|
+
const offerVoiceSetup = ({ readinessAfterStart = false, daemonState = 'stopped' } = {}) => {
|
|
346
|
+
line(heading('Voice messages'));
|
|
347
|
+
const cfgBefore = readConfigObject();
|
|
348
|
+
const probed = daemonVoiceCapability();
|
|
349
|
+
const localStatus = voiceSetupStatus(cfgBefore, process.env);
|
|
350
|
+
const voiceStatus = probed ?? localStatus;
|
|
351
|
+
if (voiceStatus.ready) {
|
|
352
|
+
line(ok(`Voice transcription is configured (${voiceStatus.provider}). API key: configured, never displayed.`));
|
|
353
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'current', note: voiceStatus.provider });
|
|
354
|
+
cont(false);
|
|
355
|
+
return { setupRan: false, restartHandled: false };
|
|
356
|
+
}
|
|
357
|
+
if (!interactive) {
|
|
358
|
+
line(info('Voice transcription is not configured. Non-interactive mode leaves it unchanged.'));
|
|
359
|
+
line(info('Run `ours-mcp voice-setup` in a terminal, or supply OURS_STT_* environment values.'));
|
|
360
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'skipped', note: 'interactive setup available on re-run' });
|
|
361
|
+
cont(false);
|
|
362
|
+
return { setupRan: false, restartHandled: false };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
line(info('Voice notes can be transcribed by a provider you choose. Audio is sent to that'));
|
|
366
|
+
line(info('provider; use a self-hosted endpoint if it must stay local. The API key is hidden.'));
|
|
367
|
+
if (!yes(' Set up voice transcription now?', true)) {
|
|
368
|
+
line(info('skipped — this will be offered again on the next installer run.'));
|
|
369
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'skipped', note: 'declined; offered again on re-run' });
|
|
370
|
+
cont(false);
|
|
371
|
+
return { setupRan: false, restartHandled: false };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
line(info('Opening the canonical ours-mcp voice setup (same command you can re-run later).'));
|
|
375
|
+
const setupArgs = ['voice-setup', ...(DRY ? ['--dry-run'] : [])];
|
|
376
|
+
const setup = run('ours-mcp', setupArgs);
|
|
377
|
+
if (!setup.ok) {
|
|
378
|
+
line(warn('Voice setup did not complete; no installer-side credential fallback was used.'));
|
|
379
|
+
line(info('Run `ours-mcp voice-setup` directly to try again.'));
|
|
380
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'failed', note: 'ours-mcp voice-setup did not complete' });
|
|
381
|
+
cont();
|
|
382
|
+
// Exit 2 is the canonical command's explicit signal that it already
|
|
383
|
+
// attempted restart/readiness and rollback. Never layer the pending
|
|
384
|
+
// installer update restart on top of that recovery transaction.
|
|
385
|
+
return { setupRan: true, restartHandled: setup.code === 2 };
|
|
386
|
+
}
|
|
387
|
+
if (DRY) {
|
|
388
|
+
line(ok('Canonical voice setup dry-run completed; no config or daemon state changed.'));
|
|
389
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'current', note: 'voice-setup dry-run' });
|
|
390
|
+
cont();
|
|
391
|
+
return { setupRan: true, restartHandled: false };
|
|
392
|
+
}
|
|
393
|
+
if (readinessAfterStart) {
|
|
394
|
+
line(ok('Voice configuration saved securely; it will be readiness-checked after the first daemon start.'));
|
|
395
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'installed', note: 'configured before first start' });
|
|
396
|
+
cont();
|
|
397
|
+
return { setupRan: true, restartHandled: false };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const verified = daemonVoiceCapability();
|
|
401
|
+
if (verified?.ready) {
|
|
402
|
+
line(ok(`Voice transcription is ready (${verified.provider}); API key remains hidden.`));
|
|
403
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'installed', note: verified.provider });
|
|
404
|
+
} else {
|
|
405
|
+
line(warn('Voice setup returned without a ready capability; inspect `ours-mcp voice-status`.'));
|
|
406
|
+
record({ key: 'voice', label: 'Voice transcription', state: 'failed', note: 'readiness check failed' });
|
|
407
|
+
}
|
|
408
|
+
cont();
|
|
409
|
+
// Exit 0 also covers config-only success for a stopped daemon or an
|
|
410
|
+
// externally launched daemon. Only a still-managed daemon proves that
|
|
411
|
+
// canonical setup owned apply + readiness and may suppress an update
|
|
412
|
+
// restart.
|
|
413
|
+
return {
|
|
414
|
+
setupRan: true,
|
|
415
|
+
restartHandled: daemonState === 'managed' && daemonLifecycleState() === 'managed',
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
|
|
334
419
|
// ============================================================================================
|
|
335
|
-
// STEP 1 / 4 — ours core (the daemon). Config-first: write config →
|
|
420
|
+
// STEP 1 / 4 — ours core (the daemon). Config-first: write config → optional voice → start ONCE.
|
|
336
421
|
// ============================================================================================
|
|
337
422
|
line(heading('1/4 — ours core (the daemon)'));
|
|
338
423
|
line(info('This is the piece that lets your agents talk to each other securely. Everything else'));
|
|
@@ -351,25 +436,55 @@ async function main() {
|
|
|
351
436
|
const patch = { port: chosenPort };
|
|
352
437
|
if (chosenBroker) patch.brokerUrl = chosenBroker;
|
|
353
438
|
await act(`write config (${configPath()}) with port ${chosenPort}${chosenBroker ? ' + custom broker' : ''}`, async () => { writeConfigPatch(patch); return { ok: true }; });
|
|
439
|
+
const voice = offerVoiceSetup({ readinessAfterStart: true });
|
|
354
440
|
const started = await act(`ours-mcp start (port ${chosenPort})`, async () => run('ours-mcp', ['start']));
|
|
355
441
|
const svc = await act('ours-mcp install-service (survives reboot)', async () => run('ours-mcp', ['install-service']));
|
|
356
442
|
if (started.ok) line(ok(`ours core ready — running on port ${chosenPort}. No problems.`));
|
|
357
443
|
else line(warn(`could not auto-start — run '${c.cyan('ours-mcp start')}' to bring it up.`));
|
|
358
444
|
if (!svc.ok && !svc.dry) line(warn(`boot-service not installed — retry '${c.cyan('ours-mcp install-service')}' later.`));
|
|
445
|
+
if (voice.setupRan && !DRY && started.ok) {
|
|
446
|
+
const verified = daemonVoiceCapability();
|
|
447
|
+
if (verified?.ready) {
|
|
448
|
+
line(ok(`Voice transcription readiness confirmed (${verified.provider}) after the first start.`));
|
|
449
|
+
} else {
|
|
450
|
+
line(warn('Voice configuration was saved, but readiness was not confirmed after start; run `ours-mcp voice-status`.'));
|
|
451
|
+
const row = summary.find((entry) => entry.key === 'voice');
|
|
452
|
+
if (row) {
|
|
453
|
+
row.state = 'failed';
|
|
454
|
+
row.note = 'readiness check failed after first start';
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
359
458
|
record({ key: 'core', label: 'ours core (daemon)', state: started.ok ? 'installed' : 'failed', version: parseVersion(daemonVersionLine()), note: 'starts on boot' });
|
|
360
459
|
} else {
|
|
361
460
|
// Installed: offer an update; never re-ask config; reuse the running port everywhere.
|
|
362
|
-
const
|
|
461
|
+
const daemonState = daemonLifecycleState();
|
|
462
|
+
const running = daemonState !== 'stopped';
|
|
363
463
|
const upd = yes(` ours core is installed (${before || '?'}) — check for an update now?`, false);
|
|
464
|
+
let pendingUpdateRestart = false;
|
|
465
|
+
let after = before;
|
|
364
466
|
if (upd) {
|
|
365
467
|
await actSpin(`updating ${spec('mcp')}…`, `npm i -g ${spec('mcp')}`, () => runAsync(NPM, ['i', '-g', spec('mcp')]));
|
|
366
|
-
|
|
367
|
-
|
|
468
|
+
after = parseVersion(daemonVersionLine());
|
|
469
|
+
pendingUpdateRestart = !!(before && after && before !== after);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const voice = offerVoiceSetup({ daemonState });
|
|
473
|
+
if (pendingUpdateRestart && !voice.restartHandled) {
|
|
474
|
+
const restartState = daemonLifecycleState();
|
|
475
|
+
if (restartState === 'external') {
|
|
476
|
+
line(warn(`ours core updated (v${before} → v${after}); restart its external launcher to load the update.`));
|
|
477
|
+
} else {
|
|
368
478
|
await act(`ours-mcp restart (now v${after})`, async () => { if (!run('ours-mcp', ['restart']).ok) run('ours-mcp', ['start']); return { ok: true }; });
|
|
369
479
|
line(ok(`ours core updated (v${before} → v${after}) and restarted. No problems.`));
|
|
370
|
-
} else {
|
|
371
|
-
line(ok(`ours core already current${after ? ` (v${after})` : ''} — nothing to change.`));
|
|
372
480
|
}
|
|
481
|
+
} else if (pendingUpdateRestart) {
|
|
482
|
+
const voiceFailed = summary.find((entry) => entry.key === 'voice')?.state === 'failed';
|
|
483
|
+
line(voiceFailed
|
|
484
|
+
? warn(`ours core updated (v${before} → v${after}); voice setup handled restart/rollback but did not change voice settings.`)
|
|
485
|
+
: ok(`ours core updated (v${before} → v${after}); voice setup performed the required restart and readiness check.`));
|
|
486
|
+
} else if (upd) {
|
|
487
|
+
line(ok(`ours core already current${after ? ` (v${after})` : ''} — nothing to change.`));
|
|
373
488
|
} else {
|
|
374
489
|
line(ok(`ours core ready — running on port ${chosenPort}${running ? '' : ' (start with ours-mcp start)'}. No problems.`));
|
|
375
490
|
}
|
|
@@ -377,130 +492,6 @@ async function main() {
|
|
|
377
492
|
}
|
|
378
493
|
cont();
|
|
379
494
|
|
|
380
|
-
// ============================================================================================
|
|
381
|
-
// Voice-message transcription — capability-based and safe to re-run. A complete setup is kept
|
|
382
|
-
// without prompting. An incomplete setup is offered every interactive run, including updates.
|
|
383
|
-
// Headless/CI never blocks or invents a provider/key. Secrets are read without echo and the
|
|
384
|
-
// config transaction is atomic + 0600; a failed daemon restart restores the previous file.
|
|
385
|
-
// ============================================================================================
|
|
386
|
-
if (summary.some((r) => r.key === 'core' && (r.state === 'installed' || r.state === 'current'))) {
|
|
387
|
-
line(heading('Voice messages'));
|
|
388
|
-
const cfgBefore = readConfigObject();
|
|
389
|
-
const probed = daemonVoiceCapability();
|
|
390
|
-
const localStatus = voiceSetupStatus(cfgBefore, process.env);
|
|
391
|
-
const voiceStatus = probed ?? localStatus;
|
|
392
|
-
if (voiceStatus.ready) {
|
|
393
|
-
line(ok(`Voice transcription is configured (${voiceStatus.provider}). API key: configured, never displayed.`));
|
|
394
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'current', note: voiceStatus.provider });
|
|
395
|
-
cont(false);
|
|
396
|
-
} else if (!interactive) {
|
|
397
|
-
line(info('Voice transcription is not configured. Non-interactive mode leaves it unchanged.'));
|
|
398
|
-
line(info('Run ours-install in a terminal to enter the provider key with masked input.'));
|
|
399
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'skipped', note: 'interactive setup available on re-run' });
|
|
400
|
-
cont(false);
|
|
401
|
-
} else {
|
|
402
|
-
line(info('Voice notes can be transcribed by a provider you choose. Audio is sent to that'));
|
|
403
|
-
line(info('provider; use a self-hosted endpoint if it must stay local. The API key is masked.'));
|
|
404
|
-
const configure = yes(' Set up voice transcription now?', true);
|
|
405
|
-
if (!configure) {
|
|
406
|
-
line(info('skipped — this will be offered again on the next installer run.'));
|
|
407
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'skipped', note: 'declined; offered again on re-run' });
|
|
408
|
-
cont(false);
|
|
409
|
-
} else {
|
|
410
|
-
const fileVoice = cfgBefore?.stt && typeof cfgBefore.stt === 'object' ? cfgBefore.stt : {};
|
|
411
|
-
const providerDefault = String(process.env.OURS_STT_PROVIDER || fileVoice.provider || '').trim().toLowerCase();
|
|
412
|
-
const provider = ask(
|
|
413
|
-
` Provider (${VOICE_PROVIDERS.join(' / ')})${providerDefault ? ` [${providerDefault}]` : ''}: `,
|
|
414
|
-
providerDefault,
|
|
415
|
-
).trim().toLowerCase();
|
|
416
|
-
let setupError = '';
|
|
417
|
-
if (!VOICE_PROVIDERS.includes(provider)) setupError = `choose one of: ${VOICE_PROVIDERS.join(', ')}`;
|
|
418
|
-
|
|
419
|
-
let model = String(process.env.OURS_STT_MODEL || fileVoice.model || '').trim();
|
|
420
|
-
let baseUrl = String(process.env.OURS_STT_BASE_URL || fileVoice.baseUrl || '').trim();
|
|
421
|
-
let customUrl = String(fileVoice.custom?.url || '').trim();
|
|
422
|
-
if (!setupError && provider === 'openai-compatible') {
|
|
423
|
-
baseUrl = ask(` Provider /v1 base URL${baseUrl ? ` [${baseUrl}]` : ''}: `, baseUrl).trim();
|
|
424
|
-
model = ask(` Model name (sent verbatim)${model ? ` [${model}]` : ''}: `, model).trim();
|
|
425
|
-
} else if (!setupError && provider === 'elevenlabs') {
|
|
426
|
-
model = ask(` ElevenLabs model id${model ? ` [${model}]` : ''}: `, model).trim();
|
|
427
|
-
baseUrl = ask(` Custom base URL (Enter for provider default)${baseUrl ? ` [${baseUrl}]` : ''}: `, baseUrl).trim();
|
|
428
|
-
} else if (!setupError && provider === 'deepgram') {
|
|
429
|
-
model = ask(` Model (optional; Enter for provider default)${model ? ` [${model}]` : ''}: `, model).trim();
|
|
430
|
-
baseUrl = ask(` Custom base URL (optional)${baseUrl ? ` [${baseUrl}]` : ''}: `, baseUrl).trim();
|
|
431
|
-
} else if (!setupError && provider === 'custom') {
|
|
432
|
-
customUrl = ask(` Full transcription endpoint URL${customUrl ? ` [${customUrl}]` : ''}: `, customUrl).trim();
|
|
433
|
-
model = ask(` Model (optional unless URL contains {model})${model ? ` [${model}]` : ''}: `, model).trim();
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const envHasKey = !!process.env.OURS_STT_API_KEY?.trim();
|
|
437
|
-
let keyToPersist = String(fileVoice.apiKey || '').trim();
|
|
438
|
-
if (!setupError && !envHasKey) {
|
|
439
|
-
const keyPrompt = keyToPersist
|
|
440
|
-
? ' Provider API key [configured; Enter keeps it]: '
|
|
441
|
-
: ' Provider API key (input hidden): ';
|
|
442
|
-
const entered = askSecret(write, ttyFd, keyPrompt, keyToPersist);
|
|
443
|
-
if (entered === null) setupError = 'secure hidden input is unavailable on this terminal';
|
|
444
|
-
else {
|
|
445
|
-
const valid = validateVoiceSecret(entered);
|
|
446
|
-
if (!valid.ok) setupError = valid.reason;
|
|
447
|
-
else keyToPersist = valid.value;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const nextStt = {
|
|
452
|
-
...fileVoice,
|
|
453
|
-
provider,
|
|
454
|
-
...(keyToPersist ? { apiKey: keyToPersist } : {}),
|
|
455
|
-
...(model ? { model } : {}),
|
|
456
|
-
...(baseUrl ? { baseUrl } : {}),
|
|
457
|
-
...(provider === 'custom' && customUrl
|
|
458
|
-
? { custom: { ...(fileVoice.custom ?? {}), url: customUrl } }
|
|
459
|
-
: {}),
|
|
460
|
-
};
|
|
461
|
-
const intended = voiceSetupStatus({ ...cfgBefore, stt: nextStt }, process.env);
|
|
462
|
-
if (!setupError && !intended.ready) setupError = intended.reason;
|
|
463
|
-
|
|
464
|
-
if (setupError) {
|
|
465
|
-
line(warn(`Voice setup was not saved: ${redactSensitive(setupError, [keyToPersist])}.`));
|
|
466
|
-
line(info('No existing configuration was changed; re-run ours-install to try again.'));
|
|
467
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'failed', note: 'incomplete setup; config unchanged' });
|
|
468
|
-
cont();
|
|
469
|
-
} else if (DRY) {
|
|
470
|
-
line(' ' + c.dim(`[dry-run] would: atomically update ${configPath()} (mode 0600) and restart ours-mcp`));
|
|
471
|
-
line(ok(`Voice transcription would be configured (${provider}); API key stays hidden.`));
|
|
472
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'installed', note: `${provider} (dry-run)` });
|
|
473
|
-
cont();
|
|
474
|
-
} else {
|
|
475
|
-
const updated = transactionalConfigUpdate(
|
|
476
|
-
configPath(),
|
|
477
|
-
mergeConfig(cfgBefore, { stt: nextStt }),
|
|
478
|
-
() => {
|
|
479
|
-
const r = run('ours-mcp', ['restart'], { capture: true });
|
|
480
|
-
if (!r.ok) return { ok: false, error: r.err || r.out };
|
|
481
|
-
const verified = daemonVoiceCapability();
|
|
482
|
-
return verified && !verified.ready
|
|
483
|
-
? { ok: false, error: verified.reason || 'voice capability remained incomplete after restart' }
|
|
484
|
-
: { ok: true };
|
|
485
|
-
},
|
|
486
|
-
);
|
|
487
|
-
if (!updated.ok && updated.stage === 'write') {
|
|
488
|
-
line(warn(`Could not save voice setup: ${redactSensitive(updated.error instanceof Error ? updated.error.message : String(updated.error), [keyToPersist])}.`));
|
|
489
|
-
line(info('The prior config is intact; no restart was attempted.'));
|
|
490
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'failed', note: 'config write failed; prior config intact' });
|
|
491
|
-
} else if (!updated.ok) {
|
|
492
|
-
line(warn(`Daemon restart failed; ${updated.rolledBack ? 'restored the prior config' : 'automatic rollback also failed — inspect the config before restarting'}.`));
|
|
493
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'failed', note: updated.rolledBack ? 'restart failed; rolled back' : 'restart and rollback failed' });
|
|
494
|
-
} else {
|
|
495
|
-
line(ok(`Voice transcription configured (${provider}) and daemon restarted. API key saved in mode-0600 config.`));
|
|
496
|
-
record({ key: 'voice', label: 'Voice transcription', state: 'installed', note: provider });
|
|
497
|
-
}
|
|
498
|
-
cont();
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
495
|
// ============================================================================================
|
|
505
496
|
// Human identity — created DURING install, right after the daemon is confirmed reachable (the
|
|
506
497
|
// owner change that supersedes "defer to the hand-off"). `ours-mcp create-root` is the internal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/install",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The unified ours.network stack installer (ours-install): one guided ~3-minute flow for ours core (the daemon) + the harness plugins (Claude Code / Codex) + ours-fleet + the Telegram connector, then a single copy-paste hand-off prompt. Self-contained (Node built-ins only); run as `ours-install` or via curl|bash (install.sh).",
|
|
6
6
|
"type": "module",
|