newmark-agent 0.4.5 → 0.4.7

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.
Files changed (40) hide show
  1. package/assets/app-icon-dark.svg +6 -0
  2. package/dist/assets/app-icon-dark.svg +6 -0
  3. package/dist/cli-commands.d.ts +1 -1
  4. package/dist/cli-commands.js +90 -7
  5. package/dist/cli-discovery.js +10 -0
  6. package/dist/conversation-utility-host.bundle.cjs +293 -148
  7. package/dist/core/agent.d.ts +38 -4
  8. package/dist/core/agent.js +231 -49
  9. package/dist/core/agentKernelRunner.d.ts +3 -0
  10. package/dist/core/agentKernelRunner.js +49 -83
  11. package/dist/core/config.js +3 -0
  12. package/dist/core/dshCompatibility.d.ts +23 -6
  13. package/dist/core/dshCompatibility.js +99 -1
  14. package/dist/core/installUpdate.d.ts +67 -0
  15. package/dist/core/installUpdate.js +268 -0
  16. package/dist/core/mobilePairing.d.ts +47 -0
  17. package/dist/core/mobilePairing.js +221 -0
  18. package/dist/core/subagent.d.ts +10 -3
  19. package/dist/core/subagent.js +23 -8
  20. package/dist/core/toolPolicy.js +11 -3
  21. package/dist/launcher.js +14 -11
  22. package/dist/main.js +64 -3
  23. package/dist/preload.js +5 -0
  24. package/dist/providers/chat-completions.adapter.js +6 -2
  25. package/dist/providers/responses.adapter.js +1 -0
  26. package/dist/server.d.ts +1 -0
  27. package/dist/server.js +721 -3
  28. package/dist/toolchain/registry-seeder.js +3 -1
  29. package/dist/tools/index.js +11 -5
  30. package/dist/tools/nativeTools.js +1 -1
  31. package/dist/tui/src/adapters/core-runtime-adapter.js +17 -1
  32. package/dist/tui/src/app.js +41 -0
  33. package/dist/tui/src/data.js +1 -0
  34. package/dist/tui/src/render.js +11 -0
  35. package/dist/tui/src/settings-schema.js +3 -1
  36. package/dist/tui/src/state.js +21 -1
  37. package/dist/ui/index.html +530 -101
  38. package/dist/ui/lucide-sprite.svg +10 -0
  39. package/dist/wsl-agent-host.bundle.cjs +293 -148
  40. package/package.json +14 -8
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,187 @@ 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
+ const mobileWorkEventSubscribers = new Set();
58
+ const mobileScopedRuntimes = new Map();
59
+ function mobileRuntimeKey(workspaceId, conversationId) {
60
+ return `${String(workspaceId || '')}::${String(conversationId || '')}`;
61
+ }
62
+ function publishMobileWorkEvent(event) {
63
+ for (const subscriber of mobileWorkEventSubscribers) {
64
+ try {
65
+ subscriber(event);
66
+ }
67
+ catch { /* ignore disconnected mobile listeners */ }
68
+ }
69
+ }
70
+ function mobileAuthorized(req) {
71
+ if (!mobileToken)
72
+ return false;
73
+ const bearer = String(req.headers.authorization || '');
74
+ if (bearer.startsWith('Bearer '))
75
+ return bearer.slice('Bearer '.length).trim() === mobileToken;
76
+ try {
77
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`);
78
+ const queryToken = url.searchParams.get('token') || '';
79
+ return queryToken === mobileToken;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ function mobileJson(res, data, code = 200) {
86
+ const body = JSON.stringify(data);
87
+ res.writeHead(code, {
88
+ 'Content-Type': 'application/json; charset=utf-8',
89
+ 'Access-Control-Allow-Origin': '*',
90
+ });
91
+ res.end(body);
92
+ }
93
+ function resolveMobileWorkspace(current, workspaceId) {
94
+ const clean = String(workspaceId || '');
95
+ return [...current.workspace.internal, ...current.workspace.external].find(workspace => workspace.id === clean)
96
+ || (current.workspace.current?.id === clean ? current.workspace.current : null);
97
+ }
98
+ const MOBILE_EDITABLE_EXTENSIONS = new Set([
99
+ '.txt', '.md', '.markdown', '.json', '.jsonc', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
100
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.htm', '.xml', '.svg',
101
+ '.kt', '.kts', '.java', '.py', '.rb', '.rs', '.go', '.c', '.h', '.cpp', '.hpp', '.cs', '.sh',
102
+ '.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
103
+ ]);
104
+ const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
105
+ function resolveMobileWorkspacePath(ws, relativePath) {
106
+ const root = path.resolve(ws.path);
107
+ const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
108
+ const target = path.resolve(root, clean || '.');
109
+ if (target !== root && !target.startsWith(root + path.sep))
110
+ throw new Error('Path escapes workspace');
111
+ return { root, target, relative: path.relative(root, target).replace(/\\/g, '/') };
112
+ }
113
+ async function assertMobileRealPathContained(root, target) {
114
+ const realRoot = await fs.promises.realpath(root);
115
+ const realTarget = await fs.promises.realpath(target);
116
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot + path.sep))
117
+ throw new Error('Path escapes workspace through a symbolic link');
118
+ }
119
+ function mobileEditableFile(target) {
120
+ const base = path.basename(target).toLowerCase();
121
+ return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
122
+ }
123
+ function mobileRightSidebarState(current, ws, conversationId) {
124
+ const scoped = mobileScopedAgent(ws, conversationId);
125
+ return {
126
+ workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
127
+ conversationId,
128
+ conversationPlan: scoped.getConversationPlan(conversationId),
129
+ linkedPlan: scoped.getLinkedPlan(conversationId),
130
+ subagents: scoped.subagents.listAll()
131
+ .filter(record => !record.conversationId || record.conversationId === conversationId)
132
+ .map(record => ({
133
+ id: record.id,
134
+ name: record.name,
135
+ displayName: record.displayName,
136
+ status: record.status,
137
+ model: record.model,
138
+ mode: record.agentMode,
139
+ inputMode: record.inputMode,
140
+ result: record.result,
141
+ error: record.error || '',
142
+ messageCount: record.messages.length,
143
+ messages: record.messages.slice(-40),
144
+ })),
145
+ };
146
+ }
147
+ function mobileWorkspaceConversationRows(current, ws) {
148
+ const activeConversationId = current.activeConversationIdForWorkspace(ws);
149
+ const isRuntimeWorkspace = !!current.workspace.current
150
+ && path.resolve(current.workspace.current.path) === path.resolve(ws.path);
151
+ const activeRuntimeStatus = current.status === 'working' ? 'running' : current.status;
152
+ return current.listWorkspaceConversationStates(ws).map(conversation => {
153
+ const scopedRuntime = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversation.id));
154
+ const scopedStatus = scopedRuntime?.agent.status === 'working' ? 'running' : scopedRuntime?.agent.status || '';
155
+ const runtimeStatus = scopedRuntime
156
+ ? scopedStatus
157
+ : isRuntimeWorkspace && conversation.id === current.activeConversationId && activeRuntimeStatus !== 'idle'
158
+ ? activeRuntimeStatus
159
+ : '';
160
+ return {
161
+ ...conversation,
162
+ active: conversation.id === activeConversationId || !!scopedRuntime,
163
+ runtimeStatus,
164
+ running: ['running', 'stopping', 'force_restarting'].includes(runtimeStatus),
165
+ };
166
+ });
167
+ }
168
+ function mobileScopedAgent(ws, conversationId) {
169
+ const scoped = new agent_1.Agent(appRoot, {
170
+ agentOnly: true,
171
+ workspaceRegistryMode: 'detached',
172
+ readOnlyConfig: true,
173
+ conversationId,
174
+ });
175
+ scoped.workspace.current = { ...ws };
176
+ scoped.config.loadWorkspaceConfig(ws.path);
177
+ scoped.setConversationFromStorage(conversationId);
178
+ return scoped;
179
+ }
180
+ function mobileConversationRuntimeBusy(current, ws, conversationId) {
181
+ const scoped = mobileScopedRuntimes.get(mobileRuntimeKey(String(ws.id || ''), conversationId));
182
+ if (scoped && scoped.agent.status !== 'idle')
183
+ return true;
184
+ const currentWs = current.workspace.current;
185
+ return !!currentWs
186
+ && path.resolve(currentWs.path) === path.resolve(ws.path)
187
+ && current.activeConversationId === conversationId
188
+ && current.status !== 'idle';
189
+ }
190
+ function handleMobileEvents(req, res) {
191
+ if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
192
+ mobileJson(res, { error: 'Remote touch disabled' }, 403);
193
+ return;
194
+ }
195
+ if (!mobileAuthorized(req)) {
196
+ mobileJson(res, { error: 'Unauthorized' }, 401);
197
+ return;
198
+ }
199
+ if (!agent) {
200
+ mobileJson(res, { error: 'Agent not initialized' }, 500);
201
+ return;
202
+ }
203
+ res.writeHead(200, {
204
+ 'Content-Type': 'text/event-stream; charset=utf-8',
205
+ 'Cache-Control': 'no-cache',
206
+ 'Connection': 'keep-alive',
207
+ 'Access-Control-Allow-Origin': '*',
208
+ });
209
+ res.write('retry: 3000\n\n');
210
+ const subscriber = (event) => {
211
+ try {
212
+ res.write(`event: work\ndata: ${JSON.stringify(event)}\n\n`);
213
+ }
214
+ catch {
215
+ // socket is gone; the close handler will clean up
216
+ }
217
+ };
218
+ mobileWorkEventSubscribers.add(subscriber);
219
+ const heartbeat = setInterval(() => {
220
+ try {
221
+ res.write(': ping\n\n');
222
+ }
223
+ catch { }
224
+ }, 15000);
225
+ res.on('close', () => {
226
+ clearInterval(heartbeat);
227
+ mobileWorkEventSubscribers.delete(subscriber);
228
+ });
229
+ }
52
230
  function resolveAppPath(root, targetPath) {
53
231
  if (!targetPath)
54
232
  return root;
@@ -214,6 +392,16 @@ function jsonResponse(res, data, code = 200) {
214
392
  async function handleApi(req, res, body) {
215
393
  const url = new URL(req.url || '/', `http://localhost:${PORT}`);
216
394
  const pathname = url.pathname;
395
+ if (pathname.startsWith('/api/mobile/')) {
396
+ if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
397
+ mobileJson(res, { error: 'Remote touch disabled' }, 403);
398
+ return;
399
+ }
400
+ if (!mobileAuthorized(req)) {
401
+ mobileJson(res, { error: 'Unauthorized' }, 401);
402
+ return;
403
+ }
404
+ }
217
405
  if (!agent) {
218
406
  jsonResponse(res, { error: 'Agent not initialized' }, 500);
219
407
  return;
@@ -603,15 +791,520 @@ async function handleApi(req, res, body) {
603
791
  jsonResponse(res, { content: agent.readArchive(aName2) });
604
792
  return;
605
793
  }
794
+ case '/api/mobile/pair-confirm': {
795
+ let pairingId = url.searchParams.get('pairingId') || '';
796
+ if (!pairingId) {
797
+ try {
798
+ pairingId = String(JSON.parse(body || '{}').pairingId || '');
799
+ }
800
+ catch { }
801
+ }
802
+ const result = (0, mobilePairing_1.confirmPairing)(appRoot, pairingId, mobileToken);
803
+ mobileJson(res, result.ok
804
+ ? { ok: true, status: result.status }
805
+ : { ok: false, error: result.error, status: result.status }, result.ok ? 200 : 401);
806
+ return;
807
+ }
808
+ case '/api/mobile/pair-status': {
809
+ mobileJson(res, { ok: true, status: (0, mobilePairing_1.pairingStatus)(appRoot) });
810
+ return;
811
+ }
812
+ case '/api/mobile/hello': {
813
+ mobileJson(res, {
814
+ ok: true,
815
+ version: (0, installUpdate_1.currentAppVersion)(),
816
+ hostname: os.hostname(),
817
+ platform: process.platform,
818
+ tailscaleIpv4: (0, mobilePairing_1.tailscaleIpv4)(),
819
+ lanIpv4: (0, mobilePairing_1.lanIpv4)(),
820
+ workspace: agent.workspace.current ? {
821
+ id: agent.workspace.current.id,
822
+ name: agent.workspace.current.name,
823
+ path: agent.workspace.current.path,
824
+ } : null,
825
+ conversationCount: agent.listConversationStates().length,
826
+ activeConversationId: agent.activeConversationId,
827
+ });
828
+ return;
829
+ }
830
+ case '/api/mobile/state': {
831
+ const active = agent.getConversationSnapshot(agent.activeConversationId, { window: 200 });
832
+ // 完整对话信息:workRuns 用持久化记录透出(含被中断的构建,不依赖运行时内存)
833
+ const persistedRuns = agent.getPersistedConversationWorkRuns(agent.activeConversationId);
834
+ if (persistedRuns.length)
835
+ active.workRuns = persistedRuns;
836
+ mobileJson(res, {
837
+ mode: agent.mode,
838
+ model: agent.modelSelectionValue(),
839
+ modelLabel: agent.modelLabel(),
840
+ status: agent.status,
841
+ activeConversationId: agent.activeConversationId,
842
+ conversations: agent.listConversationStates(),
843
+ workspaces: { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current },
844
+ pendingOptions: agent.pendingOptions,
845
+ contextWindow: agent.contextWindow(),
846
+ chatMessages: active.chatMessages,
847
+ totalMessages: active.totalMessages,
848
+ conversationLocked: agent.isConversationLocked(),
849
+ // workRuns 透出:完整 Build Block 记录(含 interrupted/force_interrupted),供移动端渲染被中断的对话
850
+ workRuns: active.workRuns,
851
+ // goal bar / flow bar:目标状态与当前 Flow 选择
852
+ goal: active.goal,
853
+ flowSelection: active.flowSelection,
854
+ });
855
+ return;
856
+ }
857
+ case '/api/mobile/conversations': {
858
+ mobileJson(res, agent.listConversationStates());
859
+ return;
860
+ }
861
+ case '/api/mobile/conversation': {
862
+ const workspaceId = url.searchParams.get('workspaceId') || '';
863
+ const windowParam = url.searchParams.get('window');
864
+ const beforeParam = url.searchParams.get('before');
865
+ const windowSize = windowParam ? Math.max(1, Math.min(500, Number(windowParam) || 200)) : 200;
866
+ const before = beforeParam ? Math.max(0, Number(beforeParam) || 0) : undefined;
867
+ const current = agent;
868
+ const ws = workspaceId ? resolveMobileWorkspace(current, workspaceId) : null;
869
+ if (workspaceId && !ws) {
870
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
871
+ return;
872
+ }
873
+ const conversationId = url.searchParams.get('conversationId')
874
+ || (ws ? current.activeConversationIdForWorkspace(ws) : current.activeConversationId);
875
+ if (ws && !current.hasConversationInWorkspace(String(conversationId), ws)) {
876
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
877
+ return;
878
+ }
879
+ const snapshot = current.getConversationSnapshot(String(conversationId), { window: windowSize, before, workspace: ws });
880
+ // 完整对话信息:workRuns 统一用持久化记录透出(含被中断的构建)
881
+ const persistedRuns = current.getPersistedConversationWorkRuns(String(conversationId), ws || undefined);
882
+ if (persistedRuns.length)
883
+ snapshot.workRuns = persistedRuns;
884
+ mobileJson(res, snapshot);
885
+ return;
886
+ }
887
+ case '/api/mobile/workspaces': {
888
+ mobileJson(res, { internal: agent.workspace.internal, external: agent.workspace.external, current: agent.workspace.current });
889
+ return;
890
+ }
891
+ case '/api/mobile/workspace-conversations': {
892
+ const workspaceId = url.searchParams.get('workspaceId') || '';
893
+ const current = agent;
894
+ const ws = resolveMobileWorkspace(current, workspaceId);
895
+ if (!ws) {
896
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
897
+ return;
898
+ }
899
+ const conversations = mobileWorkspaceConversationRows(current, ws);
900
+ mobileJson(res, { workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal }, conversations });
901
+ return;
902
+ }
903
+ case '/api/mobile/right-sidebar-state': {
904
+ const workspaceId = url.searchParams.get('workspaceId') || '';
905
+ const conversationId = url.searchParams.get('conversationId') || '';
906
+ const ws = resolveMobileWorkspace(agent, workspaceId);
907
+ if (!ws) {
908
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
909
+ return;
910
+ }
911
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
912
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
913
+ return;
914
+ }
915
+ mobileJson(res, mobileRightSidebarState(agent, ws, conversationId));
916
+ return;
917
+ }
918
+ case '/api/mobile/workspace-files': {
919
+ const workspaceId = url.searchParams.get('workspaceId') || '';
920
+ const relativePath = url.searchParams.get('path') || '';
921
+ const ws = resolveMobileWorkspace(agent, workspaceId);
922
+ if (!ws) {
923
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
924
+ return;
925
+ }
926
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
927
+ await assertMobileRealPathContained(resolved.root, resolved.target);
928
+ const stat = await fs.promises.stat(resolved.target);
929
+ if (!stat.isDirectory()) {
930
+ mobileJson(res, { error: 'Path is not a directory' }, 400);
931
+ return;
932
+ }
933
+ const entries = await fs.promises.readdir(resolved.target, { withFileTypes: true });
934
+ const rows = entries
935
+ .filter(entry => entry.name !== '.git' && entry.name !== 'node_modules')
936
+ .map(entry => ({
937
+ name: entry.name,
938
+ path: path.posix.join(resolved.relative, entry.name).replace(/^\.\//, ''),
939
+ directory: entry.isDirectory(),
940
+ }))
941
+ .sort((a, b) => Number(b.directory) - Number(a.directory) || a.name.localeCompare(b.name));
942
+ mobileJson(res, { workspaceId, path: resolved.relative, entries: rows });
943
+ return;
944
+ }
945
+ case '/api/mobile/workspace-file': {
946
+ if (req.method === 'GET') {
947
+ const workspaceId = url.searchParams.get('workspaceId') || '';
948
+ const relativePath = url.searchParams.get('path') || '';
949
+ const ws = resolveMobileWorkspace(agent, workspaceId);
950
+ if (!ws) {
951
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
952
+ return;
953
+ }
954
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
955
+ await assertMobileRealPathContained(resolved.root, resolved.target);
956
+ const stat = await fs.promises.stat(resolved.target);
957
+ if (!stat.isFile() || stat.size > MOBILE_EDITOR_MAX_BYTES || !mobileEditableFile(resolved.target)) {
958
+ mobileJson(res, { error: 'File is not an editable text file' }, 415);
959
+ return;
960
+ }
961
+ const content = (await fs.promises.readFile(resolved.target, 'utf-8')).replace(/^\uFEFF/, '');
962
+ mobileJson(res, { workspaceId, path: resolved.relative, content, size: stat.size });
963
+ return;
964
+ }
965
+ const params = JSON.parse(body || '{}');
966
+ const workspaceId = String(params.workspaceId || '');
967
+ const relativePath = String(params.path || '');
968
+ const content = String(params.content ?? '');
969
+ const ws = resolveMobileWorkspace(agent, workspaceId);
970
+ if (!ws) {
971
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
972
+ return;
973
+ }
974
+ if (Buffer.byteLength(content, 'utf-8') > MOBILE_EDITOR_MAX_BYTES) {
975
+ mobileJson(res, { error: 'File exceeds mobile editor size limit' }, 413);
976
+ return;
977
+ }
978
+ const resolved = resolveMobileWorkspacePath(ws, relativePath);
979
+ await assertMobileRealPathContained(resolved.root, resolved.target);
980
+ if (!mobileEditableFile(resolved.target)) {
981
+ mobileJson(res, { error: 'File type is not editable' }, 415);
982
+ return;
983
+ }
984
+ const stat = await fs.promises.stat(resolved.target);
985
+ if (!stat.isFile()) {
986
+ mobileJson(res, { error: 'Path is not a file' }, 400);
987
+ return;
988
+ }
989
+ await fs.promises.writeFile(resolved.target, content, 'utf-8');
990
+ mobileJson(res, { ok: true, workspaceId, path: resolved.relative, size: Buffer.byteLength(content, 'utf-8') });
991
+ return;
992
+ }
993
+ case '/api/mobile/conversation-plan-update': {
994
+ const params = JSON.parse(body || '{}');
995
+ const workspaceId = String(params.workspaceId || '');
996
+ const conversationId = String(params.conversationId || '');
997
+ const ws = resolveMobileWorkspace(agent, workspaceId);
998
+ if (!ws) {
999
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1000
+ return;
1001
+ }
1002
+ if (!conversationId || !agent.hasConversationInWorkspace(conversationId, ws)) {
1003
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1004
+ return;
1005
+ }
1006
+ const scoped = mobileScopedAgent(ws, conversationId);
1007
+ const plan = scoped.updateConversationPlan({ items: Array.isArray(params.items) ? params.items : [] }, conversationId);
1008
+ mobileJson(res, { ok: true, conversationPlan: plan });
1009
+ return;
1010
+ }
1011
+ case '/api/mobile/conversation-create': {
1012
+ const params = JSON.parse(body || '{}');
1013
+ const workspaceId = String(params.workspaceId || '');
1014
+ if (!workspaceId) {
1015
+ mobileJson(res, { error: 'No workspaceId' }, 400);
1016
+ return;
1017
+ }
1018
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1019
+ if (!ws) {
1020
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1021
+ return;
1022
+ }
1023
+ const created = agent.createConversationInWorkspace(ws, String(params.title || ''));
1024
+ mobileJson(res, {
1025
+ ok: true,
1026
+ workspace: { id: ws.id, name: ws.name, path: ws.path, isInternal: ws.isInternal },
1027
+ conversation: created,
1028
+ activeConversationId: created.id,
1029
+ conversations: mobileWorkspaceConversationRows(agent, ws),
1030
+ });
1031
+ return;
1032
+ }
1033
+ case '/api/mobile/conversation-rename': {
1034
+ const params = JSON.parse(body || '{}');
1035
+ const workspaceId = String(params.workspaceId || '');
1036
+ const conversationId = String(params.conversationId || '');
1037
+ const title = String(params.title || '');
1038
+ if (!workspaceId || !conversationId || !title.trim()) {
1039
+ mobileJson(res, { error: 'workspaceId, conversationId, and title are required' }, 400);
1040
+ return;
1041
+ }
1042
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1043
+ if (!ws) {
1044
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1045
+ return;
1046
+ }
1047
+ const ok = agent.renameConversation(conversationId, title, ws);
1048
+ if (!ok) {
1049
+ mobileJson(res, { ok: false, error: 'Conversation rename failed' }, 404);
1050
+ return;
1051
+ }
1052
+ mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(agent, ws) });
1053
+ return;
1054
+ }
1055
+ case '/api/mobile/conversation-pin': {
1056
+ const params = JSON.parse(body || '{}');
1057
+ const workspaceId = String(params.workspaceId || '');
1058
+ const conversationId = String(params.conversationId || '');
1059
+ if (!workspaceId || !conversationId) {
1060
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1061
+ return;
1062
+ }
1063
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1064
+ if (!ws) {
1065
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1066
+ return;
1067
+ }
1068
+ const pinned = params.pinned === true;
1069
+ const ok = agent.setConversationPinned(conversationId, pinned, ws);
1070
+ if (!ok) {
1071
+ mobileJson(res, { ok: false, error: 'Conversation pin update failed' }, 404);
1072
+ return;
1073
+ }
1074
+ mobileJson(res, { ok: true, pinned, conversations: mobileWorkspaceConversationRows(agent, ws) });
1075
+ return;
1076
+ }
1077
+ case '/api/mobile/conversation-reorder': {
1078
+ const params = JSON.parse(body || '{}');
1079
+ const workspaceId = String(params.workspaceId || '');
1080
+ const conversationIds = Array.isArray(params.conversationIds)
1081
+ ? params.conversationIds.map((id) => String(id || ''))
1082
+ : [];
1083
+ if (!workspaceId || conversationIds.length < 2 || conversationIds.some((id) => !id)) {
1084
+ mobileJson(res, { error: 'workspaceId and at least two conversationIds are required' }, 400);
1085
+ return;
1086
+ }
1087
+ if (new Set(conversationIds).size !== conversationIds.length) {
1088
+ mobileJson(res, { error: 'conversationIds must be unique' }, 400);
1089
+ return;
1090
+ }
1091
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1092
+ if (!ws) {
1093
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1094
+ return;
1095
+ }
1096
+ const current = agent;
1097
+ if (conversationIds.some((id) => !current.hasConversationInWorkspace(id, ws))) {
1098
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1099
+ return;
1100
+ }
1101
+ const listedById = new Map(current.listWorkspaceConversationStates(ws).map(item => [item.id, item]));
1102
+ if (new Set(conversationIds.map((id) => !!listedById.get(id)?.pinned)).size !== 1) {
1103
+ mobileJson(res, { error: 'Conversations must remain within one pinned group' }, 409);
1104
+ return;
1105
+ }
1106
+ const ok = current.reorderWorkspaceConversationGroup(conversationIds, ws);
1107
+ if (!ok) {
1108
+ mobileJson(res, { ok: false, error: 'Conversation reorder failed' }, 409);
1109
+ return;
1110
+ }
1111
+ mobileJson(res, { ok: true, conversations: mobileWorkspaceConversationRows(current, ws) });
1112
+ return;
1113
+ }
1114
+ case '/api/mobile/send': {
1115
+ const params = JSON.parse(body || '{}');
1116
+ const message = String(params.message || '');
1117
+ if (!message) {
1118
+ mobileJson(res, { error: 'No message' }, 400);
1119
+ return;
1120
+ }
1121
+ const workspaceId = String(params.workspaceId || '');
1122
+ let requestAgent = agent;
1123
+ let requestWorkspace = agent.workspace.current;
1124
+ let resolvedWorkspace = null;
1125
+ if (workspaceId) {
1126
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1127
+ if (!ws) {
1128
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1129
+ return;
1130
+ }
1131
+ resolvedWorkspace = ws;
1132
+ requestWorkspace = ws;
1133
+ }
1134
+ const conversationId = String(params.conversationId
1135
+ || (resolvedWorkspace ? agent.activeConversationIdForWorkspace(resolvedWorkspace) : agent.activeConversationId));
1136
+ if (resolvedWorkspace) {
1137
+ const ws = resolvedWorkspace;
1138
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1139
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1140
+ return;
1141
+ }
1142
+ const currentWs = agent.workspace.current;
1143
+ if (!currentWs || path.resolve(currentWs.path) !== path.resolve(ws.path)) {
1144
+ requestAgent = mobileScopedAgent(ws, conversationId);
1145
+ }
1146
+ }
1147
+ if (requestAgent === agent && params.conversationId)
1148
+ requestAgent.setConversation(conversationId);
1149
+ let scopedRuntimeKey = '';
1150
+ let scopedRuntime = null;
1151
+ let unsubscribeScoped = null;
1152
+ if (requestAgent !== agent && requestWorkspace) {
1153
+ scopedRuntimeKey = mobileRuntimeKey(String(requestWorkspace.id || ''), conversationId);
1154
+ unsubscribeScoped = requestAgent.subscribeWorkEvents(event => publishMobileWorkEvent({
1155
+ ...event,
1156
+ workspaceId: String(requestWorkspace?.id || ''),
1157
+ conversationId,
1158
+ }));
1159
+ scopedRuntime = {
1160
+ agent: requestAgent,
1161
+ workspaceId: String(requestWorkspace.id || ''),
1162
+ conversationId,
1163
+ unsubscribe: unsubscribeScoped,
1164
+ };
1165
+ mobileScopedRuntimes.set(scopedRuntimeKey, scopedRuntime);
1166
+ }
1167
+ let tokens;
1168
+ try {
1169
+ tokens = await requestAgent.process(message);
1170
+ }
1171
+ finally {
1172
+ if (scopedRuntimeKey && mobileScopedRuntimes.get(scopedRuntimeKey) === scopedRuntime) {
1173
+ mobileScopedRuntimes.delete(scopedRuntimeKey);
1174
+ }
1175
+ if (unsubscribeScoped)
1176
+ unsubscribeScoped();
1177
+ }
1178
+ const snapshot = requestAgent.getConversationSnapshot(conversationId, {
1179
+ window: 200,
1180
+ workspace: requestWorkspace,
1181
+ });
1182
+ mobileJson(res, {
1183
+ ok: true,
1184
+ conversationId,
1185
+ response: tokens.map(token => token.text).join(''),
1186
+ tokens: tokens.map(token => ({ type: token.type, text: token.text })),
1187
+ options: requestAgent.pendingOptions,
1188
+ status: requestAgent.status,
1189
+ conversations: requestWorkspace
1190
+ ? mobileWorkspaceConversationRows(requestAgent, requestWorkspace)
1191
+ : requestAgent.listConversationStates(),
1192
+ chatMessages: snapshot.chatMessages,
1193
+ totalMessages: snapshot.totalMessages,
1194
+ });
1195
+ return;
1196
+ }
1197
+ case '/api/mobile/conversation-branch-inspect':
1198
+ case '/api/mobile/conversation-branch-activate':
1199
+ case '/api/mobile/conversation-branch-create': {
1200
+ const params = JSON.parse(body || '{}');
1201
+ const workspaceId = String(params.workspaceId || '');
1202
+ const conversationId = String(params.conversationId || '');
1203
+ if (!workspaceId || !conversationId) {
1204
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1205
+ return;
1206
+ }
1207
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1208
+ if (!ws) {
1209
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1210
+ return;
1211
+ }
1212
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1213
+ mobileJson(res, { error: 'Conversation not found in workspace' }, 404);
1214
+ return;
1215
+ }
1216
+ const scoped = mobileScopedAgent(ws, conversationId);
1217
+ if (pathname === '/api/mobile/conversation-branch-inspect') {
1218
+ const branchId = String(params.branchId || '');
1219
+ if (!branchId) {
1220
+ mobileJson(res, { error: 'branchId is required' }, 400);
1221
+ return;
1222
+ }
1223
+ mobileJson(res, scoped.inspectConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
1224
+ return;
1225
+ }
1226
+ if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
1227
+ mobileJson(res, { error: 'Conversation is running' }, 423);
1228
+ return;
1229
+ }
1230
+ if (pathname === '/api/mobile/conversation-branch-activate') {
1231
+ const branchId = String(params.branchId || '');
1232
+ if (!branchId) {
1233
+ mobileJson(res, { error: 'branchId is required' }, 400);
1234
+ return;
1235
+ }
1236
+ mobileJson(res, scoped.switchConversationBranch(conversationId, branchId, String(params.branchGroupId || '')));
1237
+ return;
1238
+ }
1239
+ const messageIndex = Math.floor(Number(params.messageIndex));
1240
+ const editedText = String(params.editedText || '').trim();
1241
+ if (!Number.isFinite(messageIndex) || messageIndex < 0 || !editedText) {
1242
+ mobileJson(res, { error: 'messageIndex and editedText are required' }, 400);
1243
+ return;
1244
+ }
1245
+ const branchNodePath = Array.isArray(params.branchNodePath)
1246
+ ? params.branchNodePath.map((id) => String(id || '')).filter(Boolean)
1247
+ : [];
1248
+ const snapshot = scoped.branchConversation(conversationId, messageIndex, editedText, {
1249
+ messageId: String(params.messageId || '') || undefined,
1250
+ guideId: String(params.guideId || '') || undefined,
1251
+ clientMessageId: String(params.clientMessageId || '') || undefined,
1252
+ runId: String(params.runId || '') || undefined,
1253
+ branchNodePath,
1254
+ });
1255
+ mobileJson(res, snapshot);
1256
+ return;
1257
+ }
1258
+ case '/api/mobile/conversation-archive': {
1259
+ const params = JSON.parse(body || '{}');
1260
+ const conversationId = String(params.conversationId || '');
1261
+ const workspaceId = String(params.workspaceId || '');
1262
+ if (!workspaceId || !conversationId) {
1263
+ mobileJson(res, { error: 'workspaceId and conversationId are required' }, 400);
1264
+ return;
1265
+ }
1266
+ const ws = resolveMobileWorkspace(agent, workspaceId);
1267
+ if (!ws) {
1268
+ mobileJson(res, { error: 'Unknown workspace' }, 404);
1269
+ return;
1270
+ }
1271
+ if (!agent.hasConversationInWorkspace(conversationId, ws)) {
1272
+ mobileJson(res, { ok: false, error: 'Conversation not found in workspace' }, 404);
1273
+ return;
1274
+ }
1275
+ // 运行中拒绝:与移动端 running 判定一致(activeConversationId + agent.status 非 idle)
1276
+ if (mobileConversationRuntimeBusy(agent, ws, conversationId)) {
1277
+ mobileJson(res, { ok: false, error: 'Conversation is running' }, 423);
1278
+ return;
1279
+ }
1280
+ const filename = await agent.archiveConversationAsync(conversationId, ws);
1281
+ if (!filename) {
1282
+ mobileJson(res, { ok: false, error: 'Conversation archive could not be written.' }, 500);
1283
+ return;
1284
+ }
1285
+ mobileJson(res, {
1286
+ ok: true,
1287
+ fileName: filename,
1288
+ conversationId,
1289
+ activeConversationId: agent.activeConversationIdForWorkspace(ws),
1290
+ conversations: mobileWorkspaceConversationRows(agent, ws),
1291
+ });
1292
+ return;
1293
+ }
606
1294
  default:
607
1295
  jsonResponse(res, { error: 'Unknown API' }, 404);
608
1296
  }
609
1297
  }
610
1298
  catch (e) {
611
- jsonResponse(res, { error: e.message }, 500);
1299
+ if (pathname.startsWith('/api/mobile/'))
1300
+ mobileJson(res, { error: e.message }, 500);
1301
+ else
1302
+ jsonResponse(res, { error: e.message }, 500);
612
1303
  }
613
1304
  }
614
1305
  function startServer(root) {
1306
+ mobileToken = (0, mobilePairing_1.ensureMobileToken)(root);
1307
+ appRoot = root;
615
1308
  agent = new agent_1.Agent(root);
616
1309
  workspaceFileRouter = new workspaceFileRouter_1.WorkspaceFileRouter(() => path.resolve(agent?.workspace.current?.path || root));
617
1310
  automation = new automation_1.AutomationManager(agent.config, async (prompt, model, item) => {
@@ -640,16 +1333,25 @@ function startServer(root) {
640
1333
  agent.setConversationFromStorage(previousConversation);
641
1334
  }
642
1335
  });
1336
+ agent.subscribeWorkEvents(publishMobileWorkEvent);
643
1337
  agent.setAutomationManager(automation);
644
1338
  automation.start();
645
1339
  const uiDir = path.join(__dirname, 'ui');
646
1340
  const server = http.createServer(async (req, res) => {
647
1341
  if (req.method === 'OPTIONS') {
648
- res.writeHead(204);
1342
+ res.writeHead(204, {
1343
+ 'Access-Control-Allow-Origin': '*',
1344
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type',
1345
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
1346
+ });
649
1347
  res.end();
650
1348
  return;
651
1349
  }
652
1350
  const url = new URL(req.url || '/', `http://localhost:${PORT}`);
1351
+ if (url.pathname === '/api/mobile/events') {
1352
+ handleMobileEvents(req, res);
1353
+ return;
1354
+ }
653
1355
  if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
654
1356
  let body = '';
655
1357
  req.on('data', chunk => body += chunk);
@@ -667,13 +1369,29 @@ function startServer(root) {
667
1369
  const fullPath = path.join(uiDir, filePath);
668
1370
  serveFile(res, fullPath);
669
1371
  });
670
- server.listen(PORT, '127.0.0.1', () => {
1372
+ const bindHost = process.env.NEWMARK_BIND_HOST || '0.0.0.0';
1373
+ const tailscale = (0, mobilePairing_1.tailscaleIpv4)();
1374
+ const lan = (0, mobilePairing_1.lanIpv4)();
1375
+ const accessHost = tailscale || lan || '<lan-or-tailscale-ip>';
1376
+ const tokenPath = path.join(root, '.newmark-mobile-token');
1377
+ server.listen(PORT, bindHost, () => {
671
1378
  console.log(`\n Newmark Agent v1.0 - Server Mode`);
1379
+ console.log(` Bind: ${bindHost}:${PORT}`);
672
1380
  console.log(` GUI: http://localhost:${PORT}`);
1381
+ console.log(` Tailscale IPv4: ${tailscale || 'not detected'}`);
1382
+ console.log(` LAN IPv4: ${lan || 'not detected'}`);
1383
+ console.log(` Mobile endpoint: http://${accessHost}:${PORT}/api/mobile/hello?token=<token>`);
1384
+ console.log(` Mobile token file: ${tokenPath}`);
1385
+ console.log(` Mobile events (SSE): http://${accessHost}:${PORT}/api/mobile/events?token=<token>`);
673
1386
  console.log(` Press Ctrl+C to stop\n`);
674
1387
  });
675
1388
  }
1389
+ let hostedServerStarted = false;
1390
+ /** 托管启动 server(GUI/TUI 内嵌调用;幂等防重入,进程常驻即服务常驻) */
676
1391
  function runServer(root) {
1392
+ if (hostedServerStarted)
1393
+ return;
1394
+ hostedServerStarted = true;
677
1395
  startServer(root);
678
1396
  }
679
1397
  //# sourceMappingURL=server.js.map