@pellux/goodvibes-tui 1.25.0 → 1.27.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/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +9605 -3119
- package/package.json +9 -7
- package/scripts/postinstall.js +57 -1
- package/src/audio/activation-sound.ts +117 -0
- package/src/audio/capture.ts +112 -0
- package/src/audio/onnxruntime-assets.d.ts +24 -0
- package/src/audio/voice-capture-wiring.ts +119 -0
- package/src/audio/voice-input-session.ts +181 -0
- package/src/audio/wake-inference.ts +182 -0
- package/src/audio/wake-runtime.ts +341 -0
- package/src/cli/bundle-command.ts +4 -1
- package/src/cli/help.ts +10 -0
- package/src/cli/management-utils.ts +4 -1
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +10 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/voice-capture-status.ts +53 -0
- package/src/core/voice-stt-gateway.ts +75 -0
- package/src/core/wake-provision-runner.ts +86 -0
- package/src/core/wake-provision-status.ts +170 -0
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +86 -5
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/provision-wake-model.ts +81 -0
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +332 -0
- package/src/daemon/send/composition.ts +99 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/input/command-registry.ts +6 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/experience-runtime.ts +37 -3
- package/src/input/commands/local-provider-runtime.ts +9 -1
- package/src/input/commands/operator-runtime.ts +6 -1
- package/src/input/commands/payment-card-intake.ts +200 -0
- package/src/input/commands/profile-render.ts +250 -0
- package/src/input/commands/profile-runtime.ts +362 -0
- package/src/input/commands/profile-types.ts +381 -0
- package/src/input/commands.ts +7 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/handler-shortcuts.ts +8 -0
- package/src/input/keybindings.ts +10 -1
- package/src/input/onboarding/onboarding-feature-units.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
- package/src/input/payments-config.ts +167 -0
- package/src/input/provider-key-intake.ts +10 -2
- package/src/input/settings-modal-commit.ts +146 -0
- package/src/input/settings-modal-data.ts +30 -0
- package/src/input/settings-modal-reset.ts +6 -2
- package/src/input/settings-modal-secrets.ts +9 -2
- package/src/input/settings-modal-types.ts +30 -2
- package/src/input/settings-modal.ts +27 -107
- package/src/main.ts +12 -14
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/process-indicator.ts +66 -0
- package/src/renderer/settings-modal-helpers.ts +8 -2
- package/src/renderer/settings-modal.ts +16 -1
- package/src/renderer/shell-surface.ts +22 -3
- package/src/runtime/bootstrap-core.ts +7 -7
- package/src/runtime/bootstrap.ts +3 -3
- package/src/runtime/device-posture-composition.ts +110 -0
- package/src/runtime/disposal-wiring.ts +16 -0
- package/src/runtime/install-self-check-startup.ts +23 -0
- package/src/runtime/onboarding/types.ts +6 -1
- package/src/runtime/path-shadow-startup.ts +243 -0
- package/src/runtime/path-shadow.ts +418 -0
- package/src/runtime/reachability-notice.ts +100 -0
- package/src/runtime/runtime-services-types.ts +260 -0
- package/src/runtime/services.ts +95 -209
- package/src/runtime/voice-setup-services.ts +50 -4
- package/src/shell/voice-capture-shell.ts +69 -0
- package/src/verification/verification-ledger.ts +163 -24
- package/src/version.ts +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profile-types.ts
|
|
3
|
+
*
|
|
4
|
+
* Response shapes for the owner-profile control-plane verbs (`profile.*`), and
|
|
5
|
+
* the runtime checks that turn an untyped daemon response into one of them.
|
|
6
|
+
*
|
|
7
|
+
* ## The shapes come from the contract, not from a copy of it
|
|
8
|
+
*
|
|
9
|
+
* Every type below is derived from `OperatorMethodOutput<'profile.…'>` — the
|
|
10
|
+
* types generated from the SDK's own output schemas
|
|
11
|
+
* (`platform/control-plane/method-catalog-owner-profile.ts`). Nothing here
|
|
12
|
+
* restates a field name, so this file cannot drift from the contract: if a
|
|
13
|
+
* verb's shape changes, the checkers stop compiling against it.
|
|
14
|
+
*
|
|
15
|
+
* ## Why the responses are still checked at runtime
|
|
16
|
+
*
|
|
17
|
+
* A generated type is a compile-time claim about what the wire *should* carry,
|
|
18
|
+
* and nothing enforces it at runtime here. The operator client's
|
|
19
|
+
* `validateResponses` defaults on, but it only validates method ids with a
|
|
20
|
+
* matching exported Zod schema and `buildSchemaRegistry` silently skips the
|
|
21
|
+
* rest. Measured against the installed contracts package: 5 of 452 method ids
|
|
22
|
+
* have one, and no `profile.*` verb is among them. So response validation is
|
|
23
|
+
* not a safety net this command can lean on — for these verbs or for most
|
|
24
|
+
* others.
|
|
25
|
+
*
|
|
26
|
+
* So a daemon on a different version — or a 200 from something that is not this
|
|
27
|
+
* daemon at all — would otherwise reach a renderer and throw on a missing array,
|
|
28
|
+
* which in a slash command means a stack trace instead of an answer. Each `toX`
|
|
29
|
+
* function does the cast to `Record<string, unknown>` in ONE place, checks every
|
|
30
|
+
* property a renderer will read, and returns {@link MALFORMED} when anything is
|
|
31
|
+
* off. The command then prints an honest "this build does not recognise that
|
|
32
|
+
* response" line.
|
|
33
|
+
*
|
|
34
|
+
* A malformed *member* fails the whole response rather than being filtered out.
|
|
35
|
+
* Silently dropping a section or a line the checker did not recognise would be
|
|
36
|
+
* the same failure §4.4 forbids in the parser: content disappearing without
|
|
37
|
+
* anyone being told.
|
|
38
|
+
*/
|
|
39
|
+
import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk';
|
|
40
|
+
|
|
41
|
+
/** Returned by every checker when the response does not match the contract. */
|
|
42
|
+
export const MALFORMED: unique symbol = Symbol('malformed-profile-response');
|
|
43
|
+
export type Checked<T> = T | typeof MALFORMED;
|
|
44
|
+
|
|
45
|
+
/** The verbs this command calls. Typing the ids is what types the inputs. */
|
|
46
|
+
export type ProfileReadVerb = 'profile.read' | 'profile.provenance' | 'profile.status';
|
|
47
|
+
export type ProfileWriteVerb = 'profile.set' | 'profile.append' | 'profile.forget' | 'profile.undo';
|
|
48
|
+
export type ProfileVerb = ProfileReadVerb | ProfileWriteVerb;
|
|
49
|
+
|
|
50
|
+
/** The contract's own input shape for a verb, so a wrong field name will not compile. */
|
|
51
|
+
export type ProfileInput<TVerb extends ProfileVerb> = OperatorMethodInput<TVerb>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every key of every member of a union. `keyof` on a union yields only the keys
|
|
55
|
+
* the members SHARE, so a union target would be checked against that
|
|
56
|
+
* intersection and would wave through a key belonging to a sibling verb.
|
|
57
|
+
*/
|
|
58
|
+
type UnionKeys<T> = T extends unknown ? keyof T : never;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reject a key the contract does not declare, including on a body that is not a
|
|
62
|
+
* fresh object literal.
|
|
63
|
+
*
|
|
64
|
+
* DO NOT "simplify" this away to plain parameter typing. It is load-bearing,
|
|
65
|
+
* not a belt-and-braces backstop, and the measurements say which cases it
|
|
66
|
+
* carries alone. Four ways a stale key can arrive, all compiled against this
|
|
67
|
+
* repo's TypeScript with a `profile.forget`-shaped input:
|
|
68
|
+
*
|
|
69
|
+
* 1. written inline in a fresh literal — parameter typing already rejects
|
|
70
|
+
* 2. written inline BESIDE a spread — parameter typing already rejects
|
|
71
|
+
* 3. body built as a variable first — SLIPS PAST parameter typing
|
|
72
|
+
* 4. carried in by the spread source's type — SLIPS PAST parameter typing
|
|
73
|
+
*
|
|
74
|
+
* A spread literal is not uniformly unchecked, which is the easy thing to get
|
|
75
|
+
* wrong: anything written inline in one is still checked normally, and only what
|
|
76
|
+
* arrives THROUGH the spread escapes, because it belongs to the source's type
|
|
77
|
+
* rather than to the literal. Rows 3 and 4 are the two nothing else catches, and
|
|
78
|
+
* this guard rejects both with TS2345 — measured, not reasoned about.
|
|
79
|
+
*
|
|
80
|
+
* That is not hypothetical: `profile.forget` really did retire `lineIndex`, and
|
|
81
|
+
* a body built one line earlier would have carried it silently past a correctly
|
|
82
|
+
* typed parameter. Seeding inline literals proves only the cases someone thought
|
|
83
|
+
* to seed; this closes the shape.
|
|
84
|
+
*
|
|
85
|
+
* There is a second reason it earns its place here. The SDK's `invoke` is
|
|
86
|
+
* OVERLOADED — a typed signature plus a loose
|
|
87
|
+
* `invoke<T = unknown>(methodId: string, input?: Record<string, unknown>)`. At a
|
|
88
|
+
* bare call site a body the typed overload rejects does not raise an
|
|
89
|
+
* excess-property error at all; overload resolution simply falls through to the
|
|
90
|
+
* loose one and the result degrades to `unknown`. Any error is then incidental,
|
|
91
|
+
* downstream, and disappears entirely if the caller ignores or casts the result.
|
|
92
|
+
* `ProfileInvoke` is a single generic signature with no such fallback, so a bad
|
|
93
|
+
* body fails here, at the call, naming the offending key.
|
|
94
|
+
*
|
|
95
|
+
* Intersecting the parameter with this maps every undeclared key to `never`, so
|
|
96
|
+
* no real value satisfies it however the object was built. Distributed over the
|
|
97
|
+
* target union via {@link UnionKeys} so each member is checked rather than only
|
|
98
|
+
* the keys they have in common.
|
|
99
|
+
*/
|
|
100
|
+
export type NoExcessKeys<TBody, TShape> = Record<Exclude<keyof TBody, UnionKeys<TShape>>, never>;
|
|
101
|
+
|
|
102
|
+
/** A body that conforms to a verb's declared input and carries nothing extra. */
|
|
103
|
+
export type ExactProfileInput<TVerb extends ProfileVerb, TBody> =
|
|
104
|
+
TBody & NoExcessKeys<TBody, ProfileInput<TVerb>>;
|
|
105
|
+
|
|
106
|
+
/** What `profile.read` answers: the whole document, by section. */
|
|
107
|
+
export type ProfileDocumentView = OperatorMethodOutput<'profile.read'>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load state — what `profile.status` answers, and the `state` half of a read.
|
|
111
|
+
*
|
|
112
|
+
* `kind` is `loaded` | `unavailable` | `disabled`. The counts belong to
|
|
113
|
+
* `loaded` and `reason` to `unavailable`. There is no value property anywhere
|
|
114
|
+
* in this shape, which is what makes the status output safe to show in a
|
|
115
|
+
* diagnostic context (§11.3).
|
|
116
|
+
*/
|
|
117
|
+
export type ProfileStateView = OperatorMethodOutput<'profile.status'>;
|
|
118
|
+
|
|
119
|
+
/** What `profile.provenance` answers for one field. */
|
|
120
|
+
export type ProfileProvenanceReportView = OperatorMethodOutput<'profile.provenance'>;
|
|
121
|
+
|
|
122
|
+
/** What every write verb answers. `ok: false` always carries a reason. */
|
|
123
|
+
export type ProfileWriteResultView = OperatorMethodOutput<'profile.set'>;
|
|
124
|
+
|
|
125
|
+
/** One `## ` section, with the tier its content belongs to. */
|
|
126
|
+
export type ProfileSectionView = ProfileDocumentView['sections'][number];
|
|
127
|
+
/** One mechanical field. `valid: false` still carries the value — see §4.3. */
|
|
128
|
+
export type ProfileFieldView = ProfileSectionView['fields'][number];
|
|
129
|
+
/** One prose line, preserved as written. */
|
|
130
|
+
export type ProfileLineView = ProfileSectionView['prose'][number];
|
|
131
|
+
/** Where a line came from: which surface, when, and the owner's exact words. */
|
|
132
|
+
export type ProfileProvenanceView = NonNullable<ProfileFieldView['provenance']>;
|
|
133
|
+
/** A mechanical value that did not validate, and why. Never fails the file. */
|
|
134
|
+
export type ProfileInvalidFieldView = NonNullable<ProfileStateView['invalidFields']>[number];
|
|
135
|
+
/** A retained `<!-- was: … -->` predecessor, so a wrong correction is recoverable. */
|
|
136
|
+
export type ProfileSupersededView = ProfileProvenanceReportView['superseded'][number];
|
|
137
|
+
/** One thing a write did. Names the field; never repeats the value. */
|
|
138
|
+
export type ProfileChangeView = ProfileWriteResultView['changes'][number];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compile-time proof of two things this file assumes: that all four write verbs
|
|
142
|
+
* really do answer one shape, and that `profile.status` really is the same
|
|
143
|
+
* `state` a read carries.
|
|
144
|
+
*
|
|
145
|
+
* One write checker serves four verbs and `toProfileState` checks both the
|
|
146
|
+
* status response and a read's `state`. If the contract ever splits them,
|
|
147
|
+
* `Exact` collapses to `never` and this assignment stops compiling, rather than
|
|
148
|
+
* the difference going unnoticed until a response renders wrong.
|
|
149
|
+
*/
|
|
150
|
+
type Exact<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
|
|
151
|
+
export const PROFILE_CONTRACT_SHAPES_AGREE: Exact<OperatorMethodOutput<'profile.append'>, ProfileWriteResultView>
|
|
152
|
+
& Exact<OperatorMethodOutput<'profile.forget'>, ProfileWriteResultView>
|
|
153
|
+
& Exact<OperatorMethodOutput<'profile.undo'>, ProfileWriteResultView>
|
|
154
|
+
& Exact<ProfileDocumentView['state'], ProfileStateView> = true;
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Primitive checks
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/** The single place an untyped response is narrowed to an indexable object. */
|
|
161
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
162
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
163
|
+
return value as unknown as Record<string, unknown>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isString(value: unknown): value is string {
|
|
167
|
+
return typeof value === 'string';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isBoolean(value: unknown): value is boolean {
|
|
171
|
+
return typeof value === 'boolean';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
175
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Absent and null both mean "not supplied"; anything else must be a string. */
|
|
179
|
+
function optionalString(value: unknown): Checked<string | undefined> {
|
|
180
|
+
if (value === undefined || value === null) return undefined;
|
|
181
|
+
return isString(value) ? value : MALFORMED;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function optionalBoolean(value: unknown): Checked<boolean | undefined> {
|
|
185
|
+
if (value === undefined || value === null) return undefined;
|
|
186
|
+
return isBoolean(value) ? value : MALFORMED;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function optionalNumber(value: unknown): Checked<number | undefined> {
|
|
190
|
+
if (value === undefined || value === null) return undefined;
|
|
191
|
+
return isFiniteNumber(value) ? value : MALFORMED;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** An array whose every member checks out, or MALFORMED. Never a filtered subset. */
|
|
195
|
+
function checkedArray<T>(value: unknown, check: (entry: unknown) => Checked<T>): Checked<T[]> {
|
|
196
|
+
if (!Array.isArray(value)) return MALFORMED;
|
|
197
|
+
const out: T[] = [];
|
|
198
|
+
for (const entry of value) {
|
|
199
|
+
const parsed = check(entry);
|
|
200
|
+
if (parsed === MALFORMED) return MALFORMED;
|
|
201
|
+
out.push(parsed);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkedStringArray(value: unknown): Checked<string[]> {
|
|
207
|
+
return checkedArray(value, (entry) => (isString(entry) ? entry : MALFORMED));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Response checks
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
function toProvenance(value: unknown): Checked<ProfileProvenanceView | undefined> {
|
|
215
|
+
if (value === undefined || value === null) return undefined;
|
|
216
|
+
const record = asRecord(value);
|
|
217
|
+
if (!record) return MALFORMED;
|
|
218
|
+
const { surface, date, said } = record;
|
|
219
|
+
if (!isString(surface) || !isString(date) || !isString(said)) return MALFORMED;
|
|
220
|
+
return { surface, date, said };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toLine(value: unknown): Checked<ProfileLineView> {
|
|
224
|
+
const record = asRecord(value);
|
|
225
|
+
if (!record) return MALFORMED;
|
|
226
|
+
const { lineIndex, section, text } = record;
|
|
227
|
+
if (!isFiniteNumber(lineIndex) || !isString(section) || !isString(text)) return MALFORMED;
|
|
228
|
+
const provenance = toProvenance(record.provenance);
|
|
229
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
230
|
+
return { lineIndex, section, text, ...(provenance ? { provenance } : {}) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function toField(value: unknown): Checked<ProfileFieldView> {
|
|
234
|
+
const record = asRecord(value);
|
|
235
|
+
if (!record) return MALFORMED;
|
|
236
|
+
const { fieldId, label, value: fieldValue, valid } = record;
|
|
237
|
+
if (!isString(fieldId) || !isString(label) || !isString(fieldValue) || !isBoolean(valid)) return MALFORMED;
|
|
238
|
+
const invalidReason = optionalString(record.invalidReason);
|
|
239
|
+
if (invalidReason === MALFORMED) return MALFORMED;
|
|
240
|
+
const provenance = toProvenance(record.provenance);
|
|
241
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
242
|
+
return {
|
|
243
|
+
fieldId,
|
|
244
|
+
label,
|
|
245
|
+
value: fieldValue,
|
|
246
|
+
valid,
|
|
247
|
+
...(invalidReason === undefined ? {} : { invalidReason }),
|
|
248
|
+
...(provenance ? { provenance } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function toSection(value: unknown): Checked<ProfileSectionView> {
|
|
253
|
+
const record = asRecord(value);
|
|
254
|
+
if (!record) return MALFORMED;
|
|
255
|
+
const { heading, tier } = record;
|
|
256
|
+
if (!isString(heading) || !isString(tier)) return MALFORMED;
|
|
257
|
+
const fields = checkedArray(record.fields, toField);
|
|
258
|
+
if (fields === MALFORMED) return MALFORMED;
|
|
259
|
+
const prose = checkedArray(record.prose, toLine);
|
|
260
|
+
if (prose === MALFORMED) return MALFORMED;
|
|
261
|
+
return { heading, tier, fields, prose };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toInvalidField(value: unknown): Checked<ProfileInvalidFieldView> {
|
|
265
|
+
const record = asRecord(value);
|
|
266
|
+
if (!record) return MALFORMED;
|
|
267
|
+
const { fieldId, reason } = record;
|
|
268
|
+
if (!isString(fieldId) || !isString(reason)) return MALFORMED;
|
|
269
|
+
return { fieldId, reason };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `profile.status`, and the `state` half of `profile.read`. */
|
|
273
|
+
export function toProfileState(value: unknown): Checked<ProfileStateView> {
|
|
274
|
+
const record = asRecord(value);
|
|
275
|
+
if (!record) return MALFORMED;
|
|
276
|
+
const { kind, path } = record;
|
|
277
|
+
if (!isString(kind) || !isString(path)) return MALFORMED;
|
|
278
|
+
const exists = optionalBoolean(record.exists);
|
|
279
|
+
if (exists === MALFORMED) return MALFORMED;
|
|
280
|
+
const lineCount = optionalNumber(record.lineCount);
|
|
281
|
+
if (lineCount === MALFORMED) return MALFORMED;
|
|
282
|
+
const fieldCount = optionalNumber(record.fieldCount);
|
|
283
|
+
if (fieldCount === MALFORMED) return MALFORMED;
|
|
284
|
+
const proseLineCount = optionalNumber(record.proseLineCount);
|
|
285
|
+
if (proseLineCount === MALFORMED) return MALFORMED;
|
|
286
|
+
const reason = optionalString(record.reason);
|
|
287
|
+
if (reason === MALFORMED) return MALFORMED;
|
|
288
|
+
const sections = record.sections === undefined || record.sections === null
|
|
289
|
+
? undefined
|
|
290
|
+
: checkedStringArray(record.sections);
|
|
291
|
+
if (sections === MALFORMED) return MALFORMED;
|
|
292
|
+
const invalidFields = record.invalidFields === undefined || record.invalidFields === null
|
|
293
|
+
? undefined
|
|
294
|
+
: checkedArray(record.invalidFields, toInvalidField);
|
|
295
|
+
if (invalidFields === MALFORMED) return MALFORMED;
|
|
296
|
+
return {
|
|
297
|
+
kind,
|
|
298
|
+
path,
|
|
299
|
+
...(exists === undefined ? {} : { exists }),
|
|
300
|
+
...(lineCount === undefined ? {} : { lineCount }),
|
|
301
|
+
...(fieldCount === undefined ? {} : { fieldCount }),
|
|
302
|
+
...(proseLineCount === undefined ? {} : { proseLineCount }),
|
|
303
|
+
...(sections === undefined ? {} : { sections }),
|
|
304
|
+
...(invalidFields === undefined ? {} : { invalidFields }),
|
|
305
|
+
...(reason === undefined ? {} : { reason }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** `profile.read`. */
|
|
310
|
+
export function toProfileDocument(value: unknown): Checked<ProfileDocumentView> {
|
|
311
|
+
const record = asRecord(value);
|
|
312
|
+
if (!record) return MALFORMED;
|
|
313
|
+
const state = toProfileState(record.state);
|
|
314
|
+
if (state === MALFORMED) return MALFORMED;
|
|
315
|
+
const sections = checkedArray(record.sections, toSection);
|
|
316
|
+
if (sections === MALFORMED) return MALFORMED;
|
|
317
|
+
return { state, sections };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toSuperseded(value: unknown): Checked<ProfileSupersededView> {
|
|
321
|
+
const record = asRecord(value);
|
|
322
|
+
if (!record) return MALFORMED;
|
|
323
|
+
const { lineIndex, fieldId, section, text, value: supersededValue, supersededOn, previousLine } = record;
|
|
324
|
+
if (
|
|
325
|
+
!isFiniteNumber(lineIndex)
|
|
326
|
+
|| !isString(fieldId)
|
|
327
|
+
|| !isString(section)
|
|
328
|
+
|| !isString(text)
|
|
329
|
+
|| !isString(supersededValue)
|
|
330
|
+
|| !isString(supersededOn)
|
|
331
|
+
|| !isString(previousLine)
|
|
332
|
+
) return MALFORMED;
|
|
333
|
+
const provenance = toProvenance(record.provenance);
|
|
334
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
335
|
+
return {
|
|
336
|
+
lineIndex,
|
|
337
|
+
fieldId,
|
|
338
|
+
section,
|
|
339
|
+
text,
|
|
340
|
+
value: supersededValue,
|
|
341
|
+
supersededOn,
|
|
342
|
+
previousLine,
|
|
343
|
+
...(provenance ? { provenance } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** `profile.provenance`. */
|
|
348
|
+
export function toProfileProvenanceReport(value: unknown): Checked<ProfileProvenanceReportView> {
|
|
349
|
+
const record = asRecord(value);
|
|
350
|
+
if (!record) return MALFORMED;
|
|
351
|
+
const { fieldId, present, handEdited } = record;
|
|
352
|
+
if (!isString(fieldId) || !isBoolean(present) || !isBoolean(handEdited)) return MALFORMED;
|
|
353
|
+
const provenance = toProvenance(record.provenance);
|
|
354
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
355
|
+
const superseded = checkedArray(record.superseded, toSuperseded);
|
|
356
|
+
if (superseded === MALFORMED) return MALFORMED;
|
|
357
|
+
return { fieldId, present, handEdited, ...(provenance ? { provenance } : {}), superseded };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function toChange(value: unknown): Checked<ProfileChangeView> {
|
|
361
|
+
const record = asRecord(value);
|
|
362
|
+
if (!record) return MALFORMED;
|
|
363
|
+
const { kind, section, label, superseded } = record;
|
|
364
|
+
if (!isString(kind) || !isString(section) || !isString(label) || !isBoolean(superseded)) return MALFORMED;
|
|
365
|
+
const fieldId = optionalString(record.fieldId);
|
|
366
|
+
if (fieldId === MALFORMED) return MALFORMED;
|
|
367
|
+
return { kind, section, label, superseded, ...(fieldId === undefined ? {} : { fieldId }) };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** `profile.set`, `profile.append`, `profile.forget`, `profile.undo`. */
|
|
371
|
+
export function toProfileWriteResult(value: unknown): Checked<ProfileWriteResultView> {
|
|
372
|
+
const record = asRecord(value);
|
|
373
|
+
if (!record) return MALFORMED;
|
|
374
|
+
const { ok, disclosure } = record;
|
|
375
|
+
if (!isBoolean(ok) || !isString(disclosure)) return MALFORMED;
|
|
376
|
+
const reason = optionalString(record.reason);
|
|
377
|
+
if (reason === MALFORMED) return MALFORMED;
|
|
378
|
+
const changes = checkedArray(record.changes, toChange);
|
|
379
|
+
if (changes === MALFORMED) return MALFORMED;
|
|
380
|
+
return { ok, disclosure, changes, ...(reason === undefined ? {} : { reason }) };
|
|
381
|
+
}
|
package/src/input/commands.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { registerConfigCommand } from './commands/config.ts';
|
|
|
11
11
|
import { registerDiscoveryRuntimeCommands } from './commands/discovery-runtime.ts';
|
|
12
12
|
import { registerPlanningRuntimeCommands } from './commands/planning-runtime.ts';
|
|
13
13
|
import { registerSecretRuntimeCommands } from './commands/secret-runtime.ts';
|
|
14
|
+
import { registerPaymentCardCommands } from './commands/payment-card-intake.ts';
|
|
14
15
|
import { registerScheduleRuntimeCommands } from './commands/schedule-runtime.ts';
|
|
15
16
|
import { registerBranchRuntimeCommands } from './commands/branch-runtime.ts';
|
|
16
17
|
import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts';
|
|
@@ -82,6 +83,7 @@ import { registerCheckinRuntimeCommands } from './commands/checkin-runtime.ts';
|
|
|
82
83
|
import { registerMailRuntimeCommands } from './commands/mail-runtime.ts';
|
|
83
84
|
import { registerCalendarRuntimeCommands } from './commands/calendar-runtime.ts';
|
|
84
85
|
import { registerPrincipalsRuntimeCommands } from './commands/principals-runtime.ts';
|
|
86
|
+
import { registerProfileRuntimeCommands } from './commands/profile-runtime.ts';
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
89
|
* A named group of built-in commands. The `category` label is the single
|
|
@@ -162,6 +164,7 @@ export const BUILTIN_COMMAND_GROUPS: readonly BuiltinCommandGroup[] = [
|
|
|
162
164
|
{ category: 'Discovery', register: registerDiscoveryRuntimeCommands },
|
|
163
165
|
{ category: 'Planning', register: registerPlanningRuntimeCommands },
|
|
164
166
|
{ category: 'Secrets', register: registerSecretRuntimeCommands },
|
|
167
|
+
{ category: 'Payments', register: registerPaymentCardCommands },
|
|
165
168
|
{ category: 'Scheduling', register: registerScheduleRuntimeCommands },
|
|
166
169
|
{ category: 'Branches', register: registerBranchRuntimeCommands },
|
|
167
170
|
{ category: 'Session Content', register: registerSessionContentCommands },
|
|
@@ -178,6 +181,10 @@ export const BUILTIN_COMMAND_GROUPS: readonly BuiltinCommandGroup[] = [
|
|
|
178
181
|
{ category: 'Mail & Calendar', register: registerMailRuntimeCommands },
|
|
179
182
|
{ category: 'Mail & Calendar', register: registerCalendarRuntimeCommands },
|
|
180
183
|
{ category: 'Principals', register: registerPrincipalsRuntimeCommands },
|
|
184
|
+
// "Owner Profile", not "Profiles": the Profiles category above is
|
|
185
|
+
// /profile-sync's named-config-preset bundles, which have nothing to do with
|
|
186
|
+
// what the platform knows about the person who owns it.
|
|
187
|
+
{ category: 'Owner Profile', register: registerProfileRuntimeCommands },
|
|
181
188
|
{ category: 'Policy', register: (registry) => registry.register(policyCommand) },
|
|
182
189
|
{ category: 'Providers', register: (registry) => registry.register(providerCommand) },
|
|
183
190
|
{ category: 'Eval', register: (registry) => registry.register(evalCommand) },
|
|
@@ -66,6 +66,10 @@ export function openComposerInEditor(deps: ComposerEditorDeps): void {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
const draft = deps.readDraft();
|
|
69
|
+
// Real production scratch (runs on the end user's machine, not test
|
|
70
|
+
// infrastructure), so this stays rooted at the real OS temp dir rather
|
|
71
|
+
// than the test-only makeProjectTempDir helper. Cleaned up in the
|
|
72
|
+
// `finally` below on every exit path.
|
|
69
73
|
const dir = mkdtempSync(join(tmpdir(), 'goodvibes-composer-'));
|
|
70
74
|
const file = join(dir, 'COMPOSER.md');
|
|
71
75
|
const spawn = deps.spawn ?? defaultSpawn;
|
|
@@ -132,6 +132,14 @@ export function handleGlobalShortcutToken(
|
|
|
132
132
|
state.commandContext?.toggleMemoryProvenance?.();
|
|
133
133
|
return true;
|
|
134
134
|
|
|
135
|
+
case 'voice-input':
|
|
136
|
+
// Start recording, or stop the recording already running. Routed globally so
|
|
137
|
+
// the key works whether focus is on the composer or the panel workspace —
|
|
138
|
+
// dictation is not a composer-only act. Still consumed when voice input is
|
|
139
|
+
// not wired, so a stray Alt+V never lands in the prompt as text.
|
|
140
|
+
state.commandContext?.toggleVoiceInput?.();
|
|
141
|
+
return true;
|
|
142
|
+
|
|
135
143
|
case 'toggle-keep-awake': {
|
|
136
144
|
// Flip the daemon-held keep-awake toggle; the always-visible "sleep
|
|
137
145
|
// disabled" chip is the confirmation. Fire-and-forget (the seam renders on
|
package/src/input/keybindings.ts
CHANGED
|
@@ -71,7 +71,8 @@ export type KeyAction =
|
|
|
71
71
|
| 'yank-pop'
|
|
72
72
|
| 'cancel-tool-call'
|
|
73
73
|
| 'toggle-keep-awake'
|
|
74
|
-
| 'toggle-memory-provenance'
|
|
74
|
+
| 'toggle-memory-provenance'
|
|
75
|
+
| 'voice-input';
|
|
75
76
|
|
|
76
77
|
/** Human-readable description for each action (used in /keybindings display). */
|
|
77
78
|
export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
|
|
@@ -117,6 +118,10 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
|
|
|
117
118
|
'cancel-tool-call': 'Cancel the running tool call (the turn continues)',
|
|
118
119
|
'toggle-keep-awake': 'Toggle keep-awake (the "sleep disabled" chip)',
|
|
119
120
|
'toggle-memory-provenance': 'List/hide the memories a turn used (provenance chip drill-in)',
|
|
121
|
+
// Named for what it actually does. A terminal delivers key EVENTS, not held-key
|
|
122
|
+
// state — there is no release event to watch — so this is two discrete presses,
|
|
123
|
+
// and the description says so instead of implying a hold that cannot exist.
|
|
124
|
+
'voice-input': 'Voice input: press to start recording, press again to stop and transcribe into the composer',
|
|
120
125
|
};
|
|
121
126
|
|
|
122
127
|
/** Default key bindings for all actions. */
|
|
@@ -207,6 +212,10 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
|
|
|
207
212
|
// Alt+M: expand/collapse the "used N memories" provenance chip's drill-in.
|
|
208
213
|
// Alt+M is unused by any other default. A no-op when the chip is not showing.
|
|
209
214
|
'toggle-memory-provenance': [{ key: 'm', alt: true }],
|
|
215
|
+
// Alt+V: start/stop voice input. Alt+V is unused by any other default (Ctrl+V
|
|
216
|
+
// is paste and stays paste). Two presses rather than a hold: see the action's
|
|
217
|
+
// description — a terminal has no key-release event to hold against.
|
|
218
|
+
'voice-input': [{ key: 'v', alt: true }],
|
|
210
219
|
};
|
|
211
220
|
|
|
212
221
|
/** Resolved overrides type: each key can be a single combo or array. */
|
|
@@ -231,7 +231,7 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
|
|
|
231
231
|
{
|
|
232
232
|
flagId: 'otel-remote-export',
|
|
233
233
|
label: 'OTLP remote export',
|
|
234
|
-
hint: 'Export spans over OTLP/
|
|
234
|
+
hint: 'Export spans over OTLP/HTTP JSON to a collector. Enabling this also turns on the OpenTelemetry foundation. Span export reads the OTEL_EXPORTER_OTLP_* environment variables; decision export reads telemetry.decisionOtlpEndpoint — this unit sets the latter.',
|
|
235
235
|
requiresFlags: ['otel-foundation'],
|
|
236
236
|
impliedConfig: [{ key: 'telemetry.decisionOtlpEnabled' as ConfigKey, value: true }],
|
|
237
237
|
subOptions: [
|
|
@@ -239,10 +239,10 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
|
|
|
239
239
|
key: 'endpoint',
|
|
240
240
|
configKey: 'telemetry.decisionOtlpEndpoint' as ConfigKey,
|
|
241
241
|
label: 'Collector endpoint',
|
|
242
|
-
hint: 'OTLP collector URL (e.g. http://localhost:
|
|
242
|
+
hint: 'OTLP collector URL (e.g. http://localhost:4318). Leave blank to set it later in /settings.',
|
|
243
243
|
valueType: 'text',
|
|
244
244
|
defaultValue: '',
|
|
245
|
-
placeholder: 'http://localhost:
|
|
245
|
+
placeholder: 'http://localhost:4318',
|
|
246
246
|
},
|
|
247
247
|
],
|
|
248
248
|
},
|
|
@@ -326,7 +326,7 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
|
|
|
326
326
|
stepId: 'features-devices',
|
|
327
327
|
title: 'Paired devices and voice input',
|
|
328
328
|
shortLabel: 'Devices',
|
|
329
|
-
description: 'What a paired phone may be asked to do on your behalf, and whether GoodVibes listens for a spoken wake phrase. Every phone capability asks the person holding the phone before it runs; the wake word is off
|
|
329
|
+
description: 'What a paired phone may be asked to do on your behalf, and whether GoodVibes listens for a spoken wake phrase. Every phone capability asks the person holding the phone before it runs; the wake word is off by default because holding a microphone open must be an explicit act, and turning it on does open one on this terminal.',
|
|
330
330
|
units: [
|
|
331
331
|
{
|
|
332
332
|
flagId: 'paired-device-capabilities',
|
|
@@ -349,7 +349,7 @@ export const FEATURE_ONBOARDING_SECTIONS: readonly FeatureSection[] = [
|
|
|
349
349
|
flagId: 'wake-word-detection',
|
|
350
350
|
label: 'Wake-word detection',
|
|
351
351
|
hint: 'Listen continuously on a capture device for the pinned "hey goodvibes" phrase and hand the utterance that follows to speech-to-text. Off by default because holding a microphone open must be an explicit act.',
|
|
352
|
-
note: '
|
|
352
|
+
note: 'On this terminal, turning this on starts a recorder (pw-record, parecord, arecord, ffmpeg or sox — whichever is installed), scores every frame with the pinned classifier, plays a short chime the moment a wake confirms, and shows a persistent "listening" row in the footer for as long as it runs. What follows a wake goes to speech-to-text and lands in the composer, or is sent straight away if you turn voice.wake.autoSubmit on. The model it scores with is already here — installing GoodVibes downloads and checksum-verifies it, and a running daemon retries at boot if the install could not reach the network — so turning this on normally needs no setup step. What it will NOT do on its own is download anything at the moment you turn it on: a host whose artifacts are missing or fail verification says which and names /voice wake setup, instead of pretending to listen. That same provisioning fetches the speech gate, so voice.wake.vadThreshold above 0 screens frames once it has run and refuses to start before it has, rather than scoring frames it claims to be screening. voice.wake.noiseSuppression: speex runs here too — the filter travels with the platform, so there is nothing to install for it.',
|
|
353
353
|
},
|
|
354
354
|
],
|
|
355
355
|
},
|
|
@@ -52,6 +52,18 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
52
52
|
acknowledged: controller.getBooleanFieldValue(fieldId, false),
|
|
53
53
|
});
|
|
54
54
|
};
|
|
55
|
+
// Every credential the first-run wizard captures is one the DAEMON executes
|
|
56
|
+
// with: model provider API keys (the daemon runs the model), the Cloudflare
|
|
57
|
+
// API token, and the masked fields of the external surfaces — all of which
|
|
58
|
+
// live under daemon-owned `surfaces.*` config paths. So they all go to the
|
|
59
|
+
// daemon tier, which is the one tier every surface AND the daemon read.
|
|
60
|
+
//
|
|
61
|
+
// 'project' — what this used to pass — filed them under whatever directory
|
|
62
|
+
// the wizard happened to be run from. Run the wizard in ~/code/foo and the
|
|
63
|
+
// daemon, which has no idea that directory exists, found nothing; run it
|
|
64
|
+
// again elsewhere and the first setup silently stopped applying. First-run
|
|
65
|
+
// setup is exactly the moment where "configure once, works everywhere" has
|
|
66
|
+
// to hold.
|
|
55
67
|
const setSecret = (key: string, value: string): void => {
|
|
56
68
|
if (value.length === 0) return;
|
|
57
69
|
const medium = controller.getSelectedSecretMedium();
|
|
@@ -59,7 +71,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
59
71
|
kind: 'set-secret',
|
|
60
72
|
key,
|
|
61
73
|
value,
|
|
62
|
-
scope: '
|
|
74
|
+
scope: 'daemon',
|
|
63
75
|
medium,
|
|
64
76
|
});
|
|
65
77
|
};
|