@aryacode/dsh-aryacode-polish 0.0.0-stage → 0.3.4

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/lib/client.js ADDED
@@ -0,0 +1,561 @@
1
+ /**
2
+ * @aryacode/dsh-aryacode-polish — the browser face bundle.
3
+ *
4
+ * GENERATED FILE. Edit client/face.js or client/switches.json and run
5
+ * `pnpm build:client`; nothing here should be hand-edited.
6
+ */
7
+ window.__ModuleLoader__.load({
8
+ id: "@aryacode/dsh-aryacode-polish",
9
+ factory: (require) => {
10
+ const SWITCHES = [
11
+ {
12
+ "key": "sessionCost",
13
+ "label": "Estimated session cost",
14
+ "description": "Show what the open session has cost so far, priced from recorded usage at the published DeepSeek rates.",
15
+ "defaultOn": true
16
+ },
17
+ {
18
+ "key": "accountBalance",
19
+ "label": "Account balance",
20
+ "description": "Show the remaining account balance in the sidebar foot, above the account row.",
21
+ "defaultOn": true
22
+ }
23
+ ]
24
+ const ROW_ID = "polish"
25
+ const ROW_KEY = "@aryacode/dsh-aryacode-polish" + '#' + ROW_ID
26
+ const CLIENT_VERSION = "0.3.4"
27
+
28
+ /**
29
+ * The two pure decisions behind the balance line: which wallet to show, and how
30
+ * to render it.
31
+ *
32
+ * They live in their own file because the browser face bundle is only ever run
33
+ * inside the Shell, where nothing can be imported and asserted — so the build
34
+ * inlines this module into `lib/client.js` (with the `export` keywords dropped)
35
+ * while `test/money.spec.mjs` imports it directly. What the test covers is
36
+ * therefore the same source the browser runs, not a copy of it.
37
+ */
38
+
39
+ /**
40
+ * Pick the wallet a balance line should show.
41
+ * @param view - the balance view the account Remote returned.
42
+ * @returns the first wallet carrying a balance, or undefined when the view has none.
43
+ */
44
+ function walletOf(view) {
45
+ if (view === undefined || view === null || view.status !== 'ready' || !Array.isArray(view.value)) return undefined
46
+ return view.value.find(wallet => wallet !== null && typeof wallet === 'object' && typeof wallet.balance === 'string')
47
+ }
48
+
49
+ /**
50
+ * Format one wallet as money.
51
+ * @param wallet - a wallet with `currency` and `balance`.
52
+ * @returns the amount with its currency symbol, at two decimals.
53
+ */
54
+ function formatBalance(wallet) {
55
+ const symbol = wallet.currency === 'CNY' ? '\u00A5' : '$'
56
+ const amount = Number(wallet.balance)
57
+ return Number.isFinite(amount) ? symbol + amount.toFixed(2) : symbol + String(wallet.balance)
58
+ }
59
+
60
+ /**
61
+ * Reading the account balance from a plugin face.
62
+ *
63
+ * The account Remote methods are gated, so the read runs inside a
64
+ * `remoteAccess.whenAccess('account/watch', …)` grant and the grant is released
65
+ * as soon as the read settles. Every failure — no service in this Shell, a
66
+ * refused grant, a failed call — resolves to `undefined` instead of throwing: a
67
+ * sidebar line is never worth breaking the shell over, and a line that stays
68
+ * empty is honest about a number it never received.
69
+ *
70
+ * This module is inlined into `lib/client.js` by `scripts/build-client.mjs` and
71
+ * imported directly by `test/account.spec.mjs`, so the tested source is the one
72
+ * the browser runs.
73
+ */
74
+
75
+ /**
76
+ * Build the account request identity for one balance read.
77
+ *
78
+ * The shipped account bundle inlines `DSH_CLIENT_VERSION` at build time; a plugin
79
+ * bundle has no such define, so the build stamps this package's own version and
80
+ * prefers the shell's value when one is inlined.
81
+ *
82
+ * @param version - the version this build stamps.
83
+ * @returns the metadata the account Remote methods carry.
84
+ */
85
+ function clientMetadata(version) {
86
+ const stamped = typeof process !== 'undefined' && process.env !== undefined ? process.env.DSH_CLIENT_VERSION : undefined
87
+ return {
88
+ version: typeof stamped === 'string' && stamped !== '' ? stamped : version,
89
+ locale: typeof navigator !== 'undefined' && typeof navigator.language === 'string' && navigator.language !== ''
90
+ ? navigator.language
91
+ : 'en',
92
+ timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Build the one-shot balance reader.
98
+ *
99
+ * @param ctx - the browser plugin context; `ctx.get` resolves an optional service.
100
+ * @param version - the version this build stamps into the account metadata.
101
+ * @returns a reader that resolves to the balance view, or undefined when unavailable.
102
+ */
103
+ function makeBalanceReader(ctx, version, { timeoutMs = 10000 } = {}) {
104
+ const service = name => {
105
+ try {
106
+ return ctx.get(name)
107
+ } catch {
108
+ return undefined
109
+ }
110
+ }
111
+ return async () => {
112
+ const access = service('remoteAccess')
113
+ const remote = service('remote')
114
+ if (access === undefined || remote === undefined) return undefined
115
+ const metadata = clientMetadata(version)
116
+ return await new Promise(resolve => {
117
+ let settled = false
118
+ let stop
119
+ const timer = setTimeout(() => finish(undefined), timeoutMs)
120
+ const finish = value => {
121
+ if (settled) return
122
+ settled = true
123
+ clearTimeout(timer)
124
+ resolve(value)
125
+ if (typeof stop === 'function') {
126
+ try {
127
+ stop()
128
+ } catch {
129
+ /* the grant may already have been released */
130
+ }
131
+ }
132
+ }
133
+ // The read starts before the yield so an effect runner that stops at the
134
+ // first yielded disposer still performs the call.
135
+ stop = access.whenAccess('account/watch', function* () {
136
+ void (async () => {
137
+ try {
138
+ const result = await remote.account.getBalance(metadata)
139
+ const value = result !== undefined && result.ok === true ? result.value : undefined
140
+ finish(value !== undefined && value.status === 'ready' ? value : undefined)
141
+ } catch {
142
+ finish(undefined)
143
+ }
144
+ })()
145
+ yield () => {
146
+ if (!settled) {
147
+ settled = true
148
+ clearTimeout(timer)
149
+ resolve(undefined)
150
+ }
151
+ }
152
+ })
153
+ })
154
+ }
155
+ }
156
+
157
+ /**
158
+ * AryaCode Polish — the browser face.
159
+ *
160
+ * This file is deliberately not a module. `scripts/build-client.mjs` wraps it in
161
+ * the Shell's `__ModuleLoader__.load({ id, factory })` factory and prepends the
162
+ * switch list from `client/switches.json`, so `require` is the Shell's frozen
163
+ * module table and never Node's `require`. Keep `require(...)` limited to names
164
+ * the Shell answers; anything else must be inlined here.
165
+ *
166
+ * The Configure page is the point of this package: one boolean per feature, on
167
+ * by default, and the host half of a feature is disposed when its switch turns
168
+ * off. The page reads the row's live config form, so adding a feature needs no
169
+ * second registration here — one line in `client/switches.json` and a matching
170
+ * entry in `entries/registry.js`, and the tests fail if the two disagree.
171
+ */
172
+
173
+ const React = require('react')
174
+
175
+ /** Shared spacing for the page's prose. */
176
+ const SPACING = { margin: '0 0 10px', opacity: 0.85, lineHeight: '1.5' }
177
+
178
+ /**
179
+ * Read one switch out of a configuration object.
180
+ * @param value - the row's configuration object, when it has loaded.
181
+ * @param entry - the declared switch.
182
+ * @returns the boolean the switch holds, falling back to its declared default.
183
+ */
184
+ function switchValue(value, entry) {
185
+ const current = value === undefined || value === null ? undefined : value[entry.key]
186
+ if (typeof current === 'boolean') return current
187
+ return entry.defaultOn !== false
188
+ }
189
+
190
+ /**
191
+ * Subscribe to one switch of this row's live configuration.
192
+ * @param configForms - the Shell's config-form service, or a getter for it.
193
+ * @param key - the switch to read.
194
+ * @param fallback - the value to use while the row's configuration has not loaded.
195
+ * @returns the switch's boolean value.
196
+ */
197
+ function useSwitch(configForms, key, fallback) {
198
+ const resolved = typeof configForms === 'function' ? configForms() : configForms
199
+ const live = resolved === undefined ? undefined : resolved.get(ROW_ID)
200
+ const subscribe = React.useCallback(
201
+ listener => (live === undefined ? () => {} : live.subscribe(listener)),
202
+ [live],
203
+ )
204
+ const snapshot = React.useCallback(
205
+ () => (live === undefined ? undefined : live.getSnapshot()),
206
+ [live],
207
+ )
208
+ const state = React.useSyncExternalStore(subscribe, snapshot)
209
+ const value = state?.value
210
+ const current = value === undefined || value === null ? undefined : value[key]
211
+ return typeof current === 'boolean' ? current : fallback
212
+ }
213
+
214
+ /**
215
+ * The row's Configure page and its one-line summary.
216
+ * @param props - the page's `view`, the Host's `form`, and our lazy `configForms`.
217
+ * @returns the page node, the summary node, or a status line.
218
+ */
219
+ function PolishConfig(props) {
220
+ const { form, view } = props
221
+
222
+ // Resolved lazily: the Settings page's own client half may load after this face.
223
+ const configForms = typeof props.configForms === 'function' ? props.configForms() : props.configForms
224
+ const live = configForms === undefined ? undefined : configForms.get(ROW_ID)
225
+ const subscribe = React.useCallback(
226
+ listener => (live === undefined ? () => {} : live.subscribe(listener)),
227
+ [live],
228
+ )
229
+ const snapshot = React.useCallback(
230
+ () => (live === undefined ? form?.state : live.getSnapshot()),
231
+ [live, form],
232
+ )
233
+ const state = React.useSyncExternalStore(subscribe, snapshot)
234
+ const [draft, setDraft] = React.useState(null)
235
+ const [saving, setSaving] = React.useState(false)
236
+ const [notice, setNotice] = React.useState('')
237
+
238
+ const count = SWITCHES.length
239
+ const summary = count === 0 ? 'No switch registered yet.' : count === 1 ? '1 switch.' : count + ' switches.'
240
+
241
+ if (view === 'summary') return React.createElement('span', null, summary)
242
+
243
+ if (count === 0) {
244
+ return React.createElement('div', null,
245
+ React.createElement('p', { style: SPACING },
246
+ 'No switch is registered yet. This page grows one checkbox per feature as features land; '
247
+ + 'there is nothing here that can be toggled into a half-installed state.'),
248
+ React.createElement('p', { style: SPACING },
249
+ 'A feature needs two lines: an entry in ',
250
+ React.createElement('code', null, 'entries/registry.js'),
251
+ ' that declares its switch and host half, and a matching entry in ',
252
+ React.createElement('code', null, 'client/switches.json'),
253
+ ' that names it here. The tests fail if the two disagree.'),
254
+ )
255
+ }
256
+
257
+ if (state === undefined) {
258
+ return React.createElement('p', { style: SPACING }, 'This row does not expose its configuration to the Plugins page.')
259
+ }
260
+ const value = state.value
261
+ if (value === undefined) {
262
+ return React.createElement('p', { style: SPACING },
263
+ state.status === 'unavailable' ? 'This row has no configuration to change.' : 'Loading configuration…')
264
+ }
265
+
266
+ const current = draft === null ? value : draft
267
+ const dirty = SWITCHES.some(entry => switchValue(current, entry) !== switchValue(value, entry))
268
+ const writable = state.writable === true
269
+ const setSwitch = (key, next) => {
270
+ setDraft({ ...current, [key]: next })
271
+ setNotice('')
272
+ }
273
+ const save = async () => {
274
+ if (saving || !dirty) return
275
+ setSaving(true)
276
+ setNotice('')
277
+ try {
278
+ const ops = SWITCHES.map(entry => ({ op: 'set', path: [entry.key], value: switchValue(current, entry) }))
279
+ const owner = live === undefined ? form : live
280
+ const accepted = await owner.mutate(ops, state.revision)
281
+ if (accepted) {
282
+ setDraft(null)
283
+ setNotice('Saved. A feature switched off stops running now, not after a restart.')
284
+ } else {
285
+ setNotice('The Host refused the change.')
286
+ }
287
+ } catch (error) {
288
+ setNotice(error instanceof Error ? error.message : String(error))
289
+ }
290
+ setSaving(false)
291
+ }
292
+
293
+ return React.createElement('div', { style: { display: 'grid', gap: '16px' } },
294
+ React.createElement('p', { style: SPACING },
295
+ 'Every switch is additive and on by default. Switching one off disposes that feature only — '
296
+ + 'it stops costing the app instead of merely hiding.'),
297
+ React.createElement('div', { style: { display: 'grid', gap: '14px' } },
298
+ SWITCHES.map(entry => React.createElement('label', {
299
+ key: entry.key,
300
+ style: { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '10px', alignItems: 'start', cursor: writable ? 'pointer' : 'default' },
301
+ },
302
+ React.createElement('input', {
303
+ type: 'checkbox',
304
+ checked: switchValue(current, entry),
305
+ disabled: !writable || saving,
306
+ onChange: event => setSwitch(entry.key, event.target.checked),
307
+ }),
308
+ React.createElement('span', null,
309
+ React.createElement('span', { style: { display: 'block', fontWeight: 600 } }, entry.label),
310
+ React.createElement('span', { style: { display: 'block', opacity: 0.8, lineHeight: '1.45' } }, entry.description),
311
+ ))),
312
+ ),
313
+ React.createElement('div', { style: { display: 'flex', gap: '10px', alignItems: 'center' } },
314
+ React.createElement('button', {
315
+ type: 'button',
316
+ disabled: !writable || saving || !dirty,
317
+ onClick: save,
318
+ }, saving ? 'Saving…' : 'Save'),
319
+ React.createElement('span', { style: { opacity: 0.8 } },
320
+ notice !== '' ? notice : writable ? '' : 'This row is read-only in the current composition.')),
321
+ )
322
+ }
323
+
324
+ /** Money from the wire view, which already did the arithmetic host-side. */
325
+ function formatMoney(value) {
326
+ const amount = Number(value)
327
+ if (!Number.isFinite(amount) || amount <= 0) return ''
328
+ // Pennies matter at the top and single tokens matter at the bottom: a small
329
+ // turn really does cost a fraction of a cent, and "$0" would hide it.
330
+ const fixed = amount >= 1 ? amount.toFixed(2)
331
+ : amount >= 0.01 ? amount.toFixed(3)
332
+ : amount.toPrecision(2)
333
+ const text = fixed.includes('e') ? amount.toFixed(8) : fixed
334
+ return '$' + text.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
335
+ }
336
+
337
+ /** Token counts, grouped, the way the harness pill next to ours reads. */
338
+ function formatTokens(value) {
339
+ const count = Number(value)
340
+ return Number.isFinite(count) ? new Intl.NumberFormat('en-US').format(count) + ' tok' : ''
341
+ }
342
+
343
+ /** One label/value line of the breakdown card. */
344
+ function detailRow(label, value, key) {
345
+ return React.createElement('div', {
346
+ key,
347
+ style: { display: 'flex', justifyContent: 'space-between', gap: '16px', padding: '2px 0' },
348
+ },
349
+ React.createElement('span', { style: { opacity: 0.75 } }, label),
350
+ React.createElement('span', null, value))
351
+ }
352
+
353
+ /** The small text under the card that keeps the number honest. */
354
+ const COST_FOOTNOTE = 'Based on recorded usage and published rates. Your provider\'s bill is authoritative.'
355
+
356
+ /**
357
+ * The composer-dock pill, and the breakdown card it opens.
358
+ * Reads the priced projection the Host registers, so no rate table or money
359
+ * arithmetic runs in the browser.
360
+ * @param props - standard session props plus our lazy `configForms`.
361
+ * @returns the pill, or nothing while the feature is off or the session has no usage.
362
+ */
363
+ function CostPill(props) {
364
+ const switched = useSwitch(props.configForms, 'sessionCost', true)
365
+ const usage = typeof props.useProjection === 'function' ? props.useProjection('polishCost') : undefined
366
+ const [open, setOpen] = React.useState(false)
367
+ const root = React.useRef(null)
368
+
369
+ React.useEffect(() => {
370
+ if (!open) return undefined
371
+ const close = event => {
372
+ if (root.current !== null && !root.current.contains(event.target)) setOpen(false)
373
+ }
374
+ document.addEventListener('pointerdown', close)
375
+ return () => document.removeEventListener('pointerdown', close)
376
+ }, [open])
377
+
378
+ if (!switched) return null
379
+ const tokens = typeof usage?.tokens === 'number' ? usage.tokens : 0
380
+ if (tokens === 0) return null
381
+ const money = formatMoney(usage.costUsd)
382
+ const label = money === '' ? 'Not priced' : (usage.incomplete ? '≈' + money + ' partially priced' : '≈' + money)
383
+ const cacheHit = tokens === 0 ? 0 : Math.round((usage.cacheRead ?? 0) / tokens * 100)
384
+ const models = Array.isArray(usage.models) ? usage.models : []
385
+
386
+ const modelName = models.length === 0 ? 'unknown'
387
+ : models.length === 1 ? models[0].model
388
+ : models.length + ' models'
389
+ const providerName = models.length === 0 ? 'unknown'
390
+ : models.length === 1 ? models[0].provider
391
+ : models.length + ' providers'
392
+ const segment = (value, shade, key) => {
393
+ const countValue = Number(value ?? 0)
394
+ if (!Number.isFinite(countValue) || countValue <= 0) return null
395
+ return React.createElement('div', {
396
+ key,
397
+ style: { flexGrow: countValue, background: 'color-mix(in srgb, CanvasText ' + shade + '%, transparent)' },
398
+ })
399
+ }
400
+ const bar = React.createElement('div', {
401
+ style: {
402
+ display: 'flex',
403
+ height: '6px',
404
+ margin: '10px 0 12px',
405
+ borderRadius: '999px',
406
+ overflow: 'hidden',
407
+ background: 'color-mix(in srgb, CanvasText 10%, transparent)',
408
+ },
409
+ },
410
+ segment(usage.cacheRead, 22, 'bar-cached'),
411
+ segment(usage.uncached, 48, 'bar-uncached'),
412
+ segment(usage.cacheWrite, 62, 'bar-cache-write'),
413
+ segment(usage.output, 82, 'bar-output'))
414
+
415
+ return React.createElement('div', {
416
+ ref: root,
417
+ style: { position: 'relative', display: 'inline-flex', alignItems: 'center' },
418
+ },
419
+ React.createElement('button', {
420
+ type: 'button',
421
+ onClick: () => setOpen(!open),
422
+ title: 'Estimated session cost',
423
+ style: {
424
+ background: 'transparent',
425
+ border: 'none',
426
+ color: 'inherit',
427
+ cursor: 'pointer',
428
+ font: 'inherit',
429
+ padding: '0 2px',
430
+ opacity: 0.8,
431
+ },
432
+ }, label),
433
+ open
434
+ ? React.createElement('div', {
435
+ style: {
436
+ position: 'absolute',
437
+ bottom: '100%',
438
+ left: 0,
439
+ marginBottom: '8px',
440
+ width: '320px',
441
+ padding: '14px',
442
+ borderRadius: '10px',
443
+ border: '1px solid color-mix(in srgb, CanvasText 16%, transparent)',
444
+ background: 'Canvas',
445
+ color: 'CanvasText',
446
+ boxShadow: '0 10px 30px color-mix(in srgb, CanvasText 18%, transparent)',
447
+ fontSize: '12px',
448
+ lineHeight: '1.5',
449
+ zIndex: 40,
450
+ },
451
+ },
452
+ React.createElement('div', {
453
+ style: { display: 'flex', justifyContent: 'space-between', gap: '16px', fontWeight: 500 },
454
+ },
455
+ React.createElement('span', null, 'Estimated session cost'),
456
+ React.createElement('span', null, label)),
457
+ bar,
458
+ detailRow('Cache hit', cacheHit + '%', 'cache-hit'),
459
+ detailRow('Uncached input', formatTokens(usage.uncached), 'uncached'),
460
+ detailRow('Cached input', formatTokens(usage.cacheRead), 'cached'),
461
+ (usage.cacheWrite ?? 0) > 0 ? detailRow('Cache write', formatTokens(usage.cacheWrite), 'cache-write') : null,
462
+ detailRow('Output', formatTokens(usage.output), 'output'),
463
+ detailRow('Model', modelName, 'model'),
464
+ detailRow('Provider', providerName, 'provider'),
465
+ detailRow('Requests', String(usage.requests ?? 0), 'requests'),
466
+ React.createElement('div', { style: { borderTop: '1px solid color-mix(in srgb, CanvasText 12%, transparent)', margin: '8px 0' } }),
467
+ detailRow('Estimated session cost', money === '' ? 'Not priced' : money, 'cost'),
468
+ React.createElement('p', { style: { margin: '10px 0 0', opacity: 0.7, fontSize: '11px' } }, COST_FOOTNOTE))
469
+ : null)
470
+ }
471
+
472
+ /**
473
+ * The remaining-balance line in the sidebar foot.
474
+ *
475
+ * It renders nothing at all when the switch is off, the account exposes no
476
+ * wallet, or the read was refused — an empty line is better than a line that
477
+ * claims a number the Host did not give us.
478
+ *
479
+ * @param props - the slot's `wide` flag, our lazy `configForms`, and the balance reader.
480
+ * @returns the balance line, or null.
481
+ */
482
+ function BalanceLine(props) {
483
+ const enabled = useSwitch(props.configForms, 'accountBalance', true)
484
+ const [wallet, setWallet] = React.useState(undefined)
485
+ const read = props.loadBalance
486
+ React.useEffect(() => {
487
+ if (!enabled || typeof read !== 'function') return undefined
488
+ let live = true
489
+ Promise.resolve()
490
+ .then(() => read())
491
+ .then(view => {
492
+ if (live) setWallet(walletOf(view))
493
+ }, () => {
494
+ if (live) setWallet(undefined)
495
+ })
496
+ return () => {
497
+ live = false
498
+ }
499
+ }, [enabled, read])
500
+ if (!enabled) return null
501
+ // The wrapper stays in the DOM even when there is no number to show, so a
502
+ // reader (and the verification script) can tell "registered but empty" from
503
+ // "not registered at all". An empty flex line costs the layout nothing.
504
+ if (wallet === undefined) {
505
+ return React.createElement('div', { 'data-polish-balance': 'empty', style: { display: 'none' } })
506
+ }
507
+ const money = formatBalance(wallet)
508
+ return React.createElement('div', {
509
+ title: 'Remaining account balance',
510
+ style: {
511
+ display: 'flex',
512
+ alignItems: 'center',
513
+ gap: '6px',
514
+ padding: '2px 8px 4px',
515
+ fontSize: '12px',
516
+ lineHeight: '16px',
517
+ minWidth: 0,
518
+ overflow: 'hidden',
519
+ whiteSpace: 'nowrap',
520
+ textOverflow: 'ellipsis',
521
+ opacity: 0.75,
522
+ },
523
+ }, props.wide === false ? money : 'Balance ' + money)
524
+ }
525
+
526
+ return {
527
+ inject: ['slots'],
528
+ /**
529
+ * Register this bundle's surfaces.
530
+ * @param ctx - the browser plugin context.
531
+ */
532
+ apply(ctx) {
533
+ const configForms = () => {
534
+ try {
535
+ return ctx.get('configForms')
536
+ } catch {
537
+ return undefined
538
+ }
539
+ }
540
+ ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
541
+ name: 'plugins.row.config',
542
+ key: ROW_KEY,
543
+ inject: () => ({ configForms }),
544
+ }, PolishConfig))
545
+ ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
546
+ name: 'conversation.composer.dock',
547
+ id: 'polish-cost',
548
+ order: 2,
549
+ inject: () => ({ configForms }),
550
+ }, CostPill))
551
+ const loadBalance = makeBalanceReader(ctx, CLIENT_VERSION)
552
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
553
+ name: 'sidebar.footer.action',
554
+ id: 'polish-balance',
555
+ order: 3,
556
+ inject: () => ({ configForms, loadBalance }),
557
+ }, BalanceLine))
558
+ },
559
+ }
560
+ },
561
+ })
package/lib/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Host half of AryaCode Polish: re-exports the feature row declared in entries/polish.js. */
2
+ export { Config, apply, features, inject, name } from '../entries/polish.js'
package/lib/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Host half of AryaCode Polish.
3
+ *
4
+ * The row in `cordis.patch.yml` names this package and not a subpath, because a
5
+ * package registers exactly one client bundle and the Shell resolves it from the
6
+ * exact package specifier the mounted row names. The host work itself lives in
7
+ * `entries/polish.js` — a registry of features, each gated by one volatile
8
+ * switch — so this module exists to be that mount point and re-exports the row.
9
+ */
10
+ export { Config, apply, features, inject, name } from '../entries/polish.js'
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "AryaCode Polish",
4
+ "description": "Quality-of-life refinements for the AryaCode interface, one switch per feature: spelling corrections in the composer first."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "AryaCode 润色",
4
+ "description": "AryaCode 界面的体验改进,每个功能一个开关:首先是输入框中的拼写纠正。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-aryacode-polish",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.4",
4
+ "description": "AryaCode Polish: one install for the small things that make AryaCode feel finished. One Plugins row, one switch per quality-of-life feature.",
5
+ "keywords": [
6
+ "dsh",
7
+ "deepseek-harness",
8
+ "cordis",
9
+ "plugin",
10
+ "bundle",
11
+ "polish",
12
+ "quality-of-life",
13
+ "cost",
14
+ "usage"
15
+ ],
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "engines": {
19
+ "node": "^22.19.0 || >=24.0.0"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/KonnectSuite/dsh-aryacode-polish.git"
24
+ },
25
+ "homepage": "https://github.com/KonnectSuite/dsh-aryacode-polish#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/KonnectSuite/dsh-aryacode-polish/issues"
28
+ },
29
+ "files": [
30
+ "cordis.patch.yml",
31
+ "entries/**/*",
32
+ "client/**/*",
33
+ "lib/**/*",
34
+ "locale/*.json",
35
+ "docs/**/*",
36
+ "README.md",
37
+ "CHANGELOG.md",
38
+ "LICENSE"
39
+ ],
40
+ "dsh": {
41
+ "bundle": {
42
+ "patch": "./cordis.patch.yml"
43
+ },
44
+ "client": {
45
+ "platform": "web"
46
+ },
47
+ "engines": {
48
+ "dsh": "^0.2.1-rc.3"
49
+ }
50
+ },
51
+ "scripts": {
52
+ "build:client": "node scripts/build-client.mjs",
53
+ "prepack": "node scripts/build-client.mjs",
54
+ "test": "node --test test/patch.spec.mjs test/config.spec.mjs test/registry.spec.mjs test/cost.spec.mjs test/money.spec.mjs test/account.spec.mjs test/docs.spec.mjs test/manifests.spec.mjs test/client-face.spec.mjs"
55
+ },
56
+ "dependencies": {
57
+ "zod": "^4.4.3"
58
+ },
59
+ "peerDependencies": {
60
+ "@deepseek-ai/cordis": "~4.0.4"
61
+ },
62
+ "devDependencies": {
63
+ "@deepseek-ai/schemastery": "^3.18.5",
64
+ "js-yaml": "^4.1.0"
65
+ },
66
+ "exports": {
67
+ ".": {
68
+ "types": "./lib/index.d.ts",
69
+ "default": "./lib/index.js"
70
+ },
71
+ "./package.json": "./package.json",
72
+ "./client": "./lib/client.js",
73
+ "./polish": "./entries/polish.js"
74
+ }
75
+ }