tune-sdk 0.2.24 → 0.3.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.
package/README.md CHANGED
@@ -6,8 +6,7 @@ Tune is a handy [extension for Visual Studio Code and](https://marketplace.visua
6
6
  With tune [javascript sdk](https://www.npmjs.com/package/tune-sdk) you can make apps and agents.
7
7
 
8
8
  ## Demo
9
- <video src="https://github.com/user-attachments/assets/23f8ab30-58db-4159-8761-f212a7960e0c">
10
- </video>
9
+ [![asciicast](https://asciinema.org/a/757894.png)](https://asciinema.org/a/757894)
11
10
 
12
11
 
13
12
  ## Setup
@@ -137,6 +136,8 @@ image generated
137
136
  # install tune globally
138
137
  npm install -g tune-sdk
139
138
 
139
+ tune "hi how are you?"
140
+
140
141
  # append user message to newchat.chat run and save
141
142
  tune --user "hi how are you?" --filename newchat.chat --save
142
143
 
@@ -144,8 +145,10 @@ tune --user "hi how are you?" --filename newchat.chat --save
144
145
  # print result to console
145
146
  tune --system "You are Groot" --user "Hi how are you?"
146
147
 
147
- #set context variable
148
- tune --set-test "hello" --user "@test" --system "You are echo you print everythting back"
148
+ # set context variable
149
+ tune --set test="hello" --user "@test" --system "You are echo you print everythting back"
150
+ # prints hello
151
+
149
152
  ```
150
153
 
151
154
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tune-sdk",
3
- "version": "0.2.24",
3
+ "version": "0.3.0",
4
4
  "description": "tune - LLM chat in text file",
5
5
  "main": "dist/tune.js",
6
6
  "module": "dist/tune.mjs",
7
- "bin": {
7
+ "bin": {
8
8
  "tune": "bin/cli.js",
9
- "tune-sdk": "bin/cli.js"
9
+ "tune-sdk": "bin/cli.js"
10
10
  },
11
11
  "exports": {
12
12
  ".": {
@@ -18,6 +18,9 @@
18
18
  },
19
19
  "./rpc": {
20
20
  "require": "./src/rpc.js"
21
+ },
22
+ "./contextws": {
23
+ "require": "./src/contextws.js"
21
24
  }
22
25
  },
23
26
  "keywords": [
@@ -34,5 +37,10 @@
34
37
  "scripts": {
35
38
  "test": "node test/index.js",
36
39
  "test:watch": "node --watch test/index.js"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^14.0.3",
43
+ "mime-types": "^3.0.2",
44
+ "ws": "^8.20.0"
37
45
  }
38
46
  }
package/src/cli.js CHANGED
@@ -1,6 +1,12 @@
1
- var assert, tune, rpc, path, fs, os, cp, stream;
2
- assert = require("assert");
3
-
1
+ const { Command, Option } = require("commander");
2
+ const tune = require("../dist/tune.js");
3
+ const ws = require("./ws.js");
4
+ const rpc = require("./rpc.js");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+ const os = require("os");
8
+ const cp = require("child_process");
9
+ const stream = require("stream");
4
10
 
5
11
  // tune app - run web server from current directory serving index.html and making it availble to call ctx via websocket
6
12
  // tune ps - list of executing agents or the ones finished
@@ -10,195 +16,15 @@ assert = require("assert");
10
16
  // tune - execute call/file and quit
11
17
  // tune rpc - run rpc server
12
18
 
13
-
14
- function tpl(str) {
15
- var _i;
16
- var params = 2 <= arguments.length ? [].slice.call(arguments, 1, _i = arguments.length - 0) : (_i = 1, []);
17
- return (function(paramIndex, params) {
18
- var _ref;
19
- try {
20
- _ref = str.replace(/{(\W*)(\w*)(\W*)}/gm, (function(_, pre, name, post) {
21
- return (function(res) {
22
- paramIndex += 1;
23
- return ((typeof res !== 'undefined') ? ((pre || "") + res + (post || "")) : "");
24
- })(params[name || paramIndex]);
25
- }));
26
- } catch (e) {
27
- _ref = console.log.apply(console, [].concat([e, str]).concat(params));
28
- }
29
- return _ref;
30
- })(0, (((typeof params[0] === "object") && (params.length === 1)) ? params[0] : params));
31
- }
32
-
33
- function showHelp() {
34
- console.log("TUNE-CLI - Command Line Interface for Tune");
35
- console.log("");
36
- console.log("USAGE:");
37
- console.log(" tune [cmd] [OPTIONS]");
38
- console.log("");
39
- console.log("COMMANDS:");
40
- console.log(" rpc Start RPC server mode");
41
- console.log(" init Initialize Tune config directory");
42
- console.log("");
43
- console.log("EXAMPLES:");
44
- console.log(" # Quick chat with system prompt");
45
- console.log(" tune --system \"You are Groot\" --user \"Hi how are you?\"");
46
- console.log("");
47
- console.log(" # Continue existing chat");
48
- console.log(" tune --user \"continue the conversation\" --filename chat.chat --save");
49
- console.log("");
50
- console.log(" # Set context variables");
51
- console.log(" tune --set-test=hello --user \"@test\" --system \"Echo assistant\"");
52
- console.log("");
53
- console.log(" # RPC mode for editor integration");
54
- console.log(" tune rpc");
55
- console.log("");
56
- console.log(" # Initialize or reinitialize config directory");
57
- console.log(" tune init --force");
58
- console.log("");
59
- console.log("OPTIONS:");
60
- console.log(" --user <text> User message to send");
61
- console.log(" --system <text> System prompt to use");
62
- console.log(" --filename <file> Chat file to load/save");
63
- console.log(" --save Save conversation to file");
64
- console.log(" --stop <mode> Stop condition: assistant|step|<custom>");
65
- console.log(" --text <content> chat content");
66
- console.log(" --response <type> Response format: content|json|messages|chat (default: content)");
67
- console.log(" --set-<name>=<value> Set context parameter");
68
- console.log(" --path <paths> Additional search paths (colon-separated)");
69
- console.log(" --home <dir> Tune config directory (default: ~/.tune)");
70
- console.log(" --debug Enable debug output");
71
- console.log(" --silent Suppress output");
72
- console.log(" --force Force config initialization (with 'init')");
73
- console.log(" --help Show this help");
74
- console.log(" --version Show CLI version");
75
- return console.log("");
76
- }
77
- showHelp;
78
-
79
- function validateArgs(args) {
80
- assert(!!args && (typeof args === "object"), "Arguments must be an object");
81
- if (args.user) assert(typeof args.user === "string", "--user must be a string");
82
- if (args.system) assert(typeof args.system === "string", "--system must be a string");
83
- if (args.filename) assert(typeof args.filename === "string", "--filename must be a string");
84
- if (args.text) assert(typeof args.text === "string", "--text must be a string");
85
- if (args.response) assert(typeof args.response === "string", "--response must be a string");
86
- if (args.stop) assert(typeof args.stop === "string", "--stop must be a string");
87
- if (args.path) assert(typeof args.path === "string", "--path must be a string");
88
- if (args.home) assert(typeof args.home === "string", "--home must be a string");
89
- if (!!args.save) assert(typeof args.save === "boolean", "--save must be a boolean");
90
- if (!!args.debug) assert(typeof args.debug === "boolean" || typeof args.debug === "string", "--debug must be a boolean");
91
- if (!!args.silent) assert(typeof args.silent === "boolean", "--silent must be a boolean");
92
- if (!!args.force) assert(typeof args.force === "boolean", "--force must be a boolean");
93
- if (typeof args.rpc !== "undefined") assert(false, "Use 'tune rpc' instead of --rpc");
94
- if (typeof args.forceInit !== "undefined") assert(false, "Use 'tune init --force' instead of --force-init");
95
- if (args.params) assert(!!args.params && (typeof args.params === "object"), "--set-* parameters must form a valid object");
96
- if ((args.stop && (typeof args.stop === "string"))) assert((args.stop === "assistant") || (args.stop === "step") || (args.stop.length > 0), "--stop must be 'assistant', 'step', or a non-empty custom string");
97
- if (args.cmd) {
98
- assert(typeof args.cmd === "string", "Command must be a string");
99
- assert((args.cmd === "rpc") || (args.cmd === "init"), "Unknown command: " + args.cmd);
100
- }
101
- if ((!args.help && !args.version && !args.cmd && !args.user && !args.filename && !args.text)) assert(false, "Must specify --user, --filename, a command (rpc|init), --version, or --help");
102
- return args;
103
- }
104
- validateArgs;
105
-
106
- function parseArgs(args) {
107
- var curKey, res, res1, key, value, stop, _ref, _len;
108
- assert(Array.isArray(args), "parseArgs expects an array of arguments");
109
- var curKey;
110
- curKey = null;
111
- var res;
112
- res = args.reduce((function(memo, arg) {
113
- var key, value, _ref, _i;
114
- assert(typeof arg === "string", "Each argument must be a string");
115
- if (arg.startsWith("--")) {
116
- _ref = arg.substring(2)
117
- .split("=");
118
- key = _ref[0];
119
- value = _ref[1];
120
- assert((typeof key === "string") && (key.length > 0), "Argument key must be a non-empty string");
121
- if (!!value) {
122
- memo[key] = value;
123
- curKey = null;
124
- } else {
125
- curKey = key;
126
- memo[key] = true;
127
- }
128
- } else if (curKey) {
129
- memo[curKey] = arg;
130
- curKey = null;
131
- } else {
132
- if (!memo.__cmd) {
133
- memo.__cmd = arg;
134
- } else {
135
- assert(false, "Only a single positional command is allowed");
136
- }
137
- }
138
- return memo;
139
- }), {});
140
- assert(!!res && (typeof res === "object"), "Parsed arguments must form an object");
141
- var res1;
142
- res1 = {};
143
- _ref = res;
144
- for (key in _ref) {
145
- value = _ref[key];
146
- assert(typeof key === "string", "Argument keys must be strings");
147
- if (key === "__cmd") {
148
- res1.cmd = value;
149
- continue;
150
- }
151
- if (key.startsWith("set-")) {
152
- res1.params = res1.params || {}
153
- assert(key.substr(4).length > 0, "Set parameter name cannot be empty");
154
- res1.params[key.substr(4)] = value;
155
- } else {
156
- res1[key] = value;
157
- }
158
- }
159
- if ((res1.h || res1.help)) res1.help = true;
160
- if ((res1.v || res1.version)) res1.version = true;
161
- stop = res1.stop;
162
- if ((!!stop && (stop !== "step" && stop !== "assistant"))) {
163
- assert(typeof stop === "string", "Custom stop condition must be a string");
164
- assert(stop.length > 0, "Custom stop condition cannot be empty");
165
- res1.stop = (function(msgs) {
166
- var lastMsg;
167
- assert(Array.isArray(msgs), "Messages must be an array");
168
- if (!msgs.length) return false;
169
- var lastMsg;
170
- lastMsg = msgs["slice"](-1)[0];
171
- assert(!!lastMsg && (typeof lastMsg === "object"), "Last message must be an object");
172
- if (!lastMsg.content) return false;
173
- assert(typeof lastMsg.content === "string", "Message content must be a string");
174
- return (-1 !== lastMsg.content.indexOf(stop));
175
- });
176
- }
177
- return res1;
178
- }
179
- parseArgs;
180
- tune = require("../dist/tune.js");
181
- rpc = require("../src/rpc.js");
182
- path = require("path");
183
- fs = require("fs");
184
- os = require("os");
185
- cp = require("child_process");
186
- stream = require("stream");
187
-
188
- function getHomedir(args) {
189
- assert(!!args && (typeof args === "object"), "getHomedir expects args to be an object");
190
- if (args.home) assert(typeof args.home === "string", "args.home must be a string");
191
- return path.resolve(path.normalize((args.home || process.env.TUNE_HOME || "~/.tune")
19
+ function getHomedir(home) {
20
+ return path.resolve(path.normalize((home || process.env.TUNE_HOME || "~/.tune")
192
21
  .replace("~", os.homedir())));
193
22
  }
194
- getHomedir;
195
- async function initConfig(args) {
196
- var homedir, stdout, stderr, _ref, _i;
197
- assert(!!args && (typeof args === "object"), "initConfig expects args to be an object");
198
- var homedir;
199
- homedir = getHomedir(args);
200
- assert(typeof homedir === "string", "Home directory must be a string");
201
- if ((!args.force && fs.existsSync(homedir))) return;
23
+
24
+ async function initConfig({ home, force }) {
25
+ let stdout, stderr, _i;
26
+ const homedir = getHomedir(home);
27
+ if (!force && fs.existsSync(homedir)) return;
202
28
  console.error("[tune] initialize " + homedir);
203
29
  fs.mkdirSync(homedir, {
204
30
  recursive: true
@@ -207,11 +33,10 @@ async function initConfig(args) {
207
33
  fs.cpSync(path.resolve(__dirname, "../config"), path.resolve(homedir), { recursive: true });
208
34
  console.error("[tune] installing npm");
209
35
  try {
210
- _ref = cp.execSync("npm i", {
36
+ stdout = cp.execSync("npm i", {
211
37
  cwd: homedir,
212
38
  encoding: "utf8"
213
39
  });
214
- stdout = _ref;
215
40
  if (stdout.trim()) console.error("[tune]", stdout.trim());
216
41
  //stderr.trim() ? console.error("[tune]", stderr.trim()) : undefined;
217
42
  } catch (err) {
@@ -220,7 +45,7 @@ async function initConfig(args) {
220
45
  console.error("[tune] done");
221
46
  console.error(`[tune] edit ${homedir}/.env and add OPENAI_KEY and other keys, change ${homedir}/default.ctx.js to customize tune`);
222
47
  }
223
- initConfig;
48
+
224
49
  async function suggest(params, ctx) {
225
50
  var node, _ref;
226
51
  var node;
@@ -247,7 +72,7 @@ async function suggest(params, ctx) {
247
72
  }
248
73
  }));
249
74
  }
250
- suggest;
75
+
251
76
  async function remoteContext(name, params) {
252
77
  var server, node;
253
78
  var server;
@@ -275,20 +100,20 @@ async function remoteContext(name, params) {
275
100
  });
276
101
  return node;
277
102
  }
278
- remoteContext;
279
- async function runRpc(args) {
103
+
104
+ async function runRpc({ debug, home, path }) {
280
105
  var inStream, outStream, debugStream, ctx, server;
281
106
  inStream = stream.Readable.toWeb(process.stdin);
282
107
  outStream = stream.Writable.toWeb(process.stdout);
283
- debugStream = ((typeof args.debug === "string") ? fs.createWriteStream(args.debug, {
108
+ debugStream = ((typeof debug === "string") ? fs.createWriteStream(debug, {
284
109
  flags: "a"
285
110
  }) : undefined);
286
- ctx = await initContext(args);
111
+ ctx = await initContext({ home, path });
287
112
  let cleanCtx = ctx.clone()
288
113
  server = rpc.jsonrpc({
289
114
  inStream: inStream,
290
115
  outStream: outStream,
291
- debug: ((typeof args.debug === "string") ? (function() {
116
+ debug: ((typeof debug === "string") ? (function() {
292
117
  var _i;
293
118
  var args = 1 <= arguments.length ? [].slice.call(arguments, 0, _i = arguments.length - 0) : (_i = 0, []);
294
119
  return debugStream.write(args.join(" ") + "\n", "utf8");
@@ -346,20 +171,13 @@ async function runRpc(args) {
346
171
  // console.log("node", node)
347
172
  return null;
348
173
  }
349
- runRpc;
350
- async function run(args) {
351
- var ctx, stop, params, res;
352
- ctx = await initContext(args);
353
- var stop;
354
- stop = args.stop || "assistant";
355
- var params;
356
- params = args.params || {}
357
- delete args.params;
358
- var res;
359
- res = await ctx.file2run({ ...args, errors: "message", stop }, params);
360
- return (!args.silent ? console.log(res) : undefined);
174
+ async function run({ home, stop, params, silent, user, system, save, text , response, path, filename }) {
175
+ const ctx = await initContext({ home, path });
176
+ const res = await ctx.file2run({ user, system, save, text, response, errors: "message", stop, filename }, params);
177
+ if (!silent) {
178
+ console.log(res)
179
+ }
361
180
  }
362
- run;
363
181
 
364
182
  function flatten(array) {
365
183
  return array.reduce((memo, item) => {
@@ -371,17 +189,17 @@ function flatten(array) {
371
189
  }, [])
372
190
  }
373
191
 
374
- async function initContext(args) {
192
+ async function initContext({ home, path: addPaths }) {
375
193
  var dirs, pwd, ctx, dir, ctxName, ext, module, m, _i, _ref, _len, _i0, _ref0, _len0;
376
194
  var dirs;
377
195
  var pwd;
378
196
  dirs = [];
379
197
  pwd = process.cwd();
380
- if (args.path) dirs = args.path.split(path.delimiter)
198
+ if (addPaths) dirs = addPaths.split(path.delimiter)
381
199
  .map((function(dir) {
382
200
  return path.resolve(pwd, dir);
383
201
  }));
384
- dirs.push(getHomedir(args));
202
+ dirs.push(getHomedir(home));
385
203
  dirs.unshift(pwd);
386
204
  if (process.env.TUNE_PATH) {
387
205
  dirs = dirs.concat(process.env.TUNE_PATH.split(path.delimiter))
@@ -389,7 +207,7 @@ async function initContext(args) {
389
207
  process.env.TUNE_PATH = dirs.join(path.delimiter);
390
208
  ctx = tune.makeContext({
391
209
  TUNE_PATH: process.env.TUNE_PATH,
392
- TUNE_HOME: getHomedir(args)
210
+ TUNE_HOME: getHomedir(home)
393
211
  });
394
212
  _ref = dirs;
395
213
  for (_i = 0, _len = _ref.length; _i < _len; ++_i) {
@@ -422,61 +240,111 @@ async function initContext(args) {
422
240
  if ((typeof m === "function")) {
423
241
  ctx.use(m);
424
242
  } else {
425
- throw Error(tpl("err: Context file export is not an array of functions or function {name}: {module}", {
426
- name: ctxName,
427
- module: m
428
- }));
243
+ throw Error(`err: Context file export is not an array of functions or function ${ctxName}: ${m}`);
429
244
  }
430
245
  }
431
246
  } else {
432
- throw Error(tpl("err: Context file export is not an array of functions or function {name}: {module}", {
433
- name: ctxName,
434
- module: module
435
- }));
247
+ throw Error(`err: Context file export is not an array of functions or function ${ctxName}: ${m}`);
436
248
  }
437
249
  }
438
250
  return ctx;
439
251
  }
440
- initContext;
441
252
  async function main() {
442
- var args, _ref;
253
+
254
+ let version = "0.0.0";
443
255
  try {
444
- var args;
445
- args = parseArgs(process.argv.slice(2));
446
- if (args.help) {
447
- showHelp();
448
- process.exit(0);
449
- }
450
- if (args.version) {
451
- try {
452
- var pkg = require(path.resolve(__dirname, "../package.json"));
453
- console.log(pkg.version || "0.0.0");
454
- } catch (e) {
455
- console.log("0.0.0");
456
- }
457
- process.exit(0);
458
- }
459
- validateArgs(args);
460
- if (args.cmd === "rpc") {
461
- await initConfig(args); // ensure config exists if needed
462
- _ref = await runRpc(args);
463
- } else if (args.cmd === "init") {
464
- await initConfig(args);
465
- _ref = null;
466
- } else {
467
- await initConfig(args); // auto-init if missing
468
- _ref = await run(args);
469
- }
256
+ var pkg = require(path.resolve(__dirname, "../package.json"));
257
+ version = pkg.version || "0.0.0"
470
258
  } catch (e) {
471
- console.error(e.stack);
472
- _ref = process.exit(1);
473
259
  }
474
- return _ref;
260
+
261
+ const program = new Command();
262
+
263
+ program
264
+ .name("tune")
265
+ .description("Command Line Interface for Tune")
266
+ .version(version)
267
+ .helpOption(true)
268
+ .option("--home <dir>", "Tune config directory (default: ~/.tune)")
269
+ .option("--path <paths>", "Additional search paths (colon-separated)")
270
+ .addHelpText("after", "\nEXAMPLES:\n tune --system \"You are Groot\" --user \"Hi how are you?\"\n tune --user \"continue the conversation\" --filename chat.chat --save\n tune --set test=hello --user \"@test\" --system \"Echo assistant\"\n tune rpc\n tune init --force\n");
271
+
272
+ program
273
+ .command("gen", { isDefault: true })
274
+ .description("start or continue ai conversation,\ndefault command")
275
+ .argument("[user]", "user message, the same as --user", (value) => value.replace(/\\n/g, "\n"))
276
+ .option("-u, --user <text>", "User message ", (value) => value.replace(/\\n/g, "\n"))
277
+ .option("-s, --system <text>", "System prompt to use", (value) => value.replace(/\\n/g, "\n"))
278
+ .option("-f, --filename <file>", "Chat file to load/save")
279
+ .option("--save", "Save conversation to file")
280
+ .addOption(new Option("--stop <mode>", "Stop condition", "assistant").choices(["assistant", "step"]).default("assistant"))
281
+ .option("--text <content>", "chat file content, overwrites system message", (value) => value.replace(/\\n/g, "\n"))
282
+ .addOption(new Option("-r, --response <type>", "response format").choices(["content", "json", "messages", "chat"]).default("content"))
283
+ .option("--silent", "generate response but do not print it")
284
+ .option("--set [params...]", "set a template variables, --set a=b --set c=d")
285
+ .action(async (user, opts, cmd) => {
286
+ opts = { ...opts, ...cmd.parent.opts() }
287
+ opts.user ||= user
288
+ opts.params = (opts.set || []).reduce((memo, item) => {
289
+ const [key, value] = item.split("=");
290
+ memo[key] = value
291
+ return memo
292
+ }, {})
293
+ // console.log("gen ", opts)
294
+ if (!opts.user && !opts.text && !opts.filename && !opts.system) {
295
+ return cmd.help();
296
+ }
297
+
298
+ await initConfig({ home: opts.home });
299
+ await run(opts);
300
+ })
301
+
302
+ program
303
+ .command("rpc")
304
+ .description("Start rpc server mode over stdio")
305
+ .option("-d, --debug [file]", "Enable debug output or write debug output to file")
306
+ .action(async (opts, cmd) => {
307
+ opts = { ...opts, ...cmd.parent.opts() }
308
+ // console.log(opts)
309
+ await initConfig(opts); // ensure config exists if needed
310
+ await runRpc(opts);
311
+ })
312
+
313
+ program
314
+ .command("ws")
315
+ .description("Start static webserver with tune context available browser side")
316
+ .option("-p, --port <port>", "port or socket to listen to", 8080)
317
+ .option("-s, --static", "should it serve static files like index.html etc", true)
318
+ .option("-r, --root <path>", "root directory for server", process.cwd())
319
+ .action( async (opts, cmd) => {
320
+ opts = { ...opts, ...cmd.parent.opts() }
321
+ // console.log(opts)
322
+ const { home, path, static, port, root } = opts
323
+
324
+ await initConfig({ home });
325
+
326
+ const ctx = await initContext({ home, path });
327
+ ws({ port, static, root, ctx})
328
+ })
329
+
330
+ program
331
+ .command("init")
332
+ .description("Initialize Tune config directory")
333
+ .option("--force", "Force config initialization (with 'init')")
334
+ .action(async (opts, cmd) => {
335
+ opts = { ...opts, ...cmd.parent.opts() }
336
+ await initConfig(opts);
337
+ })
338
+
339
+ try {
340
+ await program.parseAsync(process.argv);
341
+ }catch (e) {
342
+ console.error(e.stack)
343
+ process.exit(1)
344
+ }
475
345
  }
476
- main;
477
346
 
478
- tpl;
479
- exports.parseArgs = parseArgs;
480
347
  exports.rpc = rpc;
481
348
  exports.main = main;
482
349
  exports.run = run;
350
+ exports.ws = ws;
@@ -0,0 +1,201 @@
1
+ // ContextWebsocket: explicit request/response wrapper over WebSocket.
2
+
3
+ function assert(cond, msg) {
4
+ if (!cond) throw new Error(msg);
5
+ }
6
+
7
+ class StreamController {
8
+ constructor(onClose) {
9
+ this.queue = [];
10
+ this.waiter = null;
11
+ this.closed = false;
12
+ this.err = null;
13
+ this.onClose = onClose;
14
+ }
15
+
16
+ push(value, done = false) {
17
+ if (this.closed) return;
18
+ const items = [];
19
+ if (typeof value !== 'undefined') items.push({ value });
20
+ if (done) items.push({ done });
21
+
22
+ if (this.waiter && items.length) {
23
+ const { resolve } = this.waiter;
24
+ this.waiter = null;
25
+ resolve(items.shift());
26
+ }
27
+
28
+ this.queue.push(...items);
29
+ if (done) this.close();
30
+ }
31
+
32
+ close() {
33
+ if (this.closed) return;
34
+ this.closed = true;
35
+ if (this.waiter) {
36
+ const { resolve } = this.waiter;
37
+ this.waiter = null;
38
+ resolve({ value: undefined, done: true });
39
+ }
40
+ if (this.onClose) this.onClose();
41
+ }
42
+
43
+ fail(err) {
44
+ if (this.closed) return;
45
+ this.err = err;
46
+ this.closed = true;
47
+ if (this.waiter) {
48
+ const { reject } = this.waiter;
49
+ this.waiter = null;
50
+ reject(err);
51
+ }
52
+ if (this.onClose) this.onClose();
53
+ }
54
+
55
+ async next() {
56
+ if (this.err) throw this.err;
57
+ if (this.queue.length) return this.queue.shift();
58
+ if (this.closed) return { value: undefined, done: true };
59
+
60
+ return await new Promise((resolve, reject) => {
61
+ this.waiter = { resolve, reject };
62
+ });
63
+ }
64
+
65
+ [Symbol.asyncIterator]() {
66
+ return this;
67
+ }
68
+ }
69
+
70
+ function ContextWebsocket(url, { debug } = {}) {
71
+ assert(typeof WebSocket !== 'undefined', 'WebSocket is not available');
72
+ assert(typeof url !== 'undefined', 'url is not set');
73
+
74
+ socket = new WebSocket(url);
75
+ console.log(socket)
76
+
77
+ this.socket = socket;
78
+ this.callbacks = Object.create(null);
79
+ this.iterators = Object.create(null);
80
+ this.pending = [];
81
+ this.debug = typeof debug === 'function' ? debug : () => {};
82
+ this.ready = new Promise((resolve, reject) => {
83
+ if (socket.readyState === WebSocket.OPEN) return resolve();
84
+ socket.addEventListener('open', resolve, { once: true });
85
+ socket.addEventListener('error', reject, { once: true });
86
+ });
87
+
88
+
89
+ socket.addEventListener('message', (event) => this._onMessage(event));
90
+ socket.addEventListener('close', () => this._onClose());
91
+ socket.addEventListener('error', () => this._onClose());
92
+ }
93
+
94
+ ContextWebsocket.prototype._onClose = function () {
95
+ const err = new Error('websocket closed');
96
+ for (const id of Object.keys(this.callbacks)) {
97
+ this.callbacks[id].reject(err);
98
+ delete this.callbacks[id];
99
+ }
100
+ for (const id of Object.keys(this.iterators)) {
101
+ this.iterators[id].fail(err);
102
+ delete this.iterators[id];
103
+ }
104
+ };
105
+
106
+ ContextWebsocket.prototype._send = async function (payload) {
107
+ const data = JSON.stringify(payload);
108
+ this.debug('==>', data);
109
+ await this.ready;
110
+ this.socket.send(data);
111
+ };
112
+
113
+ ContextWebsocket.prototype._call = async function (method, params, stream) {
114
+ const id = Math.random().toString(36).slice(2);
115
+ await this._send({ id, method, args: params, stream: !!stream });
116
+
117
+ if (!stream) {
118
+ return await new Promise((resolve, reject) => {
119
+ this.callbacks[id] = { resolve, reject };
120
+ });
121
+ }
122
+
123
+ const ctrl = new StreamController(() => {
124
+ delete this.iterators[id];
125
+ });
126
+ this.iterators[id] = ctrl;
127
+ return ctrl;
128
+ };
129
+
130
+ ContextWebsocket.prototype._onMessage = function (event) {
131
+ let msg;
132
+ try {
133
+ msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
134
+ } catch {
135
+ return;
136
+ }
137
+
138
+ this.debug('<==', msg);
139
+ if (!msg || !msg.id) return;
140
+
141
+ const cb = this.callbacks[msg.id];
142
+ const iter = this.iterators[msg.id];
143
+
144
+ if (msg.error) {
145
+ const err = new Error(msg.error.message || 'rpc error');
146
+ err.stack = msg.error.stack;
147
+
148
+ if (cb) {
149
+ delete this.callbacks[msg.id];
150
+ cb.reject(err);
151
+ return;
152
+ }
153
+ if (iter) {
154
+ iter.fail(err);
155
+ return;
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (cb && (Object.prototype.hasOwnProperty.call(msg, 'result') || msg.done)) {
161
+ delete this.callbacks[msg.id];
162
+ cb.resolve(msg.result);
163
+ return;
164
+ }
165
+
166
+ if (iter) {
167
+ if (msg.done && !Object.prototype.hasOwnProperty.call(msg, 'result')) {
168
+ iter.close();
169
+ } else {
170
+ iter.push(msg.result, !!msg.done);
171
+ }
172
+ }
173
+ };
174
+
175
+ ContextWebsocket.prototype.read = function (name, binary) {
176
+ return this._call('read', [name, binary], false);
177
+ };
178
+
179
+ ContextWebsocket.prototype.write = function (name, content) {
180
+ return this._call('write', [name, content], false);
181
+ };
182
+
183
+ ContextWebsocket.prototype.resolve = function (name, params) {
184
+ return this._call('resolve', [name, params], false);
185
+ };
186
+
187
+ ContextWebsocket.prototype.exec = function (name, params) {
188
+ return this._call('exec', [name, params], false);
189
+ };
190
+
191
+ ContextWebsocket.prototype.file2run = function (payload, params) {
192
+ return this._call('file2run', [payload, params], payload?.stream);
193
+ };
194
+
195
+
196
+ if (typeof module !== 'undefined' && module.exports) {
197
+ module.exports = ContextWebsocket;
198
+ } else {
199
+ window.ContextWebsocket = ContextWebsocket;
200
+ window.ctx = new ContextWebsocket(window.location.origin.replace(/^http/, 'ws'));
201
+ }
package/src/ws.js ADDED
@@ -0,0 +1,123 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const WebSocket = require('ws');
5
+ const mime = require('mime-types');
6
+
7
+ function makeServer({ port, static, root, ctx }) {
8
+ root = path.join(root || process.cwd());
9
+
10
+ function sendFile(filePath, res){
11
+ fs.readFile(filePath, (err, data) => {
12
+ if (err) {
13
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
14
+ return res.end('404 Not Found');
15
+ }
16
+ res.writeHead(200, {
17
+ 'Content-Type': mime.lookup(filePath) || 'application/octet-stream',
18
+ });
19
+ res.end(data);
20
+ });
21
+ }
22
+ const server = http.createServer((req, res) => {
23
+ console.log(`[${req.method.toUpperCase()}] ${req.url}`)
24
+ if (req.method.toUpperCase() !== "GET") {
25
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
26
+ return res.end("ok");
27
+ }
28
+ if (req.url === "/contextws.js") {
29
+ return sendFile(path.join(__dirname, "contextws.js"), res)
30
+ }
31
+ if (!static) {
32
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
33
+ return res.end("ok");
34
+ }
35
+
36
+ let filePath = req.url === '/' ? '/index.html' : req.url;
37
+ // TODO: check if resolved path is not out of root
38
+ filePath = path.join(root, filePath);
39
+ sendFile(filePath, res)
40
+ });
41
+
42
+ const wss = new WebSocket.Server({ server });
43
+
44
+ wss.on('connection', (ws) => {
45
+ ws.on('message', async (msg) => {
46
+ let lctx = ctx.clone()
47
+ let { id, method, args, ...rest } = JSON.parse(msg.toString());
48
+ let result = { id }
49
+ if (method === "read") {
50
+ const [ name, binary ] = args
51
+ let content = await lctx.read(name, binary)
52
+ if (binary) {
53
+ content = content.toString("base64");
54
+ }
55
+ result = { id, result: content, binary, done: true }
56
+ } else if (method === "write") {
57
+ let [ name, content ] = args;
58
+ const { binary } = rest
59
+ if (binary) {
60
+ content = Buffer.from(content, "base64")
61
+ }
62
+ await lctx.write(name, content)
63
+ result.done = true
64
+ } else if (method === "exec") {
65
+ const [name, params] = args;
66
+ const node = await lctx.resolve(name)
67
+ if (!node) {
68
+ result.error = { message: `${name} not found` }
69
+ } else if (!node.exec) {
70
+ result.error = { message: `${name} not executable` }
71
+ } else {
72
+ const content = await ctx.exec(name, params)
73
+ result.result = content
74
+ result.done = true
75
+ }
76
+ } else if (method === "resolve") {
77
+ const [name, params] = args;
78
+ const content = await lctx.resolve(name, params)
79
+ result.result = content
80
+ result.done = true
81
+ } else if (method === "file2run") {
82
+ const [ payload, params ] = args
83
+
84
+ const r = await lctx.file2run({ ...payload, errors: "message" }, params);
85
+ if (!payload?.stream) {
86
+ result.result = r
87
+ result.done = true
88
+ } else {
89
+ (async () => {
90
+ try {
91
+ let chunk = {};
92
+ let lastRes
93
+ while(!chunk.done) {
94
+ chunk = await r.next();
95
+ result.result = (chunk.value || "");
96
+ ws.send(JSON.stringify(result));
97
+ }
98
+ result.done = true
99
+ ws.send(JSON.stringify(result));
100
+ } catch (e) {
101
+ delete result.result
102
+ result.error = {message: e.message, stack: e.stack}
103
+ ws.send(JSON.stringify(result));
104
+ }
105
+ })()
106
+ }
107
+ } else {
108
+ result.error = { message: `method not found ${method}`}
109
+ }
110
+ ws.send(JSON.stringify(result));
111
+ });
112
+ });
113
+
114
+ server.listen(port, () => {
115
+ if (Number.isNaN(parseInt(port))) {
116
+ console.log(`listening ${port}`);
117
+ } else {
118
+ console.log(`listening http://localhost:${port}`);
119
+ }
120
+ });
121
+ }
122
+
123
+ module.exports = makeServer