@tyvm/knowhow 0.0.130 → 0.0.132

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 (97) hide show
  1. package/package.json +1 -1
  2. package/src/agents/configurable/ConfigAgent.ts +0 -1
  3. package/src/agents/tools/startAgentTask.ts +17 -13
  4. package/src/ai.ts +55 -24
  5. package/src/chat/CliChatService.ts +7 -1
  6. package/src/chat/modules/AgentModule.ts +72 -14
  7. package/src/chat/renderer/PlainRenderer.ts +218 -0
  8. package/src/chat/renderer/index.ts +1 -0
  9. package/src/chat/renderer/loadRenderer.ts +2 -0
  10. package/src/chat.ts +2 -2
  11. package/src/cli.ts +23 -0
  12. package/src/clients/anthropic.ts +9 -1
  13. package/src/clients/fireworks.ts +10 -1
  14. package/src/clients/index.ts +6 -3
  15. package/src/clients/meta.ts +9 -0
  16. package/src/clients/pricing/anthropic.ts +11 -0
  17. package/src/clients/pricing/fireworks.ts +12 -0
  18. package/src/commands/agent.ts +53 -20
  19. package/src/commands/agents.ts +556 -0
  20. package/src/commands/hash.ts +170 -0
  21. package/src/commands/misc.ts +157 -3
  22. package/src/commands/skills.ts +36 -0
  23. package/src/config.ts +54 -2
  24. package/src/generate.ts +507 -0
  25. package/src/index.ts +32 -216
  26. package/src/services/BehaviorsService.ts +32 -0
  27. package/src/services/SkillsService.ts +198 -0
  28. package/src/types.ts +36 -4
  29. package/ts_build/package.json +1 -1
  30. package/ts_build/src/agents/configurable/ConfigAgent.js.map +1 -1
  31. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -2
  32. package/ts_build/src/agents/tools/startAgentTask.js +13 -12
  33. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  34. package/ts_build/src/ai.d.ts +10 -4
  35. package/ts_build/src/ai.js +27 -25
  36. package/ts_build/src/ai.js.map +1 -1
  37. package/ts_build/src/chat/CliChatService.d.ts +1 -1
  38. package/ts_build/src/chat/CliChatService.js +4 -1
  39. package/ts_build/src/chat/CliChatService.js.map +1 -1
  40. package/ts_build/src/chat/modules/AgentModule.js +61 -4
  41. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  42. package/ts_build/src/chat/renderer/PlainRenderer.d.ts +28 -0
  43. package/ts_build/src/chat/renderer/PlainRenderer.js +169 -0
  44. package/ts_build/src/chat/renderer/PlainRenderer.js.map +1 -0
  45. package/ts_build/src/chat/renderer/index.d.ts +1 -0
  46. package/ts_build/src/chat/renderer/index.js +3 -1
  47. package/ts_build/src/chat/renderer/index.js.map +1 -1
  48. package/ts_build/src/chat/renderer/loadRenderer.js +2 -0
  49. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -1
  50. package/ts_build/src/chat.d.ts +1 -1
  51. package/ts_build/src/chat.js +2 -2
  52. package/ts_build/src/chat.js.map +1 -1
  53. package/ts_build/src/cli.js +16 -0
  54. package/ts_build/src/cli.js.map +1 -1
  55. package/ts_build/src/clients/anthropic.js +4 -1
  56. package/ts_build/src/clients/anthropic.js.map +1 -1
  57. package/ts_build/src/clients/fireworks.js +7 -0
  58. package/ts_build/src/clients/fireworks.js.map +1 -1
  59. package/ts_build/src/clients/index.js +5 -1
  60. package/ts_build/src/clients/index.js.map +1 -1
  61. package/ts_build/src/clients/meta.d.ts +2 -0
  62. package/ts_build/src/clients/meta.js +5 -0
  63. package/ts_build/src/clients/meta.js.map +1 -1
  64. package/ts_build/src/clients/pricing/anthropic.d.ts +3 -0
  65. package/ts_build/src/clients/pricing/anthropic.js +6 -0
  66. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  67. package/ts_build/src/clients/pricing/fireworks.d.ts +1 -0
  68. package/ts_build/src/clients/pricing/fireworks.js +5 -1
  69. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  70. package/ts_build/src/commands/agent.js +44 -17
  71. package/ts_build/src/commands/agent.js.map +1 -1
  72. package/ts_build/src/commands/agents.d.ts +2 -0
  73. package/ts_build/src/commands/agents.js +460 -0
  74. package/ts_build/src/commands/agents.js.map +1 -0
  75. package/ts_build/src/commands/hash.d.ts +2 -0
  76. package/ts_build/src/commands/hash.js +150 -0
  77. package/ts_build/src/commands/hash.js.map +1 -0
  78. package/ts_build/src/commands/misc.js +108 -2
  79. package/ts_build/src/commands/misc.js.map +1 -1
  80. package/ts_build/src/commands/skills.js +24 -0
  81. package/ts_build/src/commands/skills.js.map +1 -1
  82. package/ts_build/src/config.js +37 -1
  83. package/ts_build/src/config.js.map +1 -1
  84. package/ts_build/src/generate.d.ts +16 -0
  85. package/ts_build/src/generate.js +304 -0
  86. package/ts_build/src/generate.js.map +1 -0
  87. package/ts_build/src/index.d.ts +1 -3
  88. package/ts_build/src/index.js +9 -118
  89. package/ts_build/src/index.js.map +1 -1
  90. package/ts_build/src/services/BehaviorsService.d.ts +2 -0
  91. package/ts_build/src/services/BehaviorsService.js +24 -0
  92. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  93. package/ts_build/src/services/SkillsService.d.ts +7 -0
  94. package/ts_build/src/services/SkillsService.js +163 -0
  95. package/ts_build/src/services/SkillsService.js.map +1 -1
  96. package/ts_build/src/types.d.ts +14 -4
  97. package/ts_build/src/types.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.130",
3
+ "version": "0.0.132",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1,7 +1,6 @@
1
1
  import { AgentContext, BaseAgent } from "../base/base";
2
2
  import { Message } from "../../clients/types";
3
3
  import { Assistant, Config } from "../../types";
4
- import { EventService, ToolsService } from "../../services";
5
4
  import { MessageProcessor } from "../../services/MessageProcessor";
6
5
 
7
6
  export class ConfigAgent extends BaseAgent {
@@ -3,7 +3,7 @@ import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { spawn } from "child_process";
5
5
 
6
- interface StartAgentTaskParams {
6
+ export interface StartAgentTaskParams {
7
7
  messageId?: string;
8
8
  syncFs?: boolean;
9
9
  taskId?: string;
@@ -14,6 +14,8 @@ interface StartAgentTaskParams {
14
14
  agentName?: string;
15
15
  maxTimeLimit?: number;
16
16
  maxSpendLimit?: number;
17
+ /** When true, wait for the agent subprocess to exit before resolving (for generate pipelines). */
18
+ waitForCompletion?: boolean;
17
19
  }
18
20
 
19
21
  const PROCESSES_DIR = path.join(process.cwd(), ".knowhow", "processes");
@@ -63,6 +65,7 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
63
65
  agentName,
64
66
  maxTimeLimit,
65
67
  maxSpendLimit,
68
+ waitForCompletion,
66
69
  } = params;
67
70
  if (!prompt) {
68
71
  throw new Error("prompt is required to create a chat task");
@@ -156,24 +159,25 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
156
159
  `To check status, read: ${agentTaskDir}/status.txt\n`
157
160
  : "";
158
161
 
159
- // Give the agent 5 seconds to finish before detaching
160
- const detachTime = 5 * 1000;
161
- const tid = setTimeout(() => {
162
- try { child.unref(); } catch {}
163
- done(
164
- `Agent started (pid=${pid}), running in background.\n` +
165
- `Logs: ${logPath}\n` +
166
- syncFsNote
167
- );
168
- }, detachTime);
169
-
170
162
  child.once("exit", (code) => {
171
- clearTimeout(tid);
172
163
  done(
173
164
  `Agent finished with exit code ${code}.\nLogs: ${logPath}\n` +
174
165
  syncFsNote
175
166
  );
176
167
  });
168
+
169
+ if (!waitForCompletion) {
170
+ // Give the agent 5 seconds to finish before detaching (fire-and-forget mode)
171
+ const detachTime = 5 * 1000;
172
+ setTimeout(() => {
173
+ try { child.unref(); } catch {}
174
+ done(
175
+ `Agent started (pid=${pid}), running in background.\n` +
176
+ `Logs: ${logPath}\n` +
177
+ syncFsNote
178
+ );
179
+ }, detachTime);
180
+ }
177
181
  });
178
182
  }
179
183
 
package/src/ai.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import * as fs from "fs";
2
2
  import { readFile } from "./utils";
3
3
 
4
+ /** Options forwarded to startAgentTask when a generation source has an `agent` field */
5
+ export type AgentOptions = {
6
+ syncFs?: boolean;
7
+ taskId?: string;
8
+ maxTimeLimit?: number;
9
+ maxSpendLimit?: number;
10
+ };
4
11
  import OpenAI from "openai";
5
12
  import { Assistant } from "./types";
6
13
  import { convertToText } from "./conversion";
@@ -46,17 +53,24 @@ export function readPromptFile(promptFile: string, input: string) {
46
53
  return input;
47
54
  }
48
55
 
49
- export async function singlePrompt(userPrompt: string, model = "", agent = "") {
50
- const { Agents } = services();
56
+ export async function singlePrompt(
57
+ userPrompt: string,
58
+ model = "",
59
+ agent = "",
60
+ agentOptions?: AgentOptions
61
+ ) {
51
62
  if (agent) {
52
- const agentConfig = await Agents.getAgent(agent);
53
- if (!agentConfig) {
54
- throw new Error(`Agent ${agent} not found`);
55
- }
56
- if (model) {
57
- agentConfig.setModel(model);
58
- }
59
- return agentConfig.call(userPrompt);
63
+ // Route through the full AgentModule pipeline (renderer, sync-fs, limits).
64
+ // This ensures generate agent runs produce readable output and are observable
65
+ // via `knowhow agents list/tail/status`, matching `knowhow agent --input` behaviour.
66
+ const { startAgentTask } = await import("./agents/tools/startAgentTask");
67
+ return startAgentTask({
68
+ agentName: agent,
69
+ prompt: userPrompt,
70
+ model: model || undefined,
71
+ waitForCompletion: true,
72
+ ...(agentOptions ?? {}),
73
+ });
60
74
  }
61
75
 
62
76
  if (!model) {
@@ -100,6 +114,11 @@ function isContextLengthError(err: any): boolean {
100
114
  * Recursively summarize an array of texts using a split-and-summarize approach.
101
115
  * When the combined texts exceed the context window (either by estimate or actual API error),
102
116
  * split the array in half, summarize each half recursively, then combine.
117
+ *
118
+ * NOTE: when an `agent` is provided the recursive split path is skipped — agents
119
+ * can handle large inputs themselves (they can read files). Splitting would create
120
+ * multiple isolated agent runs each writing partial output. Instead the combined
121
+ * content is passed directly to the agent.
103
122
  */
104
123
  async function summarizeTextsRecursive(
105
124
  texts: string[],
@@ -107,7 +126,8 @@ async function summarizeTextsRecursive(
107
126
  model: string,
108
127
  agent: string,
109
128
  contextLimit: number,
110
- depth = 0
129
+ depth = 0,
130
+ agentOptions?: AgentOptions
111
131
  ): Promise<string> {
112
132
  const indent = " ".repeat(depth);
113
133
 
@@ -115,7 +135,7 @@ async function summarizeTextsRecursive(
115
135
  if (texts.length === 1) {
116
136
  const content = template.replaceAll("{text}", texts[0]);
117
137
  console.log(`${indent}summarizeTexts[depth=${depth}]: single text, ~${estimateTokens(content)} tokens`);
118
- return singlePrompt(content, model, agent);
138
+ return singlePrompt(content, model, agent, agentOptions);
119
139
  }
120
140
 
121
141
  // Check if combined fits in context window by estimate
@@ -127,7 +147,7 @@ async function summarizeTextsRecursive(
127
147
  // Try single combined prompt — if context error, fall through to split
128
148
  console.log(`${indent}summarizeTexts[depth=${depth}]: ${texts.length} texts, ~${estimatedTokens} tokens, trying combined`);
129
149
  try {
130
- return await singlePrompt(combinedContent, model, agent);
150
+ return await singlePrompt(combinedContent, model, agent, agentOptions);
131
151
  } catch (err: any) {
132
152
  if (!isContextLengthError(err)) throw err;
133
153
  console.log(`${indent}summarizeTexts[depth=${depth}]: API rejected (context too long), splitting in half`);
@@ -136,14 +156,22 @@ async function summarizeTextsRecursive(
136
156
  console.log(`${indent}summarizeTexts[depth=${depth}]: ${texts.length} texts, ~${estimatedTokens} tokens exceeds limit, splitting in half`);
137
157
  }
138
158
 
159
+ // When an agent is assigned, don't split — agents can handle large inputs and
160
+ // splitting would create multiple isolated agent runs that each write partial output.
161
+ // Instead, pass the combined content as-is even if it may exceed context limit.
162
+ if (agent) {
163
+ console.log(`${indent}summarizeTexts[depth=${depth}]: agent mode — skipping split, passing combined content to agent`);
164
+ return singlePrompt(combinedContent, model, agent, agentOptions);
165
+ }
166
+
139
167
  // Split texts in half and recurse
140
168
  const mid = Math.ceil(texts.length / 2);
141
169
  const left = texts.slice(0, mid);
142
170
  const right = texts.slice(mid);
143
171
 
144
172
  const [leftSummary, rightSummary] = await Promise.all([
145
- summarizeTextsRecursive(left, template, model, agent, contextLimit, depth + 1),
146
- summarizeTextsRecursive(right, template, model, agent, contextLimit, depth + 1),
173
+ summarizeTextsRecursive(left, template, model, agent, contextLimit, depth + 1, agentOptions),
174
+ summarizeTextsRecursive(right, template, model, agent, contextLimit, depth + 1, agentOptions),
147
175
  ]);
148
176
 
149
177
  // Combine the two halves with a final summary prompt
@@ -153,27 +181,28 @@ async function summarizeTextsRecursive(
153
181
  console.log(`${indent}summarizeTexts[depth=${depth}]: combining halves, ~${finalEstimate} tokens`);
154
182
 
155
183
  if (finalEstimate < contextLimit) {
156
- return singlePrompt(finalContent, model, agent);
184
+ return singlePrompt(finalContent, model, agent, agentOptions);
157
185
  }
158
186
 
159
187
  // If even the combined summaries are too long, recurse one more level
160
- return summarizeTextsRecursive([leftSummary, rightSummary], template, model, agent, contextLimit, depth + 1);
188
+ return summarizeTextsRecursive([leftSummary, rightSummary], template, model, agent, contextLimit, depth + 1, agentOptions);
161
189
  }
162
190
 
163
191
  export async function summarizeTexts(
164
192
  texts: string[],
165
193
  template: string,
166
194
  model = "",
167
- agent = ""
195
+ agent = "",
196
+ agentOptions?: AgentOptions
168
197
  ) {
169
198
  const effectiveModel = model || Models.openai.GPT_54_Nano;
170
199
  const contextLimit = getModelContextLimit(effectiveModel);
171
200
 
172
201
  console.log(
173
- `summarizeTexts: ${texts.length} text(s), context limit: ${contextLimit}, model: ${effectiveModel}`
202
+ `summarizeTexts: ${texts.length} text(s), context limit: ${contextLimit}, model: ${effectiveModel}${agent ? `, agent: ${agent}` : ""}`
174
203
  );
175
204
 
176
- return summarizeTextsRecursive(texts, template, model, agent, contextLimit).catch((err) => {
205
+ return summarizeTextsRecursive(texts, template, model, agent, contextLimit, 0, agentOptions).catch((err) => {
177
206
  return `Texts of combined length ${texts.reduce((a, t) => a + t.length, 0)} could not be summarized due to error: ${err.message}`;
178
207
  });
179
208
  }
@@ -193,23 +222,25 @@ export async function summarizeFiles(
193
222
  files: string[],
194
223
  template: string,
195
224
  model = "",
196
- agent = ""
225
+ agent = "",
226
+ agentOptions?: AgentOptions
197
227
  ) {
198
228
  const texts = [];
199
229
  for (const file of files) {
200
230
  const text = `file: ${file}\n` + (await convertToText(file));
201
231
  texts.push(text);
202
232
  }
203
- return summarizeTexts(texts, template, model, agent);
233
+ return summarizeTexts(texts, template, model, agent, agentOptions);
204
234
  }
205
235
 
206
236
  export async function summarizeFile(
207
237
  file: string,
208
238
  template: string,
209
239
  model = "",
210
- agent = ""
240
+ agent = "",
241
+ agentOptions?: AgentOptions
211
242
  ) {
212
- return await summarizeFiles([file], template, model, agent);
243
+ return await summarizeFiles([file], template, model, agent, agentOptions);
213
244
  }
214
245
 
215
246
  /*
@@ -336,12 +336,18 @@ export class CliChatService implements ChatService {
336
336
  return fullPrompt;
337
337
  }
338
338
 
339
- async startChatLoop(): Promise<void> {
339
+ async startChatLoop(initialCommand?: string): Promise<void> {
340
340
  // Display available commands like the original
341
341
  const availableCommands = this.getCommandsForActiveModes();
342
342
  const commandNames = availableCommands.map((cmd) => `/${cmd.name}`);
343
343
  console.log("Commands:", commandNames.join(", "));
344
344
 
345
+ // If an initial command was provided (e.g. from CLI), process it before
346
+ // entering the interactive loop so the user lands in the right state.
347
+ if (initialCommand) {
348
+ await this.processInput(initialCommand.trim());
349
+ }
350
+
345
351
  while (true) {
346
352
  // Recompute available commands each iteration so mode changes are reflected in autocomplete
347
353
  const currentCommandNames = this.getCommandsForActiveModes().map(
@@ -733,22 +733,24 @@ export class AgentModule extends BaseChatModule {
733
733
  run?: boolean; // whether to run immediately
734
734
  taskId?: string; // optional pre-generated taskId
735
735
  }) {
736
- if (!agentConstructors[options.agentName as AgentName]) {
737
- throw new Error(
738
- `Agent "${
739
- options.agentName
740
- }" not found. Available agents: ${Object.keys(agentConstructors).join(
741
- ", "
742
- )}`
743
- );
744
- }
745
-
746
736
  const { input, chatHistory = [], agentName } = options;
747
737
  const agentContext = services().Agents.getAgentContext();
748
- const agent = createAgent(
749
- options.agentName as AgentName,
750
- agentContext
751
- ) as BaseAgent;
738
+
739
+ // Resolve agent: built-in agents first, then fall back to ConfigAgents from knowhow.json
740
+ let agent: BaseAgent;
741
+ if (agentConstructors[options.agentName as AgentName]) {
742
+ agent = createAgent(options.agentName as AgentName, agentContext) as BaseAgent;
743
+ } else {
744
+ // Try to find a ConfigAgent registered via loadAgentsFromConfig (from knowhow.json agents[])
745
+ try {
746
+ const configAgent = services().Agents.getAgent(options.agentName);
747
+ agent = configAgent as unknown as BaseAgent;
748
+ } catch {
749
+ throw new Error(
750
+ `Agent "${options.agentName}" not found. Available agents: ${Object.keys(agentConstructors).join(", ")} (plus any agents defined in knowhow.json)`
751
+ );
752
+ }
753
+ }
752
754
 
753
755
  let done = false;
754
756
  let output = "Done";
@@ -915,7 +917,63 @@ export class AgentModule extends BaseChatModule {
915
917
  agent.agentEvents.on(agent.eventTypes.agentLog, agentLogHandler);
916
918
  agent.agentEvents.on(agent.eventTypes.agentStatus, agentStatusHandler);
917
919
 
920
+ // Wire tool call / tool result / agent message rendering so that all
921
+ // renderer types (including PlainRenderer for non-TTY) receive events
922
+ // even when running via the CLI `knowhow agent` command (which uses
923
+ // run:true and does NOT go through attachedAgentChatLoop / wireAgentRendering).
924
+ const toolCallHandler = (data: any) => {
925
+ this.renderer.render({
926
+ type: "toolCall",
927
+ taskId,
928
+ agentName: agent.name,
929
+ toolCall: data.toolCall,
930
+ });
931
+ };
932
+ const toolResultHandler = (data: any) => {
933
+ this.renderer.render({
934
+ type: "toolResult",
935
+ taskId,
936
+ agentName: agent.name,
937
+ toolCall: data.toolCall,
938
+ result: data.functionResp,
939
+ });
940
+ };
941
+ const agentSayHandler = (data: any) => {
942
+ this.renderer.render({
943
+ type: "agentMessage",
944
+ taskId,
945
+ agentName: agent.name,
946
+ message: data.message,
947
+ role: "assistant",
948
+ });
949
+ };
950
+ if (agent.eventTypes.toolCall) {
951
+ agent.agentEvents.on(agent.eventTypes.toolCall, toolCallHandler);
952
+ }
953
+ if (agent.eventTypes.toolUsed) {
954
+ agent.agentEvents.on(agent.eventTypes.toolUsed, toolResultHandler);
955
+ }
956
+ if (agent.eventTypes.agentSay) {
957
+ agent.agentEvents.on(agent.eventTypes.agentSay, agentSayHandler);
958
+ }
959
+ // Track the active task on the renderer so it doesn't filter out events
960
+ this.renderer.setActiveTaskId(taskId);
961
+ this.activeAgentTaskId = taskId;
962
+
918
963
  const taskCompleted = new Promise<string>((resolve) => {
964
+ // Remove tool rendering listeners
965
+ if (agent.eventTypes.toolCall) {
966
+ agent.agentEvents.removeListener(agent.eventTypes.toolCall, toolCallHandler);
967
+ }
968
+ if (agent.eventTypes.toolUsed) {
969
+ agent.agentEvents.removeListener(agent.eventTypes.toolUsed, toolResultHandler);
970
+ }
971
+ if (agent.eventTypes.agentSay) {
972
+ agent.agentEvents.removeListener(agent.eventTypes.agentSay, agentSayHandler);
973
+ }
974
+ // Clear active task on renderer
975
+ this.renderer.setActiveTaskId(undefined);
976
+ this.activeAgentTaskId = undefined;
919
977
  agent.agentEvents.once(agent.eventTypes.done, async (doneMsg) => {
920
978
  console.log("🎯 [AgentModule] Task Completed");
921
979
  done = true;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * PlainRenderer - A plain-text, append-only renderer for non-TTY environments.
3
+ *
4
+ * Used automatically when stdout is not a TTY (e.g. redirected to a file,
5
+ * piped, or run in background). Emits simple timestamped log lines with no
6
+ * ANSI codes, no cursor movement, and no spinner — safe for logfiles.
7
+ *
8
+ * Activate explicitly via:
9
+ * knowhow agent --renderer plain ...
10
+ * or it is selected automatically when !process.stdout.isTTY and no renderer
11
+ * is configured.
12
+ */
13
+
14
+ import {
15
+ AgentRenderer,
16
+ LogEvent,
17
+ AgentStatusEvent,
18
+ ToolCallEvent,
19
+ ToolResultEvent,
20
+ AgentMessageEvent,
21
+ AgentDoneEvent,
22
+ RenderEvent,
23
+ } from "./types";
24
+ import { EventEmitter } from "events";
25
+
26
+ function ts(): string {
27
+ return new Date().toISOString();
28
+ }
29
+
30
+ function truncate(s: string, n: number): string {
31
+ return s.length > n ? s.slice(0, n - 1) + "…" : s;
32
+ }
33
+
34
+ function formatArgs(raw: string): string {
35
+ try {
36
+ const obj = JSON.parse(raw || "{}");
37
+ const entries = Object.entries(obj);
38
+ if (!entries.length) return "{}";
39
+ return entries
40
+ .map(([k, v]) => {
41
+ const val = typeof v === "string" ? v : JSON.stringify(v);
42
+ return `${k}=${truncate(val, 80)}`;
43
+ })
44
+ .join(", ");
45
+ } catch {
46
+ return truncate(raw || "", 120);
47
+ }
48
+ }
49
+
50
+ function formatResult(result: any): string {
51
+ try {
52
+ const s = typeof result === "string" ? result : JSON.stringify(result);
53
+ return truncate(s.replace(/\s+/g, " "), 200);
54
+ } catch {
55
+ return truncate(String(result), 200);
56
+ }
57
+ }
58
+
59
+ export class PlainRenderer implements AgentRenderer {
60
+ private activeTaskId: string | undefined;
61
+ private emitter = new EventEmitter();
62
+ private paused = false;
63
+ private bufferedEvents: RenderEvent[] = [];
64
+
65
+ setActiveTaskId(taskId: string | undefined): void {
66
+ this.activeTaskId = taskId;
67
+ }
68
+
69
+ getActiveTaskId(): string | undefined {
70
+ return this.activeTaskId;
71
+ }
72
+
73
+ private isActiveTask(taskId: string): boolean {
74
+ if (!this.activeTaskId) return true;
75
+ return taskId === this.activeTaskId;
76
+ }
77
+
78
+ onLog(handler: (event: LogEvent) => void): void {
79
+ this.emitter.on("log", handler);
80
+ }
81
+
82
+ onAgentStatus(handler: (event: AgentStatusEvent) => void): void {
83
+ this.emitter.on("agentStatus", handler);
84
+ }
85
+
86
+ onToolCall(handler: (event: ToolCallEvent) => void): void {
87
+ this.emitter.on("toolCall", handler);
88
+ }
89
+
90
+ onToolResult(handler: (event: ToolResultEvent) => void): void {
91
+ this.emitter.on("toolResult", handler);
92
+ }
93
+
94
+ onAgentMessage(handler: (event: AgentMessageEvent) => void): void {
95
+ this.emitter.on("agentMessage", handler);
96
+ }
97
+
98
+ onAgentDone(handler: (event: AgentDoneEvent) => void): void {
99
+ this.emitter.on("agentDone", handler);
100
+ }
101
+
102
+ pause(): void {
103
+ this.paused = true;
104
+ }
105
+
106
+ resume(): void {
107
+ this.paused = false;
108
+ const buffered = this.bufferedEvents.splice(0);
109
+ for (const event of buffered) {
110
+ this.render(event);
111
+ }
112
+ }
113
+
114
+ render(event: RenderEvent): void {
115
+ if (!this.isActiveTask(event.taskId)) return;
116
+
117
+ if (this.paused) {
118
+ this.bufferedEvents.push(event);
119
+ return;
120
+ }
121
+
122
+ this.emitter.emit(event.type, event);
123
+
124
+ switch (event.type) {
125
+ case "log":
126
+ this._log(event);
127
+ break;
128
+ case "agentStatus":
129
+ this._status(event);
130
+ break;
131
+ case "toolCall":
132
+ this._toolCall(event);
133
+ break;
134
+ case "toolResult":
135
+ this._toolResult(event);
136
+ break;
137
+ case "agentMessage":
138
+ this._message(event);
139
+ break;
140
+ case "agentDone":
141
+ this._done(event);
142
+ break;
143
+ }
144
+ }
145
+
146
+ private _write(line: string): void {
147
+ process.stdout.write(line + "\n");
148
+ }
149
+
150
+ private _log(e: LogEvent): void {
151
+ const level = e.level.toUpperCase().padEnd(5);
152
+ this._write(`${ts()} [${level}] [${e.agentName}] ${e.message}`);
153
+ }
154
+
155
+ private _status(e: AgentStatusEvent): void {
156
+ const cost = `$${e.details.totalCostUsd.toFixed(3)}`;
157
+ const elapsed = `${Math.floor(e.details.elapsedMs / 1000)}s`;
158
+ const parts = [`cost=${cost}`, `elapsed=${elapsed}`];
159
+ if (e.details.remainingTurns !== undefined) {
160
+ parts.push(`turns_left=${e.details.remainingTurns}`);
161
+ }
162
+ if (e.details.remainingTimeMs !== undefined) {
163
+ parts.push(`time_left=${Math.floor(e.details.remainingTimeMs / 1000)}s`);
164
+ }
165
+ this._write(
166
+ `${ts()} [STATUS] [${e.agentName}] ${e.statusMessage} | ${parts.join(" | ")}`
167
+ );
168
+ }
169
+
170
+ private _toolCall(e: ToolCallEvent): void {
171
+ const args = formatArgs(e.toolCall.function.arguments);
172
+ this._write(
173
+ `${ts()} [TOOL->] [${e.agentName}] ${e.toolCall.function.name}(${args})`
174
+ );
175
+ }
176
+
177
+ private _toolResult(e: ToolResultEvent): void {
178
+ const result = formatResult(e.result);
179
+ this._write(
180
+ `${ts()} [TOOL<-] [${e.agentName}] ${e.toolCall.function.name} => ${result}`
181
+ );
182
+ }
183
+
184
+ private _message(e: AgentMessageEvent): void {
185
+ if (e.role !== "assistant") return;
186
+ // Print the message with line-by-line prefix
187
+ const lines = e.message.split("\n");
188
+ for (const line of lines) {
189
+ if (line.trim()) {
190
+ this._write(`${ts()} [MSG ] [${e.agentName}] ${line}`);
191
+ }
192
+ }
193
+ }
194
+
195
+ private _done(e: AgentDoneEvent): void {
196
+ const cost = e.totalCost ? ` | total_cost=$${e.totalCost.toFixed(4)}` : "";
197
+ this._write(`${ts()} [DONE ] [${e.agentName}] Agent finished${cost}`);
198
+ // Print final output lines
199
+ const lines = e.output.split("\n");
200
+ for (const line of lines) {
201
+ if (line.trim()) {
202
+ this._write(`${ts()} [OUT ] [${e.agentName}] ${line}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ logMessages(events: RenderEvent[], count: number = 20): void {
208
+ const recent = events.slice(-count);
209
+ this._write(`\n[LOGS ] Last ${recent.length} events:`);
210
+ this._write("-".repeat(80));
211
+ for (const event of recent) {
212
+ this.render(event);
213
+ }
214
+ this._write("-".repeat(80));
215
+ }
216
+ }
217
+
218
+ export default PlainRenderer;
@@ -1,5 +1,6 @@
1
1
  export * from "./types";
2
2
  export { ConsoleRenderer } from "./ConsoleRenderer";
3
3
  export { FancyRenderer } from "./FancyRenderer";
4
+ export { PlainRenderer } from "./PlainRenderer";
4
5
  export { CompactRenderer } from "./CompactRenderer";
5
6
  export { loadRenderer, loadRootModule, loadChatModule } from "./loadRenderer";
@@ -12,6 +12,7 @@ import path from "path";
12
12
  import fs from "fs";
13
13
  import { AgentRenderer } from "./types";
14
14
  import { ConsoleRenderer } from "./ConsoleRenderer";
15
+ import { PlainRenderer } from "./PlainRenderer";
15
16
  import { FancyRenderer } from "./FancyRenderer";
16
17
  import { CompactRenderer } from "./CompactRenderer";
17
18
 
@@ -19,6 +20,7 @@ import { CompactRenderer } from "./CompactRenderer";
19
20
  const BUILTIN_RENDERERS: Record<string, () => AgentRenderer> = {
20
21
  basic: () => new ConsoleRenderer(),
21
22
  fancy: () => new FancyRenderer(),
23
+ plain: () => new PlainRenderer(),
22
24
  compact: () => new CompactRenderer(),
23
25
  };
24
26
 
package/src/chat.ts CHANGED
@@ -21,7 +21,7 @@ import { getConfig } from "./config.js";
21
21
  import { loadRenderer, loadRootModule, loadChatModule } from "./chat/renderer/loadRenderer.js";
22
22
  import { AgentModule } from "./chat/modules/AgentModule.js";
23
23
 
24
- async function main() {
24
+ async function main(initialCommand?: string) {
25
25
  try {
26
26
  // Load configuration and plugins
27
27
  let config;
@@ -108,7 +108,7 @@ async function main() {
108
108
  }
109
109
 
110
110
  // Start the chat loop
111
- await chatService.startChatLoop();
111
+ await chatService.startChatLoop(initialCommand);
112
112
  } catch (error) {
113
113
  console.error("Error starting chat:", error);
114
114
  process.exit(1);
package/src/cli.ts CHANGED
@@ -1,4 +1,23 @@
1
1
  #!/usr/bin/env node --no-node-snapshot
2
+ // Mark this process as running the Knowhow CLI (not just the SDK)
3
+ process.env.KNOWHOW_CLI = "1";
4
+
5
+ // Determine which warnings to show based on the command being run.
6
+ // Commands that don't need AI models or a config file should not spam warnings.
7
+ // Individual commands can override by setting KNOWHOW_WARN_NEEDS before parsing.
8
+ // Format: comma-separated list of "models" | "config" (or empty string to suppress all).
9
+ const _rawArgs = process.argv.slice(2);
10
+ const _firstArg = _rawArgs.find((a) => !a.startsWith("-"));
11
+ const COMMANDS_NEEDING_MODELS = new Set(["chat", "agent", "ask", "generate", "embed", "replay"]);
12
+ const COMMANDS_NEEDING_CONFIG = new Set([
13
+ "chat", "agent", "ask", "generate", "embed", "upload", "download",
14
+ "worker", "workers", "replay", "behaviors", "skills",
15
+ ]);
16
+ const _needsModels = _firstArg && COMMANDS_NEEDING_MODELS.has(_firstArg) ? "models" : "";
17
+ const _needsConfig = _firstArg && COMMANDS_NEEDING_CONFIG.has(_firstArg) ? "config" : "";
18
+ const _needs = [_needsModels, _needsConfig].filter(Boolean).join(",");
19
+ process.env.KNOWHOW_WARN_NEEDS = process.env.KNOWHOW_WARN_NEEDS ?? _needs;
20
+
2
21
  import { Command } from "commander";
3
22
  import { version } from "../package.json";
4
23
  import { logger } from "./logger";
@@ -40,6 +59,8 @@ import { addConvertCommand } from "./commands/convert";
40
59
  import { addReplayCommand } from "./commands/replay";
41
60
  import { addBehaviorsCommand } from "./commands/behaviors";
42
61
  import { addSkillsCommand } from "./commands/skills";
62
+ import { addAgentsCommand } from "./commands/agents";
63
+ import { addHashCommand } from "./commands/hash";
43
64
 
44
65
  // Handle unhandled promise rejections gracefully — particularly from MCP SDK
45
66
  // which fires errors via event emitters that can bypass Promise.allSettled.
@@ -106,6 +127,8 @@ async function main() {
106
127
  addReplayCommand(program);
107
128
  addBehaviorsCommand(program);
108
129
  addSkillsCommand(program);
130
+ addAgentsCommand(program);
131
+ addHashCommand(program);
109
132
 
110
133
  // Load global modules early (before parse) so they can register CLI subcommands.
111
134
  // We pass only the Program in context — no services are spun up at this stage.