newmark-agent 0.4.5 → 0.4.6

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/server.js CHANGED
@@ -37,6 +37,7 @@ exports.runServer = runServer;
37
37
  const http = __importStar(require("http"));
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
+ const os = __importStar(require("os"));
40
41
  const uiPreferences_1 = require("./core/uiPreferences");
41
42
  const child_process_1 = require("child_process");
42
43
  const agent_1 = require("./core/agent");
@@ -45,10 +46,76 @@ const config_1 = require("./core/config");
45
46
  const flow_1 = require("./core/flow");
46
47
  const workspaceFileRouter_1 = require("./core/workspaceFileRouter");
47
48
  const nativeBash_1 = require("./core/nativeBash");
49
+ const installUpdate_1 = require("./core/installUpdate");
50
+ const mobilePairing_1 = require("./core/mobilePairing");
48
51
  const PORT = 47890;
49
52
  let agent = null;
50
53
  let automation = null;
51
54
  let workspaceFileRouter = null;
55
+ let mobileToken = '';
56
+ let appRoot = '';
57
+ function mobileAuthorized(req) {
58
+ if (!mobileToken)
59
+ return false;
60
+ const bearer = String(req.headers.authorization || '');
61
+ if (bearer.startsWith('Bearer '))
62
+ return bearer.slice('Bearer '.length).trim() === mobileToken;
63
+ try {
64
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`);
65
+ const queryToken = url.searchParams.get('token') || '';
66
+ return queryToken === mobileToken;
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ function mobileJson(res, data, code = 200) {
73
+ const body = JSON.stringify(data);
74
+ res.writeHead(code, {
75
+ 'Content-Type': 'application/json; charset=utf-8',
76
+ 'Access-Control-Allow-Origin': '*',
77
+ });
78
+ res.end(body);
79
+ }
80
+ function handleMobileEvents(req, res) {
81
+ if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
82
+ mobileJson(res, { error: 'Remote touch disabled' }, 403);
83
+ return;
84
+ }
85
+ if (!mobileAuthorized(req)) {
86
+ mobileJson(res, { error: 'Unauthorized' }, 401);
87
+ return;
88
+ }
89
+ if (!agent) {
90
+ mobileJson(res, { error: 'Agent not initialized' }, 500);
91
+ return;
92
+ }
93
+ res.writeHead(200, {
94
+ 'Content-Type': 'text/event-stream; charset=utf-8',
95
+ 'Cache-Control': 'no-cache',
96
+ 'Connection': 'keep-alive',
97
+ 'Access-Control-Allow-Origin': '*',
98
+ });
99
+ res.write('retry: 3000\n\n');
100
+ const unsubscribe = agent.subscribeWorkEvents(event => {
101
+ try {
102
+ res.write(`event: work\ndata: ${JSON.stringify(event)}\n\n`);
103
+ }
104
+ catch {
105
+ // socket is gone; the close handler will clean up
106
+ }
107
+ });
108
+ const heartbeat = setInterval(() => {
109
+ try {
110
+ res.write(': ping\n\n');
111
+ }
112
+ catch { }
113
+ }, 15000);
114
+ req.on('close', () => {
115
+ clearInterval(heartbeat);
116
+ unsubscribe();
117
+ });
118
+ }
52
119
  function resolveAppPath(root, targetPath) {
53
120
  if (!targetPath)
54
121
  return root;
@@ -214,6 +281,16 @@ function jsonResponse(res, data, code = 200) {
214
281
  async function handleApi(req, res, body) {
215
282
  const url = new URL(req.url || '/', `http://localhost:${PORT}`);
216
283
  const pathname = url.pathname;
284
+ if (pathname.startsWith('/api/mobile/')) {
285
+ if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
286
+ mobileJson(res, { error: 'Remote touch disabled' }, 403);
287
+ return;
288
+ }
289
+ if (!mobileAuthorized(req)) {
290
+ mobileJson(res, { error: 'Unauthorized' }, 401);
291
+ return;
292
+ }
293
+ }
217
294
  if (!agent) {
218
295
  jsonResponse(res, { error: 'Agent not initialized' }, 500);
219
296
  return;
@@ -603,15 +680,114 @@ async function handleApi(req, res, body) {
603
680
  jsonResponse(res, { content: agent.readArchive(aName2) });
604
681
  return;
605
682
  }
683
+ case '/api/mobile/pair-confirm': {
684
+ let pairingId = url.searchParams.get('pairingId') || '';
685
+ if (!pairingId) {
686
+ try {
687
+ pairingId = String(JSON.parse(body || '{}').pairingId || '');
688
+ }
689
+ catch { }
690
+ }
691
+ const result = (0, mobilePairing_1.confirmPairing)(appRoot, pairingId, mobileToken);
692
+ mobileJson(res, result.ok
693
+ ? { ok: true, status: result.status }
694
+ : { ok: false, error: result.error, status: result.status }, result.ok ? 200 : 401);
695
+ return;
696
+ }
697
+ case '/api/mobile/pair-status': {
698
+ mobileJson(res, { ok: true, status: (0, mobilePairing_1.pairingStatus)(appRoot) });
699
+ return;
700
+ }
701
+ case '/api/mobile/hello': {
702
+ mobileJson(res, {
703
+ ok: true,
704
+ version: (0, installUpdate_1.currentAppVersion)(),
705
+ hostname: os.hostname(),
706
+ platform: process.platform,
707
+ tailscaleIpv4: (0, mobilePairing_1.tailscaleIpv4)(),
708
+ workspace: agent.workspace.current ? {
709
+ id: agent.workspace.current.id,
710
+ name: agent.workspace.current.name,
711
+ path: agent.workspace.current.path,
712
+ } : null,
713
+ conversationCount: agent.listConversationStates().length,
714
+ activeConversationId: agent.activeConversationId,
715
+ });
716
+ return;
717
+ }
718
+ case '/api/mobile/state': {
719
+ const active = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
720
+ mobileJson(res, {
721
+ mode: agent.mode,
722
+ model: agent.modelSelectionValue(),
723
+ status: agent.status,
724
+ activeConversationId: agent.activeConversationId,
725
+ conversations: agent.listConversationStates(),
726
+ workspaces: { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current },
727
+ pendingOptions: agent.pendingOptions,
728
+ contextWindow: agent.contextWindow(),
729
+ chatMessages: active.chatMessages,
730
+ totalMessages: active.totalMessages,
731
+ conversationLocked: agent.isConversationLocked(),
732
+ });
733
+ return;
734
+ }
735
+ case '/api/mobile/conversations': {
736
+ mobileJson(res, agent.listConversationStates());
737
+ return;
738
+ }
739
+ case '/api/mobile/conversation': {
740
+ const conversationId = url.searchParams.get('conversationId') || agent.activeConversationId;
741
+ const windowParam = url.searchParams.get('window');
742
+ const beforeParam = url.searchParams.get('before');
743
+ const windowSize = windowParam ? Math.max(1, Math.min(500, Number(windowParam) || 200)) : 200;
744
+ const before = beforeParam ? Math.max(0, Number(beforeParam) || 0) : undefined;
745
+ const snapshot = agent.getConversationSnapshot(String(conversationId), { window: windowSize, before });
746
+ mobileJson(res, snapshot);
747
+ return;
748
+ }
749
+ case '/api/mobile/workspaces': {
750
+ mobileJson(res, { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current });
751
+ return;
752
+ }
753
+ case '/api/mobile/send': {
754
+ const params = JSON.parse(body || '{}');
755
+ const message = String(params.message || '');
756
+ if (!message) {
757
+ mobileJson(res, { error: 'No message' }, 400);
758
+ return;
759
+ }
760
+ if (params.conversationId)
761
+ agent.setConversation(String(params.conversationId));
762
+ const tokens = await agent.process(message);
763
+ const snapshot = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
764
+ mobileJson(res, {
765
+ ok: true,
766
+ conversationId: agent.activeConversationId,
767
+ response: tokens.map(token => token.text).join(''),
768
+ tokens: tokens.map(token => ({ type: token.type, text: token.text })),
769
+ options: agent.pendingOptions,
770
+ status: agent.status,
771
+ conversations: agent.listConversationStates(),
772
+ chatMessages: snapshot.chatMessages,
773
+ totalMessages: snapshot.totalMessages,
774
+ });
775
+ return;
776
+ }
606
777
  default:
607
778
  jsonResponse(res, { error: 'Unknown API' }, 404);
608
779
  }
609
780
  }
610
781
  catch (e) {
611
- jsonResponse(res, { error: e.message }, 500);
782
+ if (pathname.startsWith('/api/mobile/'))
783
+ mobileJson(res, { error: e.message }, 500);
784
+ else
785
+ jsonResponse(res, { error: e.message }, 500);
612
786
  }
613
787
  }
614
788
  function startServer(root) {
789
+ mobileToken = (0, mobilePairing_1.ensureMobileToken)(root);
790
+ appRoot = root;
615
791
  agent = new agent_1.Agent(root);
616
792
  workspaceFileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
617
793
  automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
@@ -645,11 +821,19 @@ function startServer(root) {
645
821
  const uiDir = path.join(__dirname, 'ui');
646
822
  const server = http.createServer(async (req, res) => {
647
823
  if (req.method === 'OPTIONS') {
648
- res.writeHead(204);
824
+ res.writeHead(204, {
825
+ 'Access-Control-Allow-Origin': '*',
826
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type',
827
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
828
+ });
649
829
  res.end();
650
830
  return;
651
831
  }
652
832
  const url = new URL(req.url || '/', `http://localhost:${PORT}`);
833
+ if (url.pathname === '/api/mobile/events') {
834
+ handleMobileEvents(req, res);
835
+ return;
836
+ }
653
837
  if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
654
838
  let body = '';
655
839
  req.on('data', chunk => body += chunk);
@@ -667,9 +851,17 @@ function startServer(root) {
667
851
  const fullPath = path.join(uiDir, filePath);
668
852
  serveFile(res, fullPath);
669
853
  });
670
- server.listen(PORT, '127.0.0.1', () => {
854
+ const bindHost = process.env.NEWMARK_BIND_HOST || '0.0.0.0';
855
+ const tailscale = (0, mobilePairing_1.tailscaleIpv4)();
856
+ const tokenPath = path.join(root, '.newmark-mobile-token');
857
+ server.listen(PORT, bindHost, () => {
671
858
  console.log(`\n Newmark Agent v1.0 - Server Mode`);
859
+ console.log(` Bind: ${bindHost}:${PORT}`);
672
860
  console.log(` GUI: http://localhost:${PORT}`);
861
+ console.log(` Tailscale IPv4: ${tailscale || 'not detected (install/start Tailscale)'}`);
862
+ console.log(` Mobile endpoint: http://${tailscale || '<tailscale-ip>'}:${PORT}/api/mobile/hello?token=<token>`);
863
+ console.log(` Mobile token file: ${tokenPath}`);
864
+ console.log(` Mobile events (SSE): http://${tailscale || '<tailscale-ip>'}:${PORT}/api/mobile/events?token=<token>`);
673
865
  console.log(` Press Ctrl+C to stop\n`);
674
866
  });
675
867
  }
@@ -154,6 +154,7 @@ function createCoreRuntimeAdapter(options = {}) {
154
154
  const { AutomationManager } = require(path.join(desktopDist, "core", "automation.js"));
155
155
  const { FlowEngine } = require(path.join(desktopDist, "core", "flow.js"));
156
156
  const installUpdate = require(path.join(desktopDist, "core", "installUpdate.js"));
157
+ const mobilePairing = require(path.join(desktopDist, "core", "mobilePairing.js"));
157
158
  const root = path.resolve(options.root || path.join(os.homedir(), ".Newmark"));
158
159
  const workspacePath = safeWorkspacePath(root, options.workspacePath || process.cwd());
159
160
  ensureRuntimeRoot(root, configModule);
@@ -211,7 +212,8 @@ function createCoreRuntimeAdapter(options = {}) {
211
212
  defaultTerminalShell: agent.config.getStr("terminal", "default_shell") || (process.platform === "win32" ? "powershell" : "bash"),
212
213
  status: agent.status,
213
214
  connected: true,
214
- runtimeRoot: root
215
+ runtimeRoot: root,
216
+ remoteTouchEnabled: agent.config.getBool('remote', 'touch_enabled')
215
217
  });
216
218
 
217
219
  const snapshotFor = (requested = currentTarget()) => {
@@ -473,6 +475,20 @@ function createCoreRuntimeAdapter(options = {}) {
473
475
  updateVersion() {
474
476
  return { version: installUpdate.currentAppVersion(), root };
475
477
  },
478
+ async pairingQr() {
479
+ const qr = await mobilePairing.pairingQrAscii(root);
480
+ return {
481
+ ascii: qr.ascii,
482
+ url: qr.session.url,
483
+ pairingId: qr.session.pairingId,
484
+ expiresAt: qr.session.expiresAt,
485
+ tokenFile: mobilePairing.pairingTokenPath(root),
486
+ tailscaleIpv4: mobilePairing.tailscaleIpv4(),
487
+ };
488
+ },
489
+ pairingStatus() {
490
+ return mobilePairing.pairingStatus(root);
491
+ },
476
492
  updateCheckGithub(input = {}) {
477
493
  return installUpdate.checkGitHubUpdate(input.repo, input.tag, input.asset, input.token);
478
494
  },
@@ -93,6 +93,46 @@ function executeAction(state, action) {
93
93
  const appearance = applyThemeAppearance(state, state.theme === "dark" ? "Light" : "Dark");
94
94
  state.adapter.saveConfig(appearance);
95
95
  state.notice = `${state.theme === "dark" ? "Dark" : "Light"} terminal theme`;
96
+ } else if (action === "pair-mobile") {
97
+ state.overlay = "pair";
98
+ state.pairingQrLines = ["Loading pairing QR…"];
99
+ state.pairingUrl = "";
100
+ state.pairingTokenFile = "";
101
+ if (typeof state.requestPaint === "function") state.requestPaint();
102
+ if (typeof state.adapter.pairingQr !== "function") {
103
+ state.pairingQrLines = ["Pairing QR is unavailable in this adapter."];
104
+ if (typeof state.requestPaint === "function") state.requestPaint();
105
+ return;
106
+ }
107
+ Promise.resolve(state.adapter.pairingQr())
108
+ .then((pairing) => {
109
+ state.pairingQrLines = String(pairing?.ascii || "").split(/\r?\n/);
110
+ state.pairingUrl = String(pairing?.url || "");
111
+ state.pairingTokenFile = String(pairing?.tokenFile || "");
112
+ if (typeof state.requestPaint === "function") state.requestPaint();
113
+ if (typeof state.adapter.pairingStatus === "function") {
114
+ if (state._pairingPoll) clearInterval(state._pairingPoll);
115
+ state._pairingPoll = setInterval(() => {
116
+ const status = state.adapter.pairingStatus();
117
+ if (!status) return;
118
+ if (status.confirmed) {
119
+ clearInterval(state._pairingPoll);
120
+ state.overlay = null;
121
+ state.notice = "Mobile device connected";
122
+ if (typeof state.requestPaint === "function") state.requestPaint();
123
+ } else if (status.expired || !status.active) {
124
+ clearInterval(state._pairingPoll);
125
+ state.overlay = null;
126
+ state.notice = "Pairing window expired";
127
+ if (typeof state.requestPaint === "function") state.requestPaint();
128
+ }
129
+ }, 1000);
130
+ }
131
+ })
132
+ .catch((error) => {
133
+ state.pairingQrLines = [`Pairing failed: ${error?.message || error}`];
134
+ if (typeof state.requestPaint === "function") state.requestPaint();
135
+ });
96
136
  } else if (action === "help") {
97
137
  state.overlay = "help";
98
138
  }
@@ -166,6 +206,7 @@ function start(options = {}) {
166
206
  return;
167
207
  }
168
208
  const state = createState({ adapter });
209
+ state.requestPaint = () => paint();
169
210
  let timer = null;
170
211
  let animationTimer = null;
171
212
  let closing = false;
@@ -315,6 +315,7 @@ const commands = [
315
315
  { label: "Open Automations", hint: "", action: "view:automation" },
316
316
  { label: "Open WorkFlow", hint: "", action: "view:workflow" },
317
317
  { label: "Open Settings", hint: "", action: "view:settings" },
318
+ { label: "Show mobile pairing QR", hint: "", action: "pair-mobile" },
318
319
  { label: "New conversation", hint: "N", action: "new-chat" },
319
320
  { label: "Toggle theme", hint: "T", action: "theme" },
320
321
  { label: "Keyboard shortcuts", hint: "?", action: "help" }
@@ -1154,6 +1154,17 @@ function overlayLines(state, width, p) {
1154
1154
  `${p.cyan}Press Esc or ? to close${p.reset}`
1155
1155
  ], Math.min(78, width - 4), p, "Keyboard shortcuts");
1156
1156
  }
1157
+ if (state.overlay === "pair") {
1158
+ const qrLines = Array.isArray(state.pairingQrLines) ? state.pairingQrLines : [];
1159
+ const rows = [
1160
+ ...qrLines,
1161
+ "",
1162
+ ...(state.pairingUrl ? [`${p.muted}${state.pairingUrl}${p.reset}`] : []),
1163
+ ...(state.pairingTokenFile ? [`${p.muted}Token: ${state.pairingTokenFile}${p.reset}`] : []),
1164
+ `${p.cyan}Scan with the Newmark mobile app · Esc close${p.reset}`
1165
+ ];
1166
+ return card(rows, Math.min(88, width - 4), p, "Pair mobile device");
1167
+ }
1157
1168
  if (state.overlay === "palette") {
1158
1169
  const commands = filteredCommands(state);
1159
1170
  const paletteViewport = Math.min(7, Math.max(1, commands.length));
@@ -23,7 +23,9 @@ function settingsRows(state, tab = state.settingsTab) {
23
23
  { key: "dialogStyle", label: "Conversation style", value: s.general.dialogStyle, choices: ["Formal", "Friendly"], save: ["config", "dialogStyle"] },
24
24
  { key: "feedbackLevel", label: "Option feedback", value: s.general.feedbackLevel, choices: ["Default", "Ask more", "Ask less", "Autonomous"], save: ["config", "feedbackLevel"] },
25
25
  { key: "closeBehavior", label: "Close behavior", value: s.general.closeBehavior, choices: ["Close app", "Minimize to tray"], save: ["setting", "general", "close_behavior"] },
26
- { key: "expandTools", label: "Expand tool usage", value: s.general.expandTools, choices: [true, false], save: ["setting", "general", "expand_tools"] }
26
+ { key: "expandTools", label: "Expand tool usage", value: s.general.expandTools, choices: [true, false], save: ["setting", "general", "expand_tools"] },
27
+ { key: "remoteTouch", label: "Remote behavior · Mobile remote-touch", value: s.general.remoteTouch, choices: [true, false], save: ["setting", "remote", "touch_enabled"] },
28
+ { key: "remotePair", label: "Remote behavior · Start connection", value: "Open QR", choices: [], action: "pair-mobile" }
27
29
  ];
28
30
  if (tab === "personalization") return [
29
31
  { key: "theme", label: "Theme", value: s.personalization.theme, choices: ["Dark", "Light", "System"], save: ["config", "theme"] },
@@ -253,7 +253,8 @@ function createState(options = {}) {
253
253
  dialogStyle: snapshot.dialogStyle === "friendly" ? "Friendly" : "Formal",
254
254
  feedbackLevel: { ask_more: "Ask more", ask_less: "Ask less", fully_autonomous: "Autonomous" }[snapshot.feedback] || "Default",
255
255
  closeBehavior: snapshot.closeBehavior === "minimize" ? "Minimize to tray" : "Close app",
256
- expandTools: snapshot.expandToolsDefault !== false
256
+ expandTools: snapshot.expandToolsDefault !== false,
257
+ remoteTouch: snapshot.remoteTouchEnabled !== false
257
258
  },
258
259
  personalization: {
259
260
  theme: { light: "Light", system: "System", dark: "Dark" }[String(snapshot.darkMode || "dark").toLowerCase()] || "Dark",
@@ -1071,6 +1072,25 @@ function toggleSelected(state) {
1071
1072
  }
1072
1073
  const row = settingsRows(state)[state.selected];
1073
1074
  if (!row) return;
1075
+ if (row.action === "pair-mobile") {
1076
+ state.overlay = "pair";
1077
+ state.pairingQrLines = ["Loading pairing QR…"];
1078
+ state.pairingUrl = "";
1079
+ state.pairingTokenFile = "";
1080
+ if (typeof state.adapter.pairingQr !== "function") {
1081
+ state.pairingQrLines = ["Pairing QR is unavailable in this adapter."];
1082
+ return false;
1083
+ }
1084
+ return Promise.resolve(state.adapter.pairingQr()).then((pairing) => {
1085
+ state.pairingQrLines = String(pairing?.ascii || "").split(/\r?\n/);
1086
+ state.pairingUrl = String(pairing?.url || "");
1087
+ state.pairingTokenFile = String(pairing?.tokenFile || "");
1088
+ return true;
1089
+ }).catch((error) => {
1090
+ state.pairingQrLines = [`Pairing failed: ${error?.message || error}`];
1091
+ return false;
1092
+ });
1093
+ }
1074
1094
  if (state.settingsTab === "general" && row.key === "inputBehavior") {
1075
1095
  const current = row.choices.findIndex((value) => value === row.value);
1076
1096
  state.settingChoiceTab = state.settingsTab;