tune-sdk 0.2.24 → 0.3.1

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
@@ -15,6 +14,7 @@ install tune-sdk
15
14
  ```bash
16
15
  npm install -g tune-sdk
17
16
 
17
+ # create ~/.tune folder and install batteries
18
18
  tune init
19
19
  ```
20
20
 
@@ -40,52 +40,15 @@ user:
40
40
  ```
41
41
  [read more](https://iovdin.github.io/tune/template-language)
42
42
 
43
-
44
- ## Diagram
45
-
46
- ```mermaid
47
- flowchart TD
48
-
49
- subgraph Entry[" "]
50
- Editor["VSCode/Neovim/Sublime Text"]
51
- CLI["CLI"]
52
- App["App"]
53
- end
54
-
55
- subgraph Core[" "]
56
- MD1["~/.tune/default.ctx.js"]
57
- MD2["require('tune-fs')
58
- require('tune-models')
59
- "]
60
- CTX["tune.makeContext(...middlewares)"]
61
- F2R["ctx.file2run(params)"]
62
- end
63
-
64
-
65
- MD1 --> |cli middlewares| CTX
66
- MD2 --> |app middlewares| CTX
67
- Editor -->| $ tune rpc | Core
68
- CLI --> | $ tune --user hello | Core
69
- App --> Core
70
-
71
-
72
-
73
- F2R -->|ctx.resolve #40; system.txt #124; shell #124; gpt-5 #41; | CTX
74
- CTX -->| #123; type: text #124; tool #124; llm #125; | F2R
75
-
76
- F2R --> |fetch| LLM["https://provider.com/v1/chat/completions"]
77
- F2R --> |call| Tool
78
- ```
79
-
80
43
  ## Extend with Middlewares
81
44
  Extend Tune with middlewares:
82
45
 
83
- * [tune-fs](https://www.npmjs.com/package/tune-fs) - connect tools & files from local filesystem
84
- * [tune-models](https://www.npmjs.com/package/tune-models) - connect llm models from Anthropic/OpenAI/Gemini/Openrouter/Mistral/Groq
85
- * [tune-basic-toolset](https://www.npmjs.com/package/tune-basic-toolset) - basic tools like read file, write file, shell etc.
86
- * [tune-s3](https://www.npmjs.com/package/tune-s3) - read/write files from s3
87
- * [tune-mcp](https://www.npmjs.com/package/tune-mcp) - connect tools from mcp servers
88
- * [maik](https://www.npmjs.com/package/@iovdin/maik) - fetch all you emails, and index them into sqlite database
46
+ * [tune-fs](https://github.com/iovdin/tune-fs) - connect tools & files from local filesystem
47
+ * [tune-models](https://github.com/iovdin/tune-models) - connect llm models from Anthropic/OpenAI/Gemini/Openrouter/Mistral/Groq
48
+ * [tune-basic-toolset](https://github.com/iovdin/tune-basic-toolset) - basic tools like read file, write file, shell etc.
49
+ * [tune-s3](https://github.com/iovdin/tune-s3) - read/write files from s3
50
+ * [tune-mcp](https://github.com/iovdin/tune-mcp) - connect tools from mcp servers
51
+ * [maik](https://github.com/iovdin/maik) - fetch all you emails, and index them into sqlite database
89
52
 
90
53
 
91
54
  For example:
@@ -137,6 +100,8 @@ image generated
137
100
  # install tune globally
138
101
  npm install -g tune-sdk
139
102
 
103
+ tune "hi how are you?"
104
+
140
105
  # append user message to newchat.chat run and save
141
106
  tune --user "hi how are you?" --filename newchat.chat --save
142
107
 
@@ -144,35 +109,114 @@ tune --user "hi how are you?" --filename newchat.chat --save
144
109
  # print result to console
145
110
  tune --system "You are Groot" --user "Hi how are you?"
146
111
 
147
- #set context variable
148
- tune --set-test "hello" --user "@test" --system "You are echo you print everythting back"
112
+ # set context variable
113
+ tune --set test="hello" --user "@test" --system "You are echo you print everythting back"
114
+ # prints hello
115
+
149
116
  ```
150
117
 
151
118
 
152
119
  ## Javascript SDK
153
120
  `npm install tune-sdk`
154
121
 
155
- ```javascript
156
- const tune = require("tune-sdk");
157
- const sonnet = require("./sonnet.llm.js");
122
+ Tune core is middleware-based. A context resolves `@name` references into nodes like `text`, `tool`, `llm`, and `processor`.
158
123
 
159
- require('dotenv').config();
124
+ ```javascript
125
+ const tune = require("tune-sdk")
160
126
 
161
127
  async function main() {
162
- const ctx = tune.makeContext({
163
- echo: "You are echo, you print everything back",
164
- OPENROUTER_KEY: process.env.OPENROUTER_KEY,
165
- "default": {
166
- type: "llm",
167
- exec: sonnet
128
+ const ctx = tune.makeContext()
129
+
130
+ ctx.use(async function middleware(name) {
131
+ if (name === "file.txt") {
132
+ return {
133
+ type: "text",
134
+ name: "file.txt",
135
+ read: async () => fs.readFileSync("file.txt", "utf8")
136
+ }
137
+ }
138
+
139
+ if (name === "readfile") {
140
+ return {
141
+ type: "tool",
142
+ name: "readfile",
143
+ schema: {
144
+ type: "object",
145
+ properties: {
146
+ filename: { type: "string" }
147
+ }
148
+ },
149
+ exec: async ({ filename }) => fs.readFileSync(filename, "utf8")
150
+ }
151
+ }
152
+
153
+ if (name === "gpt-5") {
154
+ return {
155
+ type: "llm",
156
+ name: "gpt-5",
157
+ exec: async (payload) => ({
158
+ url: "https://api.openai.com/v1/chat/completions",
159
+ method: "POST",
160
+ headers: {
161
+ Authorization: `Bearer ${process.env.OPENAI_KEY}`,
162
+ "Content-Type": "application/json"
163
+ },
164
+ body: JSON.stringify({
165
+ model: "gpt-5",
166
+ ...payload
167
+ })
168
+ })
169
+ }
170
+ }
171
+
172
+ if (name === "tail") {
173
+ return {
174
+ type: "processor",
175
+ name: "tail",
176
+ exec: async (node, args) => {
177
+ if (!node) return
178
+ if (node.type !== "text") throw Error("tail can only modify text nodes")
179
+ return {
180
+ ...node,
181
+ read: async () => {
182
+ const content = await node.read()
183
+ const n = parseInt(args.trim(), 10) || 20
184
+ return content.split("\n").slice(-n).join("\n")
185
+ }
186
+ }
187
+ }
188
+ }
168
189
  }
169
190
  })
170
191
 
171
- const text = "s: @echo\nu: hello world";
172
- const messages = await tune.text2run(text, ctx)
173
- console.log(tune.msg2text(messages))
174
- // a: hello world
192
+ const content = await ctx.file2run({
193
+ system: "@gpt-5 @readfile",
194
+ user: "can you read file.txt?",
195
+ stream: false,
196
+ response: "content"
197
+ })
198
+
199
+ console.log(content)
175
200
  }
201
+
176
202
  main()
177
203
  ```
204
+
178
205
  [read more](https://iovdin.github.io/tune/api) about javascript sdk
206
+
207
+ ## Help / Manual
208
+
209
+ You can access tune manuals and available middlewares manuals from
210
+
211
+ ```chat
212
+ system:
213
+ @man include all manuals for all connected packages
214
+ @man/ - list all the manuals, like list directory
215
+ @man/tune - get manual for tune core package
216
+ @man/tune-basic-toolset - get manual for tune-basic-tool set package
217
+
218
+ tool_call: rf { "filename": "man/tune-basic-toolset"}
219
+ tool_result:
220
+ @man/tune-basic-toolset
221
+
222
+ ```
@@ -1,5 +1,6 @@
1
1
  const path = require('path')
2
2
 
3
+ const man = require("tune-sdk/man");
3
4
  const models = require("tune-models")
4
5
  const basics = require("tune-basic-toolset")
5
6
  const tunefs = require("tune-fs")
@@ -11,6 +12,7 @@ if (process.env.TUNE_PATH) {
11
12
  }
12
13
 
13
14
  module.exports = [
15
+ man(),
14
16
  current(),
15
17
  basics(),
16
18
  tunefs({ paths: dirs, makeSchema: true }),
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tune-sdk",
3
- "version": "0.2.24",
4
- "description": "tune - LLM chat in text file",
3
+ "version": "0.3.1",
4
+ "description": "chat with llm in a text file. core package",
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,12 @@
18
18
  },
19
19
  "./rpc": {
20
20
  "require": "./src/rpc.js"
21
+ },
22
+ "./contextws": {
23
+ "require": "./src/contextws.js"
24
+ },
25
+ "./man": {
26
+ "require": "./src/man.js"
21
27
  }
22
28
  },
23
29
  "keywords": [
@@ -34,5 +40,10 @@
34
40
  "scripts": {
35
41
  "test": "node test/index.js",
36
42
  "test:watch": "node --watch test/index.js"
43
+ },
44
+ "dependencies": {
45
+ "commander": "^14.0.3",
46
+ "mime-types": "^3.0.2",
47
+ "ws": "^8.20.0"
37
48
  }
38
49
  }
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/man.js ADDED
@@ -0,0 +1,117 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ // all the registered manuals
4
+ // { name, description, filename/content }
5
+
6
+ let mans = [
7
+ ]
8
+
9
+ /*
10
+ @man include all manuals for all packages included
11
+ @man/ - list all the manuals, like list directory
12
+ @man/tune - get manual for tune core package
13
+ @man/tune-basic-toolset - get manual for tune-basic-tool set package
14
+
15
+ TODO:
16
+ @man/tune/js-api - lis
17
+ @man/tune/agents - lis
18
+ */
19
+
20
+ function read({ filename, content }) {
21
+ if (content) {
22
+ return content
23
+ }
24
+ if (!fs.existsSync(filename)) {
25
+ return `file ${filename} not found`
26
+ }
27
+ return fs.readFileSync(filename)
28
+ }
29
+
30
+ module.exports = ({ mount } = {}) => async (name, { type }) => {
31
+ if ((type || "any") !== 'any' && type !== 'text' ) {
32
+ return
33
+ }
34
+ mount ||= "man"
35
+
36
+ // supported only with mount
37
+ if (!mount) {
38
+ return
39
+ }
40
+
41
+ // @man include all the manuals
42
+ if (name === mount) {
43
+ return {
44
+ type: "text",
45
+ name,
46
+ read: async () => mans.map(man => `<${man.name}>\n ${read(man)} \n</${man.name}>`).join("\n")
47
+ }
48
+ }
49
+
50
+ if (!name.startsWith(mount + '/')) {
51
+ return
52
+ }
53
+
54
+ // @man/ - list all the manuals to include
55
+ if (name === mount + '/') {
56
+ return {
57
+ type: "text",
58
+ name,
59
+ read: async () => mans.map(man => `${man.name} - ${man.description}`).join("\n")
60
+ }
61
+ }
62
+
63
+
64
+ // @man/tune - get manual for tune core package
65
+ // @man/tune-basic-toolset - get manual for tune-basic-tool set package
66
+
67
+ const actualName = name.slice(mount.length + 1);
68
+
69
+ const man = mans.find(man => man.name === actualName);
70
+ if (!man) return
71
+
72
+ return {
73
+ type: "text",
74
+ name,
75
+ read: async() => read(man.filename)
76
+ }
77
+ }
78
+
79
+ const addManual = (dirname) => {
80
+ let package = path.resolve(dirname, "package.json");
81
+ if (!fs.existsSync(package)){
82
+ dirname = path.resolve(dirname, "..")
83
+ package = path.resolve(dirname, "package.json");
84
+ }
85
+
86
+ if (!fs.existsSync(package)){
87
+ throw Error(`cant make manual ${package} not found`);
88
+ }
89
+
90
+ const { name, version, description } = JSON.parse(fs.readFileSync(package, "utf8"));
91
+
92
+ const newMans = mans.filter(man => man.name !== name)
93
+
94
+ const filename = path.resolve(dirname, "README.md")
95
+
96
+ newMans.push({name, description, filename})
97
+ mans = newMans
98
+ }
99
+
100
+ addManual(__dirname)
101
+ /*
102
+ const addManual = ({name, description, content, filename}) => {
103
+ const = params;
104
+
105
+ const newMans = mans.filter(man => man.name !== name)
106
+ if (!name) {
107
+ throw Error(`name is not set for manual`)
108
+ }
109
+ if (!content && !filename) {
110
+ throw Error(`neither content nor filename is set for '${name}' manual`)
111
+ }
112
+
113
+ newMans.push({name, description, content, filename})
114
+ }
115
+ */
116
+
117
+ module.exports.addManual = addManual;
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