@1presence/bridge 0.1.17 → 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 +138 -7
- package/dist/config.js +74 -0
- package/dist/index.js +97 -23
- 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,19 +3,127 @@ 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, 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;
|
|
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;
|
|
17
125
|
const args = [
|
|
18
|
-
'-p',
|
|
126
|
+
'-p', promptText,
|
|
19
127
|
'--output-format', 'stream-json',
|
|
20
128
|
'--verbose',
|
|
21
129
|
'--allowedTools', 'mcp__1presence__*',
|
|
@@ -23,6 +131,10 @@ function spawnClaude(params) {
|
|
|
23
131
|
'--mcp-config', mcpConfigPath,
|
|
24
132
|
'--strict-mcp-config',
|
|
25
133
|
];
|
|
134
|
+
const pinnedModel = (0, config_1.getBridgeModel)();
|
|
135
|
+
if (pinnedModel) {
|
|
136
|
+
args.push('--model', pinnedModel);
|
|
137
|
+
}
|
|
26
138
|
if (claudeSessionId) {
|
|
27
139
|
args.push('--resume', claudeSessionId);
|
|
28
140
|
}
|
|
@@ -30,6 +142,7 @@ function spawnClaude(params) {
|
|
|
30
142
|
// (OAuth credentials), not an API key that would bill to a separate account.
|
|
31
143
|
const { ANTHROPIC_API_KEY: _stripped, ...safeEnv } = process.env;
|
|
32
144
|
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
145
|
+
cwd: BRIDGE_CWD,
|
|
33
146
|
env: safeEnv,
|
|
34
147
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
148
|
});
|
|
@@ -38,6 +151,7 @@ function spawnClaude(params) {
|
|
|
38
151
|
let messageCount = 0;
|
|
39
152
|
let costUsd = 0;
|
|
40
153
|
let usage = null;
|
|
154
|
+
let extractedModel = null;
|
|
41
155
|
let buffer = '';
|
|
42
156
|
proc.stdout.on('data', (chunk) => {
|
|
43
157
|
buffer += chunk.toString('utf-8');
|
|
@@ -63,7 +177,20 @@ function spawnClaude(params) {
|
|
|
63
177
|
}
|
|
64
178
|
const keySource = event['apiKeySource'];
|
|
65
179
|
const model = event['model'];
|
|
66
|
-
|
|
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
|
+
}
|
|
67
194
|
sessionIdExtracted = true;
|
|
68
195
|
}
|
|
69
196
|
// Count complete assistant turns + accumulate token usage + log tool calls
|
|
@@ -75,6 +202,8 @@ function spawnClaude(params) {
|
|
|
75
202
|
usage = {
|
|
76
203
|
input_tokens: (usage?.input_tokens ?? 0) + (u['input_tokens'] ?? 0),
|
|
77
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),
|
|
78
207
|
};
|
|
79
208
|
}
|
|
80
209
|
const content = msg?.['content'];
|
|
@@ -127,19 +256,21 @@ function spawnClaude(params) {
|
|
|
127
256
|
catch { /* ignore */ }
|
|
128
257
|
}
|
|
129
258
|
if (code !== 0 && code !== null) {
|
|
130
|
-
|
|
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);
|
|
131
262
|
}
|
|
132
263
|
else {
|
|
133
|
-
onDone(messageCount, costUsd, usage);
|
|
264
|
+
onDone(messageCount, costUsd, usage, extractedModel);
|
|
134
265
|
}
|
|
135
266
|
});
|
|
136
267
|
proc.on('error', (err) => {
|
|
137
268
|
active.delete(conversationId);
|
|
138
269
|
if (err.code === 'ENOENT') {
|
|
139
|
-
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);
|
|
140
271
|
}
|
|
141
272
|
else {
|
|
142
|
-
onError(err.message);
|
|
273
|
+
onError(err.message, usage, extractedModel);
|
|
143
274
|
}
|
|
144
275
|
});
|
|
145
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 ───────────────────────────────────────────────────────────────────
|
|
@@ -20,6 +21,7 @@ const GATEWAY_HTTP = GATEWAY_URL.replace(/^wss?:/, 'https:').replace(/\/$/, '');
|
|
|
20
21
|
const PWA_URL = process.env.BRIDGE_PWA_URL ?? GATEWAY_HTTP.replace('://api.', '://');
|
|
21
22
|
// ─── In-memory state ──────────────────────────────────────────────────────────
|
|
22
23
|
let currentAuth = null;
|
|
24
|
+
let currentWs = null;
|
|
23
25
|
// ─── Vault file fetch ─────────────────────────────────────────────────────────
|
|
24
26
|
async function fetchVaultFile(path, token) {
|
|
25
27
|
try {
|
|
@@ -34,17 +36,41 @@ async function fetchVaultFile(path, token) {
|
|
|
34
36
|
return null;
|
|
35
37
|
}
|
|
36
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
|
+
}
|
|
37
58
|
// ─── Setup files ──────────────────────────────────────────────────────────────
|
|
38
59
|
function tmpFile(name) {
|
|
39
60
|
return (0, path_1.join)((0, os_1.tmpdir)(), name);
|
|
40
61
|
}
|
|
41
62
|
async function writeSetupFiles(auth) {
|
|
42
63
|
const { uid, token } = auth;
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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))
|
|
46
72
|
?? '';
|
|
47
|
-
(0, fs_1.writeFileSync)(tmpFile(`agent-${uid}.md`),
|
|
73
|
+
(0, fs_1.writeFileSync)(tmpFile(`agent-${uid}.md`), systemPrompt, 'utf-8');
|
|
48
74
|
// MCP config pointing at gateway's /mcp endpoint (proxied to agent-api)
|
|
49
75
|
const mcpConfig = {
|
|
50
76
|
mcpServers: {
|
|
@@ -58,7 +84,7 @@ async function writeSetupFiles(auth) {
|
|
|
58
84
|
(0, fs_1.writeFileSync)(tmpFile(`mcp-${uid}.json`), JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
59
85
|
}
|
|
60
86
|
// ─── Handle a single incoming message (token refresh + spawn) ─────────────────
|
|
61
|
-
async function handleMessage(conversationId, text, sessionId,
|
|
87
|
+
async function handleMessage(conversationId, text, sessionId, auth, vaultFileOpen, clientCapabilities, syncedFolders) {
|
|
62
88
|
// Refresh JWT if <10 min remaining before spawning Claude
|
|
63
89
|
let activeAuth = auth;
|
|
64
90
|
try {
|
|
@@ -70,7 +96,17 @@ async function handleMessage(conversationId, text, sessionId, ws, auth) {
|
|
|
70
96
|
}
|
|
71
97
|
}
|
|
72
98
|
catch (err) {
|
|
73
|
-
|
|
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}`);
|
|
74
110
|
}
|
|
75
111
|
let responding = false;
|
|
76
112
|
(0, claude_1.spawnClaude)({
|
|
@@ -78,16 +114,19 @@ async function handleMessage(conversationId, text, sessionId, ws, auth) {
|
|
|
78
114
|
presenceSessionId: sessionId,
|
|
79
115
|
text,
|
|
80
116
|
uid: activeAuth.uid,
|
|
117
|
+
vaultFileOpen,
|
|
118
|
+
clientCapabilities,
|
|
119
|
+
syncedFolders,
|
|
81
120
|
onEvent: (event) => {
|
|
82
121
|
if (!responding && event['type'] === 'assistant') {
|
|
83
122
|
responding = true;
|
|
84
123
|
console.log(`[${new Date().toLocaleTimeString()}] ◐ responding…`);
|
|
85
124
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
125
|
+
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
126
|
+
currentWs.send(JSON.stringify({ type: 'stream', conversationId, event }));
|
|
88
127
|
}
|
|
89
128
|
},
|
|
90
|
-
onDone: (messageCount, costUsd, usage) => {
|
|
129
|
+
onDone: (messageCount, costUsd, usage, model) => {
|
|
91
130
|
const parts = [];
|
|
92
131
|
if (usage)
|
|
93
132
|
parts.push(`in:${usage.input_tokens} out:${usage.output_tokens}`);
|
|
@@ -95,19 +134,44 @@ async function handleMessage(conversationId, text, sessionId, ws, auth) {
|
|
|
95
134
|
parts.push(costStr);
|
|
96
135
|
const suffix = parts.length ? ` ${parts.join(' ')}` : '';
|
|
97
136
|
console.log(`[${new Date().toLocaleTimeString()}] ✓ done${suffix}`);
|
|
98
|
-
if (
|
|
99
|
-
|
|
137
|
+
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
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
|
+
}));
|
|
100
151
|
}
|
|
101
152
|
},
|
|
102
|
-
onError: (message) => {
|
|
153
|
+
onError: (message, usage, model) => {
|
|
103
154
|
console.error(`[${new Date().toLocaleTimeString()}] ✗ ${message}`);
|
|
104
|
-
if (
|
|
105
|
-
|
|
155
|
+
if (currentWs?.readyState === ws_1.default.OPEN) {
|
|
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
|
+
}));
|
|
106
168
|
}
|
|
107
169
|
},
|
|
108
170
|
});
|
|
109
171
|
}
|
|
110
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.
|
|
111
175
|
const PING_INTERVAL_MS = 30_000;
|
|
112
176
|
const PONG_TIMEOUT_MS = 10_000;
|
|
113
177
|
function connect(auth, retryDelay = 1000) {
|
|
@@ -120,7 +184,7 @@ function connect(auth, retryDelay = 1000) {
|
|
|
120
184
|
pingTimer = setInterval(() => {
|
|
121
185
|
if (ws.readyState !== ws_1.default.OPEN)
|
|
122
186
|
return;
|
|
123
|
-
ws.ping();
|
|
187
|
+
ws.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
|
|
124
188
|
pongTimer = setTimeout(() => {
|
|
125
189
|
console.log('[bridge] pong timeout — reconnecting…');
|
|
126
190
|
ws.terminate();
|
|
@@ -137,13 +201,11 @@ function connect(auth, retryDelay = 1000) {
|
|
|
137
201
|
pongTimer = null;
|
|
138
202
|
}
|
|
139
203
|
}
|
|
140
|
-
ws.on('pong', () => {
|
|
141
|
-
if (pongTimer) {
|
|
142
|
-
clearTimeout(pongTimer);
|
|
143
|
-
pongTimer = null;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
204
|
ws.on('open', () => {
|
|
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;
|
|
147
209
|
console.log('✓ Bridge connected. Local Mode active on all your devices.\n');
|
|
148
210
|
startPing();
|
|
149
211
|
});
|
|
@@ -155,13 +217,21 @@ function connect(auth, retryDelay = 1000) {
|
|
|
155
217
|
catch {
|
|
156
218
|
return;
|
|
157
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
|
+
}
|
|
158
228
|
if (msg.type !== 'message' || !msg.conversationId || !msg.text)
|
|
159
229
|
return;
|
|
160
|
-
const { conversationId, text, sessionId } = msg;
|
|
230
|
+
const { conversationId, text, sessionId, vaultFileOpen, clientCapabilities, syncedFolders } = msg;
|
|
161
231
|
const ts = new Date().toLocaleTimeString();
|
|
162
232
|
const preview = text.length > 80 ? text.slice(0, 80) + '…' : text;
|
|
163
233
|
console.log(`[${ts}] ▶ ${preview}`);
|
|
164
|
-
handleMessage(conversationId, text, sessionId ?? null,
|
|
234
|
+
handleMessage(conversationId, text, sessionId ?? null, auth, vaultFileOpen, clientCapabilities, syncedFolders).catch((err) => {
|
|
165
235
|
console.error(`[bridge] handleMessage error: ${err.message}`);
|
|
166
236
|
});
|
|
167
237
|
});
|
|
@@ -200,6 +270,10 @@ async function main() {
|
|
|
200
270
|
// Auth
|
|
201
271
|
const auth = await (0, auth_1.getValidAuth)(GATEWAY_HTTP, PWA_URL);
|
|
202
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)();
|
|
203
277
|
// Write system prompt + MCP config
|
|
204
278
|
process.stdout.write('Setting up…');
|
|
205
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
|
}
|