@tyvm/knowhow 0.0.107 → 0.0.108-dev.4a8ba55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/package.json +9 -4
- package/scripts/publish.sh +86 -0
- package/src/agents/base/base.ts +10 -0
- package/src/agents/tools/execCommand.ts +49 -6
- package/src/agents/tools/index.ts +0 -1
- package/src/agents/tools/list.ts +0 -2
- package/src/chat/CliChatService.ts +7 -1
- package/src/chat/modules/AgentModule.ts +55 -30
- package/src/chat/modules/SessionsModule.ts +7 -2
- package/src/chat/renderer/CompactRenderer.ts +20 -0
- package/src/chat/renderer/ConsoleRenderer.ts +19 -0
- package/src/chat/renderer/FancyRenderer.ts +19 -0
- package/src/chat/renderer/types.ts +11 -0
- package/src/cli.ts +79 -661
- package/src/clients/anthropic.ts +19 -16
- package/src/clients/types.ts +23 -4
- package/src/cloudWorker.ts +75 -1
- package/src/commands/agent.ts +246 -0
- package/src/commands/misc.ts +169 -0
- package/src/commands/modules.ts +182 -0
- package/src/commands/services.ts +72 -0
- package/src/commands/workers.ts +160 -0
- package/src/config.ts +37 -0
- package/src/index.ts +18 -0
- package/src/plugins/embedding.ts +11 -6
- package/src/plugins/plugins.ts +0 -21
- package/src/plugins/vim.ts +5 -16
- package/src/processors/JsonCompressor.ts +6 -6
- package/src/services/KnowhowClient.ts +22 -2
- package/src/services/S3.ts +10 -0
- package/src/services/modules/index.ts +58 -49
- package/src/services/modules/types.ts +4 -0
- package/src/tunnel.ts +216 -0
- package/src/types.ts +0 -1
- package/src/worker.ts +105 -312
- package/src/workers/auth/WsMiddleware.ts +99 -0
- package/src/workers/auth/authMiddleware.ts +104 -0
- package/src/workers/auth/types.ts +14 -2
- package/src/workers/tools/index.ts +2 -0
- package/src/workers/tools/reloadConfig.ts +84 -0
- package/tests/services/WorkerReloadConfig.test.ts +141 -0
- package/tests/unit/modules/moduleLoading.test.ts +0 -25
- package/tests/unit/plugins/pluginLoading.test.ts +0 -85
- package/ts_build/package.json +9 -4
- package/ts_build/src/agents/base/base.js +11 -0
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
- package/ts_build/src/agents/tools/execCommand.js +39 -5
- package/ts_build/src/agents/tools/execCommand.js.map +1 -1
- package/ts_build/src/agents/tools/index.d.ts +0 -1
- package/ts_build/src/agents/tools/index.js +0 -1
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +0 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +10 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +39 -19
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +7 -2
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
- package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
- package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
- package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
- package/ts_build/src/chat/renderer/types.d.ts +2 -0
- package/ts_build/src/cli.js +40 -519
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +5 -5
- package/ts_build/src/clients/anthropic.js +19 -16
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +5 -2
- package/ts_build/src/cloudWorker.d.ts +9 -0
- package/ts_build/src/cloudWorker.js +36 -0
- package/ts_build/src/cloudWorker.js.map +1 -1
- package/ts_build/src/commands/agent.d.ts +6 -0
- package/ts_build/src/commands/agent.js +229 -0
- package/ts_build/src/commands/agent.js.map +1 -0
- package/ts_build/src/commands/misc.d.ts +10 -0
- package/ts_build/src/commands/misc.js +195 -0
- package/ts_build/src/commands/misc.js.map +1 -0
- package/ts_build/src/commands/modules.d.ts +3 -0
- package/ts_build/src/commands/modules.js +160 -0
- package/ts_build/src/commands/modules.js.map +1 -0
- package/ts_build/src/commands/services.d.ts +5 -0
- package/ts_build/src/commands/services.js +86 -0
- package/ts_build/src/commands/services.js.map +1 -0
- package/ts_build/src/commands/workers.d.ts +6 -0
- package/ts_build/src/commands/workers.js +163 -0
- package/ts_build/src/commands/workers.js.map +1 -0
- package/ts_build/src/config.d.ts +1 -0
- package/ts_build/src/config.js +32 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/index.d.ts +1 -0
- package/ts_build/src/index.js +17 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/plugins/embedding.js +4 -3
- package/ts_build/src/plugins/embedding.js.map +1 -1
- package/ts_build/src/plugins/plugins.d.ts +0 -2
- package/ts_build/src/plugins/plugins.js +0 -11
- package/ts_build/src/plugins/plugins.js.map +1 -1
- package/ts_build/src/plugins/vim.js +3 -9
- package/ts_build/src/plugins/vim.js.map +1 -1
- package/ts_build/src/processors/JsonCompressor.js +4 -4
- package/ts_build/src/processors/JsonCompressor.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +12 -0
- package/ts_build/src/services/KnowhowClient.js +11 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/S3.js +7 -0
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +33 -0
- package/ts_build/src/services/modules/index.js +38 -42
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/services/modules/types.d.ts +4 -0
- package/ts_build/src/tunnel.d.ts +27 -0
- package/ts_build/src/tunnel.js +112 -0
- package/ts_build/src/tunnel.js.map +1 -0
- package/ts_build/src/types.d.ts +0 -1
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/worker.d.ts +1 -4
- package/ts_build/src/worker.js +59 -227
- package/ts_build/src/worker.js.map +1 -1
- package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
- package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
- package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
- package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
- package/ts_build/src/workers/auth/authMiddleware.js +60 -0
- package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
- package/ts_build/src/workers/auth/types.d.ts +8 -1
- package/ts_build/src/workers/tools/index.d.ts +2 -0
- package/ts_build/src/workers/tools/index.js +4 -1
- package/ts_build/src/workers/tools/index.js.map +1 -1
- package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
- package/ts_build/src/workers/tools/reloadConfig.js +48 -0
- package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
- package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js +0 -19
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
- package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
- package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
- package/src/agents/tools/executeScript/README.md +0 -94
- package/src/agents/tools/executeScript/definition.ts +0 -79
- package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
- package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
- package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
- package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
- package/src/agents/tools/executeScript/index.ts +0 -98
- package/src/services/script-execution/SandboxContext.ts +0 -282
- package/src/services/script-execution/ScriptExecutor.ts +0 -441
- package/src/services/script-execution/ScriptPolicy.ts +0 -194
- package/src/services/script-execution/ScriptTracer.ts +0 -249
- package/src/services/script-execution/types.ts +0 -134
- package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
- package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
- package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
- package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
- package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
- package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
- package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
- package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
- package/ts_build/src/agents/tools/executeScript/index.js +0 -72
- package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
- package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
- package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
- package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
- package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
- package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
- package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
- package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
- package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
- package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
- package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
- package/ts_build/src/services/script-execution/types.d.ts +0 -108
- package/ts_build/src/services/script-execution/types.js +0 -3
- package/ts_build/src/services/script-execution/types.js.map +0 -1
package/src/clients/anthropic.ts
CHANGED
|
@@ -40,11 +40,11 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
handleToolCaching(tools: Anthropic.Tool[]) {
|
|
43
|
+
handleToolCaching(tools: Anthropic.Tool[], longTtl = false) {
|
|
44
44
|
const lastTool = tools[tools.length - 1];
|
|
45
45
|
|
|
46
46
|
if (lastTool) {
|
|
47
|
-
lastTool.cache_control = { type: "ephemeral" };
|
|
47
|
+
lastTool.cache_control = longTtl ? { type: "ephemeral", ttl: "1h" } as any : { type: "ephemeral" };
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -94,7 +94,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
94
94
|
return cleaned;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
transformTools(tools?: Tool[]): Anthropic.Tool[] {
|
|
97
|
+
transformTools(tools?: Tool[], longTtl = false): Anthropic.Tool[] {
|
|
98
98
|
if (!tools) {
|
|
99
99
|
return [];
|
|
100
100
|
}
|
|
@@ -104,7 +104,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
104
104
|
input_schema: this.cleanSchemaForAnthropic(tool.function.parameters) as any,
|
|
105
105
|
}));
|
|
106
106
|
|
|
107
|
-
this.handleToolCaching(transformed);
|
|
107
|
+
this.handleToolCaching(transformed, longTtl);
|
|
108
108
|
|
|
109
109
|
return transformed;
|
|
110
110
|
}
|
|
@@ -153,16 +153,16 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
153
153
|
return messages;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
cacheLastContent(message: MessageParam) {
|
|
156
|
+
cacheLastContent(message: MessageParam, longTtl = false) {
|
|
157
157
|
if (Array.isArray(message.content)) {
|
|
158
158
|
const lastMessage = message.content[message.content.length - 1];
|
|
159
159
|
if (
|
|
160
160
|
lastMessage.type !== "thinking" &&
|
|
161
161
|
lastMessage.type !== "redacted_thinking"
|
|
162
162
|
) {
|
|
163
|
-
lastMessage.cache_control =
|
|
164
|
-
type: "ephemeral",
|
|
165
|
-
|
|
163
|
+
lastMessage.cache_control = longTtl
|
|
164
|
+
? ({ type: "ephemeral", ttl: "1h" } as any)
|
|
165
|
+
: { type: "ephemeral" };
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
}
|
|
@@ -179,7 +179,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
handleMessageCaching(groupedMessages: MessageParam[]) {
|
|
182
|
+
handleMessageCaching(groupedMessages: MessageParam[], longTtl = false) {
|
|
183
183
|
this.handleClearingCache(groupedMessages);
|
|
184
184
|
|
|
185
185
|
// find the last two messages and mark them as ephemeral
|
|
@@ -189,7 +189,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
189
189
|
|
|
190
190
|
for (const m of lastTwoUserMessages) {
|
|
191
191
|
if (Array.isArray(m.content)) {
|
|
192
|
-
this.cacheLastContent(m);
|
|
192
|
+
this.cacheLastContent(m, longTtl);
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
}
|
|
@@ -203,7 +203,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
transformMessages(messages: Message[]): MessageParam[] {
|
|
206
|
+
transformMessages(messages: Message[], longTtl = false): MessageParam[] {
|
|
207
207
|
const toolCalls = messages.flatMap((msg) => msg.tool_calls || []);
|
|
208
208
|
const claudeMessages: MessageParam[] = messages
|
|
209
209
|
.filter((msg) => msg.role !== "system")
|
|
@@ -302,7 +302,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
302
302
|
|
|
303
303
|
const groupedMessages = this.combineMessages(claudeMessages);
|
|
304
304
|
|
|
305
|
-
this.handleMessageCaching(groupedMessages);
|
|
305
|
+
this.handleMessageCaching(groupedMessages, longTtl);
|
|
306
306
|
|
|
307
307
|
return groupedMessages;
|
|
308
308
|
}
|
|
@@ -349,14 +349,15 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
349
349
|
async createChatCompletion(
|
|
350
350
|
options: CompletionOptions
|
|
351
351
|
): Promise<CompletionResponse> {
|
|
352
|
+
const longTtl = !!options.long_ttl_cache;
|
|
352
353
|
const systemMessage = options.messages
|
|
353
354
|
.filter((msg) => msg.role === "system")
|
|
354
355
|
.map((msg) => msg.content || "")
|
|
355
356
|
.join("\n");
|
|
356
357
|
|
|
357
|
-
const claudeMessages = this.transformMessages(options.messages);
|
|
358
|
+
const claudeMessages = this.transformMessages(options.messages, longTtl);
|
|
358
359
|
|
|
359
|
-
const tools = this.transformTools(options.tools);
|
|
360
|
+
const tools = this.transformTools(options.tools, longTtl);
|
|
360
361
|
try {
|
|
361
362
|
const response = await this.client.messages.create({
|
|
362
363
|
model: options.model,
|
|
@@ -365,7 +366,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
365
366
|
? [
|
|
366
367
|
{
|
|
367
368
|
text: systemMessage,
|
|
368
|
-
cache_control: { type: "ephemeral" },
|
|
369
|
+
cache_control: longTtl ? ({ type: "ephemeral", ttl: "1h" } as any) : { type: "ephemeral" },
|
|
369
370
|
type: "text",
|
|
370
371
|
},
|
|
371
372
|
]
|
|
@@ -413,6 +414,8 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
413
414
|
|
|
414
415
|
model: options.model,
|
|
415
416
|
usage: response.usage ? {
|
|
417
|
+
input_tokens: response.usage.input_tokens ?? 0,
|
|
418
|
+
output_tokens: response.usage.output_tokens ?? 0,
|
|
416
419
|
prompt_tokens: response.usage.input_tokens ?? 0,
|
|
417
420
|
completion_tokens: response.usage.output_tokens ?? 0,
|
|
418
421
|
total_tokens: (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0),
|
|
@@ -422,7 +425,7 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
422
425
|
usd_cost: this.calculateCost(options.model, response.usage),
|
|
423
426
|
};
|
|
424
427
|
} catch (err) {
|
|
425
|
-
if ("headers" in err && err.headers["x-should-retry"] === "true") {
|
|
428
|
+
if ("headers" in err && err.headers?.["x-should-retry"] === "true") {
|
|
426
429
|
console.warn("Retrying failed request", err);
|
|
427
430
|
await wait(2500);
|
|
428
431
|
return this.createChatCompletion(options);
|
package/src/clients/types.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export type ModelModality =
|
|
1
|
+
export type ModelModality =
|
|
2
|
+
| "completion"
|
|
3
|
+
| "embedding"
|
|
4
|
+
| "image"
|
|
5
|
+
| "audio"
|
|
6
|
+
| "video"
|
|
7
|
+
| "transcription";
|
|
2
8
|
|
|
3
9
|
export type MessageContent =
|
|
4
10
|
| { type: "text"; text: string }
|
|
@@ -8,7 +14,7 @@ export type MessageContent =
|
|
|
8
14
|
|
|
9
15
|
export interface Message {
|
|
10
16
|
role: "system" | "user" | "assistant" | "tool";
|
|
11
|
-
content?: string | MessageContent[];
|
|
17
|
+
content?: string | MessageContent[] | null;
|
|
12
18
|
|
|
13
19
|
name?: string;
|
|
14
20
|
tool_call_id?: string;
|
|
@@ -16,7 +22,7 @@ export interface Message {
|
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface OutputMessage extends Message {
|
|
19
|
-
content
|
|
25
|
+
content?: string | null;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface ToolProp {
|
|
@@ -61,6 +67,13 @@ export interface CompletionOptions {
|
|
|
61
67
|
* Maps to: OpenAI reasoning_effort, xAI reasoning.effort, Gemini thinkingLevel/thinkingBudget, Anthropic thinking budget.
|
|
62
68
|
* "low" = minimal thinking, "medium" = balanced, "high" = maximum reasoning */
|
|
63
69
|
reasoning_effort?: "low" | "medium" | "high";
|
|
70
|
+
/**
|
|
71
|
+
* When true, hints to the client that this task is long-running and it should
|
|
72
|
+
* use a long-TTL cache where available.
|
|
73
|
+
* - Anthropic: enables the `extended-cache-ttl-2025-02-19` beta and sets
|
|
74
|
+
* `cache_control.ttl` to 3600 (1 hour) instead of the default 5-minute ephemeral cache.
|
|
75
|
+
*/
|
|
76
|
+
long_ttl_cache?: boolean;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
/**
|
|
@@ -73,6 +86,10 @@ export interface TokenUsage {
|
|
|
73
86
|
prompt_tokens: number;
|
|
74
87
|
/** Total output/completion tokens generated */
|
|
75
88
|
completion_tokens: number;
|
|
89
|
+
/** Alternative field name for input tokens (some providers use this) */
|
|
90
|
+
input_tokens?: number;
|
|
91
|
+
/** Alternative field name for output tokens (some providers use this) */
|
|
92
|
+
output_tokens?: number;
|
|
76
93
|
/** Convenience total (prompt + completion) */
|
|
77
94
|
total_tokens?: number;
|
|
78
95
|
/** Cache details */
|
|
@@ -290,7 +307,9 @@ export interface GenericClient {
|
|
|
290
307
|
* When modality is provided, return only models for that modality (static list).
|
|
291
308
|
* When omitted, return ALL models (backward compat — may do a live API call).
|
|
292
309
|
*/
|
|
293
|
-
getModels(
|
|
310
|
+
getModels(
|
|
311
|
+
modality?: ModelModality
|
|
312
|
+
): Promise<{ id: string; modality?: ModelModality[] }[]>;
|
|
294
313
|
/**
|
|
295
314
|
* Returns the context window limit and compression threshold for a given model,
|
|
296
315
|
* or undefined if the model is not known to this client.
|
package/src/cloudWorker.ts
CHANGED
|
@@ -4,9 +4,14 @@ import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
|
4
4
|
import { loadJwt } from "./login";
|
|
5
5
|
import { getConfig, updateConfig, getLanguageConfig } from "./config";
|
|
6
6
|
import { services } from "./services";
|
|
7
|
-
import { Language, Config } from "./types";
|
|
7
|
+
import { Language, Config, McpConfig } from "./types";
|
|
8
8
|
import { S3Service } from "./services/S3";
|
|
9
9
|
|
|
10
|
+
export interface CloudWorkerPullOptions {
|
|
11
|
+
id: string;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export interface CloudWorkerOptions {
|
|
11
16
|
create?: boolean;
|
|
12
17
|
push?: string; // uid of existing cloud worker
|
|
@@ -330,3 +335,72 @@ export async function cloudWorker(options: CloudWorkerOptions) {
|
|
|
330
335
|
console.log(`\n✅ Cloud worker sync complete!`);
|
|
331
336
|
}
|
|
332
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Pull the latest workerConfigJson from the cloud worker API and update the
|
|
341
|
+
* local knowhow.json config to match.
|
|
342
|
+
*
|
|
343
|
+
* This is the "pull" half of the config sync cycle. After running this,
|
|
344
|
+
* you can reload the worker's MCPs (in-process) via the reloadConfig
|
|
345
|
+
* WebSocket message or by calling `knowhow worker` again.
|
|
346
|
+
*
|
|
347
|
+
* Merged fields from workerConfigJson:
|
|
348
|
+
* - mcps → overwrites config.mcps
|
|
349
|
+
* - modules → overwrites config.modules (optional, only if present)
|
|
350
|
+
* - plugins → overwrites config.plugins (optional, only if present)
|
|
351
|
+
* - agents → overwrites config.agents (optional, only if present)
|
|
352
|
+
*/
|
|
353
|
+
export async function pullCloudWorkerConfig(options: CloudWorkerPullOptions) {
|
|
354
|
+
const { id, apiUrl = KNOWHOW_API_URL } = options;
|
|
355
|
+
|
|
356
|
+
// Load JWT
|
|
357
|
+
const jwt = await loadJwt();
|
|
358
|
+
if (!jwt) {
|
|
359
|
+
console.error("❌ No JWT token found. Please run 'knowhow login' first.");
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const client = new KnowhowSimpleClient(apiUrl, jwt);
|
|
364
|
+
|
|
365
|
+
console.log(`🔄 Pulling config for cloud worker ${id}...`);
|
|
366
|
+
|
|
367
|
+
const resp = await client.getCloudWorker(id);
|
|
368
|
+
const remoteWorker = resp.data;
|
|
369
|
+
|
|
370
|
+
if (!remoteWorker) {
|
|
371
|
+
console.error(`❌ Cloud worker ${id} not found.`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const remoteConfig = (remoteWorker.workerConfigJson ?? {}) as {
|
|
376
|
+
mcps?: McpConfig[];
|
|
377
|
+
modules?: string[];
|
|
378
|
+
plugins?: Config["plugins"];
|
|
379
|
+
agents?: Config["agents"];
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Load current local config
|
|
383
|
+
const localConfig = await getConfig();
|
|
384
|
+
|
|
385
|
+
// Merge remote fields into local config
|
|
386
|
+
if (remoteConfig.mcps !== undefined) {
|
|
387
|
+
localConfig.mcps = remoteConfig.mcps;
|
|
388
|
+
}
|
|
389
|
+
if (remoteConfig.modules !== undefined) {
|
|
390
|
+
localConfig.modules = remoteConfig.modules;
|
|
391
|
+
}
|
|
392
|
+
if (remoteConfig.plugins !== undefined) {
|
|
393
|
+
localConfig.plugins = remoteConfig.plugins;
|
|
394
|
+
}
|
|
395
|
+
if (remoteConfig.agents !== undefined) {
|
|
396
|
+
localConfig.agents = remoteConfig.agents;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await updateConfig(localConfig);
|
|
400
|
+
|
|
401
|
+
const mcpCount = remoteConfig.mcps?.length ?? 0;
|
|
402
|
+
console.log(`✅ Config pulled! ${mcpCount} MCP(s) now configured locally.`);
|
|
403
|
+
console.log(` Run 'knowhow worker' or trigger reloadConfig to apply changes.`);
|
|
404
|
+
|
|
405
|
+
return { mcps: remoteConfig.mcps, modules: remoteConfig.modules };
|
|
406
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readPromptFile } from "../ai";
|
|
3
|
+
import { AgentModule } from "../chat/modules/AgentModule";
|
|
4
|
+
import { AskModule } from "../chat/modules/AskModule";
|
|
5
|
+
import { SearchModule } from "../chat/modules/SearchModule";
|
|
6
|
+
import { SessionsModule } from "../chat/modules/SessionsModule";
|
|
7
|
+
import { SetupModule } from "../chat/modules/SetupModule";
|
|
8
|
+
|
|
9
|
+
async function readStdin(): Promise<string> {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let data = "";
|
|
12
|
+
process.stdin.setEncoding("utf8");
|
|
13
|
+
|
|
14
|
+
if (process.stdin.isTTY) {
|
|
15
|
+
resolve("");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
process.stdin.on("readable", () => {
|
|
20
|
+
const chunk = process.stdin.read();
|
|
21
|
+
if (chunk !== null) data += chunk;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.stdin.on("end", () => resolve(data.trim()));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function addAgentCommand(program: Command, getChatService: () => any): void {
|
|
29
|
+
program
|
|
30
|
+
.command("agent")
|
|
31
|
+
.description("Spin up agents directly from CLI")
|
|
32
|
+
.option(
|
|
33
|
+
"--provider <provider>",
|
|
34
|
+
"AI provider (openai, anthropic, google, xai)"
|
|
35
|
+
)
|
|
36
|
+
.option("--model <model>", "Specific model for the provider")
|
|
37
|
+
.option("--agent-name <name>", "Which agent to use", "Patcher")
|
|
38
|
+
.option(
|
|
39
|
+
"--max-time-limit <minutes>",
|
|
40
|
+
"Time limit for agent execution (minutes)",
|
|
41
|
+
"30"
|
|
42
|
+
)
|
|
43
|
+
.option(
|
|
44
|
+
"--max-spend-limit <dollars>",
|
|
45
|
+
"Cost limit for agent execution (dollars)",
|
|
46
|
+
"10"
|
|
47
|
+
)
|
|
48
|
+
.option("--message-id <messageId>", "Knowhow message ID for task tracking")
|
|
49
|
+
.option("--sync-fs", "Enable filesystem-based synchronization")
|
|
50
|
+
.option(
|
|
51
|
+
"--task-id <taskId>",
|
|
52
|
+
"Pre-generated task ID (used with --sync-fs for predictable agent directory path)"
|
|
53
|
+
)
|
|
54
|
+
.option("--prompt-file <path>", "Custom prompt template file with {text}")
|
|
55
|
+
.option("--input <text>", "Task input (fallback to stdin if not provided)")
|
|
56
|
+
.option(
|
|
57
|
+
"--resume",
|
|
58
|
+
"Resume a previously started task using the --task-id (local FS or remote)"
|
|
59
|
+
)
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
try {
|
|
62
|
+
const { setupServices } = await import("./services");
|
|
63
|
+
await setupServices();
|
|
64
|
+
const chatService = getChatService();
|
|
65
|
+
const agentModule = new AgentModule();
|
|
66
|
+
|
|
67
|
+
if (options.resume) {
|
|
68
|
+
const threads = await agentModule.loadThreadsForTask(
|
|
69
|
+
options.taskId,
|
|
70
|
+
options.messageId
|
|
71
|
+
);
|
|
72
|
+
const resumeInput =
|
|
73
|
+
options.input || "Please continue from where you left off.";
|
|
74
|
+
|
|
75
|
+
await agentModule.initialize(chatService);
|
|
76
|
+
const { taskCompleted: resumed } =
|
|
77
|
+
await agentModule.resumeFromMessages({
|
|
78
|
+
agentName: options.agentName || "Patcher",
|
|
79
|
+
input: resumeInput,
|
|
80
|
+
threads,
|
|
81
|
+
messageId: options.messageId,
|
|
82
|
+
taskId: options.taskId,
|
|
83
|
+
});
|
|
84
|
+
await resumed;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let input = options.input;
|
|
89
|
+
|
|
90
|
+
if (!input && !options.promptFile) {
|
|
91
|
+
input = await readStdin();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
input = readPromptFile(options.promptFile, input);
|
|
95
|
+
|
|
96
|
+
if (!input) {
|
|
97
|
+
console.error(
|
|
98
|
+
"Error: No input provided. Use --input flag, pipe input via stdin, or provide --prompt-file."
|
|
99
|
+
);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await agentModule.initialize(chatService);
|
|
104
|
+
const { taskCompleted } = await agentModule.setupAgent({
|
|
105
|
+
...options,
|
|
106
|
+
input,
|
|
107
|
+
maxTimeLimit: parseInt(options.maxTimeLimit, 10),
|
|
108
|
+
maxSpendLimit: parseFloat(options.maxSpendLimit),
|
|
109
|
+
run: true,
|
|
110
|
+
});
|
|
111
|
+
await taskCompleted;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Error running agent:", error);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function addAskCommand(program: Command, getChatService: () => any, getConfig: () => any): void {
|
|
120
|
+
program
|
|
121
|
+
.command("ask")
|
|
122
|
+
.description("Direct AI questioning without agent overhead")
|
|
123
|
+
.option("--provider <provider>", "AI provider to use")
|
|
124
|
+
.option("--model <model>", "Specific model")
|
|
125
|
+
.option("--input <text>", "Question (fallback to stdin if not provided)")
|
|
126
|
+
.option("--prompt-file <path>", "Custom prompt template file")
|
|
127
|
+
.action(async (options) => {
|
|
128
|
+
try {
|
|
129
|
+
const { setupServices } = await import("./services");
|
|
130
|
+
await setupServices();
|
|
131
|
+
const chatService = getChatService();
|
|
132
|
+
const config = getConfig();
|
|
133
|
+
let input = options.input;
|
|
134
|
+
|
|
135
|
+
if (!input && !options.promptFile) {
|
|
136
|
+
input = await readStdin();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
input = readPromptFile(options.promptFile, input);
|
|
140
|
+
|
|
141
|
+
if (!input) {
|
|
142
|
+
console.error(
|
|
143
|
+
"Error: No question provided. Use --input flag, pipe input via stdin, or provide --prompt-file."
|
|
144
|
+
);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const askModule = new AskModule();
|
|
149
|
+
await askModule.initialize(chatService);
|
|
150
|
+
await askModule.processAIQuery(input, {
|
|
151
|
+
plugins: config.plugins.enabled,
|
|
152
|
+
currentModel: options.model,
|
|
153
|
+
currentProvider: options.provider,
|
|
154
|
+
chatHistory: [],
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("Error asking AI:", error);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function addSetupCommand(program: Command, getChatService: () => any): void {
|
|
164
|
+
program
|
|
165
|
+
.command("setup")
|
|
166
|
+
.description("Ask the agent to configure knowhow")
|
|
167
|
+
.action(async () => {
|
|
168
|
+
try {
|
|
169
|
+
const { setupServices } = await import("./services");
|
|
170
|
+
await setupServices();
|
|
171
|
+
const chatService = getChatService();
|
|
172
|
+
const agentModule = new AgentModule();
|
|
173
|
+
await agentModule.initialize(chatService);
|
|
174
|
+
const setupModule = new SetupModule(agentModule);
|
|
175
|
+
await setupModule.initialize(chatService);
|
|
176
|
+
await setupModule.handleSetupCommand([]);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error("Error running agent:", error);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function addSearchCommand(program: Command): void {
|
|
185
|
+
program
|
|
186
|
+
.command("search")
|
|
187
|
+
.description("Search embeddings directly from CLI")
|
|
188
|
+
.option(
|
|
189
|
+
"--input <text>",
|
|
190
|
+
"Search query (fallback to stdin if not provided)"
|
|
191
|
+
)
|
|
192
|
+
.option(
|
|
193
|
+
"-e, --embedding <path>",
|
|
194
|
+
"Specific embedding path (default: all)",
|
|
195
|
+
"all"
|
|
196
|
+
)
|
|
197
|
+
.action(async (options) => {
|
|
198
|
+
try {
|
|
199
|
+
const { setupServices } = await import("./services");
|
|
200
|
+
await setupServices();
|
|
201
|
+
let input = options.input;
|
|
202
|
+
if (!input) {
|
|
203
|
+
input = await readStdin();
|
|
204
|
+
if (!input) {
|
|
205
|
+
console.error(
|
|
206
|
+
"Error: No search query provided. Use --input flag or pipe input via stdin."
|
|
207
|
+
);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await new SearchModule().searchEmbeddingsCLI(input, options.embedding);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error("Error searching embeddings:", error);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function addSessionsCommand(program: Command, getChatService: () => any): void {
|
|
221
|
+
program
|
|
222
|
+
.command("sessions")
|
|
223
|
+
.description("Manage agent sessions from CLI")
|
|
224
|
+
.option(
|
|
225
|
+
"--all",
|
|
226
|
+
"Show all historical sessions (default: current process only)"
|
|
227
|
+
)
|
|
228
|
+
.option("--csv", "Output sessions as CSV")
|
|
229
|
+
.action(async (options) => {
|
|
230
|
+
try {
|
|
231
|
+
const chatService = getChatService();
|
|
232
|
+
const agentModule = new AgentModule();
|
|
233
|
+
await agentModule.initialize(chatService);
|
|
234
|
+
const sessionsModule = new SessionsModule(agentModule);
|
|
235
|
+
await sessionsModule.initialize(chatService);
|
|
236
|
+
await sessionsModule.logSessionTable(
|
|
237
|
+
options.all || false,
|
|
238
|
+
options.csv || false,
|
|
239
|
+
true
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error("Error listing sessions:", error);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { version } from "../../package.json";
|
|
4
|
+
import { generate, embed, upload, download, purge } from "../index";
|
|
5
|
+
import { init } from "../config";
|
|
6
|
+
import { login } from "../login";
|
|
7
|
+
import { KnowhowSimpleClient } from "../services/KnowhowClient";
|
|
8
|
+
import { startChat } from "../chat";
|
|
9
|
+
|
|
10
|
+
export function addInitCommand(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command("init")
|
|
13
|
+
.description("Initialize knowhow configuration")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
await init();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addLoginCommand(program: Command): void {
|
|
20
|
+
program
|
|
21
|
+
.command("login")
|
|
22
|
+
.description("Login to knowhow")
|
|
23
|
+
.option("--jwt", "Use manual JWT input instead of browser login")
|
|
24
|
+
.action(async (opts) => {
|
|
25
|
+
await login(opts.jwt);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function addUpdateCommand(program: Command): void {
|
|
30
|
+
program
|
|
31
|
+
.command("update")
|
|
32
|
+
.description("Update knowhow to the latest version from npm")
|
|
33
|
+
.action(async () => {
|
|
34
|
+
try {
|
|
35
|
+
console.log("🔄 Checking for knowhow updates...");
|
|
36
|
+
console.log(`Current version: ${version}`);
|
|
37
|
+
console.log("📦 Installing latest version from npm...");
|
|
38
|
+
execSync("npm install -g @tyvm/knowhow@latest", {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
});
|
|
42
|
+
console.log("✓ knowhow has been updated successfully!");
|
|
43
|
+
console.log("Run 'knowhow --version' to see the new version.");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("Error updating knowhow:", error.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function addGenerateCommand(program: Command): void {
|
|
52
|
+
program
|
|
53
|
+
.command("generate")
|
|
54
|
+
.description("Generate documentation")
|
|
55
|
+
.action(async () => {
|
|
56
|
+
const { setupServices } = await import("./services");
|
|
57
|
+
await setupServices();
|
|
58
|
+
await generate();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function addEmbedCommands(program: Command): void {
|
|
63
|
+
program
|
|
64
|
+
.command("embed")
|
|
65
|
+
.description("Create embeddings")
|
|
66
|
+
.action(async () => {
|
|
67
|
+
const { setupServices } = await import("./services");
|
|
68
|
+
await setupServices();
|
|
69
|
+
await embed();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command("embed:purge")
|
|
74
|
+
.description("Purge embeddings matching a glob pattern")
|
|
75
|
+
.argument("<pattern>", "Glob pattern to match files for purging")
|
|
76
|
+
.action(async (pattern) => {
|
|
77
|
+
await purge(pattern);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function addUploadCommand(program: Command): void {
|
|
82
|
+
program
|
|
83
|
+
.command("upload")
|
|
84
|
+
.description("Upload data")
|
|
85
|
+
.action(async () => {
|
|
86
|
+
await upload();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function addDownloadCommand(program: Command): void {
|
|
91
|
+
program
|
|
92
|
+
.command("download")
|
|
93
|
+
.description("Download data")
|
|
94
|
+
.action(async () => {
|
|
95
|
+
await download();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function addChatCommand(program: Command): void {
|
|
100
|
+
program
|
|
101
|
+
.command("chat")
|
|
102
|
+
.description("Start new chat interface")
|
|
103
|
+
.action(async () => {
|
|
104
|
+
const { setupServices } = await import("./services");
|
|
105
|
+
await setupServices();
|
|
106
|
+
await startChat();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function addGithubCredentialsCommand(program: Command): void {
|
|
111
|
+
program
|
|
112
|
+
.command("github-credentials [action]")
|
|
113
|
+
.description(
|
|
114
|
+
"Git credential helper for GitHub. Use as: git config credential.helper 'knowhow github-credentials'"
|
|
115
|
+
)
|
|
116
|
+
.option(
|
|
117
|
+
"--repo <repo>",
|
|
118
|
+
"Repository in owner/repo format (e.g. myorg/myrepo)"
|
|
119
|
+
)
|
|
120
|
+
.action(async (action: string | undefined, options: { repo?: string }) => {
|
|
121
|
+
const client = new KnowhowSimpleClient();
|
|
122
|
+
|
|
123
|
+
let repo = options.repo;
|
|
124
|
+
|
|
125
|
+
if (action === "get") {
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
const readline = await import("readline");
|
|
128
|
+
const rl = readline.createInterface({
|
|
129
|
+
input: process.stdin,
|
|
130
|
+
terminal: false,
|
|
131
|
+
});
|
|
132
|
+
await new Promise<void>((resolve) => {
|
|
133
|
+
rl.on("line", (line) => {
|
|
134
|
+
if (line.trim()) lines.push(line.trim());
|
|
135
|
+
});
|
|
136
|
+
rl.on("close", resolve);
|
|
137
|
+
});
|
|
138
|
+
} else if (action === "store" || action === "erase") {
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!repo) {
|
|
143
|
+
try {
|
|
144
|
+
const remoteUrl = execSync("git remote get-url origin", {
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
147
|
+
}).trim();
|
|
148
|
+
const match =
|
|
149
|
+
remoteUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/) ||
|
|
150
|
+
remoteUrl.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
151
|
+
if (match) {
|
|
152
|
+
repo = match[1];
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Not in a git repo or no remote — proceed without repo
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const credential = await client.getGitCredential(repo || "");
|
|
161
|
+
process.stdout.write(
|
|
162
|
+
`protocol=${credential.protocol}\nhost=${credential.host}\nusername=${credential.username}\npassword=${credential.password}\n`
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error("Failed to get git credentials:", error.message);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|