@tyvm/knowhow 0.0.147 → 0.0.149

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 (156) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +45 -32
  3. package/src/agents/tools/execCommand.ts +102 -320
  4. package/src/agents/tools/list.ts +2 -2
  5. package/src/agents/tools/startAgentTask.ts +55 -27
  6. package/src/ai.ts +4 -1
  7. package/src/auth/keyAuth.ts +10 -4
  8. package/src/auth/registerIdentity.ts +67 -0
  9. package/src/chat/CliChatService.ts +23 -2
  10. package/src/chat/modules/AgentModule.ts +39 -7
  11. package/src/chat/modules/InternalChatModule.ts +5 -0
  12. package/src/chat/modules/ToolCommandModule.ts +102 -0
  13. package/src/chat/modules/index.ts +1 -0
  14. package/src/chat/types.ts +1 -1
  15. package/src/cli.ts +9 -2
  16. package/src/clients/http.ts +67 -7
  17. package/src/clients/index.ts +36 -1
  18. package/src/commands/agent.ts +1 -0
  19. package/src/commands/agents.ts +57 -9
  20. package/src/commands/misc.ts +17 -2
  21. package/src/commands/processes.ts +203 -0
  22. package/src/commands/services.ts +23 -1
  23. package/src/index.ts +1 -0
  24. package/src/processes/ProcessManager.ts +95 -0
  25. package/src/processes/managedProcessShim.ts +127 -0
  26. package/src/processors/Base64ImageDetector.ts +28 -4
  27. package/src/processors/JsonCompressor.ts +14 -0
  28. package/src/processors/TokenCompressor.ts +17 -14
  29. package/src/services/AgentService.ts +2 -2
  30. package/src/services/AgentSyncFs.ts +12 -1
  31. package/src/services/ExtensionsService.ts +25 -0
  32. package/src/services/LazyToolsService.ts +15 -20
  33. package/src/services/Tools.ts +10 -0
  34. package/src/services/index.ts +4 -0
  35. package/src/services/modules/computerUse.ts +3 -1
  36. package/src/services/modules/index.ts +6 -0
  37. package/src/services/modules/types.ts +4 -0
  38. package/tests/clients/AIClient.test.ts +98 -15
  39. package/tests/processes/managedProcessShim.test.ts +59 -0
  40. package/tests/processors/Base64ImageDetector.test.ts +21 -0
  41. package/tests/processors/TokenCompressor.test.ts +18 -0
  42. package/tests/unit/auth/registerIdentity.test.ts +93 -0
  43. package/tests/unit/chat/ToolCommandModule.test.ts +71 -0
  44. package/tests/unit/commands/agents.test.ts +44 -1
  45. package/tests/unit/services/LazyToolsService.test.ts +24 -0
  46. package/ts_build/package.json +1 -1
  47. package/ts_build/src/agents/base/base.d.ts +1 -1
  48. package/ts_build/src/agents/base/base.js +30 -21
  49. package/ts_build/src/agents/base/base.js.map +1 -1
  50. package/ts_build/src/agents/tools/execCommand.d.ts +4 -1
  51. package/ts_build/src/agents/tools/execCommand.js +100 -223
  52. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  53. package/ts_build/src/agents/tools/list.js +2 -2
  54. package/ts_build/src/agents/tools/list.js.map +1 -1
  55. package/ts_build/src/agents/tools/startAgentTask.d.ts +14 -1
  56. package/ts_build/src/agents/tools/startAgentTask.js +37 -21
  57. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  58. package/ts_build/src/ai.js +2 -1
  59. package/ts_build/src/ai.js.map +1 -1
  60. package/ts_build/src/auth/keyAuth.js +10 -3
  61. package/ts_build/src/auth/keyAuth.js.map +1 -1
  62. package/ts_build/src/auth/registerIdentity.d.ts +7 -0
  63. package/ts_build/src/auth/registerIdentity.js +62 -0
  64. package/ts_build/src/auth/registerIdentity.js.map +1 -0
  65. package/ts_build/src/chat/CliChatService.d.ts +6 -0
  66. package/ts_build/src/chat/CliChatService.js +17 -2
  67. package/ts_build/src/chat/CliChatService.js.map +1 -1
  68. package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
  69. package/ts_build/src/chat/modules/AgentModule.js +27 -7
  70. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  71. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  72. package/ts_build/src/chat/modules/InternalChatModule.js +5 -0
  73. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  74. package/ts_build/src/chat/modules/ToolCommandModule.d.ts +10 -0
  75. package/ts_build/src/chat/modules/ToolCommandModule.js +86 -0
  76. package/ts_build/src/chat/modules/ToolCommandModule.js.map +1 -0
  77. package/ts_build/src/chat/modules/index.d.ts +1 -0
  78. package/ts_build/src/chat/modules/index.js +3 -1
  79. package/ts_build/src/chat/modules/index.js.map +1 -1
  80. package/ts_build/src/chat/types.d.ts +1 -1
  81. package/ts_build/src/cli.js +6 -2
  82. package/ts_build/src/cli.js.map +1 -1
  83. package/ts_build/src/clients/http.d.ts +4 -0
  84. package/ts_build/src/clients/http.js +68 -6
  85. package/ts_build/src/clients/http.js.map +1 -1
  86. package/ts_build/src/clients/index.js +23 -1
  87. package/ts_build/src/clients/index.js.map +1 -1
  88. package/ts_build/src/commands/agent.js +1 -0
  89. package/ts_build/src/commands/agent.js.map +1 -1
  90. package/ts_build/src/commands/agents.d.ts +1 -0
  91. package/ts_build/src/commands/agents.js +43 -8
  92. package/ts_build/src/commands/agents.js.map +1 -1
  93. package/ts_build/src/commands/misc.js +12 -1
  94. package/ts_build/src/commands/misc.js.map +1 -1
  95. package/ts_build/src/commands/processes.d.ts +2 -0
  96. package/ts_build/src/commands/processes.js +282 -0
  97. package/ts_build/src/commands/processes.js.map +1 -0
  98. package/ts_build/src/commands/services.js +11 -1
  99. package/ts_build/src/commands/services.js.map +1 -1
  100. package/ts_build/src/index.d.ts +1 -0
  101. package/ts_build/src/index.js +1 -0
  102. package/ts_build/src/index.js.map +1 -1
  103. package/ts_build/src/processes/ProcessManager.d.ts +51 -0
  104. package/ts_build/src/processes/ProcessManager.js +160 -0
  105. package/ts_build/src/processes/ProcessManager.js.map +1 -0
  106. package/ts_build/src/processes/managedProcessShim.d.ts +1 -0
  107. package/ts_build/src/processes/managedProcessShim.js +167 -0
  108. package/ts_build/src/processes/managedProcessShim.js.map +1 -0
  109. package/ts_build/src/processors/Base64ImageDetector.d.ts +1 -0
  110. package/ts_build/src/processors/Base64ImageDetector.js +19 -2
  111. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  112. package/ts_build/src/processors/JsonCompressor.js +7 -0
  113. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  114. package/ts_build/src/processors/TokenCompressor.d.ts +1 -1
  115. package/ts_build/src/processors/TokenCompressor.js +9 -8
  116. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  117. package/ts_build/src/services/AgentService.d.ts +1 -1
  118. package/ts_build/src/services/AgentService.js.map +1 -1
  119. package/ts_build/src/services/AgentSyncFs.js +6 -1
  120. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  121. package/ts_build/src/services/ExtensionsService.d.ts +10 -0
  122. package/ts_build/src/services/ExtensionsService.js +17 -0
  123. package/ts_build/src/services/ExtensionsService.js.map +1 -0
  124. package/ts_build/src/services/LazyToolsService.d.ts +1 -0
  125. package/ts_build/src/services/LazyToolsService.js +5 -7
  126. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  127. package/ts_build/src/services/Tools.d.ts +1 -0
  128. package/ts_build/src/services/Tools.js +3 -0
  129. package/ts_build/src/services/Tools.js.map +1 -1
  130. package/ts_build/src/services/index.d.ts +3 -0
  131. package/ts_build/src/services/index.js +3 -0
  132. package/ts_build/src/services/index.js.map +1 -1
  133. package/ts_build/src/services/modules/computerUse.d.ts +3 -1
  134. package/ts_build/src/services/modules/index.d.ts +2 -0
  135. package/ts_build/src/services/modules/index.js +5 -0
  136. package/ts_build/src/services/modules/index.js.map +1 -1
  137. package/ts_build/src/services/modules/types.d.ts +3 -0
  138. package/ts_build/tests/clients/AIClient.test.js +73 -11
  139. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  140. package/ts_build/tests/processes/managedProcessShim.test.d.ts +1 -0
  141. package/ts_build/tests/processes/managedProcessShim.test.js +92 -0
  142. package/ts_build/tests/processes/managedProcessShim.test.js.map +1 -0
  143. package/ts_build/tests/processors/Base64ImageDetector.test.js +18 -0
  144. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  145. package/ts_build/tests/processors/TokenCompressor.test.js +15 -0
  146. package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
  147. package/ts_build/tests/unit/auth/registerIdentity.test.d.ts +1 -0
  148. package/ts_build/tests/unit/auth/registerIdentity.test.js +86 -0
  149. package/ts_build/tests/unit/auth/registerIdentity.test.js.map +1 -0
  150. package/ts_build/tests/unit/chat/ToolCommandModule.test.d.ts +1 -0
  151. package/ts_build/tests/unit/chat/ToolCommandModule.test.js +61 -0
  152. package/ts_build/tests/unit/chat/ToolCommandModule.test.js.map +1 -0
  153. package/ts_build/tests/unit/commands/agents.test.js +28 -0
  154. package/ts_build/tests/unit/commands/agents.test.js.map +1 -1
  155. package/ts_build/tests/unit/services/LazyToolsService.test.js +20 -0
  156. package/ts_build/tests/unit/services/LazyToolsService.test.js.map +1 -1
@@ -1,4 +1,4 @@
1
- import http from "../utils/http";
1
+ import http, { HTTP_UNAUTHORIZED_HANDLER, RefreshableHeaders } from "../utils/http";
2
2
  import {
3
3
  GenericClient,
4
4
  CompletionOptions,
@@ -17,16 +17,72 @@ export interface HttpClientOptions {
17
17
  extra_body?: Record<string, any>;
18
18
  }
19
19
 
20
+ /**
21
+ * Optional callback that, when set, is called whenever a 401 is received
22
+ * (reactive refresh) or whenever `needsRefresh()` returns true before a
23
+ * request (proactive refresh). Override this in a subclass or set it
24
+ * directly on an instance:
25
+ *
26
+ * client.refreshToken = async () => { const newJwt = await ...; client.setJwt(newJwt); };
27
+ */
28
+ export type RefreshTokenFn = () => Promise<void>;
29
+
20
30
  export class HttpClient implements GenericClient {
21
31
  /** Timeout in milliseconds for HTTP requests. Default: 30000 (30s). Use 0 to disable. */
22
32
  private timeout: number;
23
- private headers: Record<string, string>;
33
+ private headers: RefreshableHeaders;
24
34
  private extra_body: Record<string, any>;
25
35
  /** Optional pricing table: model id → per-million-token prices */
26
36
  private pricingMap: Record<string, ModelPricing> = {};
27
37
 
38
+ /**
39
+ * Override this to provide automatic token refresh. Called reactively on
40
+ * 401 responses (via HTTP_UNAUTHORIZED_HANDLER) and proactively before
41
+ * each request when `needsRefresh()` returns true.
42
+ *
43
+ * The implementation should obtain a new token and call `this.setJwt()`
44
+ * (or `this.setKey()`) before returning.
45
+ */
46
+ refreshToken: RefreshTokenFn | undefined = undefined;
47
+
48
+ /**
49
+ * Override this to provide proactive expiry detection. When it returns
50
+ * `true`, `refreshToken()` is called before the next outgoing request.
51
+ * The default implementation decodes the JWT `exp` claim (< 5 min window).
52
+ */
53
+ needsRefresh(): boolean {
54
+ const authHeader = (this.headers as Record<string, string>)["Authorization"];
55
+ if (!authHeader) return false;
56
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
57
+ try {
58
+ const parts = token.split(".");
59
+ if (parts.length !== 3) return false;
60
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
61
+ return typeof payload.exp === "number" && payload.exp * 1000 - Date.now() < 5 * 60 * 1000;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Call before each outgoing request to proactively refresh an expiring
69
+ * token. No-ops when no `refreshToken` hook is set.
70
+ */
71
+ private async refreshIfNeeded(): Promise<void> {
72
+ if (this.refreshToken && this.needsRefresh()) {
73
+ await this.refreshToken();
74
+ }
75
+ }
76
+
28
77
  constructor(private baseUrl: string, options: HttpClientOptions = {}) {
29
78
  this.headers = options.headers ?? {};
79
+ // Wire the reactive 401 handler so the low-level http util can retry once
80
+ // after a token refresh without callers needing to do anything extra.
81
+ Object.defineProperty(this.headers, HTTP_UNAUTHORIZED_HANDLER, {
82
+ enumerable: false,
83
+ configurable: true,
84
+ get: () => this.refreshToken ? () => this.refreshToken!() : undefined,
85
+ });
30
86
  this.timeout = options.timeout ?? 30000;
31
87
  this.extra_body = options.extra_body ?? {};
32
88
  }
@@ -69,10 +125,8 @@ export class HttpClient implements GenericClient {
69
125
  }
70
126
 
71
127
  setJwt(jwt: string) {
72
- this.headers = {
73
- ...this.headers,
74
- Authorization: `Bearer ${jwt}`,
75
- };
128
+ // Mutate in-place to preserve the HTTP_UNAUTHORIZED_HANDLER symbol property.
129
+ this.headers.Authorization = `Bearer ${jwt}`;
76
130
  }
77
131
 
78
132
  setBaseUrl(baseUrl: string) {
@@ -163,7 +217,8 @@ export class HttpClient implements GenericClient {
163
217
  if (options.timeout !== undefined) this.timeout = options.timeout;
164
218
  if (options.extra_body !== undefined) this.extra_body = options.extra_body;
165
219
  if (options.headers) {
166
- this.headers = { ...this.headers, ...options.headers };
220
+ // Mutate in-place to preserve the HTTP_UNAUTHORIZED_HANDLER symbol property.
221
+ Object.assign(this.headers, options.headers);
167
222
  }
168
223
  }
169
224
 
@@ -183,6 +238,7 @@ export class HttpClient implements GenericClient {
183
238
  async createChatCompletion(
184
239
  options: CompletionOptions
185
240
  ): Promise<CompletionResponse> {
241
+ await this.refreshIfNeeded();
186
242
  return this.withRetry(async () => {
187
243
  const body = {
188
244
  ...options,
@@ -232,6 +288,7 @@ export class HttpClient implements GenericClient {
232
288
  async *createChatCompletionStream(
233
289
  options: CompletionOptions
234
290
  ): AsyncGenerator<StreamChunk> {
291
+ await this.refreshIfNeeded();
235
292
  const body = {
236
293
  ...options,
237
294
  model: options.model,
@@ -316,6 +373,7 @@ export class HttpClient implements GenericClient {
316
373
  options: CompletionOptions,
317
374
  store = false
318
375
  ): Promise<CompletionResponse> {
376
+ await this.refreshIfNeeded();
319
377
  return this.withRetry(async () => {
320
378
  // Extract system messages as instructions
321
379
  const systemMessages = options.messages.filter((m) => m.role === "system");
@@ -484,6 +542,7 @@ export class HttpClient implements GenericClient {
484
542
  }
485
543
 
486
544
  async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
545
+ await this.refreshIfNeeded();
487
546
  return this.withRetry(async () => {
488
547
  const response = await http.post(
489
548
  `${this.baseUrl}/v1/embeddings`,
@@ -511,6 +570,7 @@ export class HttpClient implements GenericClient {
511
570
  }
512
571
 
513
572
  async getModels(type = "all") {
573
+ await this.refreshIfNeeded();
514
574
  return this.withRetry(async () => {
515
575
  const response = await http.get(`${this.baseUrl}/v1/models?type=${type}`, {
516
576
  headers: this.headers as Record<string, string>,
@@ -528,12 +528,14 @@ export class AIClient {
528
528
  }
529
529
 
530
530
  providerHasModel(provider: string, model: string): boolean {
531
+ if (!provider) return false;
531
532
  const models = this.clientModels[provider];
532
533
  if (!models) return false;
533
534
  return models.includes(model);
534
535
  }
535
536
 
536
537
  findModel(modelPrefix: string) {
538
+ if (!modelPrefix) return undefined;
537
539
  for (const provider of Object.keys(this.clientModels)) {
538
540
  const models = this.clientModels[provider] as string[];
539
541
  const foundModel = models.find((m) => m.startsWith(modelPrefix));
@@ -542,14 +544,17 @@ export class AIClient {
542
544
  }
543
545
 
544
546
  // Handle the case when model prefix is gpt-5 and the provider is knowhow, and the actual model is openai/gpt-5
547
+ // Also handles deep prefixes like "accounts/fireworks/models/kimi-k3" matched by "kimi-k3"
545
548
  const inferredFound = models.find((m) => {
546
549
  const split = m.split("/");
547
550
  if (split.length < 2) return false;
548
551
  const inferredModel = split.slice(1).join("/");
552
+ const lastSegment = split[split.length - 1];
549
553
  return (
550
554
  m === modelPrefix ||
551
555
  inferredModel === modelPrefix ||
552
- inferredModel.startsWith(modelPrefix)
556
+ inferredModel.startsWith(modelPrefix) ||
557
+ lastSegment === modelPrefix
553
558
  );
554
559
  });
555
560
  if (inferredFound) {
@@ -624,6 +629,36 @@ export class AIClient {
624
629
  return { provider, model };
625
630
  }
626
631
 
632
+ // When an explicit provider is given and the model contains slashes (e.g.
633
+ // provider="knowhow", model="anthropic/claude-sonnet-4-6"), check if the
634
+ // explicit provider has a model that matches by ignoring the leading
635
+ // provider prefix segments. Also handle deep prefixes like
636
+ // "accounts/fireworks/models/kimi-k3" → search for "kimi-k3".
637
+ if (provider && model) {
638
+ const providerModels = this.clientModels[provider] ?? [];
639
+ if (model.includes("/")) {
640
+ // Model has slashes: try progressively stripping leading prefix segments
641
+ const modelParts = model.split("/");
642
+ for (let i = 1; i < modelParts.length; i++) {
643
+ const suffix = modelParts.slice(i).join("/");
644
+ // Exact match within the explicit provider
645
+ if (this.providerHasModel(provider, suffix)) {
646
+ return { provider, model: suffix };
647
+ }
648
+ }
649
+ }
650
+ // For any model (with or without slashes): check if a registered model's
651
+ // last path segment matches the requested model name.
652
+ // e.g. provider="fireworks", model="kimi-k3" matches
653
+ // "accounts/fireworks/models/kimi-k3"
654
+ const lastSegmentMatch = providerModels.find(
655
+ (m) => m.split("/").pop() === model
656
+ );
657
+ if (lastSegmentMatch) {
658
+ return { provider, model: lastSegmentMatch };
659
+ }
660
+ }
661
+
627
662
  // If an explicit provider was given, don't fall through to fuzzy cross-provider
628
663
  // search — that would silently pick a completely different provider (e.g. nvidia
629
664
  // instead of fireworks). Just pass through and let the API accept/reject the model.
@@ -193,6 +193,7 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
193
193
 
194
194
  const { taskCompleted } = await agentModule.setupAgent({
195
195
  ...options,
196
+ agentName: options.agentName ?? config.chat?.defaultAgent ?? "Patcher",
196
197
  input,
197
198
  maxTimeLimit: parseInt(options.maxTimeLimit, 10),
198
199
  maxSpendLimit: parseFloat(options.maxSpendLimit),
@@ -35,6 +35,18 @@ import { getConfig } from "../config";
35
35
  const AGENTS_DIR = path.join(".knowhow", "processes", "agents");
36
36
  const SESSIONS_DIR = path.join(".knowhow", "chats", "sessions");
37
37
 
38
+ const TERMINAL_STATUSES = new Set([
39
+ "completed",
40
+ "failed",
41
+ "killed",
42
+ "error",
43
+ "done",
44
+ ]);
45
+ const ACTIVE_STATUSES = new Set(["running", "paused"]);
46
+ // Older task records did not persist a pid. After this grace period, report
47
+ // them as unverifiable rather than continuing to claim they are running.
48
+ const UNVERIFIED_TASK_GRACE_MS = 5 * 60 * 1000;
49
+
38
50
  // ---------------------------------------------------------------------------
39
51
  // Helpers
40
52
  // ---------------------------------------------------------------------------
@@ -68,11 +80,42 @@ function loadStatus(taskId: string): string {
68
80
  }
69
81
  }
70
82
 
71
- /** Get the canonical status — meta.status is live/preferred; status.txt is fallback */
72
- function getStatus(taskId: string, meta: any | null): string {
73
- if (meta?.status) return meta.status;
74
- const fromFile = loadStatus(taskId);
75
- return fromFile ?? "unknown";
83
+ function pidAlive(pid: number): boolean {
84
+ try {
85
+ process.kill(pid, 0);
86
+ return true;
87
+ } catch (error: any) {
88
+ // EPERM means the process exists but cannot be signalled by this user.
89
+ return error?.code === "EPERM";
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Get the effective status, reconciling persisted state with process liveness.
95
+ * An agent can be terminated before AgentSyncFs gets a chance to finalize its
96
+ * files; those records must not remain "running" forever.
97
+ */
98
+ export function getStatus(taskId: string, meta: any | null): string {
99
+ const persisted = meta?.status || loadStatus(taskId) || "unknown";
100
+ if (TERMINAL_STATUSES.has(persisted)) return persisted;
101
+ if (meta?.inProgress === false) return "completed";
102
+ if (!ACTIVE_STATUSES.has(persisted)) return persisted;
103
+
104
+ const pid = meta?.pid ?? meta?.processId;
105
+ if (typeof pid === "number") {
106
+ return pidAlive(pid) ? persisted : "failed";
107
+ }
108
+
109
+ const lastKnownTime = meta?.lastUpdate ?? meta?.startTime;
110
+ const lastKnownMs = lastKnownTime ? new Date(lastKnownTime).getTime() : NaN;
111
+ if (
112
+ !Number.isFinite(lastKnownMs) ||
113
+ Date.now() - lastKnownMs > UNVERIFIED_TASK_GRACE_MS
114
+ ) {
115
+ return "stale";
116
+ }
117
+
118
+ return persisted;
76
119
  }
77
120
 
78
121
  function elapsedStr(startIso: string | undefined, endIso?: string): string {
@@ -203,9 +246,9 @@ function buildRows(includeAll: boolean): Row[] {
203
246
 
204
247
  for (const taskId of taskIds) {
205
248
  const meta = loadMeta(taskId);
206
- const status = meta?.status ?? loadStatus(taskId);
249
+ const status = getStatus(taskId, meta);
207
250
 
208
- if (!includeAll && (status === "completed" || status === "killed")) {
251
+ if (!includeAll && (TERMINAL_STATUSES.has(status) || status === "stale")) {
209
252
  const lastUpdate = meta?.lastUpdate ?? "";
210
253
  if (lastUpdate) {
211
254
  const age = Date.now() - new Date(lastUpdate).getTime();
@@ -221,7 +264,9 @@ function buildRows(includeAll: boolean): Row[] {
221
264
  : "";
222
265
  const elapsed = elapsedStr(
223
266
  meta?.startTime,
224
- meta?.status === "completed" ? meta?.lastUpdate : undefined
267
+ TERMINAL_STATUSES.has(status) || status === "stale"
268
+ ? meta?.lastUpdate
269
+ : undefined
225
270
  );
226
271
  const lastUpdate = meta?.lastUpdate
227
272
  ? new Date(meta.lastUpdate).toLocaleString()
@@ -247,12 +292,15 @@ function buildRows(includeAll: boolean): Row[] {
247
292
  * - A numeric index string ("1", "2", …) — 1-based row from `agents list`
248
293
  * - An exact task ID
249
294
  * - A partial task ID (substring match)
295
+ *
296
+ * NOTE: numeric index always uses the same row set as `agents list` (includeAll=false)
297
+ * so that index numbers are consistent across all subcommands.
250
298
  */
251
299
  function resolveTaskId(value: string, includeAll = false): string | null {
252
300
  const n = parseInt(value, 10);
253
301
  // Only treat as a numeric index if it's a small number (≤ 9999) — large numbers are timestamp-based task IDs
254
302
  if (!isNaN(n) && String(n) === value.trim() && n <= 9999) {
255
- const rows = buildRows(includeAll);
303
+ const rows = buildRows(false);
256
304
  const row = rows[n - 1];
257
305
  return row ? row.taskId : null;
258
306
  }
@@ -6,6 +6,7 @@ import { embed, upload, download, purge } from "../index";
6
6
  import { generate, buildWaves, normalizeInputPattern, GenerateOptions } from "../generate";
7
7
  import { init } from "../config";
8
8
  import { login } from "../login";
9
+ import { registerIdentity } from "../auth/registerIdentity";
9
10
  import { KnowhowSimpleClient } from "../services/KnowhowClient";
10
11
  import { startChat } from "../chat";
11
12
 
@@ -24,8 +25,22 @@ export function addLoginCommand(program: Command): void {
24
25
  .description("Login to knowhow")
25
26
  .option("--jwt", "Use manual JWT input instead of browser login")
26
27
  .option("-i, --identity <path>", "Ed25519 private key to use for public-key login")
27
- .action(async (options: { jwt?: boolean; identity?: string }) => {
28
- await login(options.jwt, options.identity);
28
+ .option("--register-identity", "Register an Ed25519 identity using the JWT in .knowhow/.jwt")
29
+ .option("--api-url <url>", "API base URL override (falls back to KNOWHOW_API_URL env)")
30
+ .action(async (options: {
31
+ jwt?: boolean;
32
+ identity?: string;
33
+ registerIdentity?: boolean;
34
+ apiUrl?: string;
35
+ }) => {
36
+ if (options.registerIdentity) {
37
+ await registerIdentity({
38
+ identityPath: options.identity,
39
+ apiUrl: options.apiUrl,
40
+ });
41
+ } else {
42
+ await login(options.jwt, options.identity);
43
+ }
29
44
  });
30
45
  }
31
46
 
@@ -0,0 +1,203 @@
1
+ import { Command } from "commander";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import {
5
+ getProcessesDir, listManagedProcesses, ManagedProcess,
6
+ ManagedProcessInfo, processDirectory,
7
+ } from "../processes/ProcessManager";
8
+ import { runManagedProcessShim } from "../processes/managedProcessShim";
9
+
10
+ const terminal = new Set(["exited", "failed", "stopped"]);
11
+ function resolveProcess(value?: string, index?: string, options: any = {}): ManagedProcess {
12
+ const root = getProcessesDir();
13
+ if (index !== undefined) {
14
+ const parsed = Number(index);
15
+ if (!Number.isInteger(parsed) || parsed < 0) {
16
+ throw new Error(`Invalid process index '${index}'. Use the index shown by 'knowhow processes list'.`);
17
+ }
18
+ const items = filtered(options);
19
+ const item = items[parsed];
20
+ if (!item) {
21
+ throw new Error(`Managed process index ${parsed} not found. Run 'knowhow processes list${options.all ? " --all" : ""}' to see valid indices.`);
22
+ }
23
+ return new ManagedProcess(item.id, item.directory);
24
+ }
25
+ if (!value) {
26
+ throw new Error("Provide a process id, or use -i <index> from 'knowhow processes list'.");
27
+ }
28
+ const matches = listManagedProcesses(root).filter((p) => p.id === value || p.id.includes(value));
29
+ if (!matches.length) throw new Error(`Managed process not found: ${value}`);
30
+ const exact = matches.find((p) => p.id === value);
31
+ if (!exact && matches.length > 1) throw new Error(`Ambiguous process id '${value}': ${matches.map((p) => p.id).join(", ")}`);
32
+ const item = exact || matches[0];
33
+ return new ManagedProcess(item.id, item.directory);
34
+ }
35
+ function commandOptions(options: any, command?: Command): any {
36
+ const fromOptions = typeof options?.optsWithGlobals === "function" ? options.optsWithGlobals() : options || {};
37
+ const fromCommand = typeof command?.optsWithGlobals === "function" ? command.optsWithGlobals() : {};
38
+ return { ...fromOptions, ...fromCommand };
39
+ }
40
+ function filtered(options: any): ManagedProcessInfo[] {
41
+ return listManagedProcesses().filter((item) => {
42
+ if (!options.all && terminal.has(item.status.state)) return false;
43
+ if (options.parent && item.status.parentProcessId !== options.parent) return false;
44
+ if (options.background !== undefined && item.status.background !== options.background) return false;
45
+ return true;
46
+ });
47
+ }
48
+ function truncate(value: string, width: number): string {
49
+ if (value.length <= width) return value;
50
+ if (width <= 1) return "…";
51
+ return `${value.slice(0, width - 1)}…`;
52
+ }
53
+ function truncateMiddle(value: string, width: number): string {
54
+ if (value.length <= width) return value;
55
+ if (width <= 1) return "…";
56
+ const left = Math.ceil((width - 1) / 2);
57
+ return `${value.slice(0, left)}…${value.slice(value.length - (width - left - 1))}`;
58
+ }
59
+ function printProcessTable(rows: Array<Record<string, string | number>>): void {
60
+ const terminalWidth = process.stdout.columns || 120;
61
+ const available = Math.max(80, Math.min(terminalWidth, 180));
62
+ const widths = {
63
+ index: Math.max(1, String(rows.length - 1).length),
64
+ state: Math.max(5, ...rows.map((row) => String(row.state).length)),
65
+ pid: Math.max(3, ...rows.map((row) => String(row.pid).length)),
66
+ background: 2,
67
+ id: Math.min(38, Math.max(12, ...rows.map((row) => String(row.id).length))),
68
+ parent: Math.min(20, Math.max(6, ...rows.map((row) => String(row.parent).length))),
69
+ };
70
+ const fixedWidth = widths.index + widths.state + widths.pid + widths.background
71
+ + widths.id + widths.parent + 12;
72
+ const commandWidth = Math.max(20, available - fixedWidth);
73
+ const header = ["#".padEnd(widths.index), "STATE".padEnd(widths.state),
74
+ "PID".padEnd(widths.pid), "BG".padEnd(widths.background),
75
+ "ID".padEnd(widths.id), "PARENT".padEnd(widths.parent), "COMMAND"];
76
+ console.log(header.join(" "));
77
+ console.log("─".repeat(Math.min(available, fixedWidth + commandWidth)));
78
+ for (const row of rows) {
79
+ console.log([
80
+ String(row.index).padEnd(widths.index),
81
+ String(row.state).padEnd(widths.state),
82
+ String(row.pid).padEnd(widths.pid),
83
+ String(row.background).padEnd(widths.background),
84
+ truncateMiddle(String(row.id), widths.id).padEnd(widths.id),
85
+ truncateMiddle(String(row.parent), widths.parent).padEnd(widths.parent),
86
+ truncate(String(row.command).replace(/\s+/g, " "), commandWidth),
87
+ ].join(" "));
88
+ }
89
+ }
90
+ function printList(options: any): void {
91
+ const rows = filtered(options).map(({ id, status }, index) => ({
92
+ index, id, state: status.state, pid: status.pid || "-", background: status.background ? "yes" : "no",
93
+ parent: status.parentProcessId || "-", command: [status.command, ...status.args].join(" "),
94
+ }));
95
+ if (options.json) console.log(JSON.stringify(rows, null, 2));
96
+ else if (rows.length) printProcessTable(rows);
97
+ else console.log("No managed processes.");
98
+ }
99
+ function addFilters(command: Command): Command {
100
+ return command.option("-a, --all", "include completed processes")
101
+ .option("--parent <id>", "filter by parent process/task id")
102
+ .option("--background", "show only background processes")
103
+ .option("--json", "emit JSON");
104
+ }
105
+ function tailText(file: string, lines: number): string {
106
+ if (!fs.existsSync(file)) return "";
107
+ return fs.readFileSync(file, "utf8").split("\n").slice(-lines - 1).join("\n");
108
+ }
109
+ async function followFiles(files: string[], fromEnd = true): Promise<void> {
110
+ const offsets = new Map<string, number>();
111
+ for (const file of files) offsets.set(file, fromEnd && fs.existsSync(file) ? fs.statSync(file).size : 0);
112
+ await new Promise<void>((resolve) => {
113
+ const poll = setInterval(() => {
114
+ for (const file of files) {
115
+ if (!fs.existsSync(file)) continue;
116
+ const size = fs.statSync(file).size; const offset = offsets.get(file) || 0;
117
+ if (size < offset) offsets.set(file, 0);
118
+ if (size > (offsets.get(file) || 0)) {
119
+ const start = offsets.get(file) || 0;
120
+ const fd = fs.openSync(file, "r"); const data = Buffer.alloc(size - start);
121
+ fs.readSync(fd, data, 0, data.length, start); fs.closeSync(fd);
122
+ process.stdout.write(data); offsets.set(file, size);
123
+ }
124
+ }
125
+ }, 150);
126
+ const done = () => { clearInterval(poll); resolve(); };
127
+ process.once("SIGINT", done); process.once("SIGTERM", done);
128
+ });
129
+ }
130
+
131
+ export function addProcessesCommand(program: Command): void {
132
+ const processes = addFilters(program.command("processes").description("inspect and control managed processes"));
133
+ processes.action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
134
+ addFilters(processes.command("list").description("list managed processes"))
135
+ .action((rawOptions, command) => printList(commandOptions(rawOptions, command)));
136
+
137
+ processes.command("run").description("internal managed process shim").requiredOption("--config <path>")
138
+ .action(async ({ config }) => runManagedProcessShim(path.resolve(config)));
139
+
140
+ // Avoid Commander's built-in help exit here. The optional tracing module
141
+ // flushes asynchronously on process.exit, while Commander expects it never
142
+ // to return, which can otherwise produce a spurious "unknown option" error.
143
+ addFilters(processes.command("logs [id]").alias("tail"))
144
+ .helpOption(false)
145
+ .option("-h, --help", "display help for command")
146
+ .option("-i, --index <number>", "select by the zero-based index shown by processes list")
147
+ .option("-n, --lines <number>", "lines to show", "100")
148
+ .option("-f, --follow", "follow output").option("--stderr", "show stderr instead of stdout")
149
+ .option("--both", "show stdout and stderr").action(async (id, rawOptions, command) => {
150
+ const options = commandOptions(rawOptions, command);
151
+ if (options.help) { command.outputHelp(); return; }
152
+ const managed = resolveProcess(id, options.index, options);
153
+ const files = options.both ? [managed.stdoutPath, managed.stderrPath] : [options.stderr ? managed.stderrPath : managed.stdoutPath];
154
+ for (const file of files) process.stdout.write(tailText(file, Math.max(0, Number(options.lines) || 100)));
155
+ if (options.follow) await followFiles(files);
156
+ });
157
+
158
+ processes.command("send <id> [input...]").option("-n, --no-newline", "do not append newline")
159
+ .action(async (id, input: string[], rawOptions, command) => {
160
+ const options = commandOptions(rawOptions, command);
161
+ const managed = resolveProcess(id);
162
+ await managed.write((input || []).join(" ") + (options.newline ? "\n" : ""));
163
+ });
164
+
165
+ processes.command("signal <id> [signal]").action((id, signal = "SIGTERM") => resolveProcess(id).signal(signal.toUpperCase() as NodeJS.Signals));
166
+ processes.command("stop <id>").action((id) => resolveProcess(id).signal("SIGTERM"));
167
+ processes.command("kill <id>").action((id) => resolveProcess(id).signal("SIGKILL"));
168
+
169
+ processes.command("attach <id>").description("attach terminal (Ctrl-] to detach)").action(async (id) => {
170
+ const managed = resolveProcess(id); let detached = false;
171
+ const raw = !!process.stdin.isTTY; if (raw) process.stdin.setRawMode(true);
172
+ process.stdin.resume();
173
+ const onData = async (data: Buffer) => {
174
+ if (data.includes(0x1d)) { detached = true; return; }
175
+ try { await managed.write(data.toString()); } catch (error) { console.error(String(error)); detached = true; }
176
+ };
177
+ process.stdin.on("data", onData);
178
+ const offsets = [managed.stdoutPath, managed.stderrPath].map((f) => fs.existsSync(f) ? fs.statSync(f).size : 0);
179
+ await new Promise<void>((resolve) => {
180
+ const timer = setInterval(() => {
181
+ [managed.stdoutPath, managed.stderrPath].forEach((file, i) => {
182
+ if (!fs.existsSync(file)) return; const size = fs.statSync(file).size;
183
+ if (size > offsets[i]) { const fd = fs.openSync(file, "r"); const b = Buffer.alloc(size - offsets[i]); fs.readSync(fd, b, 0, b.length, offsets[i]); fs.closeSync(fd); process.stdout.write(b); offsets[i] = size; }
184
+ });
185
+ if (detached || terminal.has(managed.status.state)) { clearInterval(timer); resolve(); }
186
+ }, 100);
187
+ });
188
+ process.stdin.off("data", onData); process.stdin.pause(); if (raw) process.stdin.setRawMode(false);
189
+ });
190
+
191
+ processes.command("tree").option("-a, --all", "include completed processes").action((rawOptions, command) => {
192
+ const options = commandOptions(rawOptions, command);
193
+ const items = listManagedProcesses().filter((p) => options.all || !terminal.has(p.status.state));
194
+ const ids = new Set(items.map((p) => p.id));
195
+ const children = new Map<string | null, ManagedProcessInfo[]>();
196
+ for (const item of items) { const parent = item.status.parentProcessId && ids.has(item.status.parentProcessId) ? item.status.parentProcessId : null; children.set(parent, [...(children.get(parent) || []), item]); }
197
+ const render = (parent: string | null, prefix: string) => (children.get(parent) || []).forEach((item, index, array) => {
198
+ const last = index === array.length - 1; console.log(`${prefix}${last ? "└─" : "├─"}${item.id} [${item.status.state}] ${item.status.command}`);
199
+ render(item.id, prefix + (last ? " " : "│ "));
200
+ });
201
+ render(null, "");
202
+ });
203
+ }
@@ -3,7 +3,8 @@ import * as allTools from "../agents/tools";
3
3
  import { LazyToolsService, services, MinimalToolsService, TracingService } from "../services";
4
4
  import { agents } from "../agents";
5
5
  import { ModulesService } from "../services/modules";
6
- import { getConfig } from "../config";
6
+ import { getConfig, getConfigSync } from "../config";
7
+ import { authenticateWithKey } from "../auth/keyAuth";
7
8
 
8
9
  /**
9
10
  * Shared service setup used by commands that need full services (chat, agent, worker, etc.)
@@ -20,6 +21,7 @@ export async function setupServices() {
20
21
  MediaProcessor,
21
22
  Behaviors,
22
23
  RuntimeReload,
24
+ Extensions,
23
25
  } = services();
24
26
 
25
27
  await Plugins.refreshConfiguredState();
@@ -76,6 +78,25 @@ export async function setupServices() {
76
78
  `⚠ Some MCP servers failed to connect (continuing without them): ${msg}`
77
79
  );
78
80
  }
81
+ // Attempt silent JWT refresh via SSH identity before registering models.
82
+ // This handles the case where the JWT has expired but the user has a valid
83
+ // SSH identity registered — they shouldn't need to run `knowhow login` again
84
+ // unless they want to switch organizations.
85
+ try {
86
+ const startupConfig = getConfigSync();
87
+ if (startupConfig.orgId && !process.env.KNOWHOW_JWT) {
88
+ await authenticateWithKey(
89
+ startupConfig.orgId,
90
+ process.env.KNOWHOW_API_URL || "https://api.knowhow.tyvm.ai",
91
+ startupConfig.cliIdentityPath
92
+ );
93
+ }
94
+ } catch (_err) {
95
+ // Non-fatal: if SSH refresh fails (no key, revoked, network issue), we
96
+ // continue with the existing JWT (or no JWT). The user will see model
97
+ // warnings if no valid JWT is available.
98
+ }
99
+
79
100
  console.log("Connecting to clients...");
80
101
  await Clients.registerConfiguredModels();
81
102
  console.log("✓ Services are set up and ready to go!");
@@ -92,6 +113,7 @@ export async function setupServices() {
92
113
  Behaviors,
93
114
  Events,
94
115
  Tracing: TracingService,
116
+ Extensions,
95
117
  };
96
118
 
97
119
  // Call destroy() on all modules when the process is shutting down so they
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ export * as embeddings from "./embeddings";
65
65
  export * as types from "./types";
66
66
  export * as processors from "./processors";
67
67
  export * as ai from "./ai";
68
+ export * from "./processes/ProcessManager";
68
69
 
69
70
  // Export module system types for external modules
70
71
  export * from "./services/modules/types";