@tyvm/knowhow 0.0.102 → 0.0.103

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 (47) hide show
  1. package/package.json +2 -2
  2. package/src/agents/base/base.ts +3 -0
  3. package/src/chat/modules/AgentModule.ts +24 -10
  4. package/src/chat/modules/InternalChatModule.ts +6 -0
  5. package/src/chat/modules/RemoteSyncModule.ts +447 -0
  6. package/src/chat/types.ts +2 -0
  7. package/src/config.ts +2 -0
  8. package/src/services/AgentSyncFs.ts +24 -2
  9. package/src/services/AgentSyncKnowhowWeb.ts +27 -5
  10. package/src/services/KnowhowClient.ts +61 -0
  11. package/src/services/SessionManager.ts +2 -0
  12. package/src/services/script-execution/ScriptPolicy.ts +0 -44
  13. package/src/types.ts +3 -0
  14. package/src/worker.ts +70 -4
  15. package/ts_build/package.json +2 -2
  16. package/ts_build/src/agents/base/base.js +1 -0
  17. package/ts_build/src/agents/base/base.js.map +1 -1
  18. package/ts_build/src/chat/modules/AgentModule.d.ts +2 -1
  19. package/ts_build/src/chat/modules/AgentModule.js +12 -7
  20. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  21. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  22. package/ts_build/src/chat/modules/InternalChatModule.js +6 -0
  23. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  24. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +27 -0
  25. package/ts_build/src/chat/modules/RemoteSyncModule.js +282 -0
  26. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -0
  27. package/ts_build/src/chat/types.d.ts +2 -0
  28. package/ts_build/src/config.js +1 -0
  29. package/ts_build/src/config.js.map +1 -1
  30. package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
  31. package/ts_build/src/services/AgentSyncFs.js +13 -2
  32. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  33. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -0
  34. package/ts_build/src/services/AgentSyncKnowhowWeb.js +13 -2
  35. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  36. package/ts_build/src/services/KnowhowClient.d.ts +21 -0
  37. package/ts_build/src/services/KnowhowClient.js +10 -0
  38. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  39. package/ts_build/src/services/Mcp.d.ts +219 -406
  40. package/ts_build/src/services/SessionManager.js +2 -0
  41. package/ts_build/src/services/SessionManager.js.map +1 -1
  42. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -35
  43. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +1 -1
  44. package/ts_build/src/types.d.ts +2 -0
  45. package/ts_build/src/types.js.map +1 -1
  46. package/ts_build/src/worker.js +51 -2
  47. package/ts_build/src/worker.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.102",
3
+ "version": "0.0.103",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -53,7 +53,7 @@
53
53
  "diff": "^5.2.0",
54
54
  "express": "^4.19.2",
55
55
  "gitignore-to-glob": "^0.3.0",
56
- "glob": "11.0.3",
56
+ "glob": "11.1.0",
57
57
  "isolated-vm": "^5.0.4",
58
58
  "jiti": "^2.6.1",
59
59
  "marked": "^10.0.0",
@@ -731,6 +731,9 @@ export abstract class BaseAgent implements IAgent {
731
731
  });
732
732
  const doneMsg = finalMessage.content || "Done";
733
733
 
734
+ // Ensure the final thread state (including the finalAnswer result) is
735
+ // captured before emitting done, so syncers see the complete thread.
736
+ this.updateCurrentThread(messages);
734
737
  this.agentEvents.emit(this.eventTypes.done, doneMsg);
735
738
  this.status = this.eventTypes.done;
736
739
  return doneMsg;
@@ -48,7 +48,6 @@ export class AgentModule extends BaseChatModule {
48
48
  // Service instances for task management, session management, and synchronization
49
49
  private taskRegistry: TaskRegistry;
50
50
  private sessionManager: SessionManager;
51
- private syncer: SyncerService;
52
51
  /** Timestamp when this process started - used to filter sessions */
53
52
  private processStartTime: number = Date.now();
54
53
  /** Currently attached agent task ID */
@@ -60,6 +59,9 @@ export class AgentModule extends BaseChatModule {
60
59
  private _wireAgentEvents: EventService | undefined;
61
60
  private _wireTaskId: string | undefined;
62
61
  private _wireAgentName: string | undefined;
62
+ /** Optional reference to RemoteSyncModule for auto-sync on new tasks */
63
+ private remoteSyncModule: any | undefined;
64
+
63
65
  private _wireEventTypes:
64
66
  | { toolCall?: string; toolUsed?: string; agentSay?: string; done: string }
65
67
  | undefined;
@@ -68,9 +70,17 @@ export class AgentModule extends BaseChatModule {
68
70
  super();
69
71
  this.taskRegistry = new TaskRegistry();
70
72
  this.sessionManager = new SessionManager();
71
- this.syncer = new SyncerService();
72
73
  }
73
74
 
75
+ /**
76
+ * Set the RemoteSyncModule reference for auto-sync support.
77
+ * Called from InternalChatModule after both modules are created.
78
+ */
79
+ public setRemoteSyncModule(module: any): void {
80
+ this.remoteSyncModule = module;
81
+ }
82
+
83
+
74
84
  getCommands(): ChatCommand[] {
75
85
  return [
76
86
  {
@@ -645,11 +655,12 @@ Please continue from where you left off and complete the original request.
645
655
  // Save initial session
646
656
  this.saveSession(taskId, taskInfo, []);
647
657
 
648
- // Reset sync services before setting up new task (removes old listeners)
649
- this.syncer.reset();
658
+ // Create a fresh SyncerService per agent task so that detaching from one
659
+ // agent and starting another doesn't tear down the first agent's sync.
660
+ const syncer = new SyncerService();
650
661
 
651
662
  // Create sync task (SyncerService decides web vs fs internally)
652
- const syncTaskId = await this.syncer.createTask({
663
+ const syncTaskId = await syncer.createTask({
653
664
  taskId,
654
665
  prompt: input,
655
666
  messageId: options.messageId,
@@ -658,14 +669,12 @@ Please continue from where you left off and complete the original request.
658
669
  agentName,
659
670
  });
660
671
 
661
- // Update TaskInfo with the sync task ID
662
- const webTaskId = this.syncer.getCreatedWebTaskId();
672
+ const webTaskId = syncer.getCreatedWebTaskId();
663
673
  knowhowTaskId = webTaskId;
664
674
  taskInfo.knowhowTaskId = webTaskId || syncTaskId;
665
675
  this.taskRegistry.register(taskId, taskInfo);
666
676
 
667
- // Wire up event listeners on the agent
668
- await this.syncer.setupAgentSync(agent, syncTaskId);
677
+ await syncer.setupAgentSync(agent, syncTaskId);
669
678
 
670
679
  // Set up session update listener
671
680
  const threadUpdateHandler = async (threadState: any) => {
@@ -784,7 +793,7 @@ Please continue from where you left off and complete the original request.
784
793
  taskInfo = this.taskRegistry.get(taskId);
785
794
 
786
795
  // Wait for AgentSync to finish before resolving
787
- await this.syncer.waitForFinalization();
796
+ await syncer.waitForFinalization();
788
797
 
789
798
  if (taskInfo) {
790
799
  taskInfo.status = "completed";
@@ -1008,6 +1017,11 @@ Please continue from where you left off and complete the original request.
1008
1017
  run: false, // Don't run yet, we need to set up event listeners first
1009
1018
  });
1010
1019
 
1020
+ // If auto-sync is enabled, push this task to the remote KnowHow app
1021
+ if (this.remoteSyncModule?.isAutoSyncEnabled()) {
1022
+ await this.remoteSyncModule.syncTask(taskId);
1023
+ }
1024
+
1011
1025
  await this.attachedAgentChatLoop(taskId, agent, formattedPrompt);
1012
1026
 
1013
1027
  return { taskId };
@@ -11,6 +11,7 @@ import { CustomCommandsModule } from "./CustomCommandsModule";
11
11
  import { ShellCommandModule } from "./ShellCommandModule";
12
12
  import { RendererModule } from "./RendererModule";
13
13
  import { SessionsModule } from "./SessionsModule";
14
+ import { RemoteSyncModule } from "./RemoteSyncModule";
14
15
 
15
16
  export class InternalChatModule implements ChatModule {
16
17
  private chatService?: CliChatService;
@@ -28,11 +29,14 @@ export class InternalChatModule implements ChatModule {
28
29
  private customCommandsModule = new CustomCommandsModule();
29
30
  private shellCommandModule = new ShellCommandModule();
30
31
  private rendererModule: RendererModule;
32
+ private remoteSyncModule: RemoteSyncModule;
31
33
 
32
34
  constructor() {
33
35
  this.rendererModule = new RendererModule(this.agentModule);
34
36
  this.setupModule = new SetupModule(this.agentModule);
35
37
  this.sessionsModule = new SessionsModule(this.agentModule);
38
+ this.remoteSyncModule = new RemoteSyncModule(this.agentModule);
39
+ this.agentModule.setRemoteSyncModule(this.remoteSyncModule);
36
40
  }
37
41
 
38
42
  async initialize(chatService: CliChatService): Promise<void> {
@@ -51,6 +55,7 @@ export class InternalChatModule implements ChatModule {
51
55
  await this.systemModule.initialize(chatService);
52
56
  await this.setupModule.initialize(chatService);
53
57
  await this.customCommandsModule.initialize(chatService);
58
+ await this.remoteSyncModule.initialize(chatService);
54
59
  await this.shellCommandModule.initialize(chatService);
55
60
 
56
61
  // Register our own commands (exit and multi) - not duplicated by BaseChatModule
@@ -84,6 +89,7 @@ export class InternalChatModule implements ChatModule {
84
89
  ...this.customCommandsModule.getCommands(),
85
90
  ...this.shellCommandModule.getCommands(),
86
91
  ...this.rendererModule.getCommands(),
92
+ ...this.remoteSyncModule.getCommands(),
87
93
  {
88
94
  name: "exit",
89
95
  description: "Exit the chat",
@@ -0,0 +1,447 @@
1
+ /**
2
+ * RemoteSyncModule - Handles /sync:remote, /sync:remote:off, /sync:status commands
3
+ * Allows CLI users to push agent tasks to the remote KnowHow web app.
4
+ */
5
+ import { BaseChatModule } from "./BaseChatModule";
6
+ import { ChatCommand, ChatContext, ChatService } from "../types";
7
+ import {
8
+ KnowhowSimpleClient,
9
+ KNOWHOW_API_URL,
10
+ } from "../../services/KnowhowClient";
11
+ import { AgentSyncKnowhowWeb } from "../../services/AgentSyncKnowhowWeb";
12
+ import { AgentModule } from "./AgentModule";
13
+ import { TaskInfo } from "../types";
14
+ import { getConfig, updateConfig } from "../../config";
15
+
16
+ export class RemoteSyncModule extends BaseChatModule {
17
+ name = "remote-sync";
18
+ description = "Remote sync functionality (/sync:remote, /sync:status)";
19
+
20
+ /** Per-process remote session ID - created once, reused for all tasks in this terminal */
21
+ private remoteSessionId: string | undefined;
22
+
23
+ /** Worker ID from config (set when running as a worker) */
24
+ private workerId: string | undefined;
25
+
26
+ /** Whether remote auto-sync is enabled for new tasks */
27
+ private autoSync: boolean = false;
28
+
29
+ /** Count of messages synced in this terminal session */
30
+ private syncedMessageCount: number = 0;
31
+
32
+ /** Reference to AgentModule for accessing task registry */
33
+ private agentModule: AgentModule;
34
+
35
+ /** KnowHow API client */
36
+ private client: KnowhowSimpleClient;
37
+
38
+ constructor(agentModule: AgentModule) {
39
+ super();
40
+ this.agentModule = agentModule;
41
+ this.client = new KnowhowSimpleClient(KNOWHOW_API_URL);
42
+ }
43
+
44
+ /**
45
+ * On initialize, read config to set initial autoSync state.
46
+ */
47
+ async initialize(service: ChatService): Promise<void> {
48
+ await super.initialize(service);
49
+ const config = await getConfig();
50
+ if (config.syncRemote === true) {
51
+ this.autoSync = true;
52
+ console.log("šŸ“” Remote auto-sync enabled (from config syncRemote: true)");
53
+ }
54
+ if (config.worker?.workerId) {
55
+ this.workerId = config.worker.workerId;
56
+ console.log(`šŸ”— Worker ID loaded: ${this.workerId}`);
57
+ }
58
+ }
59
+
60
+ getCommands(): ChatCommand[] {
61
+ return [
62
+ {
63
+ name: "sync:remote",
64
+ description:
65
+ "Push the current agent task to the remote KnowHow app. Creates a remote session+message if needed.",
66
+ handler: this.handleSyncRemote.bind(this),
67
+ },
68
+ {
69
+ name: "share",
70
+ description: "Alias for /sync:remote",
71
+ handler: this.handleSyncRemote.bind(this),
72
+ },
73
+ {
74
+ name: "sync:remote:on",
75
+ description: "Enable remote auto-sync for all future agent tasks and save to config",
76
+ handler: this.handleSyncRemoteOn.bind(this),
77
+ },
78
+ {
79
+ name: "sync:remote:off",
80
+ description: "Disable auto remote sync for future tasks",
81
+ handler: this.handleSyncRemoteOff.bind(this),
82
+ },
83
+ {
84
+ name: "sync:status",
85
+ description: "Show current remote sync state",
86
+ handler: this.handleSyncStatus.bind(this),
87
+ },
88
+ ];
89
+ }
90
+
91
+ /**
92
+ * Get the active task: first checks context for activeAgentTaskId,
93
+ * then falls back to the most recently registered task.
94
+ */
95
+ private getActiveTask(
96
+ taskIdArg?: string
97
+ ): { taskId: string; taskInfo: TaskInfo } | undefined {
98
+ const registry = this.agentModule.getTaskRegistry();
99
+ const context = this.chatService?.getContext();
100
+
101
+ // If explicit taskId provided, look it up
102
+ if (taskIdArg) {
103
+ const taskInfo = registry.get(taskIdArg);
104
+ if (taskInfo) return { taskId: taskIdArg, taskInfo };
105
+ console.log(`āš ļø Task "${taskIdArg}" not found in registry.`);
106
+ return undefined;
107
+ }
108
+
109
+ // Use context's activeAgentTaskId
110
+ const activeId = context?.activeAgentTaskId;
111
+ if (activeId) {
112
+ const taskInfo = registry.get(activeId);
113
+ if (taskInfo) return { taskId: activeId, taskInfo };
114
+ }
115
+
116
+ // Fall back to most recently registered task
117
+ const allTasks = registry.getEntries();
118
+ if (allTasks.length > 0) {
119
+ const [taskId, taskInfo] = allTasks[allTasks.length - 1];
120
+ return { taskId, taskInfo };
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ /**
127
+ * Extract a remote session UUID from a URL or raw UUID string.
128
+ * e.g. "http://localhost:3000/chat/47838f91-e918-4f77-9122-0160531f7d2a"
129
+ * or "47838f91-e918-4f77-9122-0160531f7d2a"
130
+ */
131
+ private extractSessionId(input: string): string | undefined {
132
+ // Match a UUID pattern (8-4-4-4-12 hex chars)
133
+ const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
134
+ const match = input.match(uuidRegex);
135
+ if (match) {
136
+ return match[0];
137
+ }
138
+ return undefined;
139
+ }
140
+
141
+ /**
142
+ * Ensure we have a remote session for this terminal process.
143
+ * On first call, creates a new session. Subsequent calls reuse it.
144
+ */
145
+ private async ensureRemoteSession(
146
+ title: string,
147
+ prompt: string
148
+ ): Promise<string> {
149
+ if (this.remoteSessionId) {
150
+ console.log(
151
+ `šŸ“” Using existing remote session: ${this.remoteSessionId}`
152
+ );
153
+ return this.remoteSessionId;
154
+ }
155
+
156
+ console.log("šŸ“” Creating remote session for this terminal session...");
157
+ const result = await this.client.createSessionPlaceholder({
158
+ title: title.slice(0, 80) || "CLI Session",
159
+ workerId: this.workerId,
160
+ metadata: { source: "cli", createdAt: new Date().toISOString() },
161
+ });
162
+
163
+ this.remoteSessionId = result.sessionId;
164
+ console.log(`āœ… Remote session created: ${result.sessionId}`);
165
+ return result.sessionId;
166
+ }
167
+
168
+ /**
169
+ * Create a remote message placeholder for the given task prompt.
170
+ */
171
+ private async createRemoteMessagePlaceholder(
172
+ sessionId: string,
173
+ prompt: string,
174
+ agentName?: string,
175
+ modelName?: string
176
+ ): Promise<{ messageId: string; taskId: string | undefined }> {
177
+ const result = await this.client.createMessagePlaceholder(sessionId, {
178
+ content: prompt,
179
+ agentName,
180
+ modelName,
181
+ metadata: { source: "cli", createdAt: new Date().toISOString() },
182
+ });
183
+ return { messageId: result.messageId, taskId: result.taskId };
184
+ }
185
+
186
+ /**
187
+ * Handle /sync:remote [taskId]
188
+ */
189
+ private async handleSyncRemote(args: string[]): Promise<void> {
190
+ const argRaw = args[0];
191
+
192
+ // Check if the arg is a remote session ID (UUID) or a URL containing one
193
+ if (argRaw) {
194
+ const sessionId = this.extractSessionId(argRaw);
195
+ if (sessionId) {
196
+ // User passed a remote session URL/ID — use that session for future syncs
197
+ this.remoteSessionId = sessionId;
198
+ console.log(`šŸ“” Using remote session: ${sessionId}`);
199
+ console.log(
200
+ " Future agent interactions will be appended to this session.\n" +
201
+ " Use /sync:remote:off to disable, or /sync:remote:on to persist to config."
202
+ );
203
+ // Enable auto-sync for this session too
204
+ if (!this.autoSync) {
205
+ this.autoSync = true;
206
+ }
207
+ // If there's an active task, sync it to this session now
208
+ const taskEntry = this.getActiveTask(undefined);
209
+ if (taskEntry) {
210
+ await this.syncTask(taskEntry.taskId);
211
+ }
212
+ return;
213
+ }
214
+ }
215
+
216
+ const taskIdArg = argRaw;
217
+ const taskEntry = this.getActiveTask(taskIdArg);
218
+
219
+ if (!taskEntry) {
220
+ // No task yet — enable auto-sync for future interactions (session-only, no config write)
221
+ this.autoSync = true;
222
+ console.log(
223
+ "šŸ“” Remote auto-sync enabled for this session.\n" +
224
+ " Future agent interactions will be pushed to the remote KnowHow app.\n" +
225
+ " Use /sync:remote:off to disable, or /sync:remote:on to persist to config."
226
+ );
227
+ return;
228
+ }
229
+
230
+ await this.syncTask(taskEntry.taskId);
231
+
232
+ // Also enable auto-sync for future interactions in this session
233
+ if (!this.autoSync) {
234
+ this.autoSync = true;
235
+ console.log(
236
+ "šŸ“” Remote auto-sync enabled for future agent tasks this session.\n" +
237
+ " Use /sync:remote:on to persist this setting, or /sync:remote:off to disable."
238
+ );
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Sync a specific task by taskId to the remote KnowHow app.
244
+ * Called by AgentModule for auto-sync, or directly by handleSyncRemote.
245
+ */
246
+ public async syncTask(taskId: string): Promise<void> {
247
+ const registry = this.agentModule.getTaskRegistry();
248
+ const taskInfo = registry.get(taskId);
249
+
250
+ if (!taskInfo) {
251
+ console.log(
252
+ `āš ļø Task "${taskId}" not found in registry.`
253
+ );
254
+ return;
255
+ }
256
+
257
+ // Check if already synced
258
+ if (taskInfo.knowhowTaskId && taskInfo.chatSessionId) {
259
+ console.log(
260
+ `ā„¹ļø Task "${taskId}" is already synced to remote session ${taskInfo.chatSessionId}.\n` +
261
+ ` Remote task ID: ${taskInfo.knowhowTaskId}\n` +
262
+ ` Use /sync:status to see full sync state.`
263
+ );
264
+ return;
265
+ }
266
+
267
+ try {
268
+ // Step 1: Ensure remote session exists
269
+ console.log("\nšŸ” Checking KnowHow API credentials...");
270
+ const me = await this.client.me();
271
+ console.log(`āœ… Authenticated as ${me.data?.email || "unknown"}`);
272
+
273
+ const title =
274
+ taskInfo.initialInput?.slice(0, 80) || `CLI Session – ${new Date().toLocaleString()}`;
275
+ const sessionId = await this.ensureRemoteSession(
276
+ title,
277
+ taskInfo.initialInput
278
+ );
279
+
280
+ // Step 2: Create remote message placeholder
281
+ console.log(
282
+ `\nšŸ“Ø Creating remote message for task: ${taskId}`
283
+ );
284
+ const agentName = taskInfo.agentName;
285
+ const modelName = taskInfo.agent?.getModel();
286
+ const { messageId, taskId: placeholderTaskId } = await this.createRemoteMessagePlaceholder(
287
+ sessionId,
288
+ taskInfo.formattedPrompt || taskInfo.initialInput,
289
+ agentName,
290
+ modelName
291
+ );
292
+ console.log(`āœ… Remote message created: ${messageId}`);
293
+
294
+ // Step 3: Use the task stub created by createMessagePlaceholder (avoids creating a second OrgAgentTask)
295
+ const knowhowTaskId = placeholderTaskId;
296
+ if (!knowhowTaskId) {
297
+ console.log(
298
+ "āŒ Failed to create remote agent task. Aborting sync."
299
+ );
300
+ return;
301
+ }
302
+ console.log(`\nšŸ”— Using task from message placeholder: ${knowhowTaskId}`);
303
+ const webSync = new AgentSyncKnowhowWeb(KNOWHOW_API_URL);
304
+ console.log(`āœ… Remote task created: ${knowhowTaskId}`);
305
+
306
+ // Step 4: Update local task info with remote IDs
307
+ taskInfo.knowhowMessageId = messageId;
308
+ taskInfo.knowhowTaskId = knowhowTaskId;
309
+ taskInfo.chatSessionId = sessionId;
310
+
311
+ // Update session manager
312
+ const sessionManager = this.agentModule.getSessionManager();
313
+ sessionManager.updateSession(taskId, taskInfo, taskInfo.agent?.getThreads() || []);
314
+
315
+ // Step 5: Wire up live sync
316
+ console.log(`\nšŸš€ Syncing thread progress to remote...`);
317
+ const agent = taskInfo.agent;
318
+ if (agent) {
319
+ if (taskInfo.status === "completed" || taskInfo.status === "failed") {
320
+ // Task already done — push final state directly
321
+ await webSync.setupAgentSync(agent, knowhowTaskId);
322
+ await webSync.updateChatTask(knowhowTaskId, agent, false, "Synced from CLI");
323
+ console.log(`āœ… Sync complete!`);
324
+ } else {
325
+ // Task still running — attach live sync
326
+ await webSync.setupAgentSync(agent, knowhowTaskId);
327
+ console.log(
328
+ `āœ… Live sync attached! Thread updates will push as the agent runs.`
329
+ );
330
+ }
331
+ } else {
332
+ console.log("āš ļø Agent not found in task info; skipping live sync.");
333
+ }
334
+
335
+ this.syncedMessageCount++;
336
+
337
+ const baseUrl = KNOWHOW_API_URL.replace("/api", "").replace(
338
+ "api.",
339
+ ""
340
+ );
341
+ console.log(
342
+ `\nšŸ’¾ Local session updated with remote IDs.\n` +
343
+ `🌐 View your task at: ${baseUrl}/chat/${sessionId}`
344
+ );
345
+ } catch (error: any) {
346
+ console.error(
347
+ `āŒ Remote sync failed: ${error?.message || error}\n` +
348
+ ` Local state is unaffected. You can retry with /sync:remote.`
349
+ );
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Handle /sync:remote:off
355
+ */
356
+ private async handleSyncRemoteOff(_args: string[]): Promise<void> {
357
+ this.autoSync = false;
358
+ try {
359
+ const config = await getConfig();
360
+ await updateConfig({ ...config, syncRemote: false });
361
+ console.log("šŸ’¾ Config updated: syncRemote set to false");
362
+ } catch (e: any) {
363
+ console.log(`āš ļø Could not update config: ${e?.message}`);
364
+ }
365
+ console.log(
366
+ "šŸ”• Remote auto-sync disabled for future tasks.\n" +
367
+ " Existing synced tasks will continue to push updates.\n" +
368
+ " Use /sync:remote to manually sync a task."
369
+ );
370
+ }
371
+
372
+ /**
373
+ * Handle /sync:remote:on - enables auto-sync and persists to config
374
+ */
375
+ private async handleSyncRemoteOn(_args: string[]): Promise<void> {
376
+ this.autoSync = true;
377
+ try {
378
+ const config = await getConfig();
379
+ await updateConfig({ ...config, syncRemote: true });
380
+ console.log("šŸ’¾ Config updated: syncRemote set to true");
381
+ } catch (e: any) {
382
+ console.log(`āš ļø Could not update config: ${e?.message}`);
383
+ }
384
+ console.log(
385
+ "šŸ“” Remote auto-sync enabled for all future agent tasks.\n" +
386
+ " Every new agent interaction will be pushed to the remote KnowHow app.\n" +
387
+ " Use /sync:remote:off to disable."
388
+ );
389
+ }
390
+
391
+ /**
392
+ * Handle /sync:status
393
+ */
394
+ private async handleSyncStatus(_args: string[]): Promise<void> {
395
+ const lines: string[] = ["\nšŸ“Š Remote Sync Status"];
396
+ lines.push("─".repeat(50));
397
+
398
+ if (this.workerId) {
399
+ lines.push(`šŸ”§ Worker ID: ${this.workerId}`);
400
+ } else {
401
+ lines.push(`šŸ”§ Worker ID: not set (start worker to register)`);
402
+ }
403
+
404
+ if (this.remoteSessionId) {
405
+ lines.push(`āœ… Remote session active: ${this.remoteSessionId}`);
406
+ } else {
407
+ lines.push("āŒ No remote session (run /sync:remote to create one)");
408
+ }
409
+
410
+ lines.push(
411
+ `šŸ”„ Auto-sync: ${this.autoSync ? "enabled" : "disabled"}`
412
+ );
413
+ lines.push(`šŸ“Ø Messages synced this session: ${this.syncedMessageCount}`);
414
+
415
+ // Show active tasks that have been synced
416
+ const registry = this.agentModule.getTaskRegistry();
417
+ const syncedTasks = registry
418
+ .getAll()
419
+ .filter((t) => t.knowhowTaskId && t.chatSessionId);
420
+
421
+ if (syncedTasks.length > 0) {
422
+ lines.push("\nSynced tasks:");
423
+ syncedTasks.forEach((t) => {
424
+ lines.push(
425
+ ` • ${t.taskId.slice(0, 40)} → ${t.knowhowTaskId} (${t.status})`
426
+ );
427
+ });
428
+ }
429
+
430
+ lines.push("─".repeat(50));
431
+ console.log(lines.join("\n"));
432
+ }
433
+
434
+ /**
435
+ * Whether auto-sync is currently enabled.
436
+ */
437
+ public isAutoSyncEnabled(): boolean {
438
+ return this.autoSync;
439
+ }
440
+
441
+ /**
442
+ * Get the current remote session ID (if any).
443
+ */
444
+ public getRemoteSessionId(): string | undefined {
445
+ return this.remoteSessionId;
446
+ }
447
+ }
package/src/chat/types.ts CHANGED
@@ -84,6 +84,7 @@ export interface TaskInfo {
84
84
  taskId: string;
85
85
  knowhowMessageId?: string;
86
86
  knowhowTaskId?: string;
87
+ chatSessionId?: string;
87
88
  agentName: string;
88
89
  agent: BaseAgent;
89
90
  initialInput: string;
@@ -98,6 +99,7 @@ export interface TaskInfo {
98
99
  export interface ChatSession {
99
100
  knowhowMessageId?: string;
100
101
  knowhowTaskId?: string;
102
+ chatSessionId?: string;
101
103
  sessionId: string;
102
104
  taskId: string;
103
105
  agentName: string;
package/src/config.ts CHANGED
@@ -109,6 +109,8 @@ const defaultConfig = {
109
109
  allowedPorts: [],
110
110
  },
111
111
  },
112
+
113
+ syncRemote: false,
112
114
  } as Config;
113
115
 
114
116
  const defaultLanguage = {
@@ -29,6 +29,12 @@ export class AgentSyncFs {
29
29
  private agent: BaseAgent | undefined;
30
30
  private threadUpdateHandler: ((...args: any[]) => void) | undefined;
31
31
  private doneHandler: ((...args: any[]) => void) | undefined;
32
+ /**
33
+ * Tracks the most recent in-flight filesystem metadata update.
34
+ * The done handler awaits this before finalizing, preventing a race where
35
+ * the completion call writes before the last thread sync finishes.
36
+ */
37
+ private pendingThreadUpdatePromise: Promise<void> | null = null;
32
38
 
33
39
  constructor() {
34
40
  // Start cleanup process when created
@@ -294,8 +300,12 @@ export class AgentSyncFs {
294
300
  if (!this.taskId) return;
295
301
 
296
302
  try {
297
- await this.updateMetadata(agent, true);
298
- await this.checkForChanges(agent);
303
+ // Track the pending update so the done handler can await it.
304
+ this.pendingThreadUpdatePromise = (async () => {
305
+ await this.updateMetadata(agent, true);
306
+ await this.checkForChanges(agent);
307
+ })();
308
+ await this.pendingThreadUpdatePromise;
299
309
  } catch (error) {
300
310
  console.error(`āŒ Error during threadUpdate sync:`, error);
301
311
  }
@@ -314,6 +324,17 @@ export class AgentSyncFs {
314
324
  // Store finalization promise so callers can await it (same pattern as AgentSyncKnowhowWeb)
315
325
  this.finalizationPromise = (async () => {
316
326
  try {
327
+ // Flush any in-flight thread update before finalizing.
328
+ // This prevents the race where a pending "inProgress: true" metadata write
329
+ // overwrites the finalization write.
330
+ if (this.pendingThreadUpdatePromise) {
331
+ console.log(`ā³ [AgentSyncFs] Awaiting pending thread update before finalizing...`);
332
+ await this.pendingThreadUpdatePromise.catch(() => {
333
+ // Ignore errors in pending update — we still want to finalize
334
+ });
335
+ this.pendingThreadUpdatePromise = null;
336
+ }
337
+
317
338
  await this.updateMetadata(agent, false, result);
318
339
  console.log(`āœ… Completed filesystem sync for task: ${this.taskId}`);
319
340
  await this.cleanup();
@@ -435,5 +456,6 @@ export class AgentSyncFs {
435
456
  this.eventHandlersSetup = false;
436
457
  this.lastInputContent = "";
437
458
  this.finalizationPromise = null;
459
+ this.pendingThreadUpdatePromise = null;
438
460
  }
439
461
  }