pinomad 0.8.0 → 0.9.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/README.md CHANGED
@@ -26,6 +26,8 @@ pinomad link # in another terminal: prints a browser link to the web clie
26
26
 
27
27
  Sign in to a model provider under Settings → Providers: a subscription you already pay for (Claude, ChatGPT, Copilot, …), an API key, or your own OpenAI- or Anthropic-compatible endpoint. To click around without one, `pinomad host --faux "hi"` runs a scripted fake model.
28
28
 
29
+ To reach the host from your Mac and iPhone anywhere without running a server, [use the official server](https://pinomad.si/docs/official-server/) at app.pinomad.si.
30
+
29
31
  ## Documentation
30
32
 
31
33
  User docs live on **[pinomad.si/docs](https://pinomad.si/docs/)**: [getting started](https://pinomad.si/docs/) (connecting your other devices, keeping the host running, clients) and [what the agent can do](https://pinomad.si/docs/features/). Usage, remote access, and the service reference are in Chinese for now: [中文文档](https://pinomad.si/zh/docs/).
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  packagedVersion
3
- } from "./chunk-p56xf1r8.js";
3
+ } from "./chunk-vyvyaavr.js";
4
4
  import {
5
5
  hostAddress
6
6
  } from "./chunk-ngf1zatm.js";
@@ -9,7 +9,7 @@ import {
9
9
  loadRelayKey,
10
10
  readServerConfig,
11
11
  removeServerConfig
12
- } from "./chunk-r2ssa2ex.js";
12
+ } from "./chunk-5gp9svzs.js";
13
13
  import {
14
14
  relayHostId
15
15
  } from "./chunk-khcpv54k.js";
@@ -18,10 +18,10 @@ import {
18
18
  restartService,
19
19
  serviceManager,
20
20
  withoutRelayArgs
21
- } from "./chunk-73vxwjs7.js";
21
+ } from "./chunk-76pvsg2d.js";
22
22
  import"./chunk-kzbzacz9.js";
23
- import"./chunk-p56xf1r8.js";
24
- import"./chunk-a4b61tx5.js";
23
+ import"./chunk-vyvyaavr.js";
24
+ import"./chunk-4sepcfpy.js";
25
25
  import"./chunk-jgd38rna.js";
26
26
  import"./chunk-sygeb9qb.js";
27
27
  import {
@@ -2022,6 +2022,8 @@ class RemoteClient {
2022
2022
  }
2023
2023
  }
2024
2024
  #snapshot(stream, endSatisfies = false) {
2025
+ if (!this.#wanted.has(stream))
2026
+ return Promise.resolve();
2025
2027
  return new Promise((resolve, reject) => {
2026
2028
  const waiters = this.#snapshotWaiters.get(stream) ?? [];
2027
2029
  waiters.push({ resolve, reject, endSatisfies });
@@ -2044,9 +2046,10 @@ class RemoteClient {
2044
2046
  async#acquire(id) {
2045
2047
  const missing = this.#conversationStreams(id).filter((stream) => !this.#wanted.has(stream));
2046
2048
  const stale = this.#conversationStreams(id).filter((stream) => this.#wanted.has(stream) && !this.#fresh.has(stream));
2049
+ for (const stream of missing)
2050
+ this.#wanted.add(stream);
2047
2051
  const shown = Promise.all([...missing, ...stale].map((stream) => this.#snapshot(stream)));
2048
2052
  for (const stream of missing) {
2049
- this.#wanted.add(stream);
2050
2053
  if (this.#helloReceived)
2051
2054
  this.#subscribe(stream);
2052
2055
  }
@@ -2060,6 +2063,8 @@ class RemoteClient {
2060
2063
  this.#wanted.delete(stream);
2061
2064
  this.#values.delete(stream);
2062
2065
  this.#fresh.delete(stream);
2066
+ for (const waiter of this.#snapshotWaiters.get(stream)?.splice(0) ?? [])
2067
+ waiter.resolve();
2063
2068
  this.#send({ type: "unsubscribe", stream });
2064
2069
  }
2065
2070
  }
@@ -2814,6 +2819,7 @@ class RemoteClient {
2814
2819
  }),
2815
2820
  switchConversation: (id) => this.#command(() => this.#switch(id)),
2816
2821
  showSide: (id) => this.#command(() => this.#showSide(id)),
2822
+ clearConversation: () => this.#command(() => this.#clear()),
2817
2823
  fork: (entryId, prompt, removeTools, options) => this.#command(async () => {
2818
2824
  const { conversationId: forked } = await this.#call("fork", {
2819
2825
  conversationId: conversationId(),
@@ -2956,6 +2962,10 @@ class RemoteClient {
2956
2962
  this.#release(previous, this.#side);
2957
2963
  this.#refresh();
2958
2964
  }
2965
+ async#clear() {
2966
+ this.#unshow();
2967
+ await this.#showSide(undefined);
2968
+ }
2959
2969
  async#showSide(id) {
2960
2970
  const previous = this.#side;
2961
2971
  if (id === previous)
@@ -15,7 +15,8 @@ var DevicesDoc = defineDoc({
15
15
  kind: "pinomad.devices",
16
16
  version: 1,
17
17
  scope: "session",
18
- initial: () => ({ devices: [] })
18
+ initial: () => ({ devices: [] }),
19
+ checkpointWhen: () => false
19
20
  });
20
21
  async function ensureDevices(harness, context) {
21
22
  await harness.commit(async (tx) => void await tx.doc(DevicesDoc), context);
@@ -24,7 +25,8 @@ var RevocationsDoc = defineDoc({
24
25
  kind: "pinomad.revocations",
25
26
  version: 1,
26
27
  scope: "session",
27
- initial: () => ({ revoked: [] })
28
+ initial: () => ({ revoked: [] }),
29
+ checkpointWhen: () => false
28
30
  });
29
31
  async function admitDevice(harness, input, context) {
30
32
  return await harness.commit(async (tx) => {
@@ -5,11 +5,11 @@ import {
5
5
  commandHint,
6
6
  hostEntry,
7
7
  packagedVersion
8
- } from "./chunk-p56xf1r8.js";
8
+ } from "./chunk-vyvyaavr.js";
9
9
  import {
10
10
  ProtocolMismatchError,
11
11
  connectRemoteDurable
12
- } from "./chunk-a4b61tx5.js";
12
+ } from "./chunk-4sepcfpy.js";
13
13
  import {
14
14
  DEFAULT_DATA_DIR,
15
15
  hostAddress
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  serveWebClient
4
- } from "./chunk-r6w5aqkw.js";
4
+ } from "./chunk-wb10efyq.js";
5
5
  import {
6
6
  Published,
7
7
  constantValue,
@@ -21,7 +21,7 @@ import {
21
21
  readServerBinding,
22
22
  readServerConfig,
23
23
  resolveRelayOrigin
24
- } from "./chunk-r2ssa2ex.js";
24
+ } from "./chunk-5gp9svzs.js";
25
25
  import {
26
26
  relayHostId,
27
27
  relayWsBase,
@@ -30,7 +30,7 @@ import {
30
30
  import {
31
31
  packagedVersion,
32
32
  webRoot
33
- } from "./chunk-p56xf1r8.js";
33
+ } from "./chunk-vyvyaavr.js";
34
34
  import {
35
35
  ATTACHMENT_TTL_MS,
36
36
  INVALID_FRAME_CLOSE_CODE,
@@ -153,7 +153,8 @@ var QuestionDoc = defineDoc({
153
153
  scope: "conversation",
154
154
  history: "latest",
155
155
  fork: "initial",
156
- initial: () => ({ requests: [] })
156
+ initial: () => ({ requests: [] }),
157
+ checkpointWhen: () => false
157
158
  });
158
159
  var resolutionOf = (value, requestId) => value?.requests.find((request) => request.id === requestId)?.resolution;
159
160
  var createWait = (tx, owner, requestId) => tx.createTask(QuestionWait, { requestId }, { ownership: { kind: "task", taskId: owner } });
@@ -472,7 +473,8 @@ var RoleDoc = defineDoc2({
472
473
  scope: "conversation",
473
474
  history: "rewindable",
474
475
  fork: "asOf",
475
- initial: () => ({})
476
+ initial: () => ({}),
477
+ checkpointWhen: () => false
476
478
  });
477
479
 
478
480
  // apps/host/src/organization.ts
@@ -480,7 +482,8 @@ var IndexDoc = defineDoc3({
480
482
  kind: "pinomad.index",
481
483
  version: 1,
482
484
  scope: "session",
483
- initial: () => ({ projects: [], conversations: [] })
485
+ initial: () => ({ projects: [], conversations: [] }),
486
+ checkpointWhen: () => false
484
487
  });
485
488
  async function ensureIndex(harness, context) {
486
489
  await harness.commit(async (tx) => {
@@ -1273,7 +1276,8 @@ var SubagentDoc = defineDoc4({
1273
1276
  scope: "conversation",
1274
1277
  history: "latest",
1275
1278
  fork: "initial",
1276
- initial: () => ({})
1279
+ initial: () => ({}),
1280
+ checkpointWhen: () => false
1277
1281
  });
1278
1282
 
1279
1283
  // apps/host/src/extensions/subagent.ts
@@ -1423,7 +1427,8 @@ var TodoDoc = defineDoc5({
1423
1427
  scope: "conversation",
1424
1428
  history: "rewindable",
1425
1429
  fork: "asOf",
1426
- initial: () => ({ items: [] })
1430
+ initial: () => ({ items: [] }),
1431
+ checkpointWhen: () => false
1427
1432
  });
1428
1433
  var todoWrite = defineTool3({
1429
1434
  name: "todo_write",
@@ -1760,6 +1765,7 @@ function nodeRtcFactory(loaded) {
1760
1765
  let settled = false;
1761
1766
  let deadline;
1762
1767
  let pollTimer;
1768
+ let commitTimer;
1763
1769
  let resolveAnswer;
1764
1770
  let rejectAnswer;
1765
1771
  const answer = new Promise((resolve4, reject) => {
@@ -1782,6 +1788,10 @@ function nodeRtcFactory(loaded) {
1782
1788
  timers.clearTimeout(pollTimer);
1783
1789
  pollTimer = undefined;
1784
1790
  }
1791
+ if (commitTimer !== undefined) {
1792
+ timers.clearTimeout(commitTimer);
1793
+ commitTimer = undefined;
1794
+ }
1785
1795
  };
1786
1796
  const fail = (error) => {
1787
1797
  if (settled)
@@ -1794,12 +1804,18 @@ function nodeRtcFactory(loaded) {
1794
1804
  rejectAnswer(error);
1795
1805
  };
1796
1806
  const commit = () => {
1797
- if (settled || localSdp === undefined)
1807
+ if (settled)
1808
+ return;
1809
+ const local = pc.localDescription();
1810
+ if (local !== null)
1811
+ localSdp = local.sdp;
1812
+ if (localSdp === undefined)
1798
1813
  return;
1799
1814
  for (const candidate of sdpCandidates(localSdp))
1800
1815
  addCandidate(candidate);
1801
1816
  if (candidates.size === 0) {
1802
- fail(new Error("no usable local ICE candidates"));
1817
+ if (deadlineReached)
1818
+ fail(new Error("no usable local ICE candidates"));
1803
1819
  return;
1804
1820
  }
1805
1821
  settled = true;
@@ -1807,18 +1823,37 @@ function nodeRtcFactory(loaded) {
1807
1823
  resolveAnswer(withRtcCandidates(localSdp, [...candidates]));
1808
1824
  };
1809
1825
  const maybeCommit = () => {
1826
+ if (commitTimer !== undefined && !deadlineReached)
1827
+ return;
1810
1828
  if (localSdp !== undefined && (gatheringDone || deadlineReached))
1811
1829
  commit();
1812
1830
  };
1831
+ const scheduleCommit = () => {
1832
+ if (settled)
1833
+ return;
1834
+ if (commitTimer !== undefined)
1835
+ timers.clearTimeout(commitTimer);
1836
+ commitTimer = timers.setTimeout(() => {
1837
+ commitTimer = undefined;
1838
+ maybeCommit();
1839
+ }, 0);
1840
+ };
1813
1841
  pc.onLocalDescription((sdp) => {
1814
1842
  localSdp = sdp;
1815
- maybeCommit();
1843
+ if (gatheringDone)
1844
+ scheduleCommit();
1845
+ else
1846
+ maybeCommit();
1847
+ });
1848
+ pc.onLocalCandidate((candidate) => {
1849
+ addCandidate(candidate);
1850
+ if (gatheringDone)
1851
+ scheduleCommit();
1816
1852
  });
1817
- pc.onLocalCandidate((candidate) => addCandidate(candidate));
1818
1853
  pc.onGatheringStateChange((state) => {
1819
1854
  if (state === "complete") {
1820
1855
  gatheringDone = true;
1821
- maybeCommit();
1856
+ scheduleCommit();
1822
1857
  }
1823
1858
  });
1824
1859
  pc.onStateChange((state) => {
@@ -1855,7 +1890,10 @@ function nodeRtcFactory(loaded) {
1855
1890
  const local = pc.localDescription();
1856
1891
  if (local !== null) {
1857
1892
  localSdp = local.sdp;
1858
- maybeCommit();
1893
+ if (gatheringDone)
1894
+ scheduleCommit();
1895
+ else
1896
+ maybeCommit();
1859
1897
  }
1860
1898
  if (!settled)
1861
1899
  pollTimer = timers.setTimeout(poll, 25);
@@ -2699,7 +2737,8 @@ var TitleDoc = defineDoc6({
2699
2737
  scope: "conversation",
2700
2738
  history: "latest",
2701
2739
  fork: "initial",
2702
- initial: () => ({})
2740
+ initial: () => ({}),
2741
+ checkpointWhen: () => false
2703
2742
  });
2704
2743
 
2705
2744
  // apps/host/src/auto-title.ts
@@ -2875,7 +2914,8 @@ var PushDoc = defineDoc7({
2875
2914
  version: 2,
2876
2915
  scope: "session",
2877
2916
  initial: () => ({ registrations: [] }),
2878
- migrate: () => ({ registrations: [] })
2917
+ migrate: () => ({ registrations: [] }),
2918
+ checkpointWhen: () => false
2879
2919
  });
2880
2920
  var APNS_TOKEN_PATTERN = /^(?:[0-9a-f]{2})+$/;
2881
2921
  var APNS_TOKEN_MAX_CHARS = 256;
@@ -3331,10 +3371,7 @@ async function startGateway(options) {
3331
3371
  return parsed.endpointSet;
3332
3372
  };
3333
3373
  const currentEndpointSet = () => checkedEndpointSet(currentAdvertisement().endpoints);
3334
- const register = (connection, devicePublicKey, joined) => {
3335
- if (joined?.source === "account") {
3336
- broadcast("info", `New device "${joined.name}" joined through your account`);
3337
- }
3374
+ const register = (connection, devicePublicKey) => {
3338
3375
  const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, imageVariants, {
3339
3376
  devicePublicKey,
3340
3377
  rtcHello,
@@ -6212,7 +6249,7 @@ var CUSTOM_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
6212
6249
  var LOGIN_TIMEOUT_MS = 15 * 60000;
6213
6250
  var FINISHED_LOGIN_TTL_MS = 60000;
6214
6251
  var ENDPOINT_TIMEOUT_MS = 1e4;
6215
- var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
6252
+ var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && a.images === b.images && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
6216
6253
  var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
6217
6254
  async function readModelsFile(path) {
6218
6255
  let text;
@@ -6483,6 +6520,8 @@ async function openModelAuth(runtime, options) {
6483
6520
  const ids = [...new Set(input.models.map((model) => model.trim()).filter((model) => model !== ""))];
6484
6521
  if (ids.length === 0)
6485
6522
  throw new Error("Add at least one model");
6523
+ const key = typeof apiKey === "string" && apiKey !== "" ? apiKey : apiKey !== null && custom.has(input.id) ? (await runtime.getAuth(input.id))?.auth.apiKey : undefined;
6524
+ const reported = new Map((await fetchEndpointModels(input.api, input.baseUrl, key).catch(() => [])).map((model) => [model.id, model]));
6486
6525
  await updateModelsFile((file) => {
6487
6526
  const previous = file.providers[input.id] ?? {};
6488
6527
  const previousModels = new Map((Array.isArray(previous.models) ? previous.models : []).map((model) => [model.id, model]));
@@ -6491,7 +6530,7 @@ async function openModelAuth(runtime, options) {
6491
6530
  name,
6492
6531
  api: input.api,
6493
6532
  baseUrl: input.baseUrl,
6494
- models: ids.map((id) => previousModels.get(id) ?? { id })
6533
+ models: ids.map((id) => ({ ...reported.get(id), ...previousModels.get(id) ?? { id } }))
6495
6534
  };
6496
6535
  });
6497
6536
  await runtime.refresh({ providers: [input.id], allowNetwork: false });
@@ -6518,7 +6557,7 @@ async function openModelAuth(runtime, options) {
6518
6557
  if (key === undefined && input.providerId !== undefined && custom.has(input.providerId)) {
6519
6558
  key = (await runtime.getAuth(input.providerId))?.auth.apiKey;
6520
6559
  }
6521
- return fetchEndpointModels(input.api, input.baseUrl, key);
6560
+ return (await fetchEndpointModels(input.api, input.baseUrl, key)).map((model) => model.id);
6522
6561
  },
6523
6562
  close() {
6524
6563
  closed = true;
@@ -6556,10 +6595,34 @@ async function fetchEndpointModels(api, baseUrl, key) {
6556
6595
  if (!response.ok)
6557
6596
  throw new Error(`${url.host} answered ${response.status} for /models`);
6558
6597
  const body = await response.json();
6559
- const ids = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => typeof model.name === "string" ? [model.name.replace(/^models\//, "")] : []) : (body.data ?? []).flatMap((model) => typeof model.id === "string" ? [model.id] : []);
6560
- if (ids.length === 0)
6598
+ const models = api === "google-generative-ai" ? (body.models ?? []).flatMap((model) => typeof model.name === "string" ? [
6599
+ endpointModel(model.name.replace(/^models\//, ""), {
6600
+ contextWindow: positive(model.inputTokenLimit),
6601
+ maxTokens: positive(model.outputTokenLimit),
6602
+ reasoning: model.thinking === true,
6603
+ image: false
6604
+ })
6605
+ ] : []) : (body.data ?? []).flatMap((model) => typeof model.id === "string" ? [
6606
+ endpointModel(model.id, {
6607
+ contextWindow: positive(model.context_window) ?? positive(model.context_length) ?? positive(model.max_input_tokens),
6608
+ maxTokens: positive(model.max_output_tokens),
6609
+ reasoning: model.reasoning === true,
6610
+ image: Array.isArray(model.modalities?.input) && model.modalities.input.includes("image")
6611
+ })
6612
+ ] : []);
6613
+ if (models.length === 0)
6561
6614
  throw new Error(`${url.host} listed no models`);
6562
- return ids;
6615
+ return models;
6616
+ }
6617
+ var positive = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
6618
+ function endpointModel(id, reported) {
6619
+ return {
6620
+ id,
6621
+ ...reported.contextWindow === undefined ? {} : { contextWindow: reported.contextWindow },
6622
+ ...reported.maxTokens === undefined ? {} : { maxTokens: reported.maxTokens },
6623
+ ...reported.reasoning ? { reasoning: true } : {},
6624
+ ...reported.image ? { input: ["text", "image"] } : {}
6625
+ };
6563
6626
  }
6564
6627
 
6565
6628
  // apps/host/src/main.ts
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  main,
4
4
  restartService
5
- } from "./chunk-73vxwjs7.js";
5
+ } from "./chunk-76pvsg2d.js";
6
6
  import"./chunk-kzbzacz9.js";
7
- import"./chunk-p56xf1r8.js";
8
- import"./chunk-a4b61tx5.js";
7
+ import"./chunk-vyvyaavr.js";
8
+ import"./chunk-4sepcfpy.js";
9
9
  import"./chunk-jgd38rna.js";
10
10
  import"./chunk-sygeb9qb.js";
11
11
  import"./chunk-ngf1zatm.js";
@@ -2,14 +2,14 @@
2
2
  import {
3
3
  restartService,
4
4
  serviceManager
5
- } from "./chunk-73vxwjs7.js";
5
+ } from "./chunk-76pvsg2d.js";
6
6
  import"./chunk-kzbzacz9.js";
7
7
  import {
8
8
  globalPrefixOf,
9
9
  packageDir,
10
10
  packagedVersion
11
- } from "./chunk-p56xf1r8.js";
12
- import"./chunk-a4b61tx5.js";
11
+ } from "./chunk-vyvyaavr.js";
12
+ import"./chunk-4sepcfpy.js";
13
13
  import"./chunk-jgd38rna.js";
14
14
  import"./chunk-sygeb9qb.js";
15
15
  import {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  connectRemoteDurable
3
- } from "./chunk-a4b61tx5.js";
3
+ } from "./chunk-4sepcfpy.js";
4
4
  import"./chunk-jgd38rna.js";
5
5
  import {
6
6
  isLoopbackUrl