nolo-cli 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/agentRuntimeCommands.ts +3 -3
- package/ai/agent/_executeModel.ts +118 -0
- package/ai/agent/agentSlice.ts +525 -0
- package/ai/agent/appWorkingMemory.ts +126 -0
- package/ai/agent/avatarUtils.ts +24 -0
- package/ai/agent/buildEditingContext.ts +373 -0
- package/ai/agent/buildSystemPrompt.ts +532 -0
- package/ai/agent/cleanAgentMessages.ts +140 -0
- package/ai/agent/cliChatClient.ts +119 -0
- package/ai/agent/cliExecutor.ts +733 -0
- package/ai/agent/cliPrompt.ts +10 -0
- package/ai/agent/contextCompiler.ts +107 -0
- package/ai/agent/contextLayerContract.ts +44 -0
- package/ai/agent/createAgentSchema.ts +234 -0
- package/ai/agent/executeToolCall.ts +58 -0
- package/ai/agent/fetchAgentContexts.ts +42 -0
- package/ai/agent/generatePrompt.ts +3 -0
- package/ai/agent/getFullChatContextKeys.ts +168 -0
- package/ai/agent/hooks/fetchPublicAgents.ts +133 -0
- package/ai/agent/hooks/useAgentConfig.ts +61 -0
- package/ai/agent/hooks/useAgentDialog.ts +35 -0
- package/ai/agent/hooks/useAgentFormValidation.ts +202 -0
- package/ai/agent/hooks/usePublicAgents.ts +473 -0
- package/ai/agent/machineRunPermissions.ts +95 -0
- package/ai/agent/persistMessageWithFixedId.ts +37 -0
- package/ai/agent/planSlice.ts +259 -0
- package/ai/agent/referenceUtils.ts +229 -0
- package/ai/agent/runAgentBackground.ts +238 -0
- package/ai/agent/runAgentClientLoop.ts +138 -0
- package/ai/agent/runtimeGuidance.ts +97 -0
- package/ai/agent/runtimeServerBase.ts +37 -0
- package/ai/agent/server/fetchPublicAgents.ts +128 -0
- package/ai/agent/startParallelAgentStreams.ts +424 -0
- package/ai/agent/startupProtocol.ts +53 -0
- package/ai/agent/streamAgentChatTurn.ts +1278 -0
- package/ai/agent/streamAgentChatTurnUtils.ts +738 -0
- package/ai/agent/types.ts +71 -0
- package/ai/agent/utils/imageOutput.ts +33 -0
- package/ai/agent/utils/sortUtils.ts +250 -0
- package/ai/agent/web/referencePickerUtils.ts +146 -0
- package/ai/ai.locale.ts +1079 -0
- package/ai/chat/accumulateToolCallChunks.ts +95 -0
- package/ai/chat/fetchUtils.native.ts +276 -0
- package/ai/chat/fetchUtils.ts +153 -0
- package/ai/chat/parseApiError.ts +64 -0
- package/ai/chat/parseMultilineSSE.ts +95 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +682 -0
- package/ai/chat/sendOpenAICompletionsRequest.ts +703 -0
- package/ai/chat/sendOpenAIResponseRequest.ts +491 -0
- package/ai/chat/shouldUseServerProxy.ts +18 -0
- package/ai/chat/sseClient.native.ts +91 -0
- package/ai/chat/sseClient.ts +67 -0
- package/ai/chat/streamReader.native.ts +31 -0
- package/ai/chat/streamReader.ts +62 -0
- package/ai/chat/updateTotalUsage.ts +72 -0
- package/ai/context/buildReferenceContext.ts +437 -0
- package/ai/context/calculateContextUsage.ts +133 -0
- package/ai/context/retention.ts +165 -0
- package/ai/context/tokenUtils.ts +78 -0
- package/ai/index.ts +1 -0
- package/ai/llm/calculateGeminiImageTokens.ts +57 -0
- package/ai/llm/deepinfra.ts +28 -0
- package/ai/llm/fireworks.ts +50 -0
- package/ai/llm/generateRequestBody.ts +165 -0
- package/ai/llm/getModelContextWindow.ts +84 -0
- package/ai/llm/getNoloKey.ts +31 -0
- package/ai/llm/getPricing.ts +199 -0
- package/ai/llm/hooks/useModelPricing.ts +75 -0
- package/ai/llm/imagePricing.ts +40 -0
- package/ai/llm/isResponseAPIModel.ts +13 -0
- package/ai/llm/mimo.ts +71 -0
- package/ai/llm/mistral.ts +22 -0
- package/ai/llm/modelAvatar.ts +427 -0
- package/ai/llm/models.ts +45 -0
- package/ai/llm/openrouterModels.ts +269 -0
- package/ai/llm/providers.ts +306 -0
- package/ai/llm/reasoningModels.ts +28 -0
- package/ai/llm/types.ts +59 -0
- package/ai/llm/usageRequestOptions.ts +59 -0
- package/ai/memory/capture.ts +148 -0
- package/ai/memory/consolidate.ts +104 -0
- package/ai/memory/delete.ts +147 -0
- package/ai/memory/overlay.ts +84 -0
- package/ai/memory/query.ts +38 -0
- package/ai/memory/queryShared.ts +160 -0
- package/ai/memory/rank.ts +105 -0
- package/ai/memory/recentRelationshipRecap.ts +249 -0
- package/ai/memory/remember.ts +167 -0
- package/ai/memory/runtime.ts +76 -0
- package/ai/memory/store.ts +20 -0
- package/ai/memory/storeShared.ts +76 -0
- package/ai/memory/types.ts +46 -0
- package/ai/memory/understanding.ts +349 -0
- package/ai/memory/understandingGreeting.ts +264 -0
- package/ai/messages/type.ts +20 -0
- package/ai/policy/personalizationDialog.ts +333 -0
- package/ai/policy/runtimePolicy.ts +440 -0
- package/ai/policy/selfUpdateFields.ts +48 -0
- package/ai/policy/types.ts +64 -0
- package/ai/skills/referenceRuntime.ts +274 -0
- package/ai/skills/skillDiagnostics.ts +251 -0
- package/ai/skills/skillDocBuilder.ts +139 -0
- package/ai/skills/skillDocProtocol.ts +434 -0
- package/ai/skills/skillReferenceSummary.ts +63 -0
- package/ai/skills/skillSummaryMarker.ts +26 -0
- package/ai/token/calculatePrice.ts +544 -0
- package/ai/token/db.ts +98 -0
- package/ai/token/externalToolCost.ts +330 -0
- package/ai/token/hooks/useRecords.ts +65 -0
- package/ai/token/missingUsageEstimate.ts +42 -0
- package/ai/token/modelUsageQuery.ts +252 -0
- package/ai/token/normalizeUsage.ts +84 -0
- package/ai/token/openaiImageGenerationUsage.ts +56 -0
- package/ai/token/prepareTokenUsageData.ts +88 -0
- package/ai/token/query.ts +88 -0
- package/ai/token/queryUserTokens.ts +59 -0
- package/ai/token/resolveBillingTarget.ts +52 -0
- package/ai/token/saveTokenRecord.ts +53 -0
- package/ai/token/serverDialogProjection.ts +78 -0
- package/ai/token/serverTokenWriter.ts +143 -0
- package/ai/token/stats.ts +21 -0
- package/ai/token/tokenThunks.ts +24 -0
- package/ai/token/types.ts +93 -0
- package/ai/tools/agent/agentTools.ts +176 -0
- package/ai/tools/agent/agentUpdateShared.ts +311 -0
- package/ai/tools/agent/callAgentTool.ts +139 -0
- package/ai/tools/agent/createAgentTool.ts +512 -0
- package/ai/tools/agent/createDialogTool.ts +69 -0
- package/ai/tools/agent/createSkillAgentTool.ts +62 -0
- package/ai/tools/agent/parallelBudget.ts +221 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +145 -0
- package/ai/tools/agent/runLlmTool.ts +96 -0
- package/ai/tools/agent/runStreamingAgentTool.ts +73 -0
- package/ai/tools/agent/skillAgentArgs.ts +106 -0
- package/ai/tools/agent/skillAgentPreset.ts +89 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +122 -0
- package/ai/tools/agent/updateAgentTool.ts +96 -0
- package/ai/tools/agent/updateSelfTool.ts +113 -0
- package/ai/tools/amazonProductScraperTool.ts +86 -0
- package/ai/tools/apifyActorClient.ts +45 -0
- package/ai/tools/appEditGuard.ts +372 -0
- package/ai/tools/appReadSnapshot.ts +153 -0
- package/ai/tools/appTools.ts +1549 -0
- package/ai/tools/applyEditTool.ts +256 -0
- package/ai/tools/applyLineEditsTool.ts +312 -0
- package/ai/tools/browserTools/click.ts +33 -0
- package/ai/tools/browserTools/closeSession.ts +29 -0
- package/ai/tools/browserTools/common.ts +27 -0
- package/ai/tools/browserTools/openSession.ts +48 -0
- package/ai/tools/browserTools/readContent.ts +38 -0
- package/ai/tools/browserTools/selectOption.ts +46 -0
- package/ai/tools/browserTools/typeText.ts +42 -0
- package/ai/tools/category/createCategoryTool.ts +66 -0
- package/ai/tools/category/queryContentsByCategoryTool.ts +69 -0
- package/ai/tools/category/updateContentCategoryTool.ts +75 -0
- package/ai/tools/cfBrowserTools.ts +319 -0
- package/ai/tools/cfSpeechToTextTool.ts +49 -0
- package/ai/tools/checkEnvTool.ts +65 -0
- package/ai/tools/cloudflareCrawlTool.ts +289 -0
- package/ai/tools/codeSearchTool.ts +111 -0
- package/ai/tools/codeTools.ts +101 -0
- package/ai/tools/createDocTool.ts +132 -0
- package/ai/tools/createPlanTool.ts +999 -0
- package/ai/tools/createSkillDocTool.ts +155 -0
- package/ai/tools/createWorkflowTool.ts +154 -0
- package/ai/tools/deepseekOcrTool.ts +34 -0
- package/ai/tools/delayTool.ts +31 -0
- package/ai/tools/deleteSpacesTool.ts +325 -0
- package/ai/tools/deleteSpacesToolModel.ts +159 -0
- package/ai/tools/devReloadUtils.ts +29 -0
- package/ai/tools/dialogMessageSearch.ts +137 -0
- package/ai/tools/doctorSkillTool.ts +72 -0
- package/ai/tools/ecommerceScraperTool.ts +86 -0
- package/ai/tools/emailTools.ts +549 -0
- package/ai/tools/evalSkillTool.ts +92 -0
- package/ai/tools/exaSearchTool.ts +64 -0
- package/ai/tools/execBashTool.ts +379 -0
- package/ai/tools/executeSqlTool.ts +192 -0
- package/ai/tools/fetchWebpageSupport.ts +309 -0
- package/ai/tools/fetchWebpageTool.ts +84 -0
- package/ai/tools/geminiImagePreviewTool.ts +361 -0
- package/ai/tools/generateDocxTool.ts +215 -0
- package/ai/tools/googleSearchScraperTool.ts +106 -0
- package/ai/tools/importDataTool.ts +133 -0
- package/ai/tools/importSkillTool.ts +162 -0
- package/ai/tools/index.ts +1858 -0
- package/ai/tools/listFilesTool.ts +82 -0
- package/ai/tools/listUserSpacesTool.ts +113 -0
- package/ai/tools/modelUsageTools.ts +142 -0
- package/ai/tools/olmOcrTool.ts +34 -0
- package/ai/tools/openaiImageTool.ts +218 -0
- package/ai/tools/paddleOcrTool.ts +34 -0
- package/ai/tools/prepareTools.ts +23 -0
- package/ai/tools/readDocTool.ts +84 -0
- package/ai/tools/readFileTool.ts +211 -0
- package/ai/tools/readTool.ts +163 -0
- package/ai/tools/readXPostTool.ts +233 -0
- package/ai/tools/rememberMemoryTool.ts +84 -0
- package/ai/tools/remotionVideoTool.ts +151 -0
- package/ai/tools/searchDialogMessagesTool.ts +222 -0
- package/ai/tools/searchRepoTool.ts +115 -0
- package/ai/tools/searchWorkspaceTool.ts +259 -0
- package/ai/tools/skillFollowup.ts +86 -0
- package/ai/tools/surfWeatherTool.ts +169 -0
- package/ai/tools/table/addTableRowTool.ts +217 -0
- package/ai/tools/table/createTableTool.ts +315 -0
- package/ai/tools/table/rowTools.ts +366 -0
- package/ai/tools/table/schemaTools.ts +244 -0
- package/ai/tools/table/shareTableTool.ts +148 -0
- package/ai/tools/table/toolShared.ts +129 -0
- package/ai/tools/toolApiClient.ts +198 -0
- package/ai/tools/toolNameAliases.ts +57 -0
- package/ai/tools/toolResultError.ts +42 -0
- package/ai/tools/toolRunSlice.ts +303 -0
- package/ai/tools/toolSchemaCompatibility.ts +53 -0
- package/ai/tools/toolVisibility.ts +4 -0
- package/ai/tools/types.ts +20 -0
- package/ai/tools/uiAskChoiceTool.ts +104 -0
- package/ai/tools/updateContentTitleTool.ts +84 -0
- package/ai/tools/updateDocTool.ts +105 -0
- package/ai/tools/updateUserPreferenceProfileTool.ts +145 -0
- package/ai/tools/whisperTool.ts +77 -0
- package/ai/tools/writeFileTool.ts +210 -0
- package/ai/tools/youtubeScraperTool.ts +116 -0
- package/ai/tools/ziweiChartTool.ts +678 -0
- package/ai/types.ts +55 -0
- package/ai/workflow/workflowExecutor.ts +323 -0
- package/ai/workflow/workflowSlice.ts +73 -0
- package/ai/workflow/workflowTypes.ts +106 -0
- package/client/compactDialog.ts +222 -0
- package/connector-experimental/capabilities.ts +73 -0
- package/connector-experimental/codexBinary.ts +41 -0
- package/connector-experimental/heartbeatLoop.ts +22 -0
- package/connector-experimental/index.ts +5 -0
- package/connector-experimental/machineInfo.ts +46 -0
- package/connector-experimental/protocol.ts +54 -0
- package/machineCommands.ts +4 -4
- package/package.json +22 -6
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// packages/cli/client/compactDialog.ts
|
|
2
|
+
// HTTP-only compact helper for CLI TUI (no Redux store available).
|
|
3
|
+
|
|
4
|
+
import { ulid } from "ulid";
|
|
5
|
+
|
|
6
|
+
const DB_PATH = "/api/v1/db";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract userId from a JWT-style auth token without verifying the signature.
|
|
10
|
+
* Mirrors the logic of `parseToken` in `auth/token.ts` without the crypto imports.
|
|
11
|
+
* @internal - exported for testing only
|
|
12
|
+
*/
|
|
13
|
+
export function parseTokenUserId(token: string): string | null {
|
|
14
|
+
try {
|
|
15
|
+
const parts = token.split(".");
|
|
16
|
+
if (parts.length < 2) return null;
|
|
17
|
+
const payloadBase64 = parts[1];
|
|
18
|
+
const payload = JSON.parse(
|
|
19
|
+
Buffer.from(payloadBase64, "base64").toString("utf8")
|
|
20
|
+
);
|
|
21
|
+
return typeof payload?.userId === "string" ? payload.userId : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract the custom ID (ULID) from a dialog key like `dialog-{userId}-{id}`.
|
|
29
|
+
* Mirrors `extractCustomId` from `core/prefix` without importing it.
|
|
30
|
+
*/
|
|
31
|
+
function extractCustomId(key: string): string {
|
|
32
|
+
const parts = key.split("-");
|
|
33
|
+
return parts.slice(2).join("-");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readDialogRecord(
|
|
37
|
+
fetchImpl: typeof fetch,
|
|
38
|
+
serverUrl: string,
|
|
39
|
+
authToken: string,
|
|
40
|
+
dialogKey: string
|
|
41
|
+
): Promise<Record<string, unknown>> {
|
|
42
|
+
const res = await fetchImpl(`${serverUrl}${DB_PATH}/read/${dialogKey}`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
throw new Error(`Failed to read dialog "${dialogKey}": HTTP ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
return data as Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fields carried forward from the source dialog into the fork.
|
|
54
|
+
* Conversation summary/compression state is intentionally excluded so the new
|
|
55
|
+
* dialog starts clean: summary, summarizedBeforeId, proactiveSummary,
|
|
56
|
+
* proactiveSummaryBeforeId, compressionCount, summaryPending must NOT be
|
|
57
|
+
* inherited — they describe the old conversation, not the fork.
|
|
58
|
+
*/
|
|
59
|
+
const FORKED_CARRY_FIELDS = [
|
|
60
|
+
"cybots",
|
|
61
|
+
"type",
|
|
62
|
+
"title",
|
|
63
|
+
"spaceId",
|
|
64
|
+
"category",
|
|
65
|
+
"referenceKeys",
|
|
66
|
+
"triggerType",
|
|
67
|
+
"schedule",
|
|
68
|
+
"taskPrompt",
|
|
69
|
+
"executionMode",
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
function buildForkedDialogRecord(
|
|
73
|
+
current: Record<string, unknown>,
|
|
74
|
+
userId: string
|
|
75
|
+
): Record<string, unknown> & { dbKey: string; id: string } {
|
|
76
|
+
const newId = ulid();
|
|
77
|
+
const dbKey = `dialog-${userId}-${newId}`;
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
|
|
80
|
+
// Explicitly pick only the allowed fields — never spread `current` wholesale.
|
|
81
|
+
const carried: Record<string, unknown> = {};
|
|
82
|
+
for (const field of FORKED_CARRY_FIELDS) {
|
|
83
|
+
if (current[field] !== undefined) {
|
|
84
|
+
carried[field] = current[field];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
...carried,
|
|
90
|
+
id: newId,
|
|
91
|
+
dbKey,
|
|
92
|
+
inheritedFromDialogKey: current.dbKey,
|
|
93
|
+
inheritedFromDialogTitle: current.title,
|
|
94
|
+
createdAt: now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
// reset per-dialog stats
|
|
97
|
+
inputTokens: 0,
|
|
98
|
+
outputTokens: 0,
|
|
99
|
+
totalCost: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function writeDialogRecord(
|
|
104
|
+
fetchImpl: typeof fetch,
|
|
105
|
+
serverUrl: string,
|
|
106
|
+
authToken: string,
|
|
107
|
+
record: Record<string, unknown> & { dbKey: string }
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const res = await fetchImpl(`${serverUrl}${DB_PATH}/write/`, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: {
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
Authorization: `Bearer ${authToken}`,
|
|
114
|
+
},
|
|
115
|
+
body: JSON.stringify({ data: record, customKey: record.dbKey }),
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Failed to write forked dialog "${record.dbKey}": HTTP ${res.status}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Best-effort: register the forked dialog in the space sidebar.
|
|
126
|
+
* Failure here is non-fatal since the dialog is already stored.
|
|
127
|
+
*/
|
|
128
|
+
async function addDialogToSpaceIfNeeded(
|
|
129
|
+
fetchImpl: typeof fetch,
|
|
130
|
+
serverUrl: string,
|
|
131
|
+
authToken: string,
|
|
132
|
+
record: Record<string, unknown> & { dbKey: string }
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const rawSpaceId = record.spaceId;
|
|
135
|
+
if (!rawSpaceId || typeof rawSpaceId !== "string") return;
|
|
136
|
+
|
|
137
|
+
const normalizedSpaceId = rawSpaceId.startsWith("space-")
|
|
138
|
+
? rawSpaceId.slice("space-".length)
|
|
139
|
+
: rawSpaceId;
|
|
140
|
+
const spaceKey = `space-${normalizedSpaceId}`;
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
|
|
143
|
+
const contentEntry = {
|
|
144
|
+
title: typeof record.title === "string" ? record.title : record.id,
|
|
145
|
+
type: "dialog",
|
|
146
|
+
contentKey: record.dbKey,
|
|
147
|
+
pinned: false,
|
|
148
|
+
createdAt: now,
|
|
149
|
+
updatedAt: now,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetchImpl(`${serverUrl}${DB_PATH}/patch/${spaceKey}`, {
|
|
154
|
+
method: "PATCH",
|
|
155
|
+
headers: {
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
Authorization: `Bearer ${authToken}`,
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify({ contents: { [record.dbKey]: contentEntry } }),
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
console.warn(
|
|
163
|
+
`[nolo] compact: addDialogToSpace failed for ${spaceKey}: HTTP ${res.status}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn(`[nolo] compact: addDialogToSpace error: ${error}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type CompactDialogResult = {
|
|
172
|
+
dialogId: string;
|
|
173
|
+
dialogKey: string;
|
|
174
|
+
spaceId?: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Compact the current dialog by forking it:
|
|
179
|
+
* 1. Read the current dialog config from the server.
|
|
180
|
+
* 2. Build a new dialog record that inherits from the old one.
|
|
181
|
+
* 3. Write the new record to the server.
|
|
182
|
+
* 4. Register the new dialog in the space sidebar (best-effort).
|
|
183
|
+
*
|
|
184
|
+
* Returns the new dialog's ID so the TUI can switch to it.
|
|
185
|
+
*/
|
|
186
|
+
export async function compactDialog(options: {
|
|
187
|
+
serverUrl: string;
|
|
188
|
+
authToken: string;
|
|
189
|
+
dialogId: string;
|
|
190
|
+
fetchImpl?: typeof fetch;
|
|
191
|
+
}): Promise<CompactDialogResult> {
|
|
192
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
193
|
+
|
|
194
|
+
const userId = parseTokenUserId(options.authToken);
|
|
195
|
+
if (!userId) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
"[nolo] compact: cannot compact — invalid or missing auth token"
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const dialogKey = `dialog-${userId}-${options.dialogId}`;
|
|
202
|
+
const current = await readDialogRecord(
|
|
203
|
+
fetchImpl,
|
|
204
|
+
options.serverUrl,
|
|
205
|
+
options.authToken,
|
|
206
|
+
dialogKey
|
|
207
|
+
);
|
|
208
|
+
const next = buildForkedDialogRecord(current, userId);
|
|
209
|
+
await writeDialogRecord(fetchImpl, options.serverUrl, options.authToken, next);
|
|
210
|
+
await addDialogToSpaceIfNeeded(
|
|
211
|
+
fetchImpl,
|
|
212
|
+
options.serverUrl,
|
|
213
|
+
options.authToken,
|
|
214
|
+
next
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
dialogId: extractCustomId(next.dbKey),
|
|
219
|
+
dialogKey: next.dbKey,
|
|
220
|
+
spaceId: typeof next.spaceId === "string" ? next.spaceId : undefined,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveLaunchableCodexCommand } from "./codexBinary";
|
|
5
|
+
|
|
6
|
+
type EnvLike = Record<string, string | undefined>;
|
|
7
|
+
|
|
8
|
+
type DetectRuntimeCapabilitiesOptions = {
|
|
9
|
+
commandExists?: (command: string) => boolean;
|
|
10
|
+
commandLaunchable?: (command: string, args: string[]) => boolean;
|
|
11
|
+
env?: EnvLike;
|
|
12
|
+
probeLaunchable?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type CommandProbe = {
|
|
16
|
+
command: string;
|
|
17
|
+
args: string[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const COMMAND_CAPABILITIES: Array<[probes: CommandProbe[], capability: string]> = [
|
|
21
|
+
[[{ command: "codex", args: ["--version"] }], "codex-cli"],
|
|
22
|
+
[[{ command: "claude", args: ["--version"] }], "claude-code"],
|
|
23
|
+
[
|
|
24
|
+
[
|
|
25
|
+
{ command: "gh", args: ["--version"] },
|
|
26
|
+
{ command: "gh", args: ["copilot", "--", "--help"] },
|
|
27
|
+
],
|
|
28
|
+
"copilot-cli",
|
|
29
|
+
],
|
|
30
|
+
[[{ command: "gemini", args: ["--version"] }], "gemini-cli"],
|
|
31
|
+
[[{ command: "kimi", args: ["--version"] }], "kimi-cli"],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function defaultCommandExists(command: string) {
|
|
35
|
+
const pathEntries = (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
|
|
36
|
+
const extensions = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
37
|
+
return pathEntries.some((entry) =>
|
|
38
|
+
extensions.some((extension) => existsSync(join(entry, `${command}${extension}`)))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultCommandLaunchable(command: string, args: string[]) {
|
|
43
|
+
const executable = command === "codex" ? resolveLaunchableCodexCommand() : command;
|
|
44
|
+
const result = spawnSync(executable, args, {
|
|
45
|
+
stdio: "pipe",
|
|
46
|
+
timeout: 3_000,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
});
|
|
49
|
+
return !result.error && result.status === 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function detectRuntimeCapabilities(
|
|
53
|
+
options: DetectRuntimeCapabilitiesOptions = {}
|
|
54
|
+
): string[] {
|
|
55
|
+
const commandExists = options.commandExists ?? defaultCommandExists;
|
|
56
|
+
const commandLaunchable = options.commandLaunchable ?? defaultCommandLaunchable;
|
|
57
|
+
const env = options.env ?? process.env;
|
|
58
|
+
const capabilities: string[] = [];
|
|
59
|
+
|
|
60
|
+
for (const [probes, capability] of COMMAND_CAPABILITIES) {
|
|
61
|
+
if (!probes.every((probe) => commandExists(probe.command))) continue;
|
|
62
|
+
if (options.probeLaunchable && !probes.every((probe) => commandLaunchable(probe.command, probe.args))) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
capabilities.push(capability);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (env.NOLO_LOCAL_LLM_ENDPOINT || env.LLAMA_SERVER_URL) {
|
|
69
|
+
capabilities.push("local-llm");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return capabilities;
|
|
73
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
type EnvLike = Record<string, string | undefined>;
|
|
6
|
+
|
|
7
|
+
const WINDOWS_APPS_CODEX_PATTERN = /\\WindowsApps\\OpenAI\.Codex_[^\\]+\\app\\resources$/i;
|
|
8
|
+
|
|
9
|
+
function pathEntries(env: EnvLike) {
|
|
10
|
+
return (env.PATH ?? process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function findWindowsAppsCodex(env: EnvLike) {
|
|
14
|
+
if (process.platform !== "win32") return "";
|
|
15
|
+
for (const entry of pathEntries(env)) {
|
|
16
|
+
if (!WINDOWS_APPS_CODEX_PATTERN.test(entry)) continue;
|
|
17
|
+
const candidate = join(entry, "codex.exe");
|
|
18
|
+
if (existsSync(candidate)) return candidate;
|
|
19
|
+
}
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function copyCodexToUserBin(source: string, env: EnvLike) {
|
|
24
|
+
const target =
|
|
25
|
+
env.NOLO_CODEX_SHIM_PATH ??
|
|
26
|
+
join(homedir(), ".nolo", "bin", "codex.exe");
|
|
27
|
+
if (existsSync(target)) return target;
|
|
28
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
29
|
+
copyFileSync(source, target);
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveLaunchableCodexCommand(env: EnvLike = process.env) {
|
|
34
|
+
const explicit = env.NOLO_CODEX_BIN?.trim();
|
|
35
|
+
if (explicit) return explicit;
|
|
36
|
+
|
|
37
|
+
const windowsAppsCodex = findWindowsAppsCodex(env);
|
|
38
|
+
if (windowsAppsCodex) return copyCodexToUserBin(windowsAppsCodex, env);
|
|
39
|
+
|
|
40
|
+
return "codex";
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type HeartbeatLoopOptions = {
|
|
2
|
+
intervalMs: number;
|
|
3
|
+
sendHeartbeat: () => Promise<void>;
|
|
4
|
+
sleep?: (ms: number) => Promise<void>;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
maxBeats?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
|
|
11
|
+
export async function runHeartbeatLoop(options: HeartbeatLoopOptions): Promise<void> {
|
|
12
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
13
|
+
let beats = 0;
|
|
14
|
+
|
|
15
|
+
while (!options.signal?.aborted) {
|
|
16
|
+
await options.sendHeartbeat();
|
|
17
|
+
beats += 1;
|
|
18
|
+
if (options.maxBeats && beats >= options.maxBeats) return;
|
|
19
|
+
if (options.signal?.aborted) return;
|
|
20
|
+
await sleep(options.intervalMs);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { arch, hostname, homedir, platform } from "node:os";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import { detectRuntimeCapabilities } from "./capabilities";
|
|
7
|
+
import type { MachineHeartbeat } from "./protocol";
|
|
8
|
+
|
|
9
|
+
const CONNECTOR_VERSION = "0.1.0-experimental";
|
|
10
|
+
|
|
11
|
+
function defaultMachineIdPath() {
|
|
12
|
+
return join(homedir(), ".nolo", "machine-id");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveMachineId(path = defaultMachineIdPath()) {
|
|
16
|
+
try {
|
|
17
|
+
if (existsSync(path)) {
|
|
18
|
+
const existing = readFileSync(path, "utf8").trim();
|
|
19
|
+
if (existing) return existing;
|
|
20
|
+
}
|
|
21
|
+
const next = `machine-${randomUUID()}`;
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
writeFileSync(path, `${next}\n`, "utf8");
|
|
24
|
+
return next;
|
|
25
|
+
} catch {
|
|
26
|
+
return `machine-${hostname().toLowerCase()}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function detectMachineInfo(overrides?: {
|
|
31
|
+
machineId?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
capabilities?: string[];
|
|
34
|
+
probeLaunchable?: boolean;
|
|
35
|
+
}): MachineHeartbeat {
|
|
36
|
+
return {
|
|
37
|
+
machineId: overrides?.machineId ?? resolveMachineId(),
|
|
38
|
+
name: overrides?.name ?? hostname(),
|
|
39
|
+
platform: platform(),
|
|
40
|
+
arch: arch(),
|
|
41
|
+
connectorVersion: CONNECTOR_VERSION,
|
|
42
|
+
capabilities: overrides?.capabilities ?? detectRuntimeCapabilities({
|
|
43
|
+
probeLaunchable: overrides?.probeLaunchable,
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type MachineHeartbeatInput = {
|
|
2
|
+
machineId?: unknown;
|
|
3
|
+
name?: unknown;
|
|
4
|
+
platform?: unknown;
|
|
5
|
+
arch?: unknown;
|
|
6
|
+
connectorVersion?: unknown;
|
|
7
|
+
capabilities?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type MachineHeartbeat = {
|
|
11
|
+
machineId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
platform: string;
|
|
14
|
+
arch: string;
|
|
15
|
+
connectorVersion?: string;
|
|
16
|
+
capabilities: string[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const normalizeRequiredString = (value: unknown, field: string) => {
|
|
20
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
21
|
+
throw new Error(`${field} is required`);
|
|
22
|
+
}
|
|
23
|
+
return value.trim();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const normalizeOptionalString = (value: unknown) =>
|
|
27
|
+
typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
28
|
+
|
|
29
|
+
export function normalizeCapabilityList(value: unknown): string[] {
|
|
30
|
+
if (!Array.isArray(value)) return [];
|
|
31
|
+
const seen = new Set<string>();
|
|
32
|
+
const result: string[] = [];
|
|
33
|
+
for (const item of value) {
|
|
34
|
+
if (typeof item !== "string") continue;
|
|
35
|
+
const normalized = item.trim();
|
|
36
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
37
|
+
seen.add(normalized);
|
|
38
|
+
result.push(normalized);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeMachineHeartbeat(input: MachineHeartbeatInput): MachineHeartbeat {
|
|
44
|
+
const heartbeat: MachineHeartbeat = {
|
|
45
|
+
machineId: normalizeRequiredString(input.machineId, "machineId"),
|
|
46
|
+
name: normalizeRequiredString(input.name, "name"),
|
|
47
|
+
platform: normalizeRequiredString(input.platform, "platform"),
|
|
48
|
+
arch: normalizeRequiredString(input.arch, "arch"),
|
|
49
|
+
capabilities: normalizeCapabilityList(input.capabilities),
|
|
50
|
+
};
|
|
51
|
+
const connectorVersion = normalizeOptionalString(input.connectorVersion);
|
|
52
|
+
if (connectorVersion) heartbeat.connectorVersion = connectorVersion;
|
|
53
|
+
return heartbeat;
|
|
54
|
+
}
|
package/machineCommands.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MachineHeartbeat } from "connector-experimental/protocol";
|
|
2
|
-
import { detectMachineInfo } from "connector-experimental/machineInfo";
|
|
1
|
+
import type { MachineHeartbeat } from "./connector-experimental/protocol";
|
|
2
|
+
import { detectMachineInfo } from "./connector-experimental/machineInfo";
|
|
3
3
|
import { mkdirSync, openSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
@@ -7,12 +7,12 @@ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
|
7
7
|
import {
|
|
8
8
|
type HeartbeatLoopOptions,
|
|
9
9
|
runHeartbeatLoop as defaultRunHeartbeatLoop,
|
|
10
|
-
} from "connector-experimental/heartbeatLoop";
|
|
10
|
+
} from "./connector-experimental/heartbeatLoop";
|
|
11
11
|
import {
|
|
12
12
|
assertMachineRunAllowed,
|
|
13
13
|
buildMachinePermissionPromptBlock,
|
|
14
14
|
resolveMachineRunPermissionPolicy,
|
|
15
|
-
} from "
|
|
15
|
+
} from "./ai/agent/machineRunPermissions";
|
|
16
16
|
import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
|
|
17
17
|
|
|
18
18
|
type EnvLike = Record<string, string | undefined>;
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nolo-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first terminal workspace for Nolo",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nolo": "index.ts"
|
|
8
8
|
},
|
|
9
9
|
"module": "index.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build:publish": "bun build.ts"
|
|
12
|
+
},
|
|
10
13
|
"files": [
|
|
11
14
|
"index.ts",
|
|
12
15
|
"agentRuntimeCommands.ts",
|
|
@@ -17,10 +20,13 @@
|
|
|
17
20
|
"machineCommands.ts",
|
|
18
21
|
"updateCommands.ts",
|
|
19
22
|
"client/agentRun.ts",
|
|
23
|
+
"client/compactDialog.ts",
|
|
20
24
|
"client/profileConfig.ts",
|
|
21
25
|
"tui/readlineWorkspace.ts",
|
|
22
26
|
"tui/session.ts",
|
|
23
|
-
"README.md"
|
|
27
|
+
"README.md",
|
|
28
|
+
"ai/**/*.ts",
|
|
29
|
+
"connector-experimental/**/*.ts"
|
|
24
30
|
],
|
|
25
31
|
"publishConfig": {
|
|
26
32
|
"access": "public"
|
|
@@ -28,11 +34,21 @@
|
|
|
28
34
|
"devDependencies": {
|
|
29
35
|
"bun-types": "latest"
|
|
30
36
|
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"ai": "workspace:*",
|
|
33
|
-
"connector-experimental": "workspace:*"
|
|
34
|
-
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"typescript": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@reduxjs/toolkit": "^2.5.0",
|
|
42
|
+
"iztro": "^2.5.8",
|
|
43
|
+
"ulid": "^2.3.0",
|
|
44
|
+
"pino": "^9.9.0",
|
|
45
|
+
"react": "^19.2.1",
|
|
46
|
+
"i18next": "^23.11.5",
|
|
47
|
+
"zod": "^3.25.20",
|
|
48
|
+
"react-hook-form": "^7.51.5",
|
|
49
|
+
"@hookform/resolvers": "^3.3.2",
|
|
50
|
+
"react-i18next": "^14.1.2",
|
|
51
|
+
"react-hot-toast": "^2.4.1",
|
|
52
|
+
"react-native-sse": "^1.2.1"
|
|
37
53
|
}
|
|
38
54
|
}
|