@polderlabs/bizar 10.19.2 → 10.19.5
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/cli/commands/hook.mjs
CHANGED
|
@@ -40,6 +40,7 @@ export const HOOK_PROGRAMS = Object.freeze({
|
|
|
40
40
|
'pretooluse-bash': 'pretooluse-bash.mjs',
|
|
41
41
|
'pretooluse-editwrite': 'pretooluse-editwrite.mjs',
|
|
42
42
|
'sessionend-recall': 'sessionend-recall.mjs',
|
|
43
|
+
'sessionstart-model-sync': 'sessionstart-model-sync.mjs',
|
|
43
44
|
'sessionstart-prime': 'sessionstart-prime.mjs',
|
|
44
45
|
'simplify-guard': 'simplify-guard.mjs',
|
|
45
46
|
telemetry: 'telemetry.mjs',
|
|
@@ -61,6 +62,7 @@ export const EVENT_CHAINS = Object.freeze({
|
|
|
61
62
|
]),
|
|
62
63
|
'session-start': Object.freeze([
|
|
63
64
|
'control-inbox',
|
|
65
|
+
'sessionstart-model-sync',
|
|
64
66
|
'sessionstart-prime',
|
|
65
67
|
'persistent-mode',
|
|
66
68
|
'telemetry',
|
package/cli/commands/models.mjs
CHANGED
|
@@ -592,6 +592,116 @@ export function applyModelOverrides({ settingsJsonPath, pickedIds, liveIds = []
|
|
|
592
592
|
return { wrote: true, syncedIds: synced, skippedStale: skipped, settingsPath: path };
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Derive a human-readable label for a model ID. Used to populate the
|
|
597
|
+
* `modelPicker` array in settings.json so Claude Code's `/model` picker
|
|
598
|
+
* displays picked models with something nicer than the raw `provider/name`
|
|
599
|
+
* string.
|
|
600
|
+
*
|
|
601
|
+
* minimax/MiniMax-M3 → "MiniMax M3"
|
|
602
|
+
* codex/gpt-5.6-sol → "GPT 5.6 Sol"
|
|
603
|
+
* qct/qwen3.8-max-preview → "Qwen3.8 Max Preview"
|
|
604
|
+
* openrouter/nvidia/foo:free → "nvidia/foo:free"
|
|
605
|
+
*
|
|
606
|
+
* The gateway-reported `name` (when available on the picked profile) always
|
|
607
|
+
* wins. Falls back to a title-cased rendering of the model segment.
|
|
608
|
+
*
|
|
609
|
+
* @param {string} modelId
|
|
610
|
+
* @param {object} [profile] Optional profile with `name` or `displayName`
|
|
611
|
+
* @returns {string}
|
|
612
|
+
*/
|
|
613
|
+
export function deriveModelLabel(modelId, profile) {
|
|
614
|
+
const name = profile && typeof profile.name === 'string' && profile.name.trim();
|
|
615
|
+
if (name) return name.trim();
|
|
616
|
+
const displayName = profile && typeof profile.displayName === 'string' && profile.displayName.trim();
|
|
617
|
+
if (displayName) return displayName.trim();
|
|
618
|
+
const id = String(modelId || '').trim();
|
|
619
|
+
if (!id) return '';
|
|
620
|
+
// Drop the leading provider segment (`minimax/MiniMax-M3` → `MiniMax-M3`)
|
|
621
|
+
// so the operator sees the model name, not the namespace.
|
|
622
|
+
const slash = id.indexOf('/');
|
|
623
|
+
const tail = slash >= 0 ? id.slice(slash + 1) : id;
|
|
624
|
+
// Split on word boundaries (hyphens / underscores / dots / colons / path
|
|
625
|
+
// separators) and join with spaces. Case is preserved verbatim — `gpt`
|
|
626
|
+
// stays `gpt`, `MiniMax` stays `MiniMax`, `M2.7` stays `M2.7`. Brand
|
|
627
|
+
// casing belongs to the gateway (`name` field), not us.
|
|
628
|
+
return tail
|
|
629
|
+
.replace(/[\\/]+/g, ' ')
|
|
630
|
+
.replace(/[-_]+/g, ' ')
|
|
631
|
+
.replace(/:/g, ' ')
|
|
632
|
+
.replace(/\s+/g, ' ')
|
|
633
|
+
.trim();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Sync `userSelected.models` into Claude Code's `modelPicker` setting
|
|
638
|
+
* (settings.json). The picker is what populates `/model` — `modelOverrides`
|
|
639
|
+
* alone only silences diagnostics, it does NOT add entries to the picker.
|
|
640
|
+
*
|
|
641
|
+
* Per Claude Code's settings reference: `modelPicker` is an OBJECT with an
|
|
642
|
+
* `options` array. Each option has `{ model, label?, description? }`.
|
|
643
|
+
* Scope is User-or-managed (settings.json is in scope). The `options`
|
|
644
|
+
* array preserves the operator's pick order from `userSelected.models`.
|
|
645
|
+
*
|
|
646
|
+
* Behavior:
|
|
647
|
+
* - Reads settings.json; preserves every other field (env, mcpServers,
|
|
648
|
+
* permissions, hooks, etc.).
|
|
649
|
+
* - Writes `modelPicker = { options: [{ model, label }] }`.
|
|
650
|
+
* - Filters out picks the live gateway rejects (same stale-ID contract as
|
|
651
|
+
* `applyModelOverrides`); the surviving picks fill the picker.
|
|
652
|
+
* - Empty pick list → writes `modelPicker: { options: [] }` so Claude
|
|
653
|
+
* Code falls back to its built-in picker.
|
|
654
|
+
* - Atomic replace via temp-file + rename (matches `applyModels`).
|
|
655
|
+
* - Refuses to overwrite a corrupt settings.json.
|
|
656
|
+
* - When `settingsJsonPath === null`, returns a no-op (tests).
|
|
657
|
+
*
|
|
658
|
+
* @param {{
|
|
659
|
+
* settingsJsonPath?: string|null,
|
|
660
|
+
* pickedIds: string[],
|
|
661
|
+
* profiles?: Record<string, object>,
|
|
662
|
+
* liveIds?: string[],
|
|
663
|
+
* }} opts
|
|
664
|
+
* @returns {{
|
|
665
|
+
* wrote: boolean,
|
|
666
|
+
* options: Array<{model: string, label: string, description?: string}>,
|
|
667
|
+
* skippedStale: string[],
|
|
668
|
+
* settingsPath: string|null,
|
|
669
|
+
* }}
|
|
670
|
+
*/
|
|
671
|
+
export function applyModelPicker({ settingsJsonPath, pickedIds, profiles = {}, liveIds = [] }) {
|
|
672
|
+
const path = settingsJsonPath === undefined
|
|
673
|
+
? join(homedir(), '.claude', 'settings.json')
|
|
674
|
+
: settingsJsonPath;
|
|
675
|
+
if (path === null) {
|
|
676
|
+
return { wrote: false, options: [], skippedStale: [], settingsPath: null };
|
|
677
|
+
}
|
|
678
|
+
const live = new Set(Array.isArray(liveIds) ? liveIds : []);
|
|
679
|
+
const picks = Array.isArray(pickedIds) ? pickedIds.filter((id) => typeof id === 'string' && id.trim()) : [];
|
|
680
|
+
const surviving = live.size === 0 ? picks : picks.filter((id) => live.has(id));
|
|
681
|
+
const skipped = live.size === 0 ? [] : picks.filter((id) => !live.has(id));
|
|
682
|
+
const options = surviving.map((id) => {
|
|
683
|
+
const profile = profiles?.[id];
|
|
684
|
+
const label = deriveModelLabel(id, profile);
|
|
685
|
+
const option = { model: id, label };
|
|
686
|
+
const description = profile && typeof profile.description === 'string' && profile.description.trim();
|
|
687
|
+
if (description) option.description = description.trim();
|
|
688
|
+
return option;
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
let settings = {};
|
|
692
|
+
if (existsSync(path)) {
|
|
693
|
+
try {
|
|
694
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
695
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) settings = parsed;
|
|
696
|
+
} catch {
|
|
697
|
+
return { wrote: false, options: [], skippedStale: skipped, settingsPath: path };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
settings.modelPicker = { options };
|
|
701
|
+
writeAtomic(path, JSON.stringify(settings, null, 2) + '\n');
|
|
702
|
+
return { wrote: true, options, skippedStale: skipped, settingsPath: path };
|
|
703
|
+
}
|
|
704
|
+
|
|
595
705
|
// ── F-190 / IMP-017 refresh + operator-override preservation ────────────────
|
|
596
706
|
|
|
597
707
|
/**
|
|
@@ -1409,8 +1519,9 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1409
1519
|
// `[claude-code:unrecognized_model]` for any ID the gateway later
|
|
1410
1520
|
// rejects; the operator can re-run `bizar models` to drop them.
|
|
1411
1521
|
const sync = applyModelOverrides({ pickedIds: ids, liveIds: [] });
|
|
1522
|
+
const picker = applyModelPicker({ pickedIds: ids, profiles: block.profiles || {}, liveIds: [] });
|
|
1412
1523
|
if (wantJson) {
|
|
1413
|
-
process.stdout.write(JSON.stringify({ applied: block, sync }, null, 2) + '\n');
|
|
1524
|
+
process.stdout.write(JSON.stringify({ applied: block, sync, picker }, null, 2) + '\n');
|
|
1414
1525
|
} else {
|
|
1415
1526
|
console.log(chalk.green(` v ${block.models.length} model(s) saved to userSelected`));
|
|
1416
1527
|
for (const id of block.models) {
|
|
@@ -1504,9 +1615,10 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1504
1615
|
const picked = await pickModels({ candidates, current });
|
|
1505
1616
|
if (picked.length === 0) {
|
|
1506
1617
|
const block = applyModels({ routerPath, models: [], source: 'live-pick' });
|
|
1507
|
-
// Clear Claude Code modelOverrides when the picker is
|
|
1508
|
-
// session no longer claims to recognise removed IDs.
|
|
1618
|
+
// Clear Claude Code modelOverrides + modelPicker when the picker is
|
|
1619
|
+
// emptied so the session no longer claims to recognise removed IDs.
|
|
1509
1620
|
applyModelOverrides({ pickedIds: [], liveIds: [] });
|
|
1621
|
+
applyModelPicker({ pickedIds: [], liveIds: [] });
|
|
1510
1622
|
if (wantJson) {
|
|
1511
1623
|
process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource }, null, 2) + '\n');
|
|
1512
1624
|
} else {
|
|
@@ -1533,8 +1645,11 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1533
1645
|
block.staleIds = partition.staleIds;
|
|
1534
1646
|
}
|
|
1535
1647
|
const sync = applyModelOverrides({ pickedIds: picked, liveIds });
|
|
1648
|
+
// Sync the /model picker contents (`modelPicker` setting) so the user's
|
|
1649
|
+
// picks drive the picker without relying on gateway discovery.
|
|
1650
|
+
const picker = applyModelPicker({ pickedIds: picked, profiles, liveIds });
|
|
1536
1651
|
if (wantJson) {
|
|
1537
|
-
process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync }, null, 2) + '\n');
|
|
1652
|
+
process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync, picker }, null, 2) + '\n');
|
|
1538
1653
|
} else {
|
|
1539
1654
|
console.log(chalk.green(`\n v Saved ${block.models.length} model(s) to ${routerPath}:`));
|
|
1540
1655
|
console.log(chalk.dim(` Models.dev profiles: ${Object.keys(block.profiles || {}).length}/${block.models.length}`));
|
|
@@ -1545,6 +1660,9 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1545
1660
|
if (sync.wrote && sync.skippedStale.length > 0) {
|
|
1546
1661
|
console.log(chalk.yellow(` Settings sync skipped ${sync.skippedStale.length} stale id(s): ${sync.skippedStale.join(', ')}`));
|
|
1547
1662
|
}
|
|
1663
|
+
if (picker.wrote) {
|
|
1664
|
+
console.log(chalk.dim(` /model picker populated with ${picker.options.length} entr${picker.options.length === 1 ? 'y' : 'ies'}`));
|
|
1665
|
+
}
|
|
1548
1666
|
}
|
|
1549
1667
|
return true;
|
|
1550
1668
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sessionstart-model-sync.mjs — Claude Code SessionStart hook.
|
|
4
|
+
*
|
|
5
|
+
* Re-applies the operator's `userSelected.models` block (from
|
|
6
|
+
* `~/.config/bizar/config/claude/model-router.json`) into the three
|
|
7
|
+
* Claude Code settings keys that the operator owns:
|
|
8
|
+
*
|
|
9
|
+
* - modelPicker → { options: [{ model, label }] } in user pick order
|
|
10
|
+
* - modelOverrides → { <id>: <id> } self-map for every live pick
|
|
11
|
+
* - model → reset to the first user pick if the current value
|
|
12
|
+
* starts with `claude-` (the dead gateway namespace —
|
|
13
|
+
* the live gateway rejects it with model_not_found)
|
|
14
|
+
*
|
|
15
|
+
* Why this hook exists:
|
|
16
|
+
* Claude Code's `/model` picker rewrites `~/.claude/settings.json` on
|
|
17
|
+
* every user pick. The rewrite preserves most keys but drops `modelPicker`
|
|
18
|
+
* and rewrites `model` to whatever the gateway returned (often the dead
|
|
19
|
+
* `claude-<provider>/<model>[1m]` shape). Without this hook the operator
|
|
20
|
+
* loses their picker list between every Claude Code session.
|
|
21
|
+
*
|
|
22
|
+
* Scope guarantee:
|
|
23
|
+
* This hook ONLY touches `modelPicker`, `modelOverrides`, and `model`.
|
|
24
|
+
* Env, mcpServers, permissions, hooks, and every other operator key is
|
|
25
|
+
* left untouched. The source of truth for picks
|
|
26
|
+
* (`~/.config/bizar/config/claude/model-router.json`) is also untouched.
|
|
27
|
+
*
|
|
28
|
+
* Failure policy:
|
|
29
|
+
* This is an advisory hook. Any failure (missing router, malformed JSON,
|
|
30
|
+
* unwritable settings.json, etc.) is logged to
|
|
31
|
+
* `~/.config/bizar/hook-logs/model-sync-DATE.jsonl` and swallowed. The
|
|
32
|
+
* hook always exits 0 — the operator's existing picks survive whatever
|
|
33
|
+
* Claude Code wrote last, and a broken sync must not block session start.
|
|
34
|
+
*
|
|
35
|
+
* Claude Code SessionStart input:
|
|
36
|
+
* { session_id, transcript_path, cwd, hook_event_name, source }
|
|
37
|
+
*
|
|
38
|
+
* Claude Code SessionStart output:
|
|
39
|
+
* { hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext } }
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
45
|
+
import { dirname, join } from 'node:path';
|
|
46
|
+
import { homedir } from 'node:os';
|
|
47
|
+
|
|
48
|
+
const HOOK_LOG_DIR = join(homedir(), '.config', 'bizar', 'hook-logs');
|
|
49
|
+
|
|
50
|
+
function readJsonIfObject(path) {
|
|
51
|
+
if (!existsSync(path)) return null;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
54
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readRouterPath() {
|
|
61
|
+
const envOverride = process.env.BIZAR_MODEL_ROUTER_CONFIG;
|
|
62
|
+
if (envOverride && typeof envOverride === 'string' && envOverride.trim()) {
|
|
63
|
+
return envOverride;
|
|
64
|
+
}
|
|
65
|
+
return join(homedir(), '.config', 'bizar', 'config', 'claude', 'model-router.json');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readSettingsPath() {
|
|
69
|
+
return join(homedir(), '.claude', 'settings.json');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Mirrors `cli/commands/models.mjs#deriveModelLabel` without the profile
|
|
73
|
+
// override branch. The picker label is meant to be derived from the model
|
|
74
|
+
// id; the profile `name`/`displayName` is used elsewhere in `applyModelPicker`
|
|
75
|
+
// only when present, but for our session-start re-apply the id-derived label
|
|
76
|
+
// is sufficient and matches the helper's behaviour for every live id in the
|
|
77
|
+
// router file.
|
|
78
|
+
function deriveModelLabel(modelId) {
|
|
79
|
+
const id = String(modelId || '').trim();
|
|
80
|
+
if (!id) return '';
|
|
81
|
+
const slash = id.indexOf('/');
|
|
82
|
+
const tail = slash >= 0 ? id.slice(slash + 1) : id;
|
|
83
|
+
return tail
|
|
84
|
+
.replace(/[\\/]+/g, ' ')
|
|
85
|
+
.replace(/[-_]+/g, ' ')
|
|
86
|
+
.replace(/:/g, ' ')
|
|
87
|
+
.replace(/\s+/g, ' ')
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function atomicWriteJson(path, value) {
|
|
92
|
+
const dir = dirname(path);
|
|
93
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
94
|
+
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
95
|
+
writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n');
|
|
96
|
+
renameSync(tmp, path);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function logEvent(entry) {
|
|
100
|
+
try {
|
|
101
|
+
mkdirSync(HOOK_LOG_DIR, { recursive: true });
|
|
102
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
103
|
+
appendFileSync(
|
|
104
|
+
join(HOOK_LOG_DIR, `model-sync-${today}.jsonl`),
|
|
105
|
+
JSON.stringify({ ts: new Date().toISOString(), ...entry }) + '\n',
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
/* best-effort */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPayload(appliedCount, modelAfter) {
|
|
113
|
+
const note = appliedCount > 0
|
|
114
|
+
? `Bizar reapplied ${appliedCount} model picker option(s) to ~/.claude/settings.json (model=${modelAfter}).`
|
|
115
|
+
: '';
|
|
116
|
+
return {
|
|
117
|
+
hookSpecificOutput: {
|
|
118
|
+
hookEventName: 'SessionStart',
|
|
119
|
+
additionalContext: note,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function syncOnce() {
|
|
125
|
+
const routerPath = readRouterPath();
|
|
126
|
+
const router = readJsonIfObject(routerPath);
|
|
127
|
+
const userSelected = router && typeof router === 'object' ? router.userSelected : null;
|
|
128
|
+
const models = userSelected && Array.isArray(userSelected.models) ? userSelected.models : [];
|
|
129
|
+
const profiles = userSelected && userSelected.profiles && typeof userSelected.profiles === 'object'
|
|
130
|
+
? userSelected.profiles
|
|
131
|
+
: {};
|
|
132
|
+
const liveIds = models.filter((id) => typeof id === 'string' && id.trim());
|
|
133
|
+
if (liveIds.length === 0) {
|
|
134
|
+
return { applied: 0, modelAfter: null, skipped: 'no-userSelected' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const settingsPath = readSettingsPath();
|
|
138
|
+
const settings = readJsonIfObject(settingsPath) || {};
|
|
139
|
+
const before = {
|
|
140
|
+
model: typeof settings.model === 'string' ? settings.model : null,
|
|
141
|
+
modelPicker: settings.modelPicker || null,
|
|
142
|
+
modelOverrides: settings.modelOverrides || null,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const options = liveIds.map((id) => {
|
|
146
|
+
const option = { model: id, label: deriveModelLabel(id) };
|
|
147
|
+
const profile = profiles[id];
|
|
148
|
+
if (profile && typeof profile.description === 'string' && profile.description.trim()) {
|
|
149
|
+
option.description = profile.description.trim();
|
|
150
|
+
}
|
|
151
|
+
return option;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
settings.modelPicker = { options };
|
|
155
|
+
settings.modelOverrides = Object.fromEntries(liveIds.map((id) => [id, id]));
|
|
156
|
+
|
|
157
|
+
let modelChanged = false;
|
|
158
|
+
if (typeof settings.model === 'string' && settings.model.startsWith('claude-')) {
|
|
159
|
+
settings.model = liveIds[0];
|
|
160
|
+
modelChanged = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
atomicWriteJson(settingsPath, settings);
|
|
164
|
+
|
|
165
|
+
const after = {
|
|
166
|
+
model: typeof settings.model === 'string' ? settings.model : null,
|
|
167
|
+
modelPickerOptions: options.length,
|
|
168
|
+
modelOverridesCount: Object.keys(settings.modelOverrides).length,
|
|
169
|
+
modelChanged,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
logEvent({ event: 'sessionstart-model-sync', routerPath, before, after });
|
|
173
|
+
return { applied: options.length, modelAfter: after.model, modelChanged };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function run() {
|
|
177
|
+
let raw = '';
|
|
178
|
+
process.stdin.setEncoding('utf8');
|
|
179
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
180
|
+
process.stdin.on('end', () => {
|
|
181
|
+
let input = {};
|
|
182
|
+
try { input = JSON.parse(raw || '{}'); } catch { input = {}; }
|
|
183
|
+
const sessionId = String(input.session_id || '');
|
|
184
|
+
const source = String(input.source || 'startup');
|
|
185
|
+
try {
|
|
186
|
+
const result = syncOnce();
|
|
187
|
+
const payload = buildPayload(result.applied, result.modelAfter);
|
|
188
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logEvent({
|
|
191
|
+
event: 'sessionstart-model-sync-error',
|
|
192
|
+
sessionId: sessionId || null,
|
|
193
|
+
source,
|
|
194
|
+
error: err && err.message ? err.message : String(err),
|
|
195
|
+
});
|
|
196
|
+
// Advisory hook — never block session start on a sync failure.
|
|
197
|
+
process.stdout.write('{}\n');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
run();
|
package/package.json
CHANGED