@tenex-chat/backend 0.9.5 → 0.9.6

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 (148) hide show
  1. package/README.md +5 -1
  2. package/dist/daemon-wrapper.cjs +47 -0
  3. package/dist/index.js +59268 -0
  4. package/dist/wrapper.js +171 -0
  5. package/package.json +19 -27
  6. package/src/agents/AgentRegistry.ts +9 -7
  7. package/src/agents/AgentStorage.ts +24 -1
  8. package/src/agents/agent-installer.ts +6 -0
  9. package/src/agents/agent-loader.ts +7 -2
  10. package/src/agents/constants.ts +10 -2
  11. package/src/agents/execution/AgentExecutor.ts +35 -6
  12. package/src/agents/execution/StreamCallbacks.ts +53 -13
  13. package/src/agents/execution/StreamExecutionHandler.ts +110 -16
  14. package/src/agents/execution/StreamSetup.ts +19 -9
  15. package/src/agents/execution/ToolEventHandlers.ts +112 -0
  16. package/src/agents/role-categories.ts +53 -0
  17. package/src/agents/types/runtime.ts +7 -0
  18. package/src/agents/types/storage.ts +7 -0
  19. package/src/commands/agent/import/openclaw-distiller.ts +63 -7
  20. package/src/commands/agent/import/openclaw-reader.ts +54 -0
  21. package/src/commands/agent/import/openclaw.ts +120 -29
  22. package/src/commands/agent/index.ts +83 -2
  23. package/src/commands/setup/display.ts +123 -0
  24. package/src/commands/setup/embed.ts +13 -13
  25. package/src/commands/setup/global-system-prompt.ts +15 -17
  26. package/src/commands/setup/image.ts +17 -20
  27. package/src/commands/setup/interactive.ts +37 -20
  28. package/src/commands/setup/llm.ts +12 -7
  29. package/src/commands/setup/onboarding.ts +1580 -248
  30. package/src/commands/setup/providers.ts +3 -3
  31. package/src/conversations/ConversationStore.ts +23 -2
  32. package/src/conversations/MessageBuilder.ts +51 -73
  33. package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
  34. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
  35. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
  36. package/src/conversations/services/ConversationSummarizer.ts +1 -2
  37. package/src/conversations/types.ts +11 -0
  38. package/src/daemon/Daemon.ts +78 -57
  39. package/src/daemon/ProjectRuntime.ts +6 -12
  40. package/src/daemon/SubscriptionManager.ts +13 -0
  41. package/src/daemon/index.ts +0 -1
  42. package/src/event-handler/index.ts +1 -0
  43. package/src/index.ts +20 -1
  44. package/src/llm/ChunkHandler.ts +1 -1
  45. package/src/llm/FinishHandler.ts +28 -4
  46. package/src/llm/LLMConfigEditor.ts +218 -106
  47. package/src/llm/index.ts +0 -4
  48. package/src/llm/meta/MetaModelResolver.ts +3 -18
  49. package/src/llm/middleware/message-sanitizer.ts +153 -0
  50. package/src/llm/providers/ollama-models.ts +0 -38
  51. package/src/llm/service.ts +50 -15
  52. package/src/llm/types.ts +0 -12
  53. package/src/llm/utils/ConfigurationManager.ts +88 -465
  54. package/src/llm/utils/ConfigurationTester.ts +42 -185
  55. package/src/llm/utils/ModelSelector.ts +156 -92
  56. package/src/llm/utils/ProviderConfigUI.ts +10 -141
  57. package/src/llm/utils/models-dev-cache.ts +102 -23
  58. package/src/llm/utils/provider-select-prompt.ts +284 -0
  59. package/src/llm/utils/provider-setup.ts +81 -34
  60. package/src/llm/utils/variant-list-prompt.ts +361 -0
  61. package/src/nostr/AgentEventDecoder.ts +1 -0
  62. package/src/nostr/AgentEventEncoder.ts +37 -0
  63. package/src/nostr/AgentProfilePublisher.ts +13 -0
  64. package/src/nostr/AgentPublisher.ts +26 -0
  65. package/src/nostr/kinds.ts +1 -0
  66. package/src/nostr/ndkClient.ts +4 -1
  67. package/src/nostr/types.ts +12 -0
  68. package/src/prompts/fragments/25-rag-instructions.ts +22 -21
  69. package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
  70. package/src/prompts/fragments/index.ts +2 -0
  71. package/src/prompts/utils/systemPromptBuilder.ts +18 -28
  72. package/src/services/AgentDefinitionMonitor.ts +8 -0
  73. package/src/services/ConfigService.ts +34 -0
  74. package/src/services/PubkeyService.ts +7 -1
  75. package/src/services/compression/CompressionService.ts +133 -74
  76. package/src/services/compression/compression-utils.ts +110 -19
  77. package/src/services/config/types.ts +0 -6
  78. package/src/services/dispatch/AgentDispatchService.ts +79 -0
  79. package/src/services/intervention/InterventionService.ts +78 -5
  80. package/src/services/nip46/Nip46SigningService.ts +30 -1
  81. package/src/services/projects/ProjectContext.ts +8 -6
  82. package/src/services/rag/RAGCollectionRegistry.ts +199 -0
  83. package/src/services/rag/RAGDatabaseService.ts +2 -7
  84. package/src/services/rag/RAGOperations.ts +25 -45
  85. package/src/services/rag/RAGService.ts +0 -31
  86. package/src/services/rag/RagSubscriptionService.ts +71 -122
  87. package/src/services/rag/rag-utils.ts +13 -0
  88. package/src/services/ral/RALRegistry.ts +25 -184
  89. package/src/services/reports/ReportEmbeddingService.ts +63 -113
  90. package/src/services/search/UnifiedSearchService.ts +115 -4
  91. package/src/services/search/index.ts +1 -0
  92. package/src/services/search/projectFilter.ts +20 -4
  93. package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
  94. package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
  95. package/src/services/search/providers/LessonSearchProvider.ts +1 -8
  96. package/src/services/search/providers/ReportSearchProvider.ts +1 -0
  97. package/src/services/search/types.ts +24 -3
  98. package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
  99. package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
  100. package/src/telemetry/setup.ts +2 -13
  101. package/src/tools/implementations/ask.ts +3 -3
  102. package/src/tools/implementations/conversation_get.ts +28 -268
  103. package/src/tools/implementations/fs_grep.ts +6 -6
  104. package/src/tools/implementations/fs_read.ts +2 -0
  105. package/src/tools/implementations/fs_write.ts +2 -0
  106. package/src/tools/implementations/learn.ts +38 -50
  107. package/src/tools/implementations/rag_add_documents.ts +6 -4
  108. package/src/tools/implementations/rag_create_collection.ts +37 -4
  109. package/src/tools/implementations/rag_delete_collection.ts +9 -0
  110. package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
  111. package/src/tools/registry.ts +7 -8
  112. package/src/tools/types.ts +11 -2
  113. package/src/tools/utils/transcript-args.ts +13 -0
  114. package/src/utils/cli-theme.ts +13 -0
  115. package/src/utils/logger.ts +55 -0
  116. package/src/utils/metadataKeys.ts +17 -0
  117. package/src/utils/sqlEscaping.ts +39 -0
  118. package/src/wrapper.ts +7 -3
  119. package/dist/src/index.js +0 -46790
  120. package/dist/tenex-backend-wrapper.cjs +0 -3
  121. package/src/agents/execution/constants.ts +0 -16
  122. package/src/agents/execution/index.ts +0 -3
  123. package/src/agents/index.ts +0 -4
  124. package/src/commands/agent.ts +0 -235
  125. package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
  126. package/src/conversations/formatters/index.ts +0 -9
  127. package/src/conversations/index.ts +0 -2
  128. package/src/conversations/utils/content-utils.ts +0 -69
  129. package/src/daemon/UnixSocketTransport.ts +0 -318
  130. package/src/event-handler/newConversation.ts +0 -165
  131. package/src/events/NDKProjectStatus.ts +0 -384
  132. package/src/events/index.ts +0 -4
  133. package/src/lib/json-parser.ts +0 -30
  134. package/src/llm/RecordingState.ts +0 -37
  135. package/src/llm/StreamPublisher.ts +0 -40
  136. package/src/llm/middleware/flight-recorder.ts +0 -188
  137. package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
  138. package/src/nostr/constants.ts +0 -38
  139. package/src/prompts/core/index.ts +0 -3
  140. package/src/prompts/index.ts +0 -21
  141. package/src/services/image/index.ts +0 -12
  142. package/src/services/status/index.ts +0 -11
  143. package/src/telemetry/diagnostics.ts +0 -27
  144. package/src/tools/implementations/rag_query.ts +0 -107
  145. package/src/types/index.ts +0 -46
  146. package/src/utils/agentFetcher.ts +0 -107
  147. package/src/utils/conversation-utils.ts +0 -1
  148. package/src/utils/process.ts +0 -49
@@ -1,50 +1,97 @@
1
- import inquirer from "inquirer";
2
-
1
+ import password from "@inquirer/password";
2
+ import input from "@inquirer/input";
3
+ import chalk from "chalk";
3
4
  import { AI_SDK_PROVIDERS } from "@/llm/types";
4
5
  import type { ProviderCredentials, TenexProviders } from "@/services/config/types";
5
- import { hasApiKey } from "@/llm/providers/key-manager";
6
+ import { PROVIDER_IDS } from "@/llm/providers/provider-ids";
7
+ import providerSelectPrompt, {
8
+ getKeys,
9
+ isOllama,
10
+ type PromptState,
11
+ type ProviderSelectConfig,
12
+ } from "@/llm/utils/provider-select-prompt";
6
13
  import { ProviderConfigUI } from "@/llm/utils/ProviderConfigUI";
14
+ import { inquirerTheme } from "@/utils/cli-theme";
15
+
16
+ interface ProviderSetupOptions {
17
+ providerHints?: Record<string, string>;
18
+ }
7
19
 
8
20
  /**
9
21
  * Interactive flow for configuring provider credentials.
10
- * Returns an updated TenexProviders object (merges into the existing map).
22
+ * Uses a two-level prompt: a provider list (browse/keys) and a separate
23
+ * password prompt for entering API keys.
11
24
  */
12
25
  export async function runProviderSetup(
13
- existingProviders: TenexProviders
26
+ existingProviders: TenexProviders,
27
+ options: ProviderSetupOptions = {},
14
28
  ): Promise<TenexProviders> {
15
- const providers: Record<string, ProviderCredentials> = {
16
- ...existingProviders.providers,
17
- };
18
-
19
- const choices = AI_SDK_PROVIDERS.map((provider) => {
20
- const isConfigured = hasApiKey(providers[provider]?.apiKey);
21
- const name = ProviderConfigUI.getProviderDisplayName(provider);
22
- return {
23
- name: isConfigured ? `${name} (configured)` : name,
24
- value: provider,
29
+ const providerIds = AI_SDK_PROVIDERS.filter((p) => p !== PROVIDER_IDS.CLAUDE_CODE);
30
+ const { providerHints } = options;
31
+
32
+ let resumeState: PromptState | undefined;
33
+
34
+ while (true) {
35
+ const baseConfig: ProviderSelectConfig = {
36
+ message: "Configure providers:",
37
+ providerIds: [...providerIds],
38
+ initialProviders: { ...existingProviders.providers },
39
+ providerHints,
40
+ resumeState,
41
+ theme: inquirerTheme,
25
42
  };
26
- });
27
43
 
28
- const { selected } = await inquirer.prompt([
29
- {
30
- type: "checkbox",
31
- name: "selected",
32
- message: "Select providers to configure:",
33
- choices,
34
- },
35
- ]);
36
-
37
- if (!selected || selected.length === 0) {
38
- return existingProviders;
39
- }
44
+ const result = await providerSelectPrompt(baseConfig);
45
+
46
+ if (result.action === "done") {
47
+ return { providers: result.providers };
48
+ }
40
49
 
41
- for (const provider of selected) {
42
- const providerConfig = await ProviderConfigUI.configureProvider(provider, providers);
43
- providers[provider] = {
44
- ...providers[provider],
45
- apiKey: providerConfig.apiKey,
50
+ // add-key: ask for the key via a separate prompt
51
+ const { providerId, returnTo, state } = result;
52
+ const name = ProviderConfigUI.getProviderDisplayName(providerId);
53
+ const apiKey = await askForKey(providerId, name, providerHints?.[providerId]);
54
+
55
+ if (apiKey) {
56
+ const existing = getKeys(state.providers[providerId]?.apiKey);
57
+ if (existing.length > 0) {
58
+ state.providers[providerId] = {
59
+ ...state.providers[providerId],
60
+ apiKey: [...existing, apiKey],
61
+ } as ProviderCredentials;
62
+ } else {
63
+ state.providers[providerId] = { apiKey };
64
+ }
65
+ }
66
+
67
+ // Restore the prompt in the mode we came from
68
+ resumeState = {
69
+ ...state,
70
+ mode: returnTo,
71
+ keysTarget: returnTo === "keys" ? providerId : null,
72
+ keysActive: 0,
46
73
  };
47
74
  }
75
+ }
76
+
77
+ async function askForKey(providerId: string, displayName: string, hint?: string): Promise<string | undefined> {
78
+ if (isOllama(providerId)) {
79
+ const url = await input({
80
+ message: `${displayName} URL:`,
81
+ default: "http://localhost:11434",
82
+ theme: inquirerTheme,
83
+ });
84
+ return url.trim() || undefined;
85
+ }
86
+
87
+ if (hint) {
88
+ console.log(chalk.dim(` Run ${chalk.bold("claude setup-token")} in another terminal, then paste the key (sk-ant-...) here.`));
89
+ }
48
90
 
49
- return { providers };
91
+ const key = await password({
92
+ message: `${displayName} API key:`,
93
+ mask: "*",
94
+ theme: inquirerTheme,
95
+ });
96
+ return key.trim() || undefined;
50
97
  }
@@ -0,0 +1,361 @@
1
+ import {
2
+ createPrompt,
3
+ useState,
4
+ useKeypress,
5
+ usePrefix,
6
+ isUpKey,
7
+ isDownKey,
8
+ isEnterKey,
9
+ isBackspaceKey,
10
+ makeTheme,
11
+ type Theme,
12
+ } from "@inquirer/core";
13
+ import type { PartialDeep } from "@inquirer/type";
14
+ import { cursorHide } from "@inquirer/ansi";
15
+ import chalk from "chalk";
16
+ import inquirer from "inquirer";
17
+ import { inquirerTheme } from "@/utils/cli-theme";
18
+ import type { MetaModelConfiguration, MetaModelVariant } from "@/services/config/types";
19
+ import * as display from "@/commands/setup/display";
20
+
21
+ type VariantListAction =
22
+ | { action: "edit"; variantName: string }
23
+ | { action: "add" }
24
+ | { action: "done" };
25
+
26
+ type VariantListResult = VariantListAction & {
27
+ variants: Record<string, MetaModelVariant>;
28
+ defaultVariant: string;
29
+ };
30
+
31
+ type VariantListConfig = {
32
+ message: string;
33
+ variants: Record<string, MetaModelVariant>;
34
+ defaultVariant: string;
35
+ theme?: PartialDeep<Theme>;
36
+ };
37
+
38
+ const variantListRawPrompt = createPrompt<VariantListResult, VariantListConfig>(
39
+ (config, done) => {
40
+ const theme = makeTheme(config.theme);
41
+ const prefix = usePrefix({ status: "idle", theme });
42
+
43
+ const [active, setActive] = useState(0);
44
+ const [variants, setVariants] = useState<Record<string, MetaModelVariant>>(
45
+ () => ({ ...config.variants }),
46
+ );
47
+ const [defaultVariant, setDefaultVariant] = useState(config.defaultVariant);
48
+
49
+ const variantNames = Object.keys(variants);
50
+ const addIndex = variantNames.length;
51
+ const doneIndex = variantNames.length + 1;
52
+ const itemCount = variantNames.length + 2;
53
+
54
+ useKeypress((key, rl) => {
55
+ rl.clearLine(0);
56
+
57
+ if (isUpKey(key)) {
58
+ setActive(Math.max(0, active - 1));
59
+ } else if (isDownKey(key)) {
60
+ setActive(Math.min(itemCount - 1, active + 1));
61
+ } else if (isEnterKey(key)) {
62
+ if (active < variantNames.length) {
63
+ const name = variantNames[active];
64
+ if (name) {
65
+ done({
66
+ action: "edit",
67
+ variantName: name,
68
+ variants: { ...variants },
69
+ defaultVariant,
70
+ });
71
+ }
72
+ } else if (active === addIndex) {
73
+ done({
74
+ action: "add",
75
+ variants: { ...variants },
76
+ defaultVariant,
77
+ });
78
+ } else if (active === doneIndex) {
79
+ if (variantNames.length < 2) return;
80
+ done({
81
+ action: "done",
82
+ variants: { ...variants },
83
+ defaultVariant,
84
+ });
85
+ }
86
+ } else if (key.name === "d" && active < variantNames.length) {
87
+ const name = variantNames[active];
88
+ if (name) setDefaultVariant(name);
89
+ } else if (
90
+ (isBackspaceKey(key) || key.name === "delete") &&
91
+ active < variantNames.length
92
+ ) {
93
+ if (variantNames.length <= 2) return;
94
+
95
+ const nameToDelete = variantNames[active];
96
+ if (!nameToDelete) return;
97
+
98
+ const updated = { ...variants };
99
+ delete updated[nameToDelete];
100
+ setVariants(updated);
101
+
102
+ if (defaultVariant === nameToDelete) {
103
+ const remaining = Object.keys(updated);
104
+ setDefaultVariant(remaining[0] ?? "");
105
+ }
106
+
107
+ const newCount = Object.keys(updated).length;
108
+ if (active >= newCount) {
109
+ setActive(Math.max(0, newCount - 1));
110
+ }
111
+ }
112
+ });
113
+
114
+ // Render
115
+ const cursor = chalk.hex("#FFC107")("›");
116
+ const lines: string[] = [];
117
+
118
+ lines.push(`${prefix} ${theme.style.message(config.message, "idle")}`);
119
+ lines.push("");
120
+ lines.push(chalk.dim(" Variants:"));
121
+
122
+ for (let i = 0; i < variantNames.length; i++) {
123
+ const name = variantNames[i]!;
124
+ const variant = variants[name]!;
125
+ const isDefault = name === defaultVariant;
126
+ const pfx = i === active ? `${cursor} ` : " ";
127
+ const defaultTag = isDefault ? chalk.dim(" (default)") : "";
128
+ const modelDisplay = chalk.gray(`[${variant.model}]`);
129
+
130
+ lines.push(`${pfx}${name} ${modelDisplay}${defaultTag}`);
131
+ }
132
+
133
+ lines.push(` ${"─".repeat(40)}`);
134
+
135
+ // Add variant
136
+ const addPfx = active === addIndex ? `${cursor} ` : " ";
137
+ lines.push(`${addPfx}${chalk.cyan("Add variant")}`);
138
+
139
+ // Done
140
+ const donePfx = active === doneIndex ? `${cursor} ` : " ";
141
+ if (variantNames.length < 2) {
142
+ lines.push(`${donePfx}${chalk.dim("Done (need at least 2 variants)")}`);
143
+ } else {
144
+ lines.push(`${donePfx}${display.doneLabel()}`);
145
+ }
146
+
147
+ // Help line
148
+ const helpParts = [
149
+ `${chalk.bold("↑↓")} ${chalk.dim("navigate")}`,
150
+ `${chalk.bold("⏎")} ${chalk.dim("edit")}`,
151
+ `${chalk.bold("d")} ${chalk.dim("set default")}`,
152
+ `${chalk.bold("⌫")} ${chalk.dim("remove")}`,
153
+ ];
154
+ lines.push(chalk.dim(` ${helpParts.join(chalk.dim(" • "))}`));
155
+
156
+ return `${lines.join("\n")}${cursorHide}`;
157
+ },
158
+ );
159
+
160
+ async function editVariantDetail(
161
+ variantName: string,
162
+ state: { variants: Record<string, MetaModelVariant>; defaultVariant: string },
163
+ standardConfigs: string[],
164
+ ): Promise<void> {
165
+ const variant = state.variants[variantName];
166
+ if (!variant) return;
167
+
168
+ while (true) {
169
+ const isDefault = variantName === state.defaultVariant;
170
+ const defaultTag = isDefault ? " (default)" : "";
171
+
172
+ display.blank();
173
+ display.context(`Variant: ${variantName} → ${variant.model}${defaultTag}`);
174
+ display.blank();
175
+
176
+ const { field } = await inquirer.prompt([{
177
+ type: "select",
178
+ name: "field",
179
+ message: `Edit ${variantName}:`,
180
+ choices: [
181
+ {
182
+ name: `Model ${chalk.dim(variant.model)}`,
183
+ value: "model",
184
+ },
185
+ {
186
+ name: `Trigger keyword ${chalk.dim(variant.keywords?.join(", ") || "(none)")}`,
187
+ value: "keywords",
188
+ },
189
+ {
190
+ name: `When to use ${chalk.dim(variant.description || "(none)")}`,
191
+ value: "description",
192
+ },
193
+ {
194
+ name: `Behavior when active ${chalk.dim(variant.systemPrompt || "(none)")}`,
195
+ value: "systemPrompt",
196
+ description: "Extra instructions given to the agent when this variant is selected, e.g. 'Reason step by step'",
197
+ },
198
+ {
199
+ name: "Back",
200
+ value: "back",
201
+ },
202
+ ],
203
+ theme: inquirerTheme,
204
+ }]);
205
+
206
+ if (field === "back") break;
207
+
208
+ if (field === "model") {
209
+ const { model } = await inquirer.prompt([{
210
+ type: "select",
211
+ name: "model",
212
+ message: "Select model:",
213
+ choices: standardConfigs.map((n) => ({ name: n, value: n })),
214
+ theme: inquirerTheme,
215
+ }]);
216
+ variant.model = model;
217
+ } else if (field === "keywords") {
218
+ const { keywordsInput } = await inquirer.prompt([{
219
+ type: "input",
220
+ name: "keywordsInput",
221
+ message: "Trigger keywords (comma-separated):",
222
+ default: variant.keywords?.join(", ") || "",
223
+ theme: inquirerTheme,
224
+ }]);
225
+ const keywords = keywordsInput
226
+ ? keywordsInput.split(",").map((k: string) => k.trim().toLowerCase()).filter((k: string) => k.length > 0)
227
+ : [];
228
+ if (keywords.length > 0) {
229
+ variant.keywords = keywords;
230
+ } else {
231
+ delete variant.keywords;
232
+ }
233
+ } else if (field === "description") {
234
+ const { desc } = await inquirer.prompt([{
235
+ type: "input",
236
+ name: "desc",
237
+ message: "When to use this variant:",
238
+ default: variant.description || "",
239
+ theme: inquirerTheme,
240
+ }]);
241
+ if (desc) {
242
+ variant.description = desc;
243
+ } else {
244
+ delete variant.description;
245
+ }
246
+ } else if (field === "systemPrompt") {
247
+ const { prompt } = await inquirer.prompt([{
248
+ type: "input",
249
+ name: "prompt",
250
+ message: "Behavior when active:",
251
+ default: variant.systemPrompt || "",
252
+ theme: inquirerTheme,
253
+ }]);
254
+ if (prompt) {
255
+ variant.systemPrompt = prompt;
256
+ } else {
257
+ delete variant.systemPrompt;
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ async function addVariant(
264
+ state: { variants: Record<string, MetaModelVariant>; defaultVariant: string },
265
+ standardConfigs: string[],
266
+ ): Promise<void> {
267
+ const { name } = await inquirer.prompt([{
268
+ type: "input",
269
+ name: "name",
270
+ message: "Variant name:",
271
+ validate: (input: string) => {
272
+ if (!input.trim()) return "Name is required";
273
+ if (state.variants[input]) return "Variant already exists";
274
+ return true;
275
+ },
276
+ theme: inquirerTheme,
277
+ }]);
278
+
279
+ const { model } = await inquirer.prompt([{
280
+ type: "select",
281
+ name: "model",
282
+ message: "Select model for this variant:",
283
+ choices: standardConfigs.map((n) => ({ name: n, value: n })),
284
+ theme: inquirerTheme,
285
+ }]);
286
+
287
+ const isFirst = Object.keys(state.variants).length === 0;
288
+
289
+ state.variants[name] = { model };
290
+
291
+ // First variant auto-becomes default
292
+ if (!state.defaultVariant || isFirst) {
293
+ state.defaultVariant = name;
294
+ }
295
+
296
+ // For non-first variants, ask "when to use" so the system prompt is useful
297
+ if (!isFirst) {
298
+ const { desc } = await inquirer.prompt([{
299
+ type: "input",
300
+ name: "desc",
301
+ message: "When to use this variant:",
302
+ theme: inquirerTheme,
303
+ }]);
304
+ if (desc) {
305
+ state.variants[name]!.description = desc;
306
+ }
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Interactive variant list prompt for multi-modal configuration.
312
+ * Shows variants in a navigable list with add/edit/delete/set-default actions.
313
+ * Returns a complete MetaModelConfiguration when done.
314
+ */
315
+ export async function variantListPrompt(
316
+ configName: string,
317
+ standardConfigs: string[],
318
+ ): Promise<MetaModelConfiguration> {
319
+ let variants: Record<string, MetaModelVariant> = {};
320
+ let defaultVariant = "";
321
+
322
+ // No variants yet — go straight to adding the first one
323
+ const initialState = { variants, defaultVariant };
324
+ await addVariant(initialState, standardConfigs);
325
+ variants = initialState.variants;
326
+ defaultVariant = initialState.defaultVariant;
327
+
328
+ while (true) {
329
+ const result = await variantListRawPrompt({
330
+ message: configName,
331
+ variants,
332
+ defaultVariant,
333
+ theme: inquirerTheme,
334
+ });
335
+
336
+ variants = result.variants;
337
+ defaultVariant = result.defaultVariant;
338
+
339
+ if (result.action === "done") {
340
+ return {
341
+ provider: "meta",
342
+ variants,
343
+ default: defaultVariant,
344
+ };
345
+ }
346
+
347
+ if (result.action === "edit" && result.variantName) {
348
+ const state = { variants, defaultVariant };
349
+ await editVariantDetail(result.variantName, state, standardConfigs);
350
+ variants = state.variants;
351
+ defaultVariant = state.defaultVariant;
352
+ }
353
+
354
+ if (result.action === "add") {
355
+ const state = { variants, defaultVariant };
356
+ await addVariant(state, standardConfigs);
357
+ variants = state.variants;
358
+ defaultVariant = state.defaultVariant;
359
+ }
360
+ }
361
+ }
@@ -209,6 +209,7 @@ export class AgentEventDecoder {
209
209
  NDKKind.Contacts, // kind:3 - contact list
210
210
  NDKKind.TenexProjectStatus,
211
211
  NDKKind.TenexOperationsStatus,
212
+ NDKKind.TenexStreamTextDelta,
212
213
  ];
213
214
 
214
215
  /**
@@ -17,6 +17,7 @@ import type {
17
17
  EventContext,
18
18
  InterventionReviewIntent,
19
19
  LessonIntent,
20
+ StreamTextDeltaIntent,
20
21
  ToolUseIntent,
21
22
  } from "./types";
22
23
 
@@ -561,6 +562,42 @@ export class AgentEventEncoder {
561
562
  return event;
562
563
  }
563
564
 
565
+ /**
566
+ * Encode an ephemeral stream text-delta event.
567
+ * These events are best-effort live updates and do not replace kind:1 snapshots.
568
+ */
569
+ encodeStreamTextDelta(intent: StreamTextDeltaIntent, context: EventContext): NDKEvent {
570
+ const event = new NDKEvent(getNDK());
571
+ event.kind = NDKKind.TenexStreamTextDelta;
572
+ event.content = intent.delta;
573
+
574
+ // Keep thread association via root conversation e-tag.
575
+ this.addConversationTags(event, context);
576
+
577
+ // Required project association for project-scoped filtering.
578
+ this.aTagProject(event);
579
+
580
+ // Include model when available for diagnostics/client metadata.
581
+ if (context.model) {
582
+ const modelString =
583
+ typeof context.model === "string"
584
+ ? context.model
585
+ : (context.model as { model?: string }).model;
586
+ if (modelString) {
587
+ event.tag(["llm-model", modelString]);
588
+ }
589
+ }
590
+
591
+ // Preserve RAL and strict delta ordering for client reconstruction.
592
+ event.tag(["llm-ral", context.ralNumber.toString()]);
593
+ event.tag(["stream-seq", intent.sequence.toString()]);
594
+
595
+ // Forward branch tag when present to preserve worktree context.
596
+ this.forwardBranchTag(event, context);
597
+
598
+ return event;
599
+ }
600
+
564
601
  /**
565
602
  * Encode an intervention review request event.
566
603
  * This is used by the InterventionService when a user hasn't responded
@@ -4,6 +4,7 @@ import { NDKKind } from "@/nostr/kinds";
4
4
  import { getNDK } from "@/nostr/ndkClient";
5
5
  import { config } from "@/services/ConfigService";
6
6
  import { Nip46SigningService, Nip46SigningLog } from "@/services/nip46";
7
+ import { getSystemPubkeyListService } from "@/services/trust-pubkeys/SystemPubkeyListService";
7
8
  import { logger } from "@/utils/logger";
8
9
  import {
9
10
  NDKEvent,
@@ -291,6 +292,10 @@ export class AgentProfilePublisher {
291
292
  let profileEvent: NDKEvent;
292
293
 
293
294
  try {
295
+ await getSystemPubkeyListService().syncWhitelistFile({
296
+ additionalPubkeys: [signer.pubkey, ...(whitelistedPubkeys ?? [])],
297
+ });
298
+
294
299
  // Check if there are other agents with the same slug (name) in this project
295
300
  // If so, append pubkey prefix for disambiguation
296
301
  const projectDTag = projectEvent.dTag;
@@ -493,6 +498,10 @@ export class AgentProfilePublisher {
493
498
  whitelistedPubkeys?: string[]
494
499
  ): Promise<void> {
495
500
  try {
501
+ await getSystemPubkeyListService().syncWhitelistFile({
502
+ additionalPubkeys: [signer.pubkey, ...(whitelistedPubkeys ?? [])],
503
+ });
504
+
496
505
  const avatarUrl = AgentProfilePublisher.buildAvatarUrl(signer.pubkey);
497
506
 
498
507
  const profile = {
@@ -583,6 +592,10 @@ export class AgentProfilePublisher {
583
592
  projectTitle: string
584
593
  ): Promise<void> {
585
594
  try {
595
+ await getSystemPubkeyListService().syncWhitelistFile({
596
+ additionalPubkeys: [signer.pubkey],
597
+ });
598
+
586
599
  // Hash-based deduplication: skip if instructions haven't changed
587
600
  const instructionHash = crypto
588
601
  .createHash("sha256")
@@ -16,6 +16,7 @@ import type {
16
16
  ErrorIntent,
17
17
  EventContext,
18
18
  LessonIntent,
19
+ StreamTextDeltaIntent,
19
20
  ToolUseIntent,
20
21
  } from "./types";
21
22
 
@@ -418,6 +419,31 @@ export class AgentPublisher {
418
419
  return event;
419
420
  }
420
421
 
422
+ /**
423
+ * Publish an ephemeral stream text-delta event.
424
+ * Best-effort only: failures are logged and swallowed to avoid disrupting execution.
425
+ *
426
+ * IMPORTANT: This path intentionally does NOT consume RAL runtime counters.
427
+ * Runtime accounting remains tied to persistent kind:1 publications.
428
+ */
429
+ async streamTextDelta(intent: StreamTextDeltaIntent, context: EventContext): Promise<void> {
430
+ try {
431
+ const event = this.encoder.encodeStreamTextDelta(intent, context);
432
+ injectTraceContext(event);
433
+ await this.agent.sign(event);
434
+ await event.publish();
435
+ } catch (error) {
436
+ logger.warn("[AgentPublisher.streamTextDelta] Failed to publish stream delta (best-effort)", {
437
+ error: error instanceof Error ? error.message : String(error),
438
+ agent: this.agent.slug,
439
+ conversationId: context.conversationId.substring(0, 12),
440
+ ralNumber: context.ralNumber,
441
+ sequence: intent.sequence,
442
+ deltaLength: intent.delta.length,
443
+ });
444
+ }
445
+ }
446
+
421
447
  /**
422
448
  * Publish a delegation marker event.
423
449
  * Delegation markers track the lifecycle of delegation conversations.
@@ -32,6 +32,7 @@ export const NDKKind = {
32
32
  TenexConfigUpdate: 25000 as BaseNDKKind, // Encrypted config updates (e.g., APNs device tokens)
33
33
  TenexOperationsStatus: 24133 as BaseNDKKind,
34
34
  TenexStopCommand: 24134 as BaseNDKKind,
35
+ TenexStreamTextDelta: 24135 as BaseNDKKind,
35
36
  } as const;
36
37
 
37
38
  export type NDKKind = (typeof NDKKind)[keyof typeof NDKKind];
@@ -4,7 +4,7 @@ import { logger } from "@/utils/logger";
4
4
  * TENEX CLI: NDK Singleton
5
5
  * Manages a single NDK instance for the CLI
6
6
  */
7
- import NDK from "@nostr-dev-kit/ndk";
7
+ import NDK, { NDKRelayAuthPolicies } from "@nostr-dev-kit/ndk";
8
8
 
9
9
  let ndk: NDK | undefined;
10
10
 
@@ -31,6 +31,9 @@ export async function initNDK(): Promise<void> {
31
31
  autoConnectUserRelays: true,
32
32
  });
33
33
 
34
+ // Auto-authenticate with relays that require NIP-42 auth
35
+ ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk });
36
+
34
37
  // Connect with timeout - don't block daemon startup if relays are unreachable
35
38
  const connectionTimeout = 5000; // 5 seconds
36
39
  try {
@@ -110,6 +110,17 @@ export interface ToolUseIntent {
110
110
  usage?: LanguageModelUsageWithCostUsd; // Cumulative usage from previous steps
111
111
  }
112
112
 
113
+ /**
114
+ * Intent for ephemeral stream text-delta events.
115
+ * These events are best-effort live updates and do not replace kind:1 snapshots.
116
+ */
117
+ export interface StreamTextDeltaIntent {
118
+ /** Delta text payload (coalesced by throttle interval) */
119
+ delta: string;
120
+ /** Strictly monotonic sequence number for client-side reordering */
121
+ sequence: number;
122
+ }
123
+
113
124
  /**
114
125
  * Intent for intervention review requests.
115
126
  * Used when the InterventionService detects that a user hasn't responded
@@ -160,6 +171,7 @@ export type AgentIntent =
160
171
  | LessonIntent
161
172
  | StatusIntent
162
173
  | ToolUseIntent
174
+ | StreamTextDeltaIntent
163
175
  | InterventionReviewIntent
164
176
  | DelegationMarkerIntent;
165
177