@tyvm/knowhow 0.0.146 → 0.0.147
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/autodoc/cli-reference.md +36 -7
- package/bin/knowhow.js +8 -6
- package/package.json +1 -1
- package/src/agents/base/base.ts +24 -0
- package/src/agents/historyRollback.ts +36 -0
- package/src/agents/tools/startAgentTask.ts +41 -9
- package/src/auth/browserLogin.ts +9 -3
- package/src/auth/keyAuth.ts +115 -0
- package/src/auth/keyManager.ts +166 -0
- package/src/chat/modules/AgentModule.ts +114 -33
- package/src/chat/modules/AskModule.ts +2 -0
- package/src/chat/modules/BaseChatModule.ts +0 -1
- package/src/chat/modules/CustomCommandsModule.ts +2 -0
- package/src/chat/modules/InternalChatModule.ts +2 -0
- package/src/chat/modules/RemoteSyncModule.ts +4 -0
- package/src/chat/modules/RendererModule.ts +2 -0
- package/src/chat/modules/SearchModule.ts +2 -0
- package/src/chat/modules/SessionsModule.ts +103 -34
- package/src/chat/modules/SetupModule.ts +2 -0
- package/src/chat/modules/ShellCommandModule.ts +2 -0
- package/src/chat/modules/SystemModule.ts +2 -0
- package/src/chat/types.ts +2 -0
- package/src/cli.ts +1 -1
- package/src/clients/index.ts +2 -0
- package/src/commands/agent.ts +1 -38
- package/src/commands/agents.ts +403 -53
- package/src/commands/misc.ts +3 -2
- package/src/login.ts +89 -28
- package/src/services/AgentService.ts +2 -0
- package/src/services/AgentSyncFs.ts +65 -0
- package/src/services/AgentSyncKnowhowWeb.ts +22 -4
- package/src/services/BehaviorsService.ts +2 -0
- package/src/services/DockerService.ts +2 -0
- package/src/services/EmbeddingService.ts +2 -0
- package/src/services/EmbeddingsService.ts +3 -0
- package/src/services/KnowhowClient.ts +68 -7
- package/src/services/LazyToolsService.ts +9 -0
- package/src/services/Mcp.ts +2 -0
- package/src/services/McpServer.ts +2 -0
- package/src/services/MessageProcessor.ts +2 -0
- package/src/services/MinimalToolsService.ts +2 -0
- package/src/services/SessionManager.ts +5 -0
- package/src/services/SkillsService.ts +2 -0
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +23 -8
- package/src/services/TaskRegistry.ts +2 -0
- package/src/services/Tools.ts +2 -0
- package/src/services/index.ts +2 -0
- package/src/services/modules/index.ts +4 -1
- package/src/types.ts +2 -0
- package/src/util/Trace.ts +1 -1
- package/src/utils/http.ts +18 -2
- package/tests/unit/agents/historyRollback.test.ts +53 -0
- package/tests/unit/auth/keyAuth.test.ts +102 -0
- package/tests/unit/auth/keyManager.test.ts +67 -0
- package/tests/unit/chat/historyCommandArgs.test.ts +37 -0
- package/tests/unit/commands/agents.test.ts +68 -0
- package/tests/unit/login.test.ts +119 -0
- package/tests/unit/services/AgentSyncKnowhowWeb.test.ts +38 -0
- package/tests/unit/services/LazyToolsService.test.ts +29 -0
- package/tests/unit/services/SessionManager.test.ts +43 -0
- package/tests/unit/services/SyncerService.test.ts +45 -0
- package/tests/unit/util/Trace.test.ts +119 -0
- package/tests/unit/utils/http.test.ts +43 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +1 -0
- package/ts_build/src/agents/base/base.js +31 -2
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/historyRollback.d.ts +2 -0
- package/ts_build/src/agents/historyRollback.js +22 -0
- package/ts_build/src/agents/historyRollback.js.map +1 -0
- package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -0
- package/ts_build/src/agents/tools/startAgentTask.js +36 -9
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/auth/browserLogin.d.ts +1 -0
- package/ts_build/src/auth/browserLogin.js +5 -2
- package/ts_build/src/auth/browserLogin.js.map +1 -1
- package/ts_build/src/auth/keyAuth.d.ts +5 -0
- package/ts_build/src/auth/keyAuth.js +79 -0
- package/ts_build/src/auth/keyAuth.js.map +1 -0
- package/ts_build/src/auth/keyManager.d.ts +14 -0
- package/ts_build/src/auth/keyManager.js +145 -0
- package/ts_build/src/auth/keyManager.js.map +1 -0
- package/ts_build/src/chat/modules/AgentModule.d.ts +13 -1
- package/ts_build/src/chat/modules/AgentModule.js +83 -24
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/AskModule.js +12 -2
- package/ts_build/src/chat/modules/AskModule.js.map +1 -1
- package/ts_build/src/chat/modules/BaseChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/CustomCommandsModule.js +12 -2
- package/ts_build/src/chat/modules/CustomCommandsModule.js.map +1 -1
- package/ts_build/src/chat/modules/InternalChatModule.js +16 -2
- package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -2
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/chat/modules/RendererModule.js +17 -2
- package/ts_build/src/chat/modules/RendererModule.js.map +1 -1
- package/ts_build/src/chat/modules/SearchModule.js +12 -2
- package/ts_build/src/chat/modules/SearchModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.d.ts +6 -0
- package/ts_build/src/chat/modules/SessionsModule.js +113 -18
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/modules/SetupModule.js +17 -2
- package/ts_build/src/chat/modules/SetupModule.js.map +1 -1
- package/ts_build/src/chat/modules/ShellCommandModule.js +12 -2
- package/ts_build/src/chat/modules/ShellCommandModule.js.map +1 -1
- package/ts_build/src/chat/modules/SystemModule.js +12 -2
- package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
- package/ts_build/src/chat/types.d.ts +2 -0
- package/ts_build/src/cli.js +1 -1
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/index.js +19 -4
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/commands/agent.js +1 -21
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.d.ts +1 -1
- package/ts_build/src/commands/agents.js +174 -16
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/misc.js +3 -2
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/login.d.ts +1 -1
- package/ts_build/src/login.js +53 -15
- package/ts_build/src/login.js.map +1 -1
- package/ts_build/src/services/AgentService.js +18 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
- package/ts_build/src/services/AgentSyncFs.js +62 -15
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +2 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +28 -6
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/BehaviorsService.js +12 -2
- package/ts_build/src/services/BehaviorsService.js.map +1 -1
- package/ts_build/src/services/DockerService.js +20 -8
- package/ts_build/src/services/DockerService.js.map +1 -1
- package/ts_build/src/services/EmbeddingService.js +16 -2
- package/ts_build/src/services/EmbeddingService.js.map +1 -1
- package/ts_build/src/services/EmbeddingsService.js +12 -2
- package/ts_build/src/services/EmbeddingsService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +7 -2
- package/ts_build/src/services/KnowhowClient.js +108 -10
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +1 -0
- package/ts_build/src/services/LazyToolsService.js +21 -2
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Mcp.js +12 -2
- package/ts_build/src/services/Mcp.js.map +1 -1
- package/ts_build/src/services/McpServer.js +17 -2
- package/ts_build/src/services/McpServer.js.map +1 -1
- package/ts_build/src/services/MessageProcessor.js +16 -2
- package/ts_build/src/services/MessageProcessor.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.js +16 -2
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/SessionManager.js +19 -2
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/services/SkillsService.js +16 -2
- package/ts_build/src/services/SkillsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.js +16 -2
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +1 -0
- package/ts_build/src/services/SyncerService.js +27 -5
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/TaskRegistry.js +12 -2
- package/ts_build/src/services/TaskRegistry.js.map +1 -1
- package/ts_build/src/services/Tools.js +16 -2
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/index.d.ts +2 -0
- package/ts_build/src/services/index.js +4 -2
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +2 -1
- package/ts_build/src/services/modules/index.js +1 -1
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +1 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/util/Trace.d.ts +1 -3
- package/ts_build/src/utils/http.d.ts +6 -2
- package/ts_build/src/utils/http.js +8 -2
- package/ts_build/src/utils/http.js.map +1 -1
- package/ts_build/tests/unit/agents/historyRollback.test.d.ts +1 -0
- package/ts_build/tests/unit/agents/historyRollback.test.js +42 -0
- package/ts_build/tests/unit/agents/historyRollback.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js +77 -0
- package/ts_build/tests/unit/auth/keyAuth.test.js.map +1 -0
- package/ts_build/tests/unit/auth/keyManager.test.d.ts +1 -0
- package/ts_build/tests/unit/auth/keyManager.test.js +52 -0
- package/ts_build/tests/unit/auth/keyManager.test.js.map +1 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.d.ts +1 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.js +29 -0
- package/ts_build/tests/unit/chat/historyCommandArgs.test.js.map +1 -0
- package/ts_build/tests/unit/commands/agents.test.d.ts +1 -0
- package/ts_build/tests/unit/commands/agents.test.js +91 -0
- package/ts_build/tests/unit/commands/agents.test.js.map +1 -0
- package/ts_build/tests/unit/login.test.d.ts +1 -0
- package/ts_build/tests/unit/login.test.js +97 -0
- package/ts_build/tests/unit/login.test.js.map +1 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.d.ts +1 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js +32 -0
- package/ts_build/tests/unit/services/AgentSyncKnowhowWeb.test.js.map +1 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js +27 -0
- package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -0
- package/ts_build/tests/unit/services/SessionManager.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SessionManager.test.js +72 -0
- package/ts_build/tests/unit/services/SessionManager.test.js.map +1 -0
- package/ts_build/tests/unit/services/SyncerService.test.d.ts +1 -0
- package/ts_build/tests/unit/services/SyncerService.test.js +40 -0
- package/ts_build/tests/unit/services/SyncerService.test.js.map +1 -0
- package/ts_build/tests/unit/util/Trace.test.d.ts +1 -0
- package/ts_build/tests/unit/util/Trace.test.js +107 -0
- package/ts_build/tests/unit/util/Trace.test.js.map +1 -0
- package/ts_build/tests/unit/utils/http.test.d.ts +1 -0
- package/ts_build/tests/unit/utils/http.test.js +69 -0
- package/ts_build/tests/unit/utils/http.test.js.map +1 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { Message } from "../clients/types";
|
|
7
7
|
import { EventService } from "./EventService";
|
|
8
|
+
import { TraceAll } from "../util/Trace";
|
|
8
9
|
|
|
9
10
|
export interface SyncedAgentWatcher {
|
|
10
11
|
/** Start watching for changes, emitting agent events */
|
|
@@ -73,6 +74,7 @@ export interface AttachableAgent {
|
|
|
73
74
|
* - agentEvents and eventTypes are delegated to the watcher
|
|
74
75
|
* - "done" is emitted by the watcher when the remote agent completes
|
|
75
76
|
*/
|
|
77
|
+
@TraceAll()
|
|
76
78
|
export class WatcherBackedAgent implements AttachableAgent {
|
|
77
79
|
public name: string;
|
|
78
80
|
public agentEvents: EventService;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Hides complexity of choosing and managing sync backends.
|
|
4
4
|
*/
|
|
5
5
|
import { BaseAgent } from "../agents/base/base";
|
|
6
|
+
import { TraceAll } from "../util/Trace";
|
|
6
7
|
import { AgentSyncFs } from "./AgentSyncFs";
|
|
7
8
|
import { AgentSyncKnowhowWeb } from "./AgentSyncKnowhowWeb";
|
|
8
9
|
|
|
@@ -11,12 +12,13 @@ export interface SyncerOptions {
|
|
|
11
12
|
prompt: string;
|
|
12
13
|
/** If provided → use web sync (unless syncFs is set) */
|
|
13
14
|
messageId?: string;
|
|
15
|
+
/** Existing remote Knowhow task UUID. Never inferred from the local taskId. */
|
|
16
|
+
remoteTaskId?: string;
|
|
14
17
|
/** Force fs sync even if messageId is provided */
|
|
15
18
|
syncFs?: boolean;
|
|
16
19
|
/**
|
|
17
|
-
* Push the agent's work to
|
|
18
|
-
*
|
|
19
|
-
* push updates to, rather than creating a new remote task.
|
|
20
|
+
* Push the agent's work to the existing `remoteTaskId`. The local taskId is
|
|
21
|
+
* always a filesystem identity and is never used as a remote identifier.
|
|
20
22
|
*/
|
|
21
23
|
syncRemote?: boolean;
|
|
22
24
|
/** Agent name to persist in metadata.json */
|
|
@@ -44,8 +46,9 @@ export interface AgentSyncer {
|
|
|
44
46
|
* Decision logic:
|
|
45
47
|
* - Always sets up AgentSyncFs (primary local syncer)
|
|
46
48
|
* - If messageId is present AND syncFs is not forced → creates a new remote task via AgentSyncKnowhowWeb
|
|
47
|
-
* - If syncRemote
|
|
49
|
+
* - If syncRemote and a valid remoteTaskId are set → attaches to that UUID
|
|
48
50
|
*/
|
|
51
|
+
@TraceAll()
|
|
49
52
|
export class SyncerService implements AgentSyncer {
|
|
50
53
|
private fsSync: AgentSyncFs;
|
|
51
54
|
private webSync: AgentSyncKnowhowWeb;
|
|
@@ -75,14 +78,16 @@ export class SyncerService implements AgentSyncer {
|
|
|
75
78
|
|
|
76
79
|
// Determine whether to ATTACH to an already-existing remote task and push
|
|
77
80
|
// local work to it (no creation needed). This is the "--sync-remote" path:
|
|
78
|
-
// the caller wants to push work to
|
|
81
|
+
// the caller wants to push work to an explicitly identified remote task.
|
|
79
82
|
// We only auto-attach when there is NO messageId (messageId always creates
|
|
80
83
|
// a fresh remote task above).
|
|
81
84
|
//
|
|
82
85
|
// This is orthogonal to fs sync: we can push to the remote task AND keep
|
|
83
86
|
// the local .knowhow/processes/agents/<id>/ files in sync at the same time.
|
|
84
87
|
const shouldAttachExisting =
|
|
85
|
-
!!options.syncRemote &&
|
|
88
|
+
!!options.syncRemote &&
|
|
89
|
+
!options.messageId &&
|
|
90
|
+
isUuid(options.remoteTaskId);
|
|
86
91
|
|
|
87
92
|
// Always create fs sync task first
|
|
88
93
|
console.log(
|
|
@@ -104,6 +109,7 @@ export class SyncerService implements AgentSyncer {
|
|
|
104
109
|
|
|
105
110
|
if (knowhowTaskId) {
|
|
106
111
|
this.useWebSync = true;
|
|
112
|
+
await this.fsSync.setRemoteIdentity(knowhowTaskId, options.messageId);
|
|
107
113
|
this.createdTaskId = knowhowTaskId;
|
|
108
114
|
console.log(`🌐 Web sync task created: ${knowhowTaskId}`);
|
|
109
115
|
}
|
|
@@ -112,9 +118,10 @@ export class SyncerService implements AgentSyncer {
|
|
|
112
118
|
// Attach live web sync to an existing remote task (push updates to it).
|
|
113
119
|
if (!shouldUseWebSync && shouldAttachExisting) {
|
|
114
120
|
this.useWebSync = true;
|
|
115
|
-
this.
|
|
121
|
+
await this.fsSync.setRemoteIdentity(options.remoteTaskId!);
|
|
122
|
+
this.createdTaskId = options.remoteTaskId;
|
|
116
123
|
console.log(
|
|
117
|
-
`🌐 Attaching web sync to existing remote task: ${options.
|
|
124
|
+
`🌐 Attaching web sync to existing remote task: ${options.remoteTaskId}`
|
|
118
125
|
);
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -174,3 +181,11 @@ export class SyncerService implements AgentSyncer {
|
|
|
174
181
|
return this.useWebSync ? this.createdTaskId : undefined;
|
|
175
182
|
}
|
|
176
183
|
}
|
|
184
|
+
|
|
185
|
+
/** Remote task IDs are UUIDs; local filesystem IDs are timestamped slugs. */
|
|
186
|
+
function isUuid(value: string | undefined): value is string {
|
|
187
|
+
return !!value &&
|
|
188
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
189
|
+
value
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Task Registry Service - Manages active agent tasks
|
|
3
3
|
*/
|
|
4
4
|
import { TaskInfo } from "../chat/types";
|
|
5
|
+
import { TraceAll } from "../util/Trace";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* TaskRegistry manages the in-memory registry of active agent tasks
|
|
8
9
|
*/
|
|
10
|
+
@TraceAll()
|
|
9
11
|
export class TaskRegistry {
|
|
10
12
|
private tasks = new Map<string, TaskInfo>();
|
|
11
13
|
|
package/src/services/Tools.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatCompletionTool } from "openai/resources/chat";
|
|
2
|
+
import { TraceAll } from "../util/Trace";
|
|
2
3
|
import { replaceEscapedNewLines, restoreEscapedNewLines } from "../utils";
|
|
3
4
|
import { includedTools } from "../agents/tools/list";
|
|
4
5
|
import { AgentService } from "./AgentService";
|
|
@@ -61,6 +62,7 @@ export interface ToolContext {
|
|
|
61
62
|
* larger context bloat from having every tool defined in system prompt
|
|
62
63
|
*
|
|
63
64
|
*/
|
|
65
|
+
@TraceAll()
|
|
64
66
|
export class ToolsService {
|
|
65
67
|
private context: ToolContext = {};
|
|
66
68
|
|
package/src/services/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ let Singletons = {} as {
|
|
|
51
51
|
Tools: ToolsService;
|
|
52
52
|
Events: EventService;
|
|
53
53
|
Agents: AgentService;
|
|
54
|
+
Tracing: typeof TracingService;
|
|
54
55
|
Embeddings: EmbeddingsService;
|
|
55
56
|
Flags: FlagsService;
|
|
56
57
|
Mcp: McpService;
|
|
@@ -83,6 +84,7 @@ export const services = (): typeof Singletons => {
|
|
|
83
84
|
Singletons = {
|
|
84
85
|
Agents,
|
|
85
86
|
AwsS3: new S3Service(),
|
|
87
|
+
Tracing: TracingService,
|
|
86
88
|
Clients,
|
|
87
89
|
Docker: new DockerService(),
|
|
88
90
|
Events,
|
|
@@ -284,9 +284,12 @@ export class ModulesService {
|
|
|
284
284
|
|
|
285
285
|
// Load global-config modules resolving against the global install's
|
|
286
286
|
// node_modules (~/.knowhow/node_modules) first.
|
|
287
|
+
// Pass a merged config so that keys defined in the global config (e.g.
|
|
288
|
+
// `tracing`) are visible to globally-declared modules during init().
|
|
289
|
+
// Local config keys take precedence so that local overrides still work.
|
|
287
290
|
if (globalModules.length > 0) {
|
|
288
291
|
await this.loadModulesFrom(
|
|
289
|
-
{ ...config, modules: globalModules },
|
|
292
|
+
{ ...globalConfig, ...config, modules: globalModules },
|
|
290
293
|
context,
|
|
291
294
|
globalResolvePaths()
|
|
292
295
|
);
|
package/src/types.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type Config = {
|
|
|
75
75
|
promptsDir: string;
|
|
76
76
|
lintCommands?: { [fileExtension: string]: string };
|
|
77
77
|
orgId?: string;
|
|
78
|
+
/** Private Ed25519 identity selected with `knowhow login --identity`. */
|
|
79
|
+
cliIdentityPath?: string;
|
|
78
80
|
syncRemote?: boolean;
|
|
79
81
|
micCommand?: string;
|
|
80
82
|
defaultMic?: string;
|
package/src/util/Trace.ts
CHANGED
|
@@ -141,7 +141,7 @@ export interface TraceAllOptions {
|
|
|
141
141
|
export function TraceAll(options: TraceAllOptions = {}) {
|
|
142
142
|
const skip = new Set(options.skip ?? []);
|
|
143
143
|
|
|
144
|
-
return function <T extends
|
|
144
|
+
return function <T extends abstract new (...args: unknown[]) => object>(
|
|
145
145
|
constructor: T
|
|
146
146
|
): T {
|
|
147
147
|
const className = constructor.name;
|
package/src/utils/http.ts
CHANGED
|
@@ -21,6 +21,15 @@ export class HttpError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Optional, non-enumerable hook carried by an authenticated headers object.
|
|
26
|
+
* The hook may replace an expired credential before one safe 401 retry.
|
|
27
|
+
*/
|
|
28
|
+
export const HTTP_UNAUTHORIZED_HANDLER = Symbol("httpUnauthorizedHandler");
|
|
29
|
+
export type RefreshableHeaders = Record<string, string> & {
|
|
30
|
+
[HTTP_UNAUTHORIZED_HANDLER]?: () => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
24
33
|
async function parseBody<T>(response: Response, responseType?: string): Promise<T> {
|
|
25
34
|
if (responseType === "arraybuffer") {
|
|
26
35
|
return (await response.arrayBuffer()) as unknown as T;
|
|
@@ -41,13 +50,14 @@ async function request<T = any>(
|
|
|
41
50
|
url: string,
|
|
42
51
|
options: {
|
|
43
52
|
method?: string;
|
|
44
|
-
headers?:
|
|
53
|
+
headers?: RefreshableHeaders;
|
|
45
54
|
body?: any;
|
|
46
55
|
responseType?: "json" | "arraybuffer" | "stream" | "text";
|
|
47
56
|
params?: Record<string, any>;
|
|
48
57
|
/** Timeout in milliseconds. Default: 30000 (30s). Use 0 to disable. */
|
|
49
58
|
timeout?: number;
|
|
50
|
-
} = {}
|
|
59
|
+
} = {},
|
|
60
|
+
mayRefresh = true
|
|
51
61
|
): Promise<HttpResponse<T>> {
|
|
52
62
|
const { method = "GET", headers = {}, body, responseType, params, timeout = 30000 } = options;
|
|
53
63
|
|
|
@@ -102,6 +112,12 @@ async function request<T = any>(
|
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
if (!response.ok) {
|
|
115
|
+
const refresh = headers[HTTP_UNAUTHORIZED_HANDLER];
|
|
116
|
+
if (response.status === 401 && mayRefresh && refresh) {
|
|
117
|
+
await refresh();
|
|
118
|
+
return request<T>(url, options, false);
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
const text = await response.text().catch(() => "");
|
|
106
122
|
let parsedBody: any;
|
|
107
123
|
try {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { rollbackAgentInteractions } from "../../../src/agents/historyRollback";
|
|
2
|
+
import { Message } from "../../../src/clients/types";
|
|
3
|
+
|
|
4
|
+
const message = (role: Message["role"], content: string): Message => ({ role, content });
|
|
5
|
+
|
|
6
|
+
describe("rollbackAgentInteractions", () => {
|
|
7
|
+
const threads: Message[][] = [[
|
|
8
|
+
message("user", "request"),
|
|
9
|
+
message("assistant", "first"),
|
|
10
|
+
message("tool", "first result"),
|
|
11
|
+
message("assistant", "second"),
|
|
12
|
+
message("tool", "second result"),
|
|
13
|
+
]];
|
|
14
|
+
|
|
15
|
+
it("clones history without changing it when rollback is zero", () => {
|
|
16
|
+
const result = rollbackAgentInteractions(threads, 0);
|
|
17
|
+
expect(result).toEqual(threads);
|
|
18
|
+
expect(result).not.toBe(threads);
|
|
19
|
+
expect(result[0]).not.toBe(threads[0]);
|
|
20
|
+
expect(result[0][0]).not.toBe(threads[0][0]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("removes the newest assistant and its following tool results", () => {
|
|
24
|
+
expect(rollbackAgentInteractions(threads, 1)).toEqual([threads[0].slice(0, 3)]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("can remove multiple interactions", () => {
|
|
28
|
+
expect(rollbackAgentInteractions(threads, 2)).toEqual([[threads[0][0]]]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("removes a trailing assistant without tools", () => {
|
|
32
|
+
const input = [[message("user", "request"), message("assistant", "tail")]];
|
|
33
|
+
expect(rollbackAgentInteractions(input, 1)).toEqual([[input[0][0]]]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.each([-1, 1.5])("rejects invalid rollback %p", (rollback) => {
|
|
37
|
+
expect(() => rollbackAgentInteractions(threads, rollback)).toThrow(
|
|
38
|
+
"rollback must be a non-negative integer"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects rollback beyond available interactions", () => {
|
|
43
|
+
expect(() => rollbackAgentInteractions(threads, 3)).toThrow(
|
|
44
|
+
"only has 2 assistant interaction(s)"
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("does not mutate input history", () => {
|
|
49
|
+
const before = JSON.stringify(threads);
|
|
50
|
+
rollbackAgentInteractions(threads, 1);
|
|
51
|
+
expect(JSON.stringify(threads)).toBe(before);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const post = jest.fn();
|
|
2
|
+
const publicKeyBase64 = Buffer.alloc(32, 7).toString("base64");
|
|
3
|
+
const keyPairExists = jest.fn(() => true);
|
|
4
|
+
const loadKeyPair = jest.fn(() => ({ publicKeyBase64 }));
|
|
5
|
+
const signMessage = jest.fn(() => "server-message-signature");
|
|
6
|
+
|
|
7
|
+
jest.mock("../../../src/utils/http", () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: {
|
|
10
|
+
post,
|
|
11
|
+
isHttpError: (error: unknown) =>
|
|
12
|
+
typeof error === "object" && error !== null && "status" in error,
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock("../../../src/auth/keyManager", () => ({
|
|
17
|
+
keyPairExists,
|
|
18
|
+
loadKeyPair,
|
|
19
|
+
signMessage,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("../../../src/auth/browserLogin", () => ({
|
|
23
|
+
getCliUserAgent: () => "test-agent",
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
authenticateWithKey,
|
|
28
|
+
exchangePublicKeyJwt,
|
|
29
|
+
hasKeyPair,
|
|
30
|
+
registerPublicKey,
|
|
31
|
+
} from "../../../src/auth/keyAuth";
|
|
32
|
+
|
|
33
|
+
describe("keyAuth", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
keyPairExists.mockReturnValue(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("signs the exact server challenge for the selected key, org, and environment", async () => {
|
|
40
|
+
post
|
|
41
|
+
.mockResolvedValueOnce({
|
|
42
|
+
data: {
|
|
43
|
+
challengeId: "challenge_1",
|
|
44
|
+
message: "exact message from server",
|
|
45
|
+
expiresAt: "2099-01-01T00:00:00.000Z",
|
|
46
|
+
},
|
|
47
|
+
status: 201,
|
|
48
|
+
})
|
|
49
|
+
.mockResolvedValueOnce({ data: { jwt: "renewed-jwt" }, status: 200 });
|
|
50
|
+
|
|
51
|
+
expect(hasKeyPair("/keys/work")).toBe(true);
|
|
52
|
+
await expect(
|
|
53
|
+
exchangePublicKeyJwt("org-1", "https://work.example", "/keys/work")
|
|
54
|
+
).resolves.toBe("renewed-jwt");
|
|
55
|
+
|
|
56
|
+
expect(loadKeyPair).toHaveBeenCalledWith("/keys/work");
|
|
57
|
+
expect(signMessage).toHaveBeenCalledWith("exact message from server", "/keys/work");
|
|
58
|
+
expect(post.mock.calls).toEqual([
|
|
59
|
+
[
|
|
60
|
+
"https://work.example/api/public-keys/challenge",
|
|
61
|
+
{ publicKey: publicKeyBase64, orgId: "org-1" },
|
|
62
|
+
{ headers: { "User-Agent": "test-agent" } },
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"https://work.example/api/public-keys/authenticate",
|
|
66
|
+
{ challengeId: "challenge_1", signature: "server-message-signature" },
|
|
67
|
+
{ headers: { "User-Agent": "test-agent" } },
|
|
68
|
+
],
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("falls back when the key is unavailable without signing a challenge", async () => {
|
|
73
|
+
post.mockRejectedValueOnce({ status: 401 });
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
authenticateWithKey("org-1", "https://work.example", "/keys/work")
|
|
77
|
+
).resolves.toBe(false);
|
|
78
|
+
|
|
79
|
+
expect(post).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(signMessage).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("registers only server-owned Ed25519 key fields in the selected environment", async () => {
|
|
84
|
+
post.mockResolvedValueOnce({ data: {}, status: 201 });
|
|
85
|
+
|
|
86
|
+
await registerPublicKey("browser-jwt", publicKeyBase64, "https://dev.example");
|
|
87
|
+
|
|
88
|
+
expect(post).toHaveBeenCalledWith(
|
|
89
|
+
"https://dev.example/api/public-keys/register",
|
|
90
|
+
{
|
|
91
|
+
publicKey: publicKeyBase64,
|
|
92
|
+
label: expect.any(String),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: "Bearer browser-jwt",
|
|
97
|
+
"User-Agent": "test-agent",
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import {
|
|
6
|
+
generateKeyPair,
|
|
7
|
+
loadKeyPair,
|
|
8
|
+
signMessage,
|
|
9
|
+
} from "../../../src/auth/keyManager";
|
|
10
|
+
|
|
11
|
+
describe("keyManager", () => {
|
|
12
|
+
let directory: string;
|
|
13
|
+
let identity: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
directory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-key-manager-"));
|
|
17
|
+
identity = path.join(directory, "identity");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("creates an Ed25519 identity with an OpenSSH-compatible public key", () => {
|
|
25
|
+
const generated = generateKeyPair(identity);
|
|
26
|
+
const publicKeyLine = fs.readFileSync(`${identity}.pub`, "utf8").trim();
|
|
27
|
+
|
|
28
|
+
expect(publicKeyLine).toMatch(/^ssh-ed25519 [A-Za-z0-9+/]+=* knowhow-cli$/);
|
|
29
|
+
expect(fs.statSync(identity).mode % 0o1000).toBe(0o600);
|
|
30
|
+
expect(loadKeyPair(identity)).toEqual(generated);
|
|
31
|
+
|
|
32
|
+
const challengeMessage = "exact server-provided challenge";
|
|
33
|
+
const signature = signMessage(challengeMessage, identity);
|
|
34
|
+
const publicKey = crypto.createPublicKey(fs.readFileSync(identity));
|
|
35
|
+
expect(
|
|
36
|
+
crypto.verify(
|
|
37
|
+
null,
|
|
38
|
+
Buffer.from(challengeMessage),
|
|
39
|
+
publicKey,
|
|
40
|
+
Buffer.from(signature, "base64")
|
|
41
|
+
)
|
|
42
|
+
).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("loads legacy raw-base64 public keys but rejects non-Ed25519 identities", () => {
|
|
46
|
+
const generated = generateKeyPair(identity);
|
|
47
|
+
fs.writeFileSync(`${identity}.pub`, generated.publicKeyBase64);
|
|
48
|
+
expect(loadKeyPair(identity).publicKeyBase64).toBe(generated.publicKeyBase64);
|
|
49
|
+
|
|
50
|
+
const rsaIdentity = path.join(directory, "rsa");
|
|
51
|
+
const rsa = crypto.generateKeyPairSync("rsa", {
|
|
52
|
+
modulusLength: 2048,
|
|
53
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
54
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
55
|
+
});
|
|
56
|
+
fs.writeFileSync(rsaIdentity, rsa.privateKey);
|
|
57
|
+
fs.writeFileSync(`${rsaIdentity}.pub`, generated.publicKeyBase64);
|
|
58
|
+
|
|
59
|
+
expect(() => loadKeyPair(rsaIdentity)).toThrow("must be an Ed25519 private key");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not overwrite a partial identity", () => {
|
|
63
|
+
fs.writeFileSync(identity, "existing key");
|
|
64
|
+
expect(() => generateKeyPair(identity)).toThrow("a key file already exists");
|
|
65
|
+
expect(fs.readFileSync(identity, "utf8")).toBe("existing key");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parseHistoryCommandArgs } from "../../../src/chat/modules/SessionsModule";
|
|
2
|
+
|
|
3
|
+
describe("parseHistoryCommandArgs", () => {
|
|
4
|
+
it("parses an id and rollback in either order", () => {
|
|
5
|
+
expect(parseHistoryCommandArgs(["task-1", "--rollback", "2"])).toEqual({
|
|
6
|
+
id: "task-1", rollback: 2, showAll: false,
|
|
7
|
+
});
|
|
8
|
+
expect(parseHistoryCommandArgs(["--rollback", "1", "task-1"])).toEqual({
|
|
9
|
+
id: "task-1", rollback: 1, showAll: false,
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("supports interactive --all", () => {
|
|
14
|
+
expect(parseHistoryCommandArgs(["--all"])).toEqual({
|
|
15
|
+
id: undefined, rollback: 0, showAll: true,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
[["--rollback"]],
|
|
21
|
+
[["--rollback", "-1"]],
|
|
22
|
+
[["--rollback", "1.5"]],
|
|
23
|
+
])(
|
|
24
|
+
"rejects invalid rollback arguments: %p",
|
|
25
|
+
(args) => {
|
|
26
|
+
expect(() => parseHistoryCommandArgs(args)).toThrow(
|
|
27
|
+
"--rollback must be a non-negative integer"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
it("rejects unexpected arguments", () => {
|
|
33
|
+
expect(() => parseHistoryCommandArgs(["one", "two"])).toThrow(
|
|
34
|
+
"Unexpected argument: two"
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
const startChat = jest.fn().mockResolvedValue(undefined);
|
|
7
|
+
const setupServices = jest.fn().mockResolvedValue(undefined);
|
|
8
|
+
|
|
9
|
+
jest.mock("../../../src/chat", () => ({ startChat }));
|
|
10
|
+
jest.mock("../../../src/commands/services", () => ({ setupServices }));
|
|
11
|
+
|
|
12
|
+
import { addAgentsCommand } from "../../../src/commands/agents";
|
|
13
|
+
|
|
14
|
+
describe("agents resume terminal UX", () => {
|
|
15
|
+
let previousCwd: string;
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
let isTTYDescriptor: PropertyDescriptor | undefined;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
previousCwd = process.cwd();
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-agents-command-"));
|
|
23
|
+
process.chdir(tempDir);
|
|
24
|
+
const taskId = "1700000000-resumable-task";
|
|
25
|
+
fs.mkdirSync(path.join(".knowhow", "processes", "agents", taskId), {
|
|
26
|
+
recursive: true,
|
|
27
|
+
});
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(".knowhow", "processes", "agents", taskId, "metadata.json"),
|
|
30
|
+
JSON.stringify({ taskId, status: "completed", threads: [[]] })
|
|
31
|
+
);
|
|
32
|
+
isTTYDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY");
|
|
33
|
+
Object.defineProperty(process.stdin, "isTTY", {
|
|
34
|
+
configurable: true,
|
|
35
|
+
value: true,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
process.chdir(previousCwd);
|
|
41
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
42
|
+
if (isTTYDescriptor) {
|
|
43
|
+
Object.defineProperty(process.stdin, "isTTY", isTTYDescriptor);
|
|
44
|
+
} else {
|
|
45
|
+
delete (process.stdin as NodeJS.ReadStream & { isTTY?: boolean }).isTTY;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("opens chat with the interactive resume command by default", async () => {
|
|
50
|
+
const program = new Command();
|
|
51
|
+
addAgentsCommand(program, jest.fn());
|
|
52
|
+
|
|
53
|
+
await program.parseAsync([
|
|
54
|
+
"node",
|
|
55
|
+
"knowhow",
|
|
56
|
+
"agents",
|
|
57
|
+
"resume",
|
|
58
|
+
"1700000000-resumable-task",
|
|
59
|
+
"--rollback",
|
|
60
|
+
"2",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
expect(setupServices).toHaveBeenCalledTimes(1);
|
|
64
|
+
expect(startChat).toHaveBeenCalledWith(
|
|
65
|
+
"/resume 1700000000-resumable-task --rollback 2"
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const httpGet = jest.fn();
|
|
2
|
+
const browserLogin = jest.fn();
|
|
3
|
+
const hasKeyPair = jest.fn();
|
|
4
|
+
const authenticateWithKey = jest.fn();
|
|
5
|
+
const registerPublicKey = jest.fn();
|
|
6
|
+
const getOrCreatePublicKey = jest.fn();
|
|
7
|
+
const getConfig = jest.fn();
|
|
8
|
+
const updateConfig = jest.fn();
|
|
9
|
+
|
|
10
|
+
jest.mock("../../src/utils/http", () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
default: { get: httpGet, isHttpError: () => false },
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock("../../src/services/KnowhowClient", () => ({
|
|
16
|
+
KNOWHOW_API_URL: "https://api.example",
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.mock("../../src/auth/browserLogin", () => ({
|
|
20
|
+
BrowserLoginService: jest.fn().mockImplementation(() => ({ login: browserLogin })),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock("../../src/auth/keyAuth", () => ({
|
|
24
|
+
hasKeyPair,
|
|
25
|
+
authenticateWithKey,
|
|
26
|
+
registerPublicKey,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.mock("../../src/auth/keyManager", () => ({ getOrCreatePublicKey }));
|
|
30
|
+
jest.mock("../../src/config", () => ({ getConfig, updateConfig }));
|
|
31
|
+
jest.mock("../../src/utils", () => ({ ask: jest.fn() }));
|
|
32
|
+
|
|
33
|
+
import fs from "fs";
|
|
34
|
+
import os from "os";
|
|
35
|
+
import path from "path";
|
|
36
|
+
import { login } from "../../src/login";
|
|
37
|
+
|
|
38
|
+
describe("login public-key bootstrap", () => {
|
|
39
|
+
const originalCwd = process.cwd();
|
|
40
|
+
let workingDirectory: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
45
|
+
jest.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
46
|
+
workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "knowhow-login-"));
|
|
47
|
+
fs.mkdirSync(path.join(workingDirectory, ".knowhow"));
|
|
48
|
+
fs.writeFileSync(path.join(workingDirectory, ".knowhow", ".jwt"), "raw-jwt");
|
|
49
|
+
process.chdir(workingDirectory);
|
|
50
|
+
hasKeyPair.mockReturnValue(false);
|
|
51
|
+
browserLogin.mockResolvedValue(undefined);
|
|
52
|
+
getConfig.mockResolvedValue({ modelProviders: [] });
|
|
53
|
+
getOrCreatePublicKey.mockReturnValue({
|
|
54
|
+
publicKeyBase64: "raw-key",
|
|
55
|
+
fingerprint: "SHA256:key",
|
|
56
|
+
});
|
|
57
|
+
httpGet.mockResolvedValue({
|
|
58
|
+
data: {
|
|
59
|
+
orgId: "org-1",
|
|
60
|
+
user: {
|
|
61
|
+
email: "user@example.com",
|
|
62
|
+
orgs: [{ organizationId: "org-1", organization: { id: "org-1", name: "Org" } }],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
process.chdir(originalCwd);
|
|
70
|
+
fs.rmSync(workingDirectory, { recursive: true, force: true });
|
|
71
|
+
jest.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("bootstraps the selected identity after PKCE without invalidating browser login on registration failure", async () => {
|
|
75
|
+
registerPublicKey.mockRejectedValueOnce(new Error("endpoint unavailable"));
|
|
76
|
+
|
|
77
|
+
await expect(login(false, "/keys/custom")).resolves.toBeUndefined();
|
|
78
|
+
|
|
79
|
+
expect(browserLogin).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/custom");
|
|
81
|
+
expect(registerPublicKey).toHaveBeenCalledWith(
|
|
82
|
+
"raw-jwt",
|
|
83
|
+
"raw-key",
|
|
84
|
+
"https://api.example"
|
|
85
|
+
);
|
|
86
|
+
expect(updateConfig).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({ orgId: "org-1", modelProviders: [{ provider: "knowhow" }] })
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("safely falls back to browser PKCE when the selected key cannot authenticate", async () => {
|
|
92
|
+
hasKeyPair.mockReturnValue(true);
|
|
93
|
+
getConfig.mockResolvedValue({ orgId: "org-1", modelProviders: [] });
|
|
94
|
+
authenticateWithKey.mockRejectedValueOnce(new Error("old backend"));
|
|
95
|
+
registerPublicKey.mockResolvedValueOnce(undefined);
|
|
96
|
+
|
|
97
|
+
await expect(login(false, "/keys/dev")).resolves.toBeUndefined();
|
|
98
|
+
|
|
99
|
+
expect(authenticateWithKey).toHaveBeenCalledWith(
|
|
100
|
+
"org-1",
|
|
101
|
+
"https://api.example",
|
|
102
|
+
"/keys/dev"
|
|
103
|
+
);
|
|
104
|
+
expect(browserLogin).toHaveBeenCalledTimes(1);
|
|
105
|
+
expect(getOrCreatePublicKey).toHaveBeenCalledWith("/keys/dev");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does not attempt key authentication without a configured organization", async () => {
|
|
109
|
+
hasKeyPair.mockReturnValue(true);
|
|
110
|
+
getConfig.mockResolvedValue({ modelProviders: [] });
|
|
111
|
+
registerPublicKey.mockResolvedValueOnce(undefined);
|
|
112
|
+
|
|
113
|
+
await expect(login(false, "/keys/dev")).resolves.toBeUndefined();
|
|
114
|
+
|
|
115
|
+
expect(hasKeyPair).not.toHaveBeenCalled();
|
|
116
|
+
expect(authenticateWithKey).not.toHaveBeenCalled();
|
|
117
|
+
expect(browserLogin).toHaveBeenCalledTimes(1);
|
|
118
|
+
});
|
|
119
|
+
});
|