palmier 0.2.5 → 0.2.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.
- package/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +288 -219
- package/dist/agents/agent.d.ts +6 -3
- package/dist/agents/agent.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +12 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +12 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +13 -9
- package/dist/agents/openclaw.d.ts +2 -2
- package/dist/agents/openclaw.js +8 -7
- package/dist/agents/shared-prompt.d.ts +5 -4
- package/dist/agents/shared-prompt.js +10 -8
- package/dist/commands/agents.js +11 -0
- package/dist/commands/init.js +109 -49
- package/dist/commands/mcpserver.js +11 -21
- package/dist/commands/plan-generation.md +24 -32
- package/dist/commands/restart.d.ts +5 -0
- package/dist/commands/restart.js +9 -0
- package/dist/commands/run.js +293 -101
- package/dist/commands/serve.js +83 -4
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/events.d.ts +10 -0
- package/dist/events.js +29 -0
- package/dist/index.js +7 -0
- package/dist/platform/linux.d.ts +2 -0
- package/dist/platform/linux.js +22 -1
- package/dist/platform/platform.d.ts +4 -0
- package/dist/platform/windows.d.ts +3 -0
- package/dist/platform/windows.js +99 -82
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +43 -28
- package/dist/spawn-command.d.ts +29 -6
- package/dist/spawn-command.js +38 -15
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +4 -2
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -43
- package/src/agents/codex.ts +11 -12
- package/src/agents/gemini.ts +12 -10
- package/src/agents/openclaw.ts +8 -7
- package/src/agents/shared-prompt.ts +10 -8
- package/src/commands/agents.ts +11 -0
- package/src/commands/init.ts +120 -56
- package/src/commands/mcpserver.ts +11 -22
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +365 -119
- package/src/commands/serve.ts +101 -5
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +38 -0
- package/src/index.ts +8 -0
- package/src/platform/linux.ts +25 -1
- package/src/platform/platform.ts +6 -0
- package/src/platform/windows.ts +100 -89
- package/src/rpc-handler.ts +46 -29
- package/src/spawn-command.ts +120 -83
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -2
package/src/rpc-handler.ts
CHANGED
|
@@ -4,11 +4,13 @@ import * as fs from "fs";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { parse as parseYaml } from "yaml";
|
|
7
|
+
import { type NatsConnection } from "nats";
|
|
7
8
|
import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, getTaskCreatedAt, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList } from "./task.js";
|
|
8
9
|
import { getPlatform } from "./platform/index.js";
|
|
9
10
|
import { spawnCommand } from "./spawn-command.js";
|
|
10
11
|
import { getAgent } from "./agents/agent.js";
|
|
11
12
|
import { validateSession } from "./session-store.js";
|
|
13
|
+
import { publishHostEvent } from "./events.js";
|
|
12
14
|
import type { HostConfig, ParsedTask, RpcMessage } from "./types.js";
|
|
13
15
|
|
|
14
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -82,12 +84,13 @@ async function generatePlan(
|
|
|
82
84
|
): Promise<{ name: string; body: string }> {
|
|
83
85
|
const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
|
|
84
86
|
const planAgent = getAgent(agentName);
|
|
85
|
-
const { command, args } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
87
|
+
const { command, args, stdin } = planAgent.getPlanGenerationCommandLine(fullPrompt);
|
|
86
88
|
console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
|
|
87
89
|
|
|
88
90
|
const { output } = await spawnCommand(command, args, {
|
|
89
91
|
cwd: projectRoot,
|
|
90
92
|
timeout: 120_000,
|
|
93
|
+
stdin,
|
|
91
94
|
});
|
|
92
95
|
|
|
93
96
|
let name = "";
|
|
@@ -109,7 +112,7 @@ async function generatePlan(
|
|
|
109
112
|
/**
|
|
110
113
|
* Create a transport-agnostic RPC handler bound to the given config.
|
|
111
114
|
*/
|
|
112
|
-
export function createRpcHandler(config: HostConfig) {
|
|
115
|
+
export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
113
116
|
function flattenTask(task: ParsedTask) {
|
|
114
117
|
const taskDir = getTaskDir(config.projectRoot, task.frontmatter.id);
|
|
115
118
|
return {
|
|
@@ -142,12 +145,13 @@ export function createRpcHandler(config: HostConfig) {
|
|
|
142
145
|
triggers?: Array<{ type: "cron" | "once"; value: string }>;
|
|
143
146
|
triggers_enabled?: boolean;
|
|
144
147
|
requires_confirmation?: boolean;
|
|
148
|
+
command?: string;
|
|
145
149
|
};
|
|
146
150
|
|
|
147
|
-
//
|
|
151
|
+
// Only generate a plan for longer prompts that benefit from it
|
|
148
152
|
let name = "";
|
|
149
153
|
let body = "";
|
|
150
|
-
if (params.user_prompt.length
|
|
154
|
+
if (params.user_prompt.length <= 50) {
|
|
151
155
|
name = params.user_prompt;
|
|
152
156
|
} else {
|
|
153
157
|
try {
|
|
@@ -171,16 +175,14 @@ export function createRpcHandler(config: HostConfig) {
|
|
|
171
175
|
triggers: params.triggers ?? [],
|
|
172
176
|
triggers_enabled: params.triggers_enabled ?? true,
|
|
173
177
|
requires_confirmation: params.requires_confirmation ?? true,
|
|
178
|
+
...(params.command ? { command: params.command } : {}),
|
|
174
179
|
},
|
|
175
180
|
body,
|
|
176
181
|
};
|
|
177
182
|
|
|
178
183
|
writeTaskFile(taskDir, task);
|
|
179
184
|
appendTaskList(config.projectRoot, id);
|
|
180
|
-
|
|
181
|
-
if (task.frontmatter.triggers_enabled) {
|
|
182
|
-
platform.installTaskTimer(config, task);
|
|
183
|
-
}
|
|
185
|
+
getPlatform().installTaskTimer(config, task);
|
|
184
186
|
|
|
185
187
|
return flattenTask(task);
|
|
186
188
|
}
|
|
@@ -193,6 +195,7 @@ export function createRpcHandler(config: HostConfig) {
|
|
|
193
195
|
triggers?: Array<{ type: "cron" | "once"; value: string }>;
|
|
194
196
|
triggers_enabled?: boolean;
|
|
195
197
|
requires_confirmation?: boolean;
|
|
198
|
+
command?: string;
|
|
196
199
|
};
|
|
197
200
|
|
|
198
201
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
@@ -210,32 +213,35 @@ export function createRpcHandler(config: HostConfig) {
|
|
|
210
213
|
if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
|
|
211
214
|
if (params.requires_confirmation !== undefined)
|
|
212
215
|
existing.frontmatter.requires_confirmation = params.requires_confirmation;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (existing.frontmatter.user_prompt.length < 50) {
|
|
217
|
-
existing.frontmatter.name = existing.frontmatter.user_prompt;
|
|
218
|
-
existing.body = "";
|
|
216
|
+
if (params.command !== undefined) {
|
|
217
|
+
if (params.command) {
|
|
218
|
+
existing.frontmatter.command = params.command;
|
|
219
219
|
} else {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
220
|
+
delete existing.frontmatter.command;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Regenerate plan if needed (only for longer prompts)
|
|
225
|
+
if (existing.frontmatter.user_prompt.length <= 50) {
|
|
226
|
+
existing.frontmatter.name = existing.frontmatter.user_prompt;
|
|
227
|
+
existing.body = "";
|
|
228
|
+
} else if (needsRegeneration) {
|
|
229
|
+
try {
|
|
230
|
+
const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
|
|
231
|
+
existing.frontmatter.name = plan.name;
|
|
232
|
+
existing.body = plan.body;
|
|
233
|
+
} catch (err: unknown) {
|
|
234
|
+
const error = err as { stdout?: string; stderr?: string };
|
|
235
|
+
return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
writeTaskFile(taskDir, existing);
|
|
232
240
|
|
|
233
|
-
// Reinstall
|
|
241
|
+
// Reinstall service and timers
|
|
234
242
|
const platform = getPlatform();
|
|
235
243
|
platform.removeTaskTimer(params.id);
|
|
236
|
-
|
|
237
|
-
platform.installTaskTimer(config, existing);
|
|
238
|
-
}
|
|
244
|
+
platform.installTaskTimer(config, existing);
|
|
239
245
|
|
|
240
246
|
return flattenTask(existing);
|
|
241
247
|
}
|
|
@@ -263,14 +269,25 @@ export function createRpcHandler(config: HostConfig) {
|
|
|
263
269
|
|
|
264
270
|
case "task.abort": {
|
|
265
271
|
const params = request.params as { id: string };
|
|
272
|
+
// Write abort status BEFORE killing so the dying process's signal
|
|
273
|
+
// handler can detect this was RPC-initiated and skip publishing.
|
|
274
|
+
const abortTaskDir = getTaskDir(config.projectRoot, params.id);
|
|
275
|
+
writeTaskStatus(abortTaskDir, {
|
|
276
|
+
running_state: "aborted",
|
|
277
|
+
time_stamp: Date.now(),
|
|
278
|
+
});
|
|
266
279
|
try {
|
|
267
280
|
await getPlatform().stopTask(params.id);
|
|
268
|
-
return { ok: true, task_id: params.id };
|
|
269
281
|
} catch (err: unknown) {
|
|
270
282
|
const e = err as { stderr?: string; message?: string };
|
|
271
283
|
console.error(`task.abort failed for ${params.id}: ${e.stderr || e.message}`);
|
|
272
284
|
return { error: `Failed to abort task: ${e.stderr || e.message}` };
|
|
273
285
|
}
|
|
286
|
+
// Notify connected clients (NATS + HTTP SSE)
|
|
287
|
+
const abortPayload: Record<string, unknown> = { event_type: "running-state", running_state: "aborted" };
|
|
288
|
+
const useHttp = (config.mode ?? "nats") === "lan" || (config.mode ?? "nats") === "auto";
|
|
289
|
+
await publishHostEvent(nc, config, params.id, abortPayload, useHttp);
|
|
290
|
+
return { ok: true, task_id: params.id };
|
|
274
291
|
}
|
|
275
292
|
|
|
276
293
|
case "task.status": {
|
|
@@ -327,11 +344,11 @@ export function createRpcHandler(config: HostConfig) {
|
|
|
327
344
|
}
|
|
328
345
|
|
|
329
346
|
case "task.user_input": {
|
|
330
|
-
const params = request.params as { id: string; value: string };
|
|
347
|
+
const params = request.params as { id: string; value: string[] };
|
|
331
348
|
const taskDir = getTaskDir(config.projectRoot, params.id);
|
|
332
349
|
|
|
333
350
|
const currentStatus = readTaskStatus(taskDir);
|
|
334
|
-
if (!currentStatus?.pending_confirmation && !currentStatus?.pending_permission?.length) {
|
|
351
|
+
if (!currentStatus?.pending_confirmation && !currentStatus?.pending_permission?.length && !currentStatus?.pending_input?.length) {
|
|
335
352
|
return { ok: false, error: "not pending" };
|
|
336
353
|
}
|
|
337
354
|
|
package/src/spawn-command.ts
CHANGED
|
@@ -1,83 +1,120 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* (
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1
|
+
import crossSpawn from "cross-spawn";
|
|
2
|
+
import type { ChildProcess } from "child_process";
|
|
3
|
+
|
|
4
|
+
export interface SpawnStreamingOptions {
|
|
5
|
+
cwd: string;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Spawn a command with shell interpretation, returning the ChildProcess
|
|
11
|
+
* with stdout piped for line-by-line reading.
|
|
12
|
+
*
|
|
13
|
+
* Unlike spawnCommand(), this does NOT collect output into a buffer —
|
|
14
|
+
* the caller reads from child.stdout directly (e.g. via readline).
|
|
15
|
+
*
|
|
16
|
+
* shell: true is required so users can write piped commands like
|
|
17
|
+
* "tail -f log | grep ERROR".
|
|
18
|
+
*
|
|
19
|
+
* stdin is "pipe" (kept open, never written to) rather than "ignore"
|
|
20
|
+
* (/dev/null). Some long-running commands exit when stdin is closed/EOF.
|
|
21
|
+
* This differs from spawnCommand() which uses "ignore" because agent
|
|
22
|
+
* CLIs like `claude -p` hang on an open stdin pipe.
|
|
23
|
+
*/
|
|
24
|
+
export function spawnStreamingCommand(
|
|
25
|
+
command: string,
|
|
26
|
+
opts: SpawnStreamingOptions,
|
|
27
|
+
): ChildProcess {
|
|
28
|
+
return crossSpawn(command, [], {
|
|
29
|
+
cwd: opts.cwd,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
shell: true,
|
|
32
|
+
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
33
|
+
windowsHide: true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SpawnCommandOptions {
|
|
38
|
+
cwd: string;
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
/** Echo stdout to process.stdout (useful for journald logging). */
|
|
42
|
+
echoStdout?: boolean;
|
|
43
|
+
/** Resolve with output even on non-zero exit (instead of rejecting). */
|
|
44
|
+
resolveOnFailure?: boolean;
|
|
45
|
+
/** If provided, write this string to the process's stdin and then close the pipe. */
|
|
46
|
+
stdin?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Spawn a command with additional arguments.
|
|
51
|
+
*
|
|
52
|
+
* Uses cross-spawn to correctly resolve .cmd shims and escape arguments
|
|
53
|
+
* on Windows without shell: true (which mishandles special characters).
|
|
54
|
+
*
|
|
55
|
+
* On other platforms the command is executed directly (no shell), so no
|
|
56
|
+
* escaping is needed.
|
|
57
|
+
*
|
|
58
|
+
* stdin is set to "ignore" by default (equivalent to < /dev/null) because
|
|
59
|
+
* tools like `claude -p` hang indefinitely on an open stdin pipe.
|
|
60
|
+
* When opts.stdin is provided, stdin is set to "pipe" and the string is
|
|
61
|
+
* written to the process before closing the pipe.
|
|
62
|
+
*/
|
|
63
|
+
export interface SpawnCommandResult {
|
|
64
|
+
output: string;
|
|
65
|
+
exitCode: number | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function spawnCommand(
|
|
69
|
+
command: string,
|
|
70
|
+
args: string[],
|
|
71
|
+
opts: SpawnCommandOptions,
|
|
72
|
+
): Promise<SpawnCommandResult> {
|
|
73
|
+
return new Promise<SpawnCommandResult>((resolve, reject) => {
|
|
74
|
+
// Collapse newlines to spaces — cmd.exe can't handle literal newlines
|
|
75
|
+
// in arguments, and CLI prompts don't need them.
|
|
76
|
+
const finalArgs = process.platform === "win32"
|
|
77
|
+
? args.map((a) => a.replace(/[\r\n]+/g, " "))
|
|
78
|
+
: args;
|
|
79
|
+
|
|
80
|
+
// console.log(`[spawn] ${command} ${finalArgs.join(" ")}`);
|
|
81
|
+
|
|
82
|
+
const child = crossSpawn(command, finalArgs, {
|
|
83
|
+
cwd: opts.cwd,
|
|
84
|
+
stdio: [opts.stdin != null ? "pipe" : "ignore", "pipe", "pipe"],
|
|
85
|
+
env: opts.env ? { ...process.env, ...opts.env } : undefined,
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (opts.stdin != null) {
|
|
90
|
+
child.stdin!.end(opts.stdin);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const chunks: Buffer[] = [];
|
|
94
|
+
child.stdout!.on("data", (d: Buffer) => {
|
|
95
|
+
chunks.push(d);
|
|
96
|
+
if (opts.echoStdout) process.stdout.write(d);
|
|
97
|
+
});
|
|
98
|
+
child.stderr!.on("data", (d: Buffer) => process.stderr.write(d));
|
|
99
|
+
|
|
100
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
101
|
+
if (opts.timeout) {
|
|
102
|
+
timer = setTimeout(() => {
|
|
103
|
+
child.kill();
|
|
104
|
+
reject(new Error("command timed out"));
|
|
105
|
+
}, opts.timeout);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
child.on("close", (code: number | null) => {
|
|
109
|
+
if (timer) clearTimeout(timer);
|
|
110
|
+
const output = Buffer.concat(chunks).toString("utf-8");
|
|
111
|
+
if (code === 0 || opts.resolveOnFailure) resolve({ output, exitCode: code });
|
|
112
|
+
else reject(new Error(`process exited with code ${code}`));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
child.on("error", (err: Error) => {
|
|
116
|
+
if (timer) clearTimeout(timer);
|
|
117
|
+
reject(err);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { StringCodec, type Msg, type Subscription } from "nats";
|
|
2
|
-
import { connectNats } from "../nats-client.js";
|
|
1
|
+
import { StringCodec, type NatsConnection, type Msg, type Subscription } from "nats";
|
|
3
2
|
import type { HostConfig, RpcMessage } from "../types.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Start the NATS transport
|
|
5
|
+
* Start the NATS transport using an existing connection.
|
|
6
|
+
* Subscribe to RPC subjects and dispatch to handler.
|
|
7
7
|
*/
|
|
8
8
|
export async function startNatsTransport(
|
|
9
9
|
config: HostConfig,
|
|
10
10
|
handleRpc: (req: RpcMessage) => Promise<unknown>,
|
|
11
|
+
nc: NatsConnection,
|
|
11
12
|
): Promise<void> {
|
|
12
|
-
const nc = await connectNats(config);
|
|
13
13
|
const sc = StringCodec();
|
|
14
14
|
|
|
15
15
|
const subject = `host.${config.hostId}.rpc.>`;
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface TaskFrontmatter {
|
|
|
25
25
|
triggers_enabled: boolean;
|
|
26
26
|
requires_confirmation: boolean;
|
|
27
27
|
permissions?: RequiredPermission[];
|
|
28
|
+
command?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface Trigger {
|
|
@@ -37,14 +38,15 @@ export interface ParsedTask {
|
|
|
37
38
|
body: string;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export type TaskRunningState = "
|
|
41
|
+
export type TaskRunningState = "started" | "finished" | "aborted" | "failed";
|
|
41
42
|
|
|
42
43
|
export interface TaskStatus {
|
|
43
44
|
running_state: TaskRunningState;
|
|
44
45
|
time_stamp: number;
|
|
45
46
|
pending_confirmation?: boolean;
|
|
46
47
|
pending_permission?: RequiredPermission[];
|
|
47
|
-
|
|
48
|
+
pending_input?: string[];
|
|
49
|
+
user_input?: string[];
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface HistoryEntry {
|