runtimeuse 0.2.0

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 (120) hide show
  1. package/.env.example +4 -0
  2. package/README.md +222 -0
  3. package/dist/agent-handler.d.ts +26 -0
  4. package/dist/agent-handler.d.ts.map +1 -0
  5. package/dist/agent-handler.js +2 -0
  6. package/dist/agent-handler.js.map +1 -0
  7. package/dist/artifact-manager.d.ts +27 -0
  8. package/dist/artifact-manager.d.ts.map +1 -0
  9. package/dist/artifact-manager.js +125 -0
  10. package/dist/artifact-manager.js.map +1 -0
  11. package/dist/artifact-manager.test.d.ts +2 -0
  12. package/dist/artifact-manager.test.d.ts.map +1 -0
  13. package/dist/artifact-manager.test.js +251 -0
  14. package/dist/artifact-manager.test.js.map +1 -0
  15. package/dist/claude-handler.d.ts +3 -0
  16. package/dist/claude-handler.d.ts.map +1 -0
  17. package/dist/claude-handler.js +76 -0
  18. package/dist/claude-handler.js.map +1 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +87 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/command-handler.d.ts +22 -0
  24. package/dist/command-handler.d.ts.map +1 -0
  25. package/dist/command-handler.js +75 -0
  26. package/dist/command-handler.js.map +1 -0
  27. package/dist/command-handler.test.d.ts +2 -0
  28. package/dist/command-handler.test.d.ts.map +1 -0
  29. package/dist/command-handler.test.js +267 -0
  30. package/dist/command-handler.test.js.map +1 -0
  31. package/dist/constants.d.ts +3 -0
  32. package/dist/constants.d.ts.map +1 -0
  33. package/dist/constants.js +13 -0
  34. package/dist/constants.js.map +1 -0
  35. package/dist/default-handler.d.ts +3 -0
  36. package/dist/default-handler.d.ts.map +1 -0
  37. package/dist/default-handler.js +76 -0
  38. package/dist/default-handler.js.map +1 -0
  39. package/dist/download-handler.d.ts +8 -0
  40. package/dist/download-handler.d.ts.map +1 -0
  41. package/dist/download-handler.js +36 -0
  42. package/dist/download-handler.js.map +1 -0
  43. package/dist/download-handler.test.d.ts +2 -0
  44. package/dist/download-handler.test.d.ts.map +1 -0
  45. package/dist/download-handler.test.js +123 -0
  46. package/dist/download-handler.test.js.map +1 -0
  47. package/dist/index.d.ts +20 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +21 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/logger.d.ts +8 -0
  52. package/dist/logger.d.ts.map +1 -0
  53. package/dist/logger.js +14 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/openai-handler.d.ts +3 -0
  56. package/dist/openai-handler.d.ts.map +1 -0
  57. package/dist/openai-handler.js +86 -0
  58. package/dist/openai-handler.js.map +1 -0
  59. package/dist/server.d.ts +21 -0
  60. package/dist/server.d.ts.map +1 -0
  61. package/dist/server.js +52 -0
  62. package/dist/server.js.map +1 -0
  63. package/dist/session.d.ts +29 -0
  64. package/dist/session.d.ts.map +1 -0
  65. package/dist/session.js +244 -0
  66. package/dist/session.js.map +1 -0
  67. package/dist/session.test.d.ts +2 -0
  68. package/dist/session.test.d.ts.map +1 -0
  69. package/dist/session.test.js +339 -0
  70. package/dist/session.test.js.map +1 -0
  71. package/dist/storage.d.ts +3 -0
  72. package/dist/storage.d.ts.map +1 -0
  73. package/dist/storage.js +21 -0
  74. package/dist/storage.js.map +1 -0
  75. package/dist/types.d.ts +62 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +2 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/upload-tracker.d.ts +10 -0
  80. package/dist/upload-tracker.d.ts.map +1 -0
  81. package/dist/upload-tracker.js +27 -0
  82. package/dist/upload-tracker.js.map +1 -0
  83. package/dist/upload-tracker.test.d.ts +2 -0
  84. package/dist/upload-tracker.test.d.ts.map +1 -0
  85. package/dist/upload-tracker.test.js +89 -0
  86. package/dist/upload-tracker.test.js.map +1 -0
  87. package/dist/utils.d.ts +7 -0
  88. package/dist/utils.d.ts.map +1 -0
  89. package/dist/utils.js +32 -0
  90. package/dist/utils.js.map +1 -0
  91. package/dist/utils.test.d.ts +2 -0
  92. package/dist/utils.test.d.ts.map +1 -0
  93. package/dist/utils.test.js +92 -0
  94. package/dist/utils.test.js.map +1 -0
  95. package/package.json +40 -0
  96. package/scripts/dev-publish.sh +45 -0
  97. package/src/agent-handler.ts +26 -0
  98. package/src/artifact-manager.test.ts +320 -0
  99. package/src/artifact-manager.ts +170 -0
  100. package/src/claude-handler.ts +95 -0
  101. package/src/cli.ts +107 -0
  102. package/src/command-handler.test.ts +507 -0
  103. package/src/command-handler.ts +102 -0
  104. package/src/constants.ts +12 -0
  105. package/src/download-handler.test.ts +183 -0
  106. package/src/download-handler.ts +45 -0
  107. package/src/index.ts +59 -0
  108. package/src/logger.ts +20 -0
  109. package/src/openai-handler.ts +120 -0
  110. package/src/server.ts +68 -0
  111. package/src/session.test.ts +448 -0
  112. package/src/session.ts +319 -0
  113. package/src/storage.ts +28 -0
  114. package/src/types.ts +101 -0
  115. package/src/upload-tracker.test.ts +112 -0
  116. package/src/upload-tracker.ts +30 -0
  117. package/src/utils.test.ts +120 -0
  118. package/src/utils.ts +35 -0
  119. package/tsconfig.json +20 -0
  120. package/vitest.config.ts +7 -0
@@ -0,0 +1,170 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chokidar from "chokidar";
4
+ import ignore, { type Ignore } from "ignore";
5
+ import { uploadFile } from "./storage.js";
6
+ import type { UploadTracker } from "./upload-tracker.js";
7
+ import type {
8
+ ArtifactUploadRequestMessage,
9
+ ArtifactUploadResponseMessage,
10
+ } from "./types.js";
11
+ import { DEFAULT_ARTIFACT_IGNORE } from "./constants.js";
12
+ import { defaultLogger, type Logger } from "./logger.js";
13
+
14
+ type ArtifactType = ArtifactUploadRequestMessage["artifact_type"];
15
+
16
+ const EXTENSION_TO_TYPE: Record<string, ArtifactType> = {
17
+ ".webm": "video",
18
+ ".png": "screenshot",
19
+ ".ndjson": "tool_calls",
20
+ ".js": "javascript",
21
+ ".ts": "javascript",
22
+ ".py": "python",
23
+ ".sh": "shellscript",
24
+ };
25
+
26
+ export interface ArtifactManagerConfig {
27
+ artifactsDir: string;
28
+ uploadTracker: UploadTracker;
29
+ send: (message: ArtifactUploadRequestMessage) => void;
30
+ }
31
+
32
+ export class ArtifactManager {
33
+ private readonly watcher: ReturnType<typeof chokidar.watch>;
34
+ private readonly pendingRequests = new Map<
35
+ string,
36
+ { promise: Promise<void>; resolve: () => void }
37
+ >();
38
+ private readonly artifactsDir: string;
39
+ private readonly uploadTracker: UploadTracker;
40
+ private readonly send: (message: ArtifactUploadRequestMessage) => void;
41
+ private ig: Ignore = ignore();
42
+ private logger: Logger = defaultLogger;
43
+ private loggingLevel: "info" | "debug" = "info";
44
+
45
+ constructor(config: ArtifactManagerConfig) {
46
+ this.artifactsDir = config.artifactsDir;
47
+ this.uploadTracker = config.uploadTracker;
48
+ this.send = config.send;
49
+
50
+ this.reloadIgnorePatterns();
51
+
52
+ this.watcher = chokidar.watch(config.artifactsDir, {
53
+ awaitWriteFinish: true,
54
+ alwaysStat: true,
55
+ });
56
+
57
+ this.watcher.on("add", (p, s) => this.onFileEvent(p, s));
58
+ this.watcher.on("change", (p, s) => this.onFileEvent(p, s));
59
+ }
60
+
61
+ private reloadIgnorePatterns(): void {
62
+ this.ig = ignore();
63
+ const ignorePath = path.join(this.artifactsDir, ".artifactignore");
64
+ if (fs.existsSync(ignorePath)) {
65
+ this.ig.add(fs.readFileSync(ignorePath, "utf-8"));
66
+ this.logger.log(`Loaded .artifactignore from ${ignorePath}`);
67
+ } else {
68
+ this.ig.add(DEFAULT_ARTIFACT_IGNORE);
69
+ }
70
+ }
71
+
72
+ setLogger(logger: Logger): void {
73
+ this.logger = logger;
74
+ }
75
+
76
+ async handleUploadResponse(
77
+ message: ArtifactUploadResponseMessage,
78
+ ): Promise<void> {
79
+ this.logger.log(
80
+ `Uploading artifact: ${message.filename} ${message.filepath}`,
81
+ );
82
+
83
+ const promise = uploadFile(
84
+ message.filepath,
85
+ message.presigned_url,
86
+ message.content_type,
87
+ this.logger,
88
+ );
89
+ this.uploadTracker.track(promise);
90
+
91
+ try {
92
+ await promise;
93
+ } catch (error) {
94
+ if (error instanceof Error && error.name === "ENOENT") {
95
+ this.logger.log(`Artifact file not found: ${message.filepath}`);
96
+ return;
97
+ }
98
+ throw error;
99
+ }
100
+
101
+ const pending = this.pendingRequests.get(message.filename);
102
+ if (pending) {
103
+ pending.resolve();
104
+ this.pendingRequests.delete(message.filename);
105
+ }
106
+ }
107
+
108
+ async waitForPendingRequests(timeoutMs: number): Promise<void> {
109
+ const promises = [...this.pendingRequests.values()].map((r) => r.promise);
110
+ if (promises.length === 0) return;
111
+ this.logger.log(`Waiting for ${promises.length} artifact round-trips...`);
112
+ await Promise.race([
113
+ Promise.allSettled(promises),
114
+ new Promise<void>((r) => setTimeout(r, timeoutMs)),
115
+ ]);
116
+ }
117
+
118
+ async stopWatching(): Promise<void> {
119
+ await this.watcher.close();
120
+ }
121
+
122
+ private onFileEvent(filePath: string, stats?: fs.Stats): void {
123
+ if (this.loggingLevel === "debug") {
124
+ this.logger.log(
125
+ `Artifact event: ${filePath}. Size: ${stats?.size ?? 0} bytes`,
126
+ );
127
+ }
128
+
129
+ if (path.basename(filePath) === ".artifactignore") {
130
+ this.reloadIgnorePatterns();
131
+ return;
132
+ }
133
+
134
+ if (!stats?.isFile() || !stats.size) {
135
+ if (this.loggingLevel === "debug") {
136
+ this.logger.debug(`Skipping: ${filePath}`);
137
+ }
138
+ return;
139
+ }
140
+
141
+ const relativePath = path.relative(this.artifactsDir, filePath);
142
+ if (!relativePath.startsWith("..") && this.ig.ignores(relativePath)) {
143
+ if (this.loggingLevel === "debug") {
144
+ this.logger.debug(`Skipping ignored artifact: ${relativePath}`);
145
+ }
146
+ return;
147
+ }
148
+
149
+ this.requestUpload(filePath);
150
+ }
151
+
152
+ private requestUpload(filePath: string): void {
153
+ const filename = path.basename(filePath);
154
+ const ext = path.extname(filePath);
155
+
156
+ let resolve!: () => void;
157
+ const promise = new Promise<void>((r) => {
158
+ resolve = r;
159
+ });
160
+ this.pendingRequests.set(filename, { promise, resolve });
161
+
162
+ this.logger.log(`Requesting upload for artifact: ${filename}`);
163
+ this.send({
164
+ message_type: "artifact_upload_request_message",
165
+ artifact_type: EXTENSION_TO_TYPE[ext] ?? "other",
166
+ filename,
167
+ filepath: filePath,
168
+ });
169
+ }
170
+ }
@@ -0,0 +1,95 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import type {
3
+ AgentHandler,
4
+ AgentInvocation,
5
+ AgentResult,
6
+ MessageSender,
7
+ } from "./agent-handler.js";
8
+
9
+ function extractTextFromContent(content: unknown[]): string {
10
+ const parts: string[] = [];
11
+ for (const block of content) {
12
+ if (
13
+ typeof block === "object" &&
14
+ block !== null &&
15
+ "type" in block &&
16
+ block.type === "text" &&
17
+ "text" in block
18
+ ) {
19
+ parts.push(String(block.text));
20
+ }
21
+ }
22
+ return parts.join("\n");
23
+ }
24
+
25
+ export const claudeHandler: AgentHandler = {
26
+ async run(
27
+ invocation: AgentInvocation,
28
+ sender: MessageSender,
29
+ ): Promise<AgentResult> {
30
+ const abortController = new AbortController();
31
+
32
+ const onAbort = () => abortController.abort();
33
+ invocation.signal.addEventListener("abort", onAbort, { once: true });
34
+
35
+ try {
36
+ const conversation = query({
37
+ prompt: invocation.userPrompt,
38
+ options: {
39
+ systemPrompt: invocation.systemPrompt,
40
+ model: invocation.model,
41
+ outputFormat: invocation.outputFormat,
42
+ abortController,
43
+ cwd: process.cwd(),
44
+ env: { ...process.env, ...invocation.env },
45
+ tools: { type: "preset", preset: "claude_code" },
46
+ permissionMode: "bypassPermissions",
47
+ allowDangerouslySkipPermissions: true,
48
+ },
49
+ });
50
+
51
+ let structuredOutput: Record<string, unknown> = {};
52
+ const metadata: Record<string, unknown> = {};
53
+
54
+ for await (const message of conversation) {
55
+ if (message.type === "assistant") {
56
+ const text = extractTextFromContent(
57
+ message.message?.content ?? [],
58
+ );
59
+ if (text) {
60
+ sender.sendAssistantMessage([text]);
61
+ }
62
+ } else if (message.type === "result") {
63
+ metadata.duration_ms = message.duration_ms;
64
+ metadata.duration_api_ms = message.duration_api_ms;
65
+ metadata.num_turns = message.num_turns;
66
+ metadata.total_cost_usd = message.total_cost_usd;
67
+ metadata.usage = message.usage;
68
+ metadata.session_id = message.session_id;
69
+
70
+ if (message.subtype === "success") {
71
+ if (message.structured_output != null) {
72
+ structuredOutput =
73
+ message.structured_output as Record<string, unknown>;
74
+ } else {
75
+ structuredOutput = { result: message.result };
76
+ }
77
+ } else {
78
+ structuredOutput = {
79
+ error: message.subtype,
80
+ errors: "errors" in message ? message.errors : [],
81
+ };
82
+ sender.sendErrorMessage(
83
+ `Agent ended with ${message.subtype}`,
84
+ metadata,
85
+ );
86
+ }
87
+ }
88
+ }
89
+
90
+ return { structuredOutput, metadata };
91
+ } finally {
92
+ invocation.signal.removeEventListener("abort", onAbort);
93
+ }
94
+ },
95
+ };
package/src/cli.ts ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "child_process";
4
+ import path from "path";
5
+ import { RuntimeUseServer } from "./server.js";
6
+ import { openaiHandler } from "./openai-handler.js";
7
+ import { claudeHandler } from "./claude-handler.js";
8
+ import type { AgentHandler } from "./agent-handler.js";
9
+
10
+ const BUILTIN_AGENTS = ["openai", "claude"] as const;
11
+ type BuiltinAgent = (typeof BUILTIN_AGENTS)[number];
12
+
13
+ function usage(): never {
14
+ console.log(`Usage: runtimeuse [options]
15
+
16
+ Options:
17
+ --handler <path> Path to a custom JS/TS module exporting an AgentHandler
18
+ --agent <openai|claude> Built-in agent SDK to use (default: openai)
19
+ --port <number> WebSocket server port (default: 8080)
20
+ --default-model <name> Default model identifier
21
+ -h, --help Show this help message`);
22
+ process.exit(0);
23
+ }
24
+
25
+ function parseArgs(args: string[]): Record<string, string> {
26
+ const result: Record<string, string> = {};
27
+ for (let i = 0; i < args.length; i++) {
28
+ const arg = args[i];
29
+ if (arg === "-h" || arg === "--help") {
30
+ usage();
31
+ }
32
+ if (arg.startsWith("--") && i + 1 < args.length) {
33
+ const key = arg.slice(2);
34
+ result[key] = args[++i];
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ function checkClaudeCli(): void {
41
+ try {
42
+ execSync("claude --version", { stdio: "ignore" });
43
+ } catch {
44
+ console.error(
45
+ "Error: the Claude handler requires the Claude CLI (`claude`) to be installed.\n" +
46
+ "Install it with: npm install -g @anthropic-ai/claude-code\n" +
47
+ "Or use the OpenAI handler with --agent openai (default)",
48
+ );
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ function getBuiltinHandler(agent: BuiltinAgent): AgentHandler {
54
+ if (agent === "claude") {
55
+ checkClaudeCli();
56
+ return claudeHandler;
57
+ }
58
+ return openaiHandler;
59
+ }
60
+
61
+ async function loadHandler(handlerPath: string): Promise<AgentHandler> {
62
+ const resolved = path.resolve(handlerPath);
63
+ const mod = await import(resolved);
64
+ const handler: AgentHandler | undefined =
65
+ mod.default?.run ? mod.default : mod.handler;
66
+
67
+ if (!handler?.run) {
68
+ console.error(
69
+ `Error: module at ${handlerPath} must export an AgentHandler (as default or named "handler")`,
70
+ );
71
+ process.exit(1);
72
+ }
73
+ return handler;
74
+ }
75
+
76
+ async function main() {
77
+ const args = parseArgs(process.argv.slice(2));
78
+
79
+ let handler: AgentHandler;
80
+ if (args.handler) {
81
+ handler = await loadHandler(args.handler);
82
+ } else {
83
+ const agent = (args.agent ?? "openai") as BuiltinAgent;
84
+ if (!BUILTIN_AGENTS.includes(agent)) {
85
+ console.error(
86
+ `Error: unknown agent "${args.agent}". Choose one of: ${BUILTIN_AGENTS.join(", ")}`,
87
+ );
88
+ process.exit(1);
89
+ }
90
+ handler = getBuiltinHandler(agent);
91
+ }
92
+
93
+ const port = args.port ? parseInt(args.port, 10) : undefined;
94
+
95
+ const server = new RuntimeUseServer({
96
+ handler,
97
+ port,
98
+ defaultModel: args["default-model"],
99
+ });
100
+
101
+ await server.start();
102
+ }
103
+
104
+ main().catch((err) => {
105
+ console.error(err);
106
+ process.exit(1);
107
+ });