@1presence/bridge 0.13.0 → 0.15.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/dist/claude.js CHANGED
@@ -65,11 +65,14 @@ chat interface. Call them by their MCP-prefixed names (\`mcp__1presence__<name>\
65
65
  again later only if the topic clearly shifts. Without this, the user's
66
66
  conversation list shows untitled threads.
67
67
 
68
- - **ui_payload** — Emit at most ONE per turn, AFTER your reply text ends.
69
- Pass \`hints: []\` most turns (aim ~1 hint per 3 early turns, less later)
70
- and \`suggestions\`: 2–4 follow-on prompts the user can tap. Hints are
68
+ - **ui_payload** — Call exactly ONCE per turn, AFTER your reply text ends.
69
+ \`suggestions\` (2–4 tappable follow-on prompts) is REQUIRED on every turn
70
+ with a user-facing reply never skip it. \`hints\` is an independent,
71
+ sparse decision: pass \`[]\` most turns (aim ~1 hint per 3 early turns,
72
+ less later). Do not drop suggestions because there is no hint. Hints are
71
73
  end-user product coaching only — never mention models, AI vendors,
72
- engineering, or roadmap. Skip entirely on pure tool-ingestion turns.
74
+ engineering, or roadmap. The only legitimate skip is a turn with no
75
+ user-facing reply text at all.
73
76
 
74
77
  - **plan** — Show a checklist when a task has ≥3 distinct, user-visible
75
78
  steps with side effects. Call once at the start with all steps; update
@@ -224,7 +227,7 @@ function spawnClaude(params) {
224
227
  // First conversation since bridge started — announce prominently
225
228
  // so the user can confirm which model and credential is in use.
226
229
  const source = keySource === 'none' || !keySource ? 'claude.ai subscription' : keySource;
227
- const pin = (0, config_1.getBridgeModel)() ? ' (pinned via 1presence config)' : '';
230
+ const pin = (0, config_1.getBridgeModel)() ? ' (selected at startup)' : '';
228
231
  process.stdout.write(`\n model: ${model ?? 'unknown'}${pin}\n auth: ${source}\n\n`);
229
232
  modelAnnounced = true;
230
233
  }
package/dist/config.js CHANGED
@@ -2,35 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ensureModelChoice = ensureModelChoice;
4
4
  exports.getBridgeModel = getBridgeModel;
5
- const fs_1 = require("fs");
6
- const os_1 = require("os");
7
- const path_1 = require("path");
8
5
  const readline_1 = require("readline");
9
6
  const child_process_1 = require("child_process");
10
- // ─── Storage ──────────────────────────────────────────────────────────────────
7
+ // ─── In-memory model choice ───────────────────────────────────────────────────
11
8
  //
12
- // `~/.1presence/config.json` holds bridge-wide preferences that should persist
13
- // across runs but aren't sensitive (unlike auth.json). Today: model choice.
14
- const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.1presence');
15
- const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, 'config.json');
16
- let cached = null;
17
- function load() {
18
- if (cached)
19
- return cached;
20
- try {
21
- cached = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
22
- }
23
- catch {
24
- cached = {};
25
- }
26
- return cached;
27
- }
28
- function persist(c) {
29
- (0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
30
- (0, fs_1.writeFileSync)(CONFIG_FILE, JSON.stringify(c, null, 2), 'utf-8');
31
- cached = c;
32
- }
33
- // ─── Model choice ─────────────────────────────────────────────────────────────
9
+ // The bridge prompts for a model on every interactive startup. The choice is
10
+ // kept in memory for the life of the process — nothing is written to disk.
11
+ // In a non-TTY environment the prompt is skipped and Claude Code's own default
12
+ // is used.
13
+ let selectedModel = null;
14
+ // ─── Default-model probe ──────────────────────────────────────────────────────
34
15
  /**
35
16
  * Asks the local `claude` CLI which model it would pick by default, by reading
36
17
  * the `model` field of the `system/init` stream-json event and killing the
@@ -106,27 +87,18 @@ const MODEL_OPTIONS = [
106
87
  { num: 4, model: 'claude-haiku-4-5', label: 'claude-haiku-4-5' },
107
88
  ];
108
89
  const PROMPT_TIMEOUT_MS = 10_000;
109
- function defaultOptionNum(previous) {
110
- if (typeof previous === 'string') {
111
- const match = MODEL_OPTIONS.find((o) => o.model === previous);
112
- if (match)
113
- return match.num;
114
- }
115
- // Either never asked (undefined) or explicit no-override (null) → option 1.
116
- return 1;
117
- }
118
- function promptForModel(defaultModel, previousChoice) {
90
+ const DEFAULT_OPTION_NUM = 1;
91
+ function promptForModel(defaultModel) {
119
92
  return new Promise((resolve) => {
120
93
  const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
121
- const defaultNum = defaultOptionNum(previousChoice);
122
94
  process.stdout.write('\nWhich Claude model should the bridge use?\n');
123
95
  for (const opt of MODEL_OPTIONS) {
124
- const isDefault = opt.num === defaultNum;
96
+ const isDefault = opt.num === DEFAULT_OPTION_NUM;
125
97
  const marker = isDefault ? '*' : ' ';
126
98
  const suffix = opt.num === 1 && defaultModel ? ` (${defaultModel})` : '';
127
99
  process.stdout.write(` ${marker} ${opt.num}) ${opt.label}${suffix}\n`);
128
100
  }
129
- process.stdout.write(` (* = current; auto-selected in ${PROMPT_TIMEOUT_MS / 1000}s if nothing pressed)\n`);
101
+ process.stdout.write(` (* = default; auto-selected in ${PROMPT_TIMEOUT_MS / 1000}s if nothing pressed)\n`);
130
102
  let settled = false;
131
103
  const finish = (model) => {
132
104
  if (settled)
@@ -137,14 +109,14 @@ function promptForModel(defaultModel, previousChoice) {
137
109
  resolve(model);
138
110
  };
139
111
  const timer = setTimeout(() => {
140
- const def = MODEL_OPTIONS.find((o) => o.num === defaultNum);
141
- process.stdout.write(`\n(timed out — using option ${defaultNum})\n`);
112
+ const def = MODEL_OPTIONS.find((o) => o.num === DEFAULT_OPTION_NUM);
113
+ process.stdout.write(`\n(timed out — using option ${DEFAULT_OPTION_NUM})\n`);
142
114
  finish(def.model);
143
115
  }, PROMPT_TIMEOUT_MS);
144
116
  rl.question(' choice: ', (answer) => {
145
117
  const trimmed = answer.trim();
146
118
  if (!trimmed) {
147
- const def = MODEL_OPTIONS.find((o) => o.num === defaultNum);
119
+ const def = MODEL_OPTIONS.find((o) => o.num === DEFAULT_OPTION_NUM);
148
120
  finish(def.model);
149
121
  return;
150
122
  }
@@ -160,32 +132,25 @@ function promptForModel(defaultModel, previousChoice) {
160
132
  });
161
133
  }
162
134
  /**
163
- * Asks the user which model to pin every time the bridge starts interactively.
164
- * The current pick (or option 1 on first run) is the timeout default — wait
165
- * 10 seconds and the previous choice carries over. In a non-TTY environment
166
- * the prompt is skipped: an existing pin is kept, and a first non-TTY start
167
- * records "no override" so we don't keep trying.
135
+ * Asks the user which model to use for this bridge session. The choice lives
136
+ * in memory only every startup re-prompts. In a non-TTY environment the
137
+ * prompt is skipped and Claude Code's own default is used.
168
138
  */
169
139
  async function ensureModelChoice() {
170
- const c = load();
171
140
  if (!process.stdin.isTTY) {
172
- if (!('model' in c))
173
- persist({ ...c, model: null });
141
+ selectedModel = null;
174
142
  return;
175
143
  }
176
144
  const defaultModel = await detectClaudeDefaultModel();
177
- const chosen = await promptForModel(defaultModel, c.model);
178
- persist({ ...c, model: chosen });
179
- if (chosen) {
180
- console.log(`\nPinned model: ${chosen}`);
181
- console.log(`(Edit or delete ~/.1presence/config.json to change.)\n`);
145
+ selectedModel = await promptForModel(defaultModel);
146
+ if (selectedModel) {
147
+ console.log(`\nUsing model: ${selectedModel} (this session only)\n`);
182
148
  }
183
149
  else {
184
- console.log(`\nUsing your Claude Code default${defaultModel ? ` (${defaultModel})` : ''}.`);
185
- console.log(`(Edit ~/.1presence/config.json to pin a model later.)\n`);
150
+ console.log(`\nUsing your Claude Code default${defaultModel ? ` (${defaultModel})` : ''}.\n`);
186
151
  }
187
152
  }
188
- /** Returns the pinned model id, or null to defer to Claude Code's own default. */
153
+ /** Returns the model id chosen for this session, or null to defer to Claude Code's own default. */
189
154
  function getBridgeModel() {
190
- return load().model ?? null;
155
+ return selectedModel;
191
156
  }
package/dist/index.js CHANGED
@@ -75,13 +75,13 @@ async function fetchSystemPrompt(token) {
75
75
  function tmpFile(name) {
76
76
  return (0, path_1.join)((0, os_1.tmpdir)(), name);
77
77
  }
78
- async function writeSetupFiles(auth) {
78
+ // Fetch the system prompt and write it to /tmp/agent-${uid}.md. The hosted
79
+ // runtime rebuilds buildSystemBlocks() per turn (dynamic context: vault state,
80
+ // connector status, palace, onboarding phase, skills) — call this per turn in
81
+ // the bridge too, otherwise newly shipped skills and mid-session vault writes
82
+ // never reach a long-running bridge.
83
+ async function writeSystemPrompt(auth) {
79
84
  const { uid, token } = auth;
80
- // Prefer the fully-built hosted system prompt so the bridge runtime behaves
81
- // identically to the cloud agent (tool-use policy, ui_payload sparsity,
82
- // plan thresholds, Gmail safety, connector pivots, personal AGENT.md, etc.).
83
- // If the pod isn't reachable yet, fall back to the user's AGENT.md alone —
84
- // Claude will still run, just without the platform policy layer.
85
85
  const systemPrompt = (await fetchSystemPrompt(token))
86
86
  ?? (await fetchVaultFile('AGENT.md', token))
87
87
  ?? (await fetchVaultFile('CLAUDE.md', token))
@@ -92,7 +92,9 @@ async function writeSetupFiles(auth) {
92
92
  console.log(systemPrompt);
93
93
  console.log('[bridge:verbose] ─── end system prompt ───────────────────\n');
94
94
  }
95
- // MCP config pointing at gateway's /mcp endpoint (proxied to agent-api)
95
+ }
96
+ function writeMcpConfig(auth) {
97
+ const { uid, token } = auth;
96
98
  const mcpConfig = {
97
99
  mcpServers: {
98
100
  '1presence': {
@@ -104,6 +106,10 @@ async function writeSetupFiles(auth) {
104
106
  };
105
107
  writeRestricted(tmpFile(`mcp-${uid}.json`), JSON.stringify(mcpConfig, null, 2));
106
108
  }
109
+ async function writeSetupFiles(auth) {
110
+ await writeSystemPrompt(auth);
111
+ writeMcpConfig(auth);
112
+ }
107
113
  // The MCP config embeds a Bearer JWT and the system prompt may contain vault
108
114
  // state. writeFileSync's mode only takes effect on file creation — chmodSync
109
115
  // covers the overwrite case so a legacy 0644 file gets tightened on next run.
@@ -136,6 +142,16 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
136
142
  }
137
143
  console.warn(`[bridge] token refresh failed (proceeding with current token): ${err.message}`);
138
144
  }
145
+ // Refresh the system prompt on every turn — the hosted runtime rebuilds its
146
+ // dynamic context per turn (vault state, connector status, palace, onboarding
147
+ // phase, newly enabled skills). Without this the bridge holds a frozen
148
+ // snapshot from process start and misses anything added since.
149
+ try {
150
+ await writeSystemPrompt(activeAuth);
151
+ }
152
+ catch (err) {
153
+ console.warn(`[bridge] system prompt refresh failed (using cached): ${err.message}`);
154
+ }
139
155
  let responding = false;
140
156
  (0, claude_1.spawnClaude)({
141
157
  conversationId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1presence/bridge",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Run 1Presence on your Mac and use your Claude.ai Pro subscription from any device",
5
5
  "bin": {
6
6
  "1presence-bridge": "dist/index.js"