neoctl 0.1.20 → 0.1.22

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.
@@ -1,2 +1,241 @@
1
1
  #!/usr/bin/env node
2
+ import { type ServerResponse } from "node:http";
3
+ import { QueryEngine } from "../core/query-engine.js";
4
+ import { type ModelProviderName } from "../model/config.js";
5
+ import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
6
+ import type { ModelUsage, ReasoningConfig } from "../model/model-gateway.js";
7
+ import { ToolRegistry } from "../tools/registry.js";
8
+ import { type AgentToolRuntime } from "../agents/agent-tool.js";
9
+ import { TaskStore } from "../tasks/task-store.js";
10
+ import type { ContextMetrics } from "../types/events.js";
11
+ export interface WebRuntime {
12
+ engine: QueryEngine;
13
+ communicationLogger: CommunicationLogger;
14
+ modelGateway: LoggingModelGateway;
15
+ agentRuntime: AgentToolRuntime;
16
+ usage: SessionUsageTracker;
17
+ taskStore: TaskStore;
18
+ tools: ToolRegistry;
19
+ initialMetrics: ContextMetrics;
20
+ defaultReasoning?: ReasoningConfig | null;
21
+ envPath: string;
22
+ envNotice?: string;
23
+ }
24
+ export interface UsageTotals {
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ totalTokens: number;
28
+ reasoningTokens: number;
29
+ cachedTokens: number;
30
+ requests: number;
31
+ computedTotalTokens: boolean;
32
+ }
33
+ export declare class SessionUsageTracker {
34
+ private totals;
35
+ private lastUsage?;
36
+ add(usage: ModelUsage): void;
37
+ reset(): void;
38
+ snapshot(): UsageTotals;
39
+ }
40
+ interface UiLineImage {
41
+ src: string;
42
+ label?: string;
43
+ mimeType: string;
44
+ }
45
+ interface UiLine {
46
+ id: number;
47
+ kind: "system" | "user" | "assistant" | "thinking" | "tool" | "error" | "meta";
48
+ text: string;
49
+ title?: string;
50
+ bodyTitle?: string;
51
+ titleStatus?: "success" | "failure";
52
+ format?: "markdown" | "ansi" | "plain" | "diff";
53
+ previewStyle?: "summary";
54
+ summaryMaxLines?: number;
55
+ live?: boolean;
56
+ pendingReplacement?: boolean;
57
+ collapsible?: boolean;
58
+ image?: UiLineImage;
59
+ }
60
+ interface UiStatus {
61
+ phase: string;
62
+ detail?: string;
63
+ metrics?: ContextMetrics;
64
+ usage?: ModelUsage;
65
+ streamedOutputTokens: number;
66
+ activityTick: number;
67
+ inputTokenUpdatedAt?: number;
68
+ outputTokenUpdatedAt?: number;
69
+ retryCooldownUntil?: number;
70
+ }
71
+ export interface WebServerOptions {
72
+ host: string;
73
+ port: number;
74
+ }
75
+ export interface CreateWebRuntimeOptions {
76
+ /** Override the initial session id for this runtime. */
77
+ sessionId?: string;
78
+ /** Override whether the initial session should resume transcript history. */
79
+ resume?: boolean;
80
+ /** Override the QueryEngine agent id. Defaults to main. */
81
+ agentId?: string;
82
+ }
83
+ export interface WebRuntimeScope {
84
+ /** Browser-tab or client-instance identifier. Omit for the legacy singleton runtime. */
85
+ tabId?: string;
86
+ /** Optional session id used when a scoped runtime is created after page refresh/process restart. */
87
+ sessionId?: string;
88
+ }
89
+ export interface WebRuntimeRouterOptions {
90
+ createRuntime?: (options?: CreateWebRuntimeOptions) => Promise<WebRuntime>;
91
+ createRepl?: (runtime: WebRuntime) => WebRepl;
92
+ }
93
+ type LoginProviderName = ModelProviderName;
94
+ interface LoginFieldDefinition {
95
+ key: string;
96
+ label: string;
97
+ envKey: string;
98
+ scope: "provider" | "shared";
99
+ required?: boolean;
100
+ secret?: boolean;
101
+ placeholder?: string;
102
+ options?: readonly string[];
103
+ }
104
+ interface LoginFormPayload {
105
+ envPath: string;
106
+ providers: LoginProviderName[];
107
+ provider: LoginProviderName;
108
+ fields: LoginFieldDefinition[];
109
+ values: Record<string, string>;
110
+ }
111
+ interface WebAttachmentPayload {
112
+ kind: "image";
113
+ label: string;
114
+ mimeType: string;
115
+ data: string;
116
+ }
2
117
  export declare function runWebServer(argv?: string[]): Promise<void>;
118
+ export declare function createWebRuntime(options?: CreateWebRuntimeOptions): Promise<WebRuntime>;
119
+ export declare class WebRuntimeRouter {
120
+ private readonly repls;
121
+ private readonly createRuntime;
122
+ private readonly createRepl;
123
+ constructor(options?: WebRuntimeRouterOptions);
124
+ get(scope?: WebRuntimeScope): Promise<WebRepl>;
125
+ snapshot(scope?: WebRuntimeScope, includeCatalog?: boolean): Promise<ReturnType<WebRepl["snapshot"]>>;
126
+ activeScopes(): string[];
127
+ }
128
+ export declare function createWebRuntimeRouter(options?: WebRuntimeRouterOptions): Promise<WebRuntimeRouter>;
129
+ export declare class WebRepl {
130
+ private runtime;
131
+ private readonly subscribers;
132
+ private lineId;
133
+ private assistantLineId;
134
+ private thinkingLineId;
135
+ private finalizedThinkingLineId;
136
+ private activeAbortController;
137
+ private interruptArmed;
138
+ private readonly toolLineIds;
139
+ private lines;
140
+ private status;
141
+ private busy;
142
+ private queuedInput;
143
+ private foregroundRun;
144
+ private foregroundRunToken;
145
+ private readonly backgroundSessionRuns;
146
+ private readonly suppressReattachedStreaming;
147
+ private backgroundTaskCount;
148
+ constructor(runtime: WebRuntime);
149
+ subscribe(res: ServerResponse): void;
150
+ snapshot(includeCatalog?: boolean): {
151
+ lines: UiLine[];
152
+ status: UiStatus;
153
+ busy: boolean;
154
+ queuedInput: string | undefined;
155
+ backgroundTaskCount: number;
156
+ backgroundTasks: {
157
+ taskId: string;
158
+ agentId: string;
159
+ type: import("../index.js").LocalAgentTaskType;
160
+ status: import("../index.js").LocalAgentTaskStatus;
161
+ description: string;
162
+ createdAt: string;
163
+ }[];
164
+ backgroundSessionRunCount: number;
165
+ runningSessionIds: string[];
166
+ session: import("../index.js").SessionStoreSnapshot | undefined;
167
+ catalog: {
168
+ commands: import("../repl/commands.js").ReplCommandDefinition[];
169
+ modelIds: string[];
170
+ reasoning: string[];
171
+ envPath: string;
172
+ } | undefined;
173
+ interactive: {
174
+ sessions: boolean;
175
+ login: LoginFormPayload;
176
+ } | undefined;
177
+ tips: import("../tips.js").AppTip[] | undefined;
178
+ tipIndex: number;
179
+ };
180
+ submit(text: string, attachments?: WebAttachmentPayload[]): Promise<{
181
+ ok: true;
182
+ } | {
183
+ ok: false;
184
+ error: string;
185
+ }>;
186
+ listSessions(): Promise<{
187
+ sessions: import("../index.js").SessionSummary[];
188
+ runningSessionIds: string[];
189
+ }>;
190
+ resumeSession(sessionId: string): Promise<{
191
+ ok: true;
192
+ } | {
193
+ ok: false;
194
+ error: string;
195
+ }>;
196
+ newSession(): Promise<{
197
+ ok: true;
198
+ } | {
199
+ ok: false;
200
+ error: string;
201
+ }>;
202
+ private refreshSessionView;
203
+ deleteSession(sessionId: string): Promise<{
204
+ ok: true;
205
+ } | {
206
+ ok: false;
207
+ error: string;
208
+ }>;
209
+ loginForm(providerValue?: string): LoginFormPayload;
210
+ saveLogin(providerValue: string, values: Record<string, string>): Promise<{
211
+ ok: true;
212
+ } | {
213
+ ok: false;
214
+ error: string;
215
+ }>;
216
+ interrupt(): {
217
+ ok: true;
218
+ interrupted: boolean;
219
+ };
220
+ private append;
221
+ private updateLine;
222
+ private replaceLineText;
223
+ private replaceLine;
224
+ private setBusy;
225
+ private setStatus;
226
+ private finalizeForegroundView;
227
+ private stopForegroundRun;
228
+ private backgroundTasks;
229
+ private detachRunningForeground;
230
+ private reattachRunningSession;
231
+ private reduce;
232
+ private finalizeLiveLine;
233
+ private finalizeThinkingLine;
234
+ private finalizeActiveToolLines;
235
+ private handleEvent;
236
+ private handleCommandOrPrompt;
237
+ private runCompaction;
238
+ private send;
239
+ private broadcastSync;
240
+ }
241
+ export {};
package/dist/web/index.js CHANGED
@@ -34,7 +34,7 @@ const highlightPackageDir = path.dirname(require.resolve("@highlightjs/cdn-asset
34
34
  const markedAssetPath = path.join(markedPackageDir, "lib", "marked.esm.js");
35
35
  const highlightAssetPath = path.join(highlightPackageDir, "highlight.min.js");
36
36
  const highlightThemeAssetPath = path.join(highlightPackageDir, "styles", "atom-one-dark.min.css");
37
- class SessionUsageTracker {
37
+ export class SessionUsageTracker {
38
38
  totals = emptyUsageTotals();
39
39
  lastUsage;
40
40
  add(usage) {
@@ -78,11 +78,11 @@ function sumUsageTokens(left, right) {
78
78
  return undefined;
79
79
  return (left ?? 0) + (right ?? 0);
80
80
  }
81
+ const DEFAULT_WEB_RUNTIME_KEY = "__default__";
81
82
  export async function runWebServer(argv = process.argv.slice(2)) {
82
83
  const options = parseWebArgs(argv);
83
- const runtime = await createRuntime();
84
- const repl = new WebRepl(runtime);
85
- const server = http.createServer((req, res) => void route(req, res, repl));
84
+ const router = await createWebRuntimeRouter();
85
+ const server = http.createServer((req, res) => void route(req, res, router));
86
86
  await new Promise((resolve) => server.listen(options.port, options.host, resolve));
87
87
  const address = server.address();
88
88
  const actualPort = typeof address === "object" && address ? address.port : options.port;
@@ -106,7 +106,7 @@ function parseWebArgs(argv) {
106
106
  port = 3000;
107
107
  return { host, port: Math.round(port) };
108
108
  }
109
- async function createRuntime() {
109
+ export async function createWebRuntime(options = {}) {
110
110
  const envLoad = loadDefaultDotEnvFiles({ override: true });
111
111
  const modelConfig = readModelProviderConfig(process.env);
112
112
  const communicationLogger = new CommunicationLogger();
@@ -137,7 +137,7 @@ async function createRuntime() {
137
137
  for (const tool of createTaskTools(taskStore, resumeHandler))
138
138
  tools.register(tool);
139
139
  const engine = new QueryEngine({
140
- agentId: "main",
140
+ agentId: options.agentId ?? "main",
141
141
  model: modelConfig?.model,
142
142
  fallbackModel: modelConfig?.fallbackModel,
143
143
  reasoning: modelConfig?.defaultReasoning,
@@ -148,9 +148,9 @@ async function createRuntime() {
148
148
  commands: replCommandDefinitions.map((command) => command.usage),
149
149
  session: {
150
150
  enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
151
- sessionId: process.env.AGENT_SESSION_ID,
151
+ sessionId: options.sessionId ?? process.env.AGENT_SESSION_ID,
152
152
  rootDir: process.env.AGENT_SESSION_DIR,
153
- resume: parseResumeFlag(process.env.AGENT_SESSION_RESUME),
153
+ resume: options.resume ?? parseResumeFlag(process.env.AGENT_SESSION_RESUME),
154
154
  toolResultThresholdChars: process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS ? Number(process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS) : undefined,
155
155
  },
156
156
  });
@@ -199,7 +199,46 @@ function parseResumeFlag(value) {
199
199
  return false;
200
200
  return ["1", "true", "yes", "latest"].includes(value.toLowerCase());
201
201
  }
202
- class WebRepl {
202
+ export class WebRuntimeRouter {
203
+ repls = new Map();
204
+ createRuntime;
205
+ createRepl;
206
+ constructor(options = {}) {
207
+ this.createRuntime = options.createRuntime ?? createWebRuntime;
208
+ this.createRepl = options.createRepl ?? ((runtime) => new WebRepl(runtime));
209
+ }
210
+ get(scope = {}) {
211
+ const key = webRuntimeScopeKey(scope);
212
+ let repl = this.repls.get(key);
213
+ if (!repl) {
214
+ repl = this.createRuntime({ sessionId: scope.sessionId, resume: scope.sessionId ? true : scope.tabId ? false : undefined }).then((runtime) => this.createRepl(runtime));
215
+ this.repls.set(key, repl);
216
+ repl.catch(() => this.repls.delete(key));
217
+ }
218
+ return repl;
219
+ }
220
+ async snapshot(scope = {}, includeCatalog = true) {
221
+ return (await this.get(scope)).snapshot(includeCatalog);
222
+ }
223
+ activeScopes() {
224
+ return [...this.repls.keys()];
225
+ }
226
+ }
227
+ export async function createWebRuntimeRouter(options = {}) {
228
+ const router = new WebRuntimeRouter(options);
229
+ await router.get();
230
+ return router;
231
+ }
232
+ function webRuntimeScopeKey(scope) {
233
+ const tabId = scope.tabId?.trim();
234
+ if (tabId)
235
+ return `tab:${tabId}`;
236
+ const sessionId = scope.sessionId?.trim();
237
+ if (sessionId)
238
+ return `session:${sessionId}`;
239
+ return DEFAULT_WEB_RUNTIME_KEY;
240
+ }
241
+ export class WebRepl {
203
242
  runtime;
204
243
  subscribers = new Set();
205
244
  lineId = 0;
@@ -801,7 +840,7 @@ function reqKeepAlive(res) {
801
840
  timer.unref?.();
802
841
  res.on("close", () => clearInterval(timer));
803
842
  }
804
- async function route(req, res, repl) {
843
+ async function route(req, res, router) {
805
844
  const url = new URL(req.url ?? "/", "http://localhost");
806
845
  try {
807
846
  if (req.method === "GET" && url.pathname === "/")
@@ -812,6 +851,8 @@ async function route(req, res, repl) {
812
851
  return sendFile(res, highlightAssetPath, "text/javascript; charset=utf-8");
813
852
  if (req.method === "GET" && url.pathname === "/vendor/highlight-theme.css")
814
853
  return sendFile(res, highlightThemeAssetPath, "text/css; charset=utf-8");
854
+ const scope = webRuntimeScopeFromUrl(url);
855
+ const repl = await router.get(scope);
815
856
  if (req.method === "GET" && url.pathname === "/events")
816
857
  return repl.subscribe(res);
817
858
  if (req.method === "GET" && url.pathname === "/api/state")
@@ -846,6 +887,16 @@ async function route(req, res, repl) {
846
887
  sendJson(res, { error: error instanceof Error ? error.message : String(error) }, 500);
847
888
  }
848
889
  }
890
+ function webRuntimeScopeFromUrl(url) {
891
+ return {
892
+ tabId: optionalSearchParam(url, "tabId"),
893
+ sessionId: optionalSearchParam(url, "sessionId"),
894
+ };
895
+ }
896
+ function optionalSearchParam(url, key) {
897
+ const value = url.searchParams.get(key)?.trim();
898
+ return value ? value : undefined;
899
+ }
849
900
  function sendHtml(res, body) {
850
901
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
851
902
  res.end(body);