anon-pi 0.5.0 → 0.7.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/src/anon-pi.ts CHANGED
@@ -90,12 +90,30 @@ export const CONTAINER_STAGE_DIR = '/opt/anon-pi-seed/agent';
90
90
  */
91
91
  export const CONTAINER_MODELS_SEED = '/anon-pi-seed/models.json';
92
92
 
93
+ /**
94
+ * Where anon-pi mounts the generated settings SEED (the local-model default
95
+ * selection: defaultProvider/defaultModel/enabledModels) read-only, so the
96
+ * first-launch seed can MERGE it into the fresh home's settings.json (never
97
+ * clobbering image-staged packages/extensions).
98
+ */
99
+ export const CONTAINER_SETTINGS_SEED = '/anon-pi-seed/settings.json';
100
+
93
101
  /** Marker file written into the agent dir after seeding; holds the seed version. */
94
102
  export const SEED_MARKER = '.anon-pi-seed';
95
103
 
96
- /** The single file the host-side seed carries: pi's model/provider registry. */
104
+ /** The file the host-side seed carries: pi's model/provider registry. */
97
105
  export const MODELS_FILE = 'models.json';
98
106
 
107
+ /** pi's settings file (holds defaultModel/defaultProvider/enabledModels + more). */
108
+ export const SETTINGS_FILE = 'settings.json';
109
+
110
+ /**
111
+ * The settings SEED file anon-pi writes next to a machine (the local-model
112
+ * selection fragment). Distinct name so it never collides with a real
113
+ * settings.json; the seed MERGES it into the home's settings on first launch.
114
+ */
115
+ export const SETTINGS_SEED_FILE = 'settings-seed.json';
116
+
99
117
  /**
100
118
  * containerRunCmd builds the container command: on a FRESH home (no seed
101
119
  * marker), promote the image's staged defaults + the mounted models.json into
@@ -145,6 +163,12 @@ export interface AnonPiEnv {
145
163
  llmDirect?: string;
146
164
  /** XDG_CONFIG_HOME, if set (used to derive the default anon-pi home). */
147
165
  xdgConfigHome?: string;
166
+ /**
167
+ * The host pi agent dir (PI_CODING_AGENT_DIR), used ONLY to locate the host
168
+ * `~/.pi/agent/models.json` that `init` reads the matching local provider
169
+ * from. Defaults to ~/.pi/agent. Never written.
170
+ */
171
+ piAgentDir?: string;
148
172
  /**
149
173
  * Absolute path to the Dockerfile.pi that ships with anon-pi, used only to
150
174
  * make the missing-image error's build command concrete. cli.ts resolves it
@@ -213,6 +237,67 @@ export function machineJsonPath(env: AnonPiEnv, name: string): string {
213
237
  return join(machineDir(env, name), 'machine.json');
214
238
  }
215
239
 
240
+ /**
241
+ * The GLOBAL local-model models.json seed: `<home>/models.json`. The local model
242
+ * is a WORKSPACE-level thing (config.json holds ONE global `llm`, the single
243
+ * `--allow-direct` hole shared by every machine), so its generated models.json
244
+ * lives once at the workspace root and seeds EVERY machine's fresh home. A
245
+ * machine may override it with its own `machines/<M>/models.json` (see
246
+ * resolveModelsSeedPath) for the rare "this machine uses a different local
247
+ * model" case; by default all machines share this one.
248
+ */
249
+ export function globalModelsSeedPath(env: AnonPiEnv): string {
250
+ return join(resolveAnonPiHome(env), MODELS_FILE);
251
+ }
252
+
253
+ /** The GLOBAL settings seed (the default-model selection): `<home>/settings-seed.json`. */
254
+ export function globalSettingsSeedPath(env: AnonPiEnv): string {
255
+ return join(resolveAnonPiHome(env), SETTINGS_SEED_FILE);
256
+ }
257
+
258
+ /** A machine's OPTIONAL per-machine models.json override: `machines/<M>/models.json`. */
259
+ export function machineModelsSeedPath(env: AnonPiEnv, name: string): string {
260
+ return join(machineDir(env, name), MODELS_FILE);
261
+ }
262
+
263
+ /** A machine's OPTIONAL per-machine settings seed override: `machines/<M>/settings-seed.json`. */
264
+ export function machineSettingsSeedPath(env: AnonPiEnv, name: string): string {
265
+ return join(machineDir(env, name), SETTINGS_SEED_FILE);
266
+ }
267
+
268
+ /**
269
+ * PURE: resolve the models.json SEED path for a machine, per-machine override
270
+ * first, else the global one. `exists` is injected (the CLI passes existsSync)
271
+ * so this stays pure/testable. Returns the chosen path, or undefined when
272
+ * NEITHER exists (a machine with no local-model seed at all — pi starts with no
273
+ * models). The precedence is: `machines/<M>/models.json` (a deliberate
274
+ * per-machine override) > `<home>/models.json` (the global default).
275
+ */
276
+ export function resolveModelsSeedPath(
277
+ env: AnonPiEnv,
278
+ machine: string,
279
+ exists: (p: string) => boolean,
280
+ ): string | undefined {
281
+ const perMachine = machineModelsSeedPath(env, machine);
282
+ if (exists(perMachine)) return perMachine;
283
+ const global = globalModelsSeedPath(env);
284
+ if (exists(global)) return global;
285
+ return undefined;
286
+ }
287
+
288
+ /** PURE: the settings-seed path for a machine (per-machine override > global), or undefined. */
289
+ export function resolveSettingsSeedPath(
290
+ env: AnonPiEnv,
291
+ machine: string,
292
+ exists: (p: string) => boolean,
293
+ ): string | undefined {
294
+ const perMachine = machineSettingsSeedPath(env, machine);
295
+ if (exists(perMachine)) return perMachine;
296
+ const global = globalSettingsSeedPath(env);
297
+ if (exists(global)) return global;
298
+ return undefined;
299
+ }
300
+
216
301
  /** The sessions dirname pi keeps its per-cwd conversation dirs under (in the agent dir). */
217
302
  export const SESSIONS_DIRNAME = 'sessions';
218
303
 
@@ -628,6 +713,12 @@ export interface LaunchIntent {
628
713
  * starts with no models; you add them in-session).
629
714
  */
630
715
  modelsSeed?: string;
716
+ /**
717
+ * The settings SEED to mount read-only for the first-launch seed (the
718
+ * local-model default selection, e.g. <machine-dir>/settings-seed.json).
719
+ * Omitted => no settings seed (no default model is pre-selected).
720
+ */
721
+ settingsSeed?: string;
631
722
  /** The seed version stamped into a fresh home's marker. Default SEED_VERSION. */
632
723
  seedVersion?: string;
633
724
  }
@@ -903,6 +994,12 @@ export function resolveRunPlan(
903
994
  if (modelsSeed !== undefined) {
904
995
  netcageArgs.push('-v', `${modelsSeed}:${CONTAINER_MODELS_SEED}:ro`);
905
996
  }
997
+ // The generated settings SEED (the local-model default selection) read-only,
998
+ // when present; the seed-if-fresh MERGES it into the home's settings.json.
999
+ const settingsSeed = nonEmpty(intent.settingsSeed);
1000
+ if (settingsSeed !== undefined) {
1001
+ netcageArgs.push('-v', `${settingsSeed}:${CONTAINER_SETTINGS_SEED}:ro`);
1002
+ }
906
1003
  // The jail cwd.
907
1004
  netcageArgs.push('-w', cwd);
908
1005
  // The image, then the command: a marker-guarded seed-if-fresh then the tool.
@@ -943,11 +1040,25 @@ export function resolveRunPlan(
943
1040
  function containerSeedThen(seedVersion: string, exec: string): string {
944
1041
  const agent = CONTAINER_AGENT_DIR;
945
1042
  const marker = `${agent}/${SEED_MARKER}`;
1043
+ const settings = `${agent}/${SETTINGS_FILE}`;
1044
+ // Merge the settings SEED (the local-model default selection) into the home's
1045
+ // settings.json, overwriting ONLY the three selection keys so any staged
1046
+ // packages/extensions survive. Done with a node one-liner (pi is a node app,
1047
+ // so node is on PATH). The seed path + target are shell-quoted single args.
1048
+ const mergeSettings =
1049
+ `{ [ -f "${CONTAINER_SETTINGS_SEED}" ] && node -e '` +
1050
+ `const fs=require("fs");` +
1051
+ `const seed=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));` +
1052
+ `let cur={};try{cur=JSON.parse(fs.readFileSync(process.argv[2],"utf8"))}catch(e){}` +
1053
+ `cur.defaultProvider=seed.defaultProvider;cur.defaultModel=seed.defaultModel;cur.enabledModels=seed.enabledModels;` +
1054
+ `fs.writeFileSync(process.argv[2],JSON.stringify(cur,null,"\\t")+"\\n")` +
1055
+ `' "${CONTAINER_SETTINGS_SEED}" "${settings}" || true; }`;
946
1056
  return (
947
1057
  `mkdir -p "${agent}" && ` +
948
1058
  `if [ ! -f "${marker}" ]; then ` +
949
1059
  `{ [ -d "${CONTAINER_STAGE_DIR}" ] && cp -a "${CONTAINER_STAGE_DIR}/." "${agent}/" || true; } && ` +
950
1060
  `{ [ -f "${CONTAINER_MODELS_SEED}" ] && cp "${CONTAINER_MODELS_SEED}" "${agent}/${MODELS_FILE}" || true; } && ` +
1061
+ `${mergeSettings} && ` +
951
1062
  `printf '%s\\n' "${seedVersion}" > "${marker}"; ` +
952
1063
  `fi && ` +
953
1064
  `${exec}`
@@ -1347,31 +1458,275 @@ export const LOCAL_PROVIDER_API = 'openai-completions';
1347
1458
  export const LOCAL_PROVIDER_API_KEY = 'none';
1348
1459
 
1349
1460
  /**
1350
- * PURE: synthesize a barebones pi `models.json` from a single `llm` endpoint
1351
- * (a URL, `ip:port`, or bare ip). It normalises the endpoint with `hostPortKey`
1352
- * (drops scheme/path/user:pass@, lowercases) and returns a models.json carrying
1353
- * exactly ONE local provider pointed at that endpoint.
1354
- *
1355
- * This REPLACES the old `import`-from-host-models.json flow: it reads NO host pi
1356
- * config, so no other provider, no paid API key, no session identity can leak
1357
- * into the seed. Endpoint in -> object out; `init` / seed-if-fresh write the
1358
- * result into the machine home.
1461
+ * apiKey values that are NOT real secrets (safe to carry into the anonymized
1462
+ * seed verbatim). Anything else is treated as a REAL secret: `init` refuses to
1463
+ * seed it (which would put a host credential into the anon home) unless the
1464
+ * operator passes `--force-allow-local-llm-api-key`.
1465
+ */
1466
+ export const BENIGN_API_KEYS: ReadonlySet<string> = new Set([
1467
+ '',
1468
+ 'none',
1469
+ 'ollama',
1470
+ 'no-key',
1471
+ 'nokey',
1472
+ 'local',
1473
+ 'dummy',
1474
+ 'sk-no-key-required',
1475
+ ]);
1476
+
1477
+ /** PURE: whether an apiKey looks like a REAL secret (i.e. not in the benign set). */
1478
+ export function apiKeyLooksReal(apiKey: string | undefined): boolean {
1479
+ if (apiKey === undefined) return false;
1480
+ return !BENIGN_API_KEYS.has(apiKey.trim().toLowerCase());
1481
+ }
1482
+
1483
+ /**
1484
+ * A pi model entry as anon-pi seeds it for the local provider. pi keys a model
1485
+ * by `id`; `name` is the display label and `cost` is all-zero (a LAN model is
1486
+ * free). A "server"-sourced entry is minimal (id/name/cost); a "configured"
1487
+ * entry (imported from the host models.json) preserves whatever extra fields it
1488
+ * carried (`contextWindow`, `maxTokens`, `reasoning`, `input`, ...) via the
1489
+ * index signature.
1490
+ */
1491
+ export interface GeneratedModel {
1492
+ id: string;
1493
+ name: string;
1494
+ cost?: {input: number; output: number; cacheRead: number; cacheWrite: number};
1495
+ [k: string]: unknown;
1496
+ }
1497
+
1498
+ /**
1499
+ * PURE: a candidate model for the `init` picker. `configured` means it came from
1500
+ * the host `~/.pi/agent/models.json` provider that matches the endpoint (a
1501
+ * well-tuned entry with its real config); otherwise it was only reported by the
1502
+ * endpoint's `/v1/models` (a bare id we synthesize a minimal entry for). The
1503
+ * picker marks configured ones so the user knows which are more likely correct.
1504
+ */
1505
+ export interface ModelCandidate {
1506
+ id: string;
1507
+ configured: boolean;
1508
+ /** The full pi model entry to seed (rich for configured, minimal otherwise). */
1509
+ entry: GeneratedModel;
1510
+ }
1511
+
1512
+ /**
1513
+ * PURE: turn a discovered model `id` into a minimal-but-valid pi model entry.
1514
+ * `name` defaults to the id; a LAN model is free, so every cost is 0.
1515
+ */
1516
+ export function localModelEntry(id: string): GeneratedModel {
1517
+ return {
1518
+ id,
1519
+ name: id,
1520
+ cost: {input: 0, output: 0, cacheRead: 0, cacheWrite: 0},
1521
+ };
1522
+ }
1523
+
1524
+ /**
1525
+ * PURE: extract the model ids from a parsed OpenAI-compatible `/v1/models`
1526
+ * response (`{ data: [{ id }, ...] }`, as llama.cpp / vLLM / LM Studio serve).
1527
+ * Tolerates a bare array, a `models` key, missing/garbage input (returns []), so
1528
+ * `init` can feed whatever the endpoint returned straight in.
1529
+ */
1530
+ export function parseModelsListing(raw: unknown): string[] {
1531
+ const rows: unknown[] = Array.isArray(raw)
1532
+ ? raw
1533
+ : raw && typeof raw === 'object'
1534
+ ? (((raw as Record<string, unknown>).data as unknown[]) ??
1535
+ ((raw as Record<string, unknown>).models as unknown[]) ??
1536
+ [])
1537
+ : [];
1538
+ if (!Array.isArray(rows)) return [];
1539
+ const ids: string[] = [];
1540
+ for (const r of rows) {
1541
+ if (typeof r === 'string') {
1542
+ if (r.trim() !== '') ids.push(r.trim());
1543
+ } else if (r && typeof r === 'object') {
1544
+ const id = (r as Record<string, unknown>).id;
1545
+ if (typeof id === 'string' && id.trim() !== '') ids.push(id.trim());
1546
+ }
1547
+ }
1548
+ return ids;
1549
+ }
1550
+
1551
+ /** The result of scanning a host models.json for the endpoint's provider. */
1552
+ export interface HostProviderMatch {
1553
+ /** The matching provider's models as full pi entries (verbatim host config). */
1554
+ models: GeneratedModel[];
1555
+ /** The matching provider's apiKey (verbatim), for the benign/real check. */
1556
+ apiKey?: string;
1557
+ /** True iff that apiKey looks like a REAL secret (init refuses without --force). */
1558
+ apiKeyLooksReal: boolean;
1559
+ }
1560
+
1561
+ /**
1562
+ * PURE: find, in a parsed host `~/.pi/agent/models.json`, the provider whose
1563
+ * `baseUrl` points at `llmEndpoint` (matched via hostPortKey), and return ONLY
1564
+ * that provider's models + apiKey. This is the anonymity-critical scoping: the
1565
+ * ONLY provider considered is the one served by the `--allow-direct` endpoint,
1566
+ * so no other provider (etherplay/google/a paid API) — and no other provider's
1567
+ * key — can ever enter the seed. Returns undefined when no provider matches.
1359
1568
  *
1360
- * The baseUrl is `http://<host[:port]>/v1` (the OpenAI-compatible convention the
1361
- * completions api uses); the api dialect + benign apiKey are the LOCAL_PROVIDER_*
1362
- * constants.
1569
+ * The `--allow-direct` target and this match both go through hostPortKey, so a
1570
+ * URL / ip:port / bare-ip host config all match the same endpoint.
1571
+ */
1572
+ export function pickLocalProviderModels(
1573
+ hostModels: PiModelsFile,
1574
+ llmEndpoint: string,
1575
+ ): HostProviderMatch | undefined {
1576
+ const providers = hostModels.providers ?? {};
1577
+ const want = hostPortKey(llmEndpoint);
1578
+ for (const p of Object.values(providers)) {
1579
+ if (!p || typeof p !== 'object' || !p.baseUrl) continue;
1580
+ if (hostPortKey(p.baseUrl) !== want) continue;
1581
+ const models: GeneratedModel[] = [];
1582
+ for (const m of p.models ?? []) {
1583
+ if (m && typeof m === 'object') {
1584
+ const id = (m as Record<string, unknown>).id;
1585
+ if (typeof id === 'string' && id.trim() !== '') {
1586
+ models.push({...(m as GeneratedModel), id: id.trim()});
1587
+ }
1588
+ } else if (typeof m === 'string' && m.trim() !== '') {
1589
+ models.push(localModelEntry(m.trim()));
1590
+ }
1591
+ }
1592
+ return {
1593
+ models,
1594
+ apiKey: p.apiKey,
1595
+ apiKeyLooksReal: apiKeyLooksReal(p.apiKey),
1596
+ };
1597
+ }
1598
+ return undefined;
1599
+ }
1600
+
1601
+ /**
1602
+ * PURE: merge the host-config models (rich, `configured: true`) with the
1603
+ * endpoint's live `/v1/models` ids (`configured: false` for any the host did not
1604
+ * already carry), into ONE deduped, sorted candidate list. Host config wins on
1605
+ * an id present in both (it has the real config). Every candidate here is served
1606
+ * by the endpoint, so every one is `--allow-direct`-reachable; the merge just
1607
+ * unions "what you already configured" with "what the server also offers".
1363
1608
  */
1364
- export function generateModelsJson(llmEndpoint: string): PiModelsFile {
1609
+ export function mergeModelSources(
1610
+ hostModels: readonly GeneratedModel[],
1611
+ serverIds: readonly string[],
1612
+ ): ModelCandidate[] {
1613
+ const byId = new Map<string, ModelCandidate>();
1614
+ for (const m of hostModels) {
1615
+ const id = m.id.trim();
1616
+ if (id === '') continue;
1617
+ byId.set(id, {id, configured: true, entry: {...m, id}});
1618
+ }
1619
+ for (const raw of serverIds) {
1620
+ const id = raw.trim();
1621
+ if (id === '' || byId.has(id)) continue;
1622
+ byId.set(id, {id, configured: false, entry: localModelEntry(id)});
1623
+ }
1624
+ return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
1625
+ }
1626
+
1627
+ /**
1628
+ * PURE: synthesize a pi `models.json` for the local provider from an endpoint
1629
+ * and the CHOSEN model entries. It normalises the endpoint with hostPortKey and
1630
+ * returns a models.json carrying exactly ONE provider (named LOCAL_PROVIDER_NAME
1631
+ * — a neutral name, no host fingerprint) pointed at that endpoint.
1632
+ *
1633
+ * `apiKey` defaults to the benign LOCAL_PROVIDER_API_KEY. A caller may pass the
1634
+ * host provider's real key ONLY under an explicit force flag; the benign/real
1635
+ * decision (and the refusal) lives in `init`, not here — this pure function just
1636
+ * writes what it is given.
1637
+ *
1638
+ * Accepts either full model entries (from the host config) or bare id strings
1639
+ * (which it turns into minimal entries). Empty models => a provider pointed at
1640
+ * the endpoint with no pickable model (the degraded fallback).
1641
+ */
1642
+ export function generateModelsJson(
1643
+ llmEndpoint: string,
1644
+ models: readonly (GeneratedModel | string)[] = [],
1645
+ apiKey: string = LOCAL_PROVIDER_API_KEY,
1646
+ ): PiModelsFile {
1365
1647
  const hostPort = hostPortKey(llmEndpoint);
1648
+ const entries: GeneratedModel[] = [];
1649
+ const seen = new Set<string>();
1650
+ for (const m of models) {
1651
+ const entry = typeof m === 'string' ? localModelEntry(m.trim()) : m;
1652
+ const id = entry.id.trim();
1653
+ if (id === '' || seen.has(id)) continue;
1654
+ seen.add(id);
1655
+ entries.push({...entry, id});
1656
+ }
1657
+ entries.sort((a, b) => a.id.localeCompare(b.id));
1366
1658
  const provider: PiProvider = {
1367
1659
  api: LOCAL_PROVIDER_API,
1368
- apiKey: LOCAL_PROVIDER_API_KEY,
1660
+ apiKey,
1369
1661
  baseUrl: `http://${hostPort}/v1`,
1370
- models: [],
1662
+ models: entries,
1371
1663
  };
1372
1664
  return {providers: {[LOCAL_PROVIDER_NAME]: provider}};
1373
1665
  }
1374
1666
 
1667
+ /** The pi settings.json keys anon-pi sets for the local-model default selection. */
1668
+ export interface ModelSelection {
1669
+ defaultProvider: string;
1670
+ defaultModel: string;
1671
+ enabledModels: string[];
1672
+ }
1673
+
1674
+ /**
1675
+ * PURE: the model-selection settings.json fragment for the seeded local
1676
+ * provider: `defaultProvider` = LOCAL_PROVIDER_NAME, `defaultModel` = the chosen
1677
+ * default id, `enabledModels` = `local/<id>` for each imported model (pi's
1678
+ * `<provider>/<id>` convention). The caller MERGES this into any existing
1679
+ * settings so image-staged settings (packages/extensions) are preserved.
1680
+ */
1681
+ export function generateModelSelection(
1682
+ modelIds: readonly string[],
1683
+ defaultId: string,
1684
+ ): ModelSelection {
1685
+ const ids = Array.from(
1686
+ new Set(modelIds.map((m) => m.trim()).filter((m) => m !== '')),
1687
+ ).sort((a, b) => a.localeCompare(b));
1688
+ return {
1689
+ defaultProvider: LOCAL_PROVIDER_NAME,
1690
+ defaultModel: defaultId.trim(),
1691
+ enabledModels: ids.map((id) => `${LOCAL_PROVIDER_NAME}/${id}`),
1692
+ };
1693
+ }
1694
+
1695
+ /**
1696
+ * PURE: shallow-merge the local-model selection into an existing (parsed)
1697
+ * settings.json object, returning the merged object. Only the three selection
1698
+ * keys are overwritten; every other key the user/image had (packages,
1699
+ * extensions, thinking level, ...) is preserved. `existing` undefined/garbage is
1700
+ * treated as `{}`.
1701
+ */
1702
+ export function mergeModelSelection(
1703
+ existing: unknown,
1704
+ selection: ModelSelection,
1705
+ ): Record<string, unknown> {
1706
+ const base: Record<string, unknown> =
1707
+ existing && typeof existing === 'object'
1708
+ ? {...(existing as Record<string, unknown>)}
1709
+ : {};
1710
+ base.defaultProvider = selection.defaultProvider;
1711
+ base.defaultModel = selection.defaultModel;
1712
+ base.enabledModels = selection.enabledModels;
1713
+ return base;
1714
+ }
1715
+
1716
+ /**
1717
+ * The host `~/.pi/agent/models.json` path `init` reads the matching local
1718
+ * provider from. Uses the container-less host HOME (or PI_CODING_AGENT_DIR when
1719
+ * the user relocated pi's agent dir). This is READ-ONLY (init copies only the
1720
+ * ONE matching provider's models); it is never written.
1721
+ */
1722
+ export function resolveHostModelsPath(env: AnonPiEnv): string {
1723
+ const agentDir =
1724
+ env.piAgentDir && env.piAgentDir.trim() !== ''
1725
+ ? env.piAgentDir
1726
+ : join(env.home, '.pi', 'agent');
1727
+ return join(agentDir, MODELS_FILE);
1728
+ }
1729
+
1375
1730
  /**
1376
1731
  * A pi provider entry (as it appears under models.json `providers[name]`). Only
1377
1732
  * the fields anon-pi reads are typed; the rest is preserved verbatim.
@@ -1914,6 +2269,7 @@ export function envFromProcess(
1914
2269
  image: penv.ANON_PI_IMAGE,
1915
2270
  llmDirect: penv.ANON_PI_LLM,
1916
2271
  xdgConfigHome: penv.XDG_CONFIG_HOME,
2272
+ piAgentDir: penv.PI_CODING_AGENT_DIR,
1917
2273
  dockerfilePath: shippedDockerfilePath(),
1918
2274
  webveilDockerfilePath: shippedWebveilDockerfilePath(),
1919
2275
  };