@1presence/bridge 0.1.18 → 0.1.19
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 +6 -0
- package/dist/auth.js +1 -0
- package/dist/claude.js +137 -7
- package/dist/config.js +74 -0
- package/dist/index.js +90 -19
- package/dist/sessions.js +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,12 @@ Conversations are stateful — the bridge maps each 1Presence conversation to a
|
|
|
25
25
|
|
|
26
26
|
Your OAuth tokens and vault data stay server-side — nothing sensitive is stored locally beyond the auth token.
|
|
27
27
|
|
|
28
|
+
## Model
|
|
29
|
+
|
|
30
|
+
On first run the bridge asks which Claude model you want it to use. Leave the prompt blank to defer to your local Claude Code default (recommended — typically Sonnet on a Pro subscription), or type a specific model id to pin it (e.g. `claude-opus-4-7`, `claude-sonnet-4-5`, `claude-haiku-4-5`). The choice is saved to `~/.1presence/config.json` and reused on every subsequent run.
|
|
31
|
+
|
|
32
|
+
The first reply each session prints the model and credential source so you can confirm what's running. To change your pick later, edit or delete `~/.1presence/config.json` — the bridge will prompt again on the next start.
|
|
33
|
+
|
|
28
34
|
## Signing out
|
|
29
35
|
|
|
30
36
|
To sign out and reset the cached credentials:
|
package/dist/auth.js
CHANGED
package/dist/claude.js
CHANGED
|
@@ -3,18 +3,125 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.spawnClaude = spawnClaude;
|
|
4
4
|
exports.killAll = killAll;
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
|
+
const fs_1 = require("fs");
|
|
6
7
|
const os_1 = require("os");
|
|
7
8
|
const path_1 = require("path");
|
|
8
9
|
const sessions_1 = require("./sessions");
|
|
10
|
+
// ─── Bridge working directory ─────────────────────────────────────────────────
|
|
11
|
+
//
|
|
12
|
+
// Claude Code always loads CLAUDE.md files from cwd upward plus the global
|
|
13
|
+
// ~/.claude/CLAUDE.md. If the bridge is launched from within a development
|
|
14
|
+
// repo (e.g. PresenceAI), it would pick up project CLAUDE.md instructions that
|
|
15
|
+
// direct it to write to local vault paths and call MemPalace — both wrong in
|
|
16
|
+
// bridge/Local Mode. We run Claude in a dedicated temp dir with a LOCAL MODE
|
|
17
|
+
// CLAUDE.md that explicitly overrides those behaviors.
|
|
18
|
+
const BRIDGE_CWD = (0, path_1.join)((0, os_1.tmpdir)(), '1presence-bridge');
|
|
19
|
+
const BRIDGE_CLAUDE_MD = `# Bridge / Local Mode — Overrides
|
|
20
|
+
|
|
21
|
+
You are running as a **1Presence bridge agent** on the user's local machine.
|
|
22
|
+
The rules below **override all other CLAUDE.md instructions**, including the global one.
|
|
23
|
+
|
|
24
|
+
## Filesystem — DO NOT WRITE LOCALLY
|
|
25
|
+
|
|
26
|
+
You have NO permission to write files to this machine.
|
|
27
|
+
- Do NOT write chat history, vault notes, or memory files to the local filesystem.
|
|
28
|
+
- Do NOT write to any path under /Users/, ~/Library/, ~/.obsidian/, or similar.
|
|
29
|
+
- Attempting to use Write/Edit tools on local paths is forbidden in this mode.
|
|
30
|
+
|
|
31
|
+
## Vault operations
|
|
32
|
+
|
|
33
|
+
All vault reads and writes go through the \`mcp__1presence__*\` MCP tools.
|
|
34
|
+
Use those tools instead of any local file paths.
|
|
35
|
+
|
|
36
|
+
## MemPalace / memory
|
|
37
|
+
|
|
38
|
+
MemPalace is hosted in the cloud, not locally. If mempalace tools are available
|
|
39
|
+
via MCP, use them. If a mempalace call fails with "not initialised", skip it
|
|
40
|
+
silently — do NOT attempt to write memory files locally as a fallback.
|
|
41
|
+
|
|
42
|
+
## Session notes / chat history
|
|
43
|
+
|
|
44
|
+
Do NOT write session notes or chat history to the local filesystem.
|
|
45
|
+
If vault MCP tools are available, update vault files through those.
|
|
46
|
+
Otherwise, skip the write and continue — do not error or warn the user.
|
|
47
|
+
|
|
48
|
+
## UI tools — call these so the chat UI matches the hosted assistant
|
|
49
|
+
|
|
50
|
+
These tools are wired through MCP and produce dedicated UI events in the user's
|
|
51
|
+
chat interface. Call them by their MCP-prefixed names (\`mcp__1presence__<name>\`).
|
|
52
|
+
|
|
53
|
+
- **set_conversation_title** — Call ONCE on your first reply in a new
|
|
54
|
+
conversation with a 2–4 word topic title (sentence case, no verbs). Call
|
|
55
|
+
again later only if the topic clearly shifts. Without this, the user's
|
|
56
|
+
conversation list shows untitled threads.
|
|
57
|
+
|
|
58
|
+
- **ui_payload** — Emit at most ONE per turn, AFTER your reply text ends.
|
|
59
|
+
Pass \`hints: []\` most turns (aim ~1 hint per 3 early turns, less later)
|
|
60
|
+
and \`suggestions\`: 2–4 follow-on prompts the user can tap. Hints are
|
|
61
|
+
end-user product coaching only — never mention models, AI vendors,
|
|
62
|
+
engineering, or roadmap. Skip entirely on pure tool-ingestion turns.
|
|
63
|
+
|
|
64
|
+
- **plan** — Show a checklist when a task has ≥3 distinct, user-visible
|
|
65
|
+
steps with side effects. Call once at the start with all steps; update
|
|
66
|
+
the FULL list each time a step finishes. Never use for single-tool
|
|
67
|
+
answers, lookups, or conversational replies.
|
|
68
|
+
|
|
69
|
+
- **copy_to_clipboard** — Use whenever the user asks you to copy something,
|
|
70
|
+
or when you produce content (code, a draft, a URL, a list) they will
|
|
71
|
+
clearly want to paste elsewhere. Call once per piece of content; the
|
|
72
|
+
text also streams into the chat.
|
|
73
|
+
|
|
74
|
+
- **request_feature** — Log a feature request when declining a user ask
|
|
75
|
+
because the capability doesn't exist. Not for transient errors. After
|
|
76
|
+
calling, tell the user their request was noted.
|
|
77
|
+
|
|
78
|
+
## Sending email — ALWAYS use gmail_draft
|
|
79
|
+
|
|
80
|
+
To send email on the user's behalf, ALWAYS call \`gmail_draft\`. This opens
|
|
81
|
+
an inline review panel in the chat UI with Send and Cancel buttons; the
|
|
82
|
+
email is NOT delivered until the user clicks Send themselves.
|
|
83
|
+
|
|
84
|
+
You DO NOT have direct send capability in this mode — \`gmail_send\` is not
|
|
85
|
+
exposed and the user's explicit confirmation in the review panel is the only
|
|
86
|
+
way mail leaves their account. After calling gmail_draft, tell the user the
|
|
87
|
+
draft is ready to review and wait for them to confirm. Never claim the
|
|
88
|
+
email was sent until the user tells you they clicked Send.
|
|
89
|
+
|
|
90
|
+
Use the bare tool name (the part after \`mcp__1presence__\`) when reasoning
|
|
91
|
+
about WHEN to call them — the prefix is just the MCP namespace.
|
|
92
|
+
`;
|
|
93
|
+
// Write the override CLAUDE.md once on module load
|
|
94
|
+
(0, fs_1.mkdirSync)(BRIDGE_CWD, { recursive: true });
|
|
95
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(BRIDGE_CWD, 'CLAUDE.md'), BRIDGE_CLAUDE_MD, 'utf-8');
|
|
96
|
+
const config_1 = require("./config");
|
|
97
|
+
// Track whether we've already announced the model this process — printing it
|
|
98
|
+
// per-spawn is noisy; once on startup is what the user actually wants to see.
|
|
99
|
+
let modelAnnounced = false;
|
|
9
100
|
// ─── Active processes ─────────────────────────────────────────────────────────
|
|
10
101
|
const active = new Map();
|
|
11
102
|
// ─── Spawn ────────────────────────────────────────────────────────────────────
|
|
12
103
|
function spawnClaude(params) {
|
|
13
|
-
const { conversationId, presenceSessionId, text, uid, vaultFileOpen, onEvent, onDone, onError } = params;
|
|
104
|
+
const { conversationId, presenceSessionId, text, uid, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError } = params;
|
|
14
105
|
const systemPromptPath = (0, path_1.join)((0, os_1.tmpdir)(), `agent-${uid}.md`);
|
|
15
106
|
const mcpConfigPath = (0, path_1.join)((0, os_1.tmpdir)(), `mcp-${uid}.json`);
|
|
107
|
+
// If a prior process is still running for this conversation (user sent a
|
|
108
|
+
// follow-up before the previous turn finished), supersede it. The latest
|
|
109
|
+
// user intent wins; the orphan would otherwise keep streaming events.
|
|
110
|
+
const existing = active.get(conversationId);
|
|
111
|
+
if (existing) {
|
|
112
|
+
process.stderr.write(`[bridge] superseding active conversation ${conversationId}\n`);
|
|
113
|
+
existing.kill('SIGTERM');
|
|
114
|
+
active.delete(conversationId);
|
|
115
|
+
}
|
|
16
116
|
const claudeSessionId = presenceSessionId ? (0, sessions_1.getClaudeSession)(presenceSessionId) : undefined;
|
|
17
|
-
const
|
|
117
|
+
const ctxParts = [];
|
|
118
|
+
if (vaultFileOpen)
|
|
119
|
+
ctxParts.push(`vault_file_open: ${vaultFileOpen}`);
|
|
120
|
+
if (clientCapabilities?.length)
|
|
121
|
+
ctxParts.push(`client_capabilities: ${clientCapabilities.join(', ')}`);
|
|
122
|
+
if (syncedFolders?.length)
|
|
123
|
+
ctxParts.push(`synced_folders: ${syncedFolders.join(', ')}`);
|
|
124
|
+
const promptText = ctxParts.length > 0 ? `[${ctxParts.join(' | ')}]\n\n${text}` : text;
|
|
18
125
|
const args = [
|
|
19
126
|
'-p', promptText,
|
|
20
127
|
'--output-format', 'stream-json',
|
|
@@ -24,6 +131,10 @@ function spawnClaude(params) {
|
|
|
24
131
|
'--mcp-config', mcpConfigPath,
|
|
25
132
|
'--strict-mcp-config',
|
|
26
133
|
];
|
|
134
|
+
const pinnedModel = (0, config_1.getBridgeModel)();
|
|
135
|
+
if (pinnedModel) {
|
|
136
|
+
args.push('--model', pinnedModel);
|
|
137
|
+
}
|
|
27
138
|
if (claudeSessionId) {
|
|
28
139
|
args.push('--resume', claudeSessionId);
|
|
29
140
|
}
|
|
@@ -31,6 +142,7 @@ function spawnClaude(params) {
|
|
|
31
142
|
// (OAuth credentials), not an API key that would bill to a separate account.
|
|
32
143
|
const { ANTHROPIC_API_KEY: _stripped, ...safeEnv } = process.env;
|
|
33
144
|
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
145
|
+
cwd: BRIDGE_CWD,
|
|
34
146
|
env: safeEnv,
|
|
35
147
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
148
|
});
|
|
@@ -39,6 +151,7 @@ function spawnClaude(params) {
|
|
|
39
151
|
let messageCount = 0;
|
|
40
152
|
let costUsd = 0;
|
|
41
153
|
let usage = null;
|
|
154
|
+
let extractedModel = null;
|
|
42
155
|
let buffer = '';
|
|
43
156
|
proc.stdout.on('data', (chunk) => {
|
|
44
157
|
buffer += chunk.toString('utf-8');
|
|
@@ -64,7 +177,20 @@ function spawnClaude(params) {
|
|
|
64
177
|
}
|
|
65
178
|
const keySource = event['apiKeySource'];
|
|
66
179
|
const model = event['model'];
|
|
67
|
-
|
|
180
|
+
if (model)
|
|
181
|
+
extractedModel = model;
|
|
182
|
+
if (!modelAnnounced) {
|
|
183
|
+
// First conversation since bridge started — announce prominently
|
|
184
|
+
// so the user can confirm which model and credential is in use.
|
|
185
|
+
const source = keySource === 'none' || !keySource ? 'claude.ai subscription' : keySource;
|
|
186
|
+
const pin = (0, config_1.getBridgeModel)() ? ' (pinned via 1presence config)' : '';
|
|
187
|
+
process.stdout.write(`\n model: ${model ?? 'unknown'}${pin}\n auth: ${source}\n\n`);
|
|
188
|
+
modelAnnounced = true;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Subsequent conversations — quiet line for power users.
|
|
192
|
+
process.stderr.write(`[bridge] model: ${model ?? 'unknown'} apiKeySource: ${keySource ?? 'none'}\n`);
|
|
193
|
+
}
|
|
68
194
|
sessionIdExtracted = true;
|
|
69
195
|
}
|
|
70
196
|
// Count complete assistant turns + accumulate token usage + log tool calls
|
|
@@ -76,6 +202,8 @@ function spawnClaude(params) {
|
|
|
76
202
|
usage = {
|
|
77
203
|
input_tokens: (usage?.input_tokens ?? 0) + (u['input_tokens'] ?? 0),
|
|
78
204
|
output_tokens: (usage?.output_tokens ?? 0) + (u['output_tokens'] ?? 0),
|
|
205
|
+
cache_read_input_tokens: (usage?.cache_read_input_tokens ?? 0) + (u['cache_read_input_tokens'] ?? 0),
|
|
206
|
+
cache_creation_input_tokens: (usage?.cache_creation_input_tokens ?? 0) + (u['cache_creation_input_tokens'] ?? 0),
|
|
79
207
|
};
|
|
80
208
|
}
|
|
81
209
|
const content = msg?.['content'];
|
|
@@ -128,19 +256,21 @@ function spawnClaude(params) {
|
|
|
128
256
|
catch { /* ignore */ }
|
|
129
257
|
}
|
|
130
258
|
if (code !== 0 && code !== null) {
|
|
131
|
-
|
|
259
|
+
// Pass any partial token usage we observed before the failure so the
|
|
260
|
+
// PWA and the gateway's bridge usage store can still record it.
|
|
261
|
+
onError(`claude exited with code ${code}`, usage, extractedModel);
|
|
132
262
|
}
|
|
133
263
|
else {
|
|
134
|
-
onDone(messageCount, costUsd, usage);
|
|
264
|
+
onDone(messageCount, costUsd, usage, extractedModel);
|
|
135
265
|
}
|
|
136
266
|
});
|
|
137
267
|
proc.on('error', (err) => {
|
|
138
268
|
active.delete(conversationId);
|
|
139
269
|
if (err.code === 'ENOENT') {
|
|
140
|
-
onError('claude CLI not found. Please install Claude Code: https://claude.ai/code');
|
|
270
|
+
onError('claude CLI not found. Please install Claude Code: https://claude.ai/code', usage, extractedModel);
|
|
141
271
|
}
|
|
142
272
|
else {
|
|
143
|
-
onError(err.message);
|
|
273
|
+
onError(err.message, usage, extractedModel);
|
|
144
274
|
}
|
|
145
275
|
});
|
|
146
276
|
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureModelChoice = ensureModelChoice;
|
|
4
|
+
exports.getBridgeModel = getBridgeModel;
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const readline_1 = require("readline");
|
|
9
|
+
// ─── Storage ──────────────────────────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// `~/.1presence/config.json` holds bridge-wide preferences that should persist
|
|
12
|
+
// across runs but aren't sensitive (unlike auth.json). Today: model choice.
|
|
13
|
+
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.1presence');
|
|
14
|
+
const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, 'config.json');
|
|
15
|
+
let cached = null;
|
|
16
|
+
function load() {
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
try {
|
|
20
|
+
cached = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
cached = {};
|
|
24
|
+
}
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
function persist(c) {
|
|
28
|
+
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
29
|
+
(0, fs_1.writeFileSync)(CONFIG_FILE, JSON.stringify(c, null, 2), 'utf-8');
|
|
30
|
+
cached = c;
|
|
31
|
+
}
|
|
32
|
+
// ─── Model choice ─────────────────────────────────────────────────────────────
|
|
33
|
+
function promptForModel() {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const rl = (0, readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
36
|
+
process.stdout.write('\nWhich Claude model should the bridge use?\n');
|
|
37
|
+
process.stdout.write(' Leave blank to use your local Claude Code default (recommended).\n');
|
|
38
|
+
process.stdout.write(' Or enter a model id (e.g. claude-sonnet-4-5, claude-opus-4-7, claude-haiku-4-5).\n');
|
|
39
|
+
rl.question(' model: ', (answer) => {
|
|
40
|
+
rl.close();
|
|
41
|
+
const trimmed = answer.trim();
|
|
42
|
+
resolve(trimmed || null);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ensures the user has picked a model preference. On first interactive run,
|
|
48
|
+
* prompts and persists. Subsequent runs return the saved value without asking.
|
|
49
|
+
* In a non-TTY environment (background process, CI) the prompt is skipped and
|
|
50
|
+
* we record an explicit "no override" so we don't keep trying.
|
|
51
|
+
*/
|
|
52
|
+
async function ensureModelChoice() {
|
|
53
|
+
const c = load();
|
|
54
|
+
if ('model' in c)
|
|
55
|
+
return; // already configured (string or explicit null)
|
|
56
|
+
if (!process.stdin.isTTY) {
|
|
57
|
+
persist({ ...c, model: null });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const chosen = await promptForModel();
|
|
61
|
+
persist({ ...c, model: chosen });
|
|
62
|
+
if (chosen) {
|
|
63
|
+
console.log(`\nPinned model: ${chosen}`);
|
|
64
|
+
console.log(`(Edit or delete ~/.1presence/config.json to change.)\n`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.log(`\nUsing your Claude Code default.`);
|
|
68
|
+
console.log(`(Edit ~/.1presence/config.json to pin a model later.)\n`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Returns the pinned model id, or null to defer to Claude Code's own default. */
|
|
72
|
+
function getBridgeModel() {
|
|
73
|
+
return load().model ?? null;
|
|
74
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const os_1 = require("os");
|
|
|
10
10
|
const path_1 = require("path");
|
|
11
11
|
const auth_1 = require("./auth");
|
|
12
12
|
const claude_1 = require("./claude");
|
|
13
|
+
const config_1 = require("./config");
|
|
13
14
|
const update_1 = require("./update");
|
|
14
15
|
const package_json_1 = require("../package.json");
|
|
15
16
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
@@ -35,17 +36,41 @@ async function fetchVaultFile(path, token) {
|
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
39
|
+
// Pulls the fully-built system prompt from agent-api (via gateway proxy).
|
|
40
|
+
// This matches the hosted runtime exactly: STATIC_SYSTEM_PROMPT + dynamic
|
|
41
|
+
// context (timezone, connector scopes, vault state, personal AGENT.md, etc.).
|
|
42
|
+
// Falls back to null on failure — caller decides whether to use AGENT.md only.
|
|
43
|
+
async function fetchSystemPrompt(token) {
|
|
44
|
+
try {
|
|
45
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
46
|
+
const res = await fetch(`${GATEWAY_HTTP}/system-prompt?timezone=${encodeURIComponent(tz)}`, {
|
|
47
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok)
|
|
50
|
+
return null;
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
return data.text ?? null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
38
58
|
// ─── Setup files ──────────────────────────────────────────────────────────────
|
|
39
59
|
function tmpFile(name) {
|
|
40
60
|
return (0, path_1.join)((0, os_1.tmpdir)(), name);
|
|
41
61
|
}
|
|
42
62
|
async function writeSetupFiles(auth) {
|
|
43
63
|
const { uid, token } = auth;
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
// Prefer the fully-built hosted system prompt so the bridge runtime behaves
|
|
65
|
+
// identically to the cloud agent (tool-use policy, ui_payload sparsity,
|
|
66
|
+
// plan thresholds, Gmail safety, connector pivots, personal AGENT.md, etc.).
|
|
67
|
+
// If the pod isn't reachable yet, fall back to the user's AGENT.md alone —
|
|
68
|
+
// Claude will still run, just without the platform policy layer.
|
|
69
|
+
const systemPrompt = (await fetchSystemPrompt(token))
|
|
70
|
+
?? (await fetchVaultFile('AGENT.md', token))
|
|
71
|
+
?? (await fetchVaultFile('CLAUDE.md', token))
|
|
47
72
|
?? '';
|
|
48
|
-
(0, fs_1.writeFileSync)(tmpFile(`agent-${uid}.md`),
|
|
73
|
+
(0, fs_1.writeFileSync)(tmpFile(`agent-${uid}.md`), systemPrompt, 'utf-8');
|
|
49
74
|
// MCP config pointing at gateway's /mcp endpoint (proxied to agent-api)
|
|
50
75
|
const mcpConfig = {
|
|
51
76
|
mcpServers: {
|
|
@@ -59,7 +84,7 @@ async function writeSetupFiles(auth) {
|
|
|
59
84
|
(0, fs_1.writeFileSync)(tmpFile(`mcp-${uid}.json`), JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
60
85
|
}
|
|
61
86
|
// ─── Handle a single incoming message (token refresh + spawn) ─────────────────
|
|
62
|
-
async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpen) {
|
|
87
|
+
async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
|
|
63
88
|
// Refresh JWT if <10 min remaining before spawning Claude
|
|
64
89
|
let activeAuth = auth;
|
|
65
90
|
try {
|
|
@@ -71,7 +96,17 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
catch (err) {
|
|
74
|
-
|
|
99
|
+
// If the cached token still has time, proceed — refresh was preemptive.
|
|
100
|
+
// If it's already invalid, MCP calls will 401 mid-turn — fail fast instead.
|
|
101
|
+
if (!(0, auth_1.isTokenValid)(auth.token)) {
|
|
102
|
+
const message = 'Authentication expired and refresh failed — please restart the bridge to sign in again.';
|
|
103
|
+
console.error(`[bridge] ${message} (${err.message})`);
|
|
104
|
+
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
105
|
+
currentWs.send(JSON.stringify({ type: 'error', conversationId, message }));
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.warn(`[bridge] token refresh failed (proceeding with current token): ${err.message}`);
|
|
75
110
|
}
|
|
76
111
|
let responding = false;
|
|
77
112
|
(0, claude_1.spawnClaude)({
|
|
@@ -80,6 +115,8 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
80
115
|
text,
|
|
81
116
|
uid: activeAuth.uid,
|
|
82
117
|
vaultFileOpen,
|
|
118
|
+
clientCapabilities,
|
|
119
|
+
syncedFolders,
|
|
83
120
|
onEvent: (event) => {
|
|
84
121
|
if (!responding && event['type'] === 'assistant') {
|
|
85
122
|
responding = true;
|
|
@@ -89,7 +126,7 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
89
126
|
currentWs.send(JSON.stringify({ type: 'stream', conversationId, event }));
|
|
90
127
|
}
|
|
91
128
|
},
|
|
92
|
-
onDone: (messageCount, costUsd, usage) => {
|
|
129
|
+
onDone: (messageCount, costUsd, usage, model) => {
|
|
93
130
|
const parts = [];
|
|
94
131
|
if (usage)
|
|
95
132
|
parts.push(`in:${usage.input_tokens} out:${usage.output_tokens}`);
|
|
@@ -98,18 +135,43 @@ async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpe
|
|
|
98
135
|
const suffix = parts.length ? ` ${parts.join(' ')}` : '';
|
|
99
136
|
console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
|
|
100
137
|
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
101
|
-
currentWs.send(JSON.stringify({
|
|
138
|
+
currentWs.send(JSON.stringify({
|
|
139
|
+
type: 'done',
|
|
140
|
+
conversationId,
|
|
141
|
+
messageCount,
|
|
142
|
+
costUsd,
|
|
143
|
+
model,
|
|
144
|
+
usage: usage ? {
|
|
145
|
+
inputTokens: usage.input_tokens,
|
|
146
|
+
outputTokens: usage.output_tokens,
|
|
147
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
148
|
+
cacheCreationTokens: usage.cache_creation_input_tokens,
|
|
149
|
+
} : null,
|
|
150
|
+
}));
|
|
102
151
|
}
|
|
103
152
|
},
|
|
104
|
-
onError: (message) => {
|
|
153
|
+
onError: (message, usage, model) => {
|
|
105
154
|
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
|
|
106
155
|
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
107
|
-
currentWs.send(JSON.stringify({
|
|
156
|
+
currentWs.send(JSON.stringify({
|
|
157
|
+
type: 'error',
|
|
158
|
+
conversationId,
|
|
159
|
+
message,
|
|
160
|
+
model,
|
|
161
|
+
usage: usage ? {
|
|
162
|
+
inputTokens: usage.input_tokens,
|
|
163
|
+
outputTokens: usage.output_tokens,
|
|
164
|
+
cacheReadTokens: usage.cache_read_input_tokens,
|
|
165
|
+
cacheCreationTokens: usage.cache_creation_input_tokens,
|
|
166
|
+
} : null,
|
|
167
|
+
}));
|
|
108
168
|
}
|
|
109
169
|
},
|
|
110
170
|
});
|
|
111
171
|
}
|
|
112
172
|
// ─── WebSocket connection ─────────────────────────────────────────────────────
|
|
173
|
+
// Application-level heartbeat — avoids relying on WebSocket control frames (ping/pong),
|
|
174
|
+
// which some proxies (GKE LB) may not forward reliably.
|
|
113
175
|
const PING_INTERVAL_MS = 30_000;
|
|
114
176
|
const PONG_TIMEOUT_MS = 10_000;
|
|
115
177
|
function connect(auth, retryDelay = 1000) {
|
|
@@ -122,7 +184,7 @@ function connect(auth, retryDelay = 1000) {
|
|
|
122
184
|
pingTimer = setInterval(() => {
|
|
123
185
|
if (ws.readyState !== ws_1.default.OPEN)
|
|
124
186
|
return;
|
|
125
|
-
ws.ping();
|
|
187
|
+
ws.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
|
|
126
188
|
pongTimer = setTimeout(() => {
|
|
127
189
|
console.log('[bridge] pong timeout — reconnecting…');
|
|
128
190
|
ws.terminate();
|
|
@@ -139,14 +201,11 @@ function connect(auth, retryDelay = 1000) {
|
|
|
139
201
|
pongTimer = null;
|
|
140
202
|
}
|
|
141
203
|
}
|
|
142
|
-
ws.on('pong', () => {
|
|
143
|
-
if (pongTimer) {
|
|
144
|
-
clearTimeout(pongTimer);
|
|
145
|
-
pongTimer = null;
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
204
|
ws.on('open', () => {
|
|
149
205
|
currentWs = ws;
|
|
206
|
+
// Reset backoff so that a disconnect after a long-stable session
|
|
207
|
+
// reconnects quickly instead of waiting at the 30s cap.
|
|
208
|
+
retryDelay = 1000;
|
|
150
209
|
console.log('✓ Bridge connected. Local Mode active on all your devices.\n');
|
|
151
210
|
startPing();
|
|
152
211
|
});
|
|
@@ -158,13 +217,21 @@ function connect(auth, retryDelay = 1000) {
|
|
|
158
217
|
catch {
|
|
159
218
|
return;
|
|
160
219
|
}
|
|
220
|
+
// Application-level pong — clear the timeout
|
|
221
|
+
if (msg.type === 'pong') {
|
|
222
|
+
if (pongTimer) {
|
|
223
|
+
clearTimeout(pongTimer);
|
|
224
|
+
pongTimer = null;
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
161
228
|
if (msg.type !== 'message' || !msg.conversationId || !msg.text)
|
|
162
229
|
return;
|
|
163
|
-
const { conversationId, text, sessionId, vaultFileOpen } = msg;
|
|
230
|
+
const { conversationId, text, sessionId, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
|
|
164
231
|
const ts = new Date().toLocaleTimeString();
|
|
165
232
|
const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
|
|
166
233
|
console.log(`[${ts}] ▶ ${preview}`);
|
|
167
|
-
handleMessage(conversationId, text, sessionId ?? null, auth, vaultFileOpen).catch((err) => {
|
|
234
|
+
handleMessage(conversationId, text, sessionId ?? null, auth, vaultFileOpen, clientCapabilities, syncedFolders).catch((err) => {
|
|
168
235
|
console.error(`[bridge] handleMessage error: ${err.message}`);
|
|
169
236
|
});
|
|
170
237
|
});
|
|
@@ -203,6 +270,10 @@ async function main() {
|
|
|
203
270
|
// Auth
|
|
204
271
|
const auth = await (0, auth_1.getValidAuth)(GATEWAY_HTTP, PWA_URL);
|
|
205
272
|
currentAuth = auth;
|
|
273
|
+
// One-time interactive model choice (only prompts on first run; saved to
|
|
274
|
+
// ~/.1presence/config.json). In a non-TTY environment this is a no-op and
|
|
275
|
+
// Claude Code's own default is used.
|
|
276
|
+
await (0, config_1.ensureModelChoice)();
|
|
206
277
|
// Write system prompt + MCP config
|
|
207
278
|
process.stdout.write('Setting up…');
|
|
208
279
|
await writeSetupFiles(auth);
|
package/dist/sessions.js
CHANGED
|
@@ -8,25 +8,29 @@ const path_1 = require("path");
|
|
|
8
8
|
// ─── Storage ──────────────────────────────────────────────────────────────────
|
|
9
9
|
const CONFIG_DIR = (0, path_1.join)((0, os_1.homedir)(), '.1presence');
|
|
10
10
|
const SESSIONS_FILE = (0, path_1.join)(CONFIG_DIR, 'sessions.json');
|
|
11
|
-
|
|
11
|
+
let cache = null;
|
|
12
|
+
function getMap() {
|
|
13
|
+
if (cache)
|
|
14
|
+
return cache;
|
|
12
15
|
try {
|
|
13
16
|
const raw = (0, fs_1.readFileSync)(SESSIONS_FILE, 'utf-8');
|
|
14
|
-
|
|
17
|
+
cache = JSON.parse(raw);
|
|
15
18
|
}
|
|
16
19
|
catch {
|
|
17
|
-
|
|
20
|
+
cache = {};
|
|
18
21
|
}
|
|
22
|
+
return cache;
|
|
19
23
|
}
|
|
20
|
-
function
|
|
24
|
+
function persist(map) {
|
|
21
25
|
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
22
26
|
(0, fs_1.writeFileSync)(SESSIONS_FILE, JSON.stringify(map, null, 2), 'utf-8');
|
|
23
27
|
}
|
|
24
28
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
25
29
|
function getClaudeSession(presenceSessionId) {
|
|
26
|
-
return
|
|
30
|
+
return getMap()[presenceSessionId];
|
|
27
31
|
}
|
|
28
32
|
function saveClaudeSession(presenceSessionId, claudeSessionId) {
|
|
29
|
-
const map =
|
|
33
|
+
const map = getMap();
|
|
30
34
|
map[presenceSessionId] = claudeSessionId;
|
|
31
|
-
|
|
35
|
+
persist(map);
|
|
32
36
|
}
|