qwall-work 1.0.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 (47) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +62 -0
  4. package/SECURITY.md +5 -0
  5. package/bin/qwall-work.js +8 -0
  6. package/docs/configuration.md +15 -0
  7. package/docs/models.md +17 -0
  8. package/docs/security.md +7 -0
  9. package/docs/tools.md +13 -0
  10. package/package.json +40 -0
  11. package/src/ai/chat.js +56 -0
  12. package/src/ai/client.js +12 -0
  13. package/src/ai/models.js +23 -0
  14. package/src/ai/streaming.js +15 -0
  15. package/src/ai/tool-handler.js +13 -0
  16. package/src/ai/tools.js +59 -0
  17. package/src/cli/commands.js +41 -0
  18. package/src/cli/help.js +25 -0
  19. package/src/cli/prompts.js +26 -0
  20. package/src/cli/renderer.js +11 -0
  21. package/src/cli/repl.js +72 -0
  22. package/src/config/api-key.js +2 -0
  23. package/src/config/index.js +40 -0
  24. package/src/config/model.js +2 -0
  25. package/src/config/paths.js +5 -0
  26. package/src/constants/commands.js +6 -0
  27. package/src/constants/version.js +3 -0
  28. package/src/index.js +7 -0
  29. package/src/providers/google/client.js +9 -0
  30. package/src/providers/google/errors.js +11 -0
  31. package/src/providers/google/models.js +2 -0
  32. package/src/session/context.js +8 -0
  33. package/src/session/history.js +12 -0
  34. package/src/session/index.js +2 -0
  35. package/src/tools/edit-file.js +14 -0
  36. package/src/tools/index.js +7 -0
  37. package/src/tools/list-files.js +12 -0
  38. package/src/tools/nano.js +16 -0
  39. package/src/tools/read-file.js +8 -0
  40. package/src/tools/write-file.js +11 -0
  41. package/src/utils/errors.js +8 -0
  42. package/src/utils/logger.js +4 -0
  43. package/src/utils/paths.js +3 -0
  44. package/src/utils/platform.js +7 -0
  45. package/src/workspace/index.js +9 -0
  46. package/src/workspace/manager.js +9 -0
  47. package/src/workspace/security.js +16 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ - Initial production npm release.
6
+ - Gemini/Gemma model discovery.
7
+ - Interactive AI chat.
8
+ - Workspace file tools.
9
+ - Nano integration.
10
+ - User API-key configuration.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Codetable Software
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Qwall Work
2
+
3
+ AI workspace CLI powered by the Google Gemini API.
4
+
5
+ ## Features
6
+
7
+ - Gemini and Gemma model discovery
8
+ - User-owned Gemini API key
9
+ - Interactive AI chat
10
+ - File read/write/edit/list tools
11
+ - Optional nano integration
12
+ - Workspace path protection
13
+ - CommonJS Node.js CLI
14
+ - npm global installation
15
+
16
+ ## Requirements
17
+
18
+ - Node.js >= 18
19
+ - A Google Gemini API key
20
+ - `nano` only if you want the `open_in_nano` tool
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g qwall-work
26
+ ```
27
+
28
+ Run:
29
+
30
+ ```bash
31
+ qwall
32
+ ```
33
+
34
+ Configure:
35
+
36
+ ```bash
37
+ qwall-work config
38
+ ```
39
+
40
+ List available Gemini/Gemma models:
41
+
42
+ ```bash
43
+ qwall-work models
44
+ ```
45
+
46
+ ## Workspace
47
+
48
+ By default, AI file operations are restricted to:
49
+
50
+ ```text
51
+ <current-directory>/.qwall-workspace
52
+ ```
53
+
54
+ You can choose another workspace:
55
+
56
+ ```bash
57
+ QWALL_WORKSPACE=/path/to/project qwall
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT - Copyright (c) Codetable Software
package/SECURITY.md ADDED
@@ -0,0 +1,5 @@
1
+ # Security Policy
2
+
3
+ Please do not publish API keys, credentials, private project files, or other secrets in issues.
4
+
5
+ Report security problems privately to the Codetable Software maintainers.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ require("../src/index.js").main().catch((error) => {
5
+ console.error(`\nQwall Work: ${error.message}`);
6
+ if (process.env.QWALL_DEBUG) console.error(error.stack);
7
+ process.exitCode = 1;
8
+ });
@@ -0,0 +1,15 @@
1
+ # Configuration
2
+
3
+ Set the API key with `qwall-work config` or:
4
+
5
+ ```bash
6
+ export GEMINI_API_KEY="your-key"
7
+ ```
8
+
9
+ Optional model:
10
+
11
+ ```bash
12
+ export QWALL_MODEL="gemini-2.5-flash"
13
+ ```
14
+
15
+ Configuration is stored in `~/.qwall-work/config.json` with restrictive file permissions.
package/docs/models.md ADDED
@@ -0,0 +1,17 @@
1
+ # Models
2
+
3
+ Qwall Work queries the Google Gemini API model list and filters it to Gemini and Gemma models that expose content generation.
4
+
5
+ Use:
6
+
7
+ ```bash
8
+ qwall-work models
9
+ ```
10
+
11
+ Then select a model with:
12
+
13
+ ```text
14
+ /model
15
+ ```
16
+
17
+ Availability depends on the user's API key and Google's current API catalog.
@@ -0,0 +1,7 @@
1
+ # Security
2
+
3
+ The AI file tools are restricted to the Qwall Work workspace. Parent-directory traversal such as `../secret` is rejected.
4
+
5
+ Never commit or publish an API key. Use `GEMINI_API_KEY` or the local Qwall Work configuration.
6
+
7
+ The model can request file tools, so review generated changes when working on important projects.
package/docs/tools.md ADDED
@@ -0,0 +1,13 @@
1
+ # Tools
2
+
3
+ Qwall Work exposes these AI tools:
4
+
5
+ - `read_file`
6
+ - `write_file`
7
+ - `edit_file`
8
+ - `list_files`
9
+ - `open_in_nano`
10
+
11
+ All file paths are constrained to the configured Qwall Work workspace.
12
+
13
+ `open_in_nano` requires the `nano` executable to be installed and available on PATH.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "qwall-work",
3
+ "version": "1.0.0",
4
+ "description": "AI workspace CLI powered by Google Gemini API",
5
+ "main": "src/index.js",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "qwall-work": "./bin/qwall-work.js",
9
+ "qwall": "./bin/qwall-work.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "docs",
15
+ "README.md",
16
+ "CHANGELOG.md",
17
+ "LICENSE",
18
+ "SECURITY.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "node test/workspace/security.test.js && node test/tools/write-file.test.js && node test/tools/edit-file.test.js"
22
+ },
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "dependencies": {
27
+ "@google/genai": "^1.20.0"
28
+ },
29
+ "keywords": [
30
+ "qwall",
31
+ "qwall-work",
32
+ "ai",
33
+ "cli",
34
+ "gemini",
35
+ "gemma",
36
+ "google"
37
+ ],
38
+ "author": "Codetable Software",
39
+ "license": "MIT"
40
+ }
package/src/ai/chat.js ADDED
@@ -0,0 +1,56 @@
1
+ const { getClient } = require("./client");
2
+ const { buildTools } = require("./tools");
3
+ const { executeTool } = require("./tool-handler");
4
+ const { getWorkspace } = require("../workspace");
5
+
6
+ async function ask({ model, input, history, onTool }) {
7
+ const ai = getClient();
8
+ const contents = [...history, { role: "user", parts: [{ text: input }] }];
9
+
10
+ const response = await ai.models.generateContent({
11
+ model,
12
+ contents,
13
+ config: {
14
+ tools: buildTools()
15
+ }
16
+ });
17
+
18
+ const candidate = response.candidates?.[0];
19
+ const parts = candidate?.content?.parts || [];
20
+ const toolResults = [];
21
+
22
+ for (const part of parts) {
23
+ if (part.functionCall) {
24
+ const call = part.functionCall;
25
+ onTool?.({ name: call.name, status: "running" });
26
+ const result = await executeTool(call.name, call.args || {}, getWorkspace());
27
+ toolResults.push({ name: call.name, result });
28
+ onTool?.({ name: call.name, path: call.args?.path, status: "completed" });
29
+ }
30
+ }
31
+
32
+ let text = parts.filter(p => p.text).map(p => p.text).join("");
33
+ let nextHistory = [...contents];
34
+
35
+ if (toolResults.length) {
36
+ const functionResponses = toolResults.map(({ name, result }) => ({
37
+ functionResponse: { name, response: result }
38
+ }));
39
+ nextHistory.push({ role: "model", parts: parts.filter(p => p.functionCall) });
40
+ nextHistory.push({ role: "user", parts: functionResponses });
41
+
42
+ const followup = await ai.models.generateContent({
43
+ model,
44
+ contents: nextHistory,
45
+ config: { tools: buildTools() }
46
+ });
47
+ text = followup.text || text;
48
+ nextHistory.push(followup.candidates?.[0]?.content || { role: "model", parts: [{ text }] });
49
+ } else {
50
+ nextHistory.push(candidate?.content || { role: "model", parts: [{ text }] });
51
+ }
52
+
53
+ return { text, history: nextHistory };
54
+ }
55
+
56
+ module.exports = { ask };
@@ -0,0 +1,12 @@
1
+ const { GoogleGenAI } = require("@google/genai");
2
+ const config = require("../config");
3
+
4
+ function getClient() {
5
+ const apiKey = config.getApiKey();
6
+ if (!apiKey) {
7
+ throw new Error("Gemini API key is not configured. Run `qwall-work config` or set GEMINI_API_KEY.");
8
+ }
9
+ return new GoogleGenAI({ apiKey });
10
+ }
11
+
12
+ module.exports = { getClient };
@@ -0,0 +1,23 @@
1
+ const { getClient } = require("./client");
2
+
3
+ async function fetchModels() {
4
+ const ai = getClient();
5
+ const result = [];
6
+ for await (const model of await ai.models.list()) {
7
+ const name = model.name || "";
8
+ const methods = model.supportedActions || model.supportedGenerationMethods || [];
9
+ const supported = methods.length === 0 || methods.some(m =>
10
+ String(m).toLowerCase().includes("generatecontent")
11
+ );
12
+ if (supported && (/gemini|gemma/i.test(name) || /gemini|gemma/i.test(model.displayName || ""))) {
13
+ result.push({
14
+ name,
15
+ displayName: model.displayName || name,
16
+ description: model.description || ""
17
+ });
18
+ }
19
+ }
20
+ return result;
21
+ }
22
+
23
+ module.exports = { fetchModels };
@@ -0,0 +1,15 @@
1
+ const { getClient } = require("./client");
2
+
3
+ async function streamText({ model, contents, onText }) {
4
+ const ai = getClient();
5
+ const stream = await ai.models.generateContentStream({ model, contents });
6
+ let full = "";
7
+ for await (const chunk of stream) {
8
+ const text = chunk.text || "";
9
+ full += text;
10
+ onText?.(text);
11
+ }
12
+ return full;
13
+ }
14
+
15
+ module.exports = { streamText };
@@ -0,0 +1,13 @@
1
+ const tools = require("../tools");
2
+
3
+ async function executeTool(name, args, workspace) {
4
+ switch (name) {
5
+ case "read_file": return { content: await tools.readFile(args.path, workspace) };
6
+ case "write_file": return await tools.writeFile(args.path, args.content, workspace);
7
+ case "edit_file": return await tools.editFile(args.path, args.oldText, args.newText, workspace);
8
+ case "list_files": return { files: await tools.listFiles(args.path || ".", workspace) };
9
+ case "open_in_nano": return await tools.openNano(args.path, workspace);
10
+ default: throw new Error(`Unknown tool: ${name}`);
11
+ }
12
+ }
13
+ module.exports = { executeTool };
@@ -0,0 +1,59 @@
1
+ function buildTools() {
2
+ return [{
3
+ functionDeclarations: [
4
+ {
5
+ name: "read_file",
6
+ description: "Read a UTF-8 text file inside the Qwall Work workspace.",
7
+ parameters: {
8
+ type: "OBJECT",
9
+ properties: { path: { type: "STRING", description: "Relative file path." } },
10
+ required: ["path"]
11
+ }
12
+ },
13
+ {
14
+ name: "write_file",
15
+ description: "Create or replace a UTF-8 text file inside the Qwall Work workspace.",
16
+ parameters: {
17
+ type: "OBJECT",
18
+ properties: {
19
+ path: { type: "STRING", description: "Relative file path." },
20
+ content: { type: "STRING", description: "Complete file content." }
21
+ },
22
+ required: ["path", "content"]
23
+ }
24
+ },
25
+ {
26
+ name: "edit_file",
27
+ description: "Replace an exact text fragment in a UTF-8 file inside the workspace.",
28
+ parameters: {
29
+ type: "OBJECT",
30
+ properties: {
31
+ path: { type: "STRING" },
32
+ oldText: { type: "STRING" },
33
+ newText: { type: "STRING" }
34
+ },
35
+ required: ["path", "oldText", "newText"]
36
+ }
37
+ },
38
+ {
39
+ name: "list_files",
40
+ description: "List files and directories inside the workspace.",
41
+ parameters: {
42
+ type: "OBJECT",
43
+ properties: { path: { type: "STRING", description: "Relative directory path." } }
44
+ }
45
+ },
46
+ {
47
+ name: "open_in_nano",
48
+ description: "Open a workspace file in the nano editor. Use when the user explicitly asks to edit with nano.",
49
+ parameters: {
50
+ type: "OBJECT",
51
+ properties: { path: { type: "STRING" } },
52
+ required: ["path"]
53
+ }
54
+ }
55
+ ]
56
+ }];
57
+ }
58
+
59
+ module.exports = { buildTools };
@@ -0,0 +1,41 @@
1
+ const { startRepl } = require("./repl");
2
+ const { showHelp } = require("./help");
3
+ const { getVersion } = require("../constants/version");
4
+ const config = require("../config");
5
+ const { fetchModels } = require("../ai/models");
6
+
7
+ async function runCLI(args) {
8
+ const command = args[0];
9
+
10
+ if (args.includes("--help") || command === "help") {
11
+ showHelp();
12
+ return;
13
+ }
14
+
15
+ if (args.includes("--version") || command === "version") {
16
+ console.log(`Qwall Work ${getVersion()}`);
17
+ return;
18
+ }
19
+
20
+ if (command === "config") {
21
+ await config.interactive();
22
+ return;
23
+ }
24
+
25
+ if (command === "models") {
26
+ const models = await fetchModels();
27
+ for (const model of models) {
28
+ console.log(`${model.displayName} (${model.name})`);
29
+ }
30
+ return;
31
+ }
32
+
33
+ await startRepl({ initialModel: getOption(args, "--model") });
34
+ }
35
+
36
+ function getOption(args, flag) {
37
+ const index = args.indexOf(flag);
38
+ return index >= 0 ? args[index + 1] : undefined;
39
+ }
40
+
41
+ module.exports = { runCLI };
@@ -0,0 +1,25 @@
1
+ function showHelp() {
2
+ console.log(`
3
+ Qwall Work - AI workspace CLI
4
+
5
+ Usage:
6
+ qwall-work
7
+ qwall-work config
8
+ qwall-work models
9
+ qwall-work --model <model>
10
+ qwall-work --help
11
+ qwall-work --version
12
+
13
+ Inside chat:
14
+ /model Select a Gemini/Gemma model
15
+ /clear Clear conversation history
16
+ /help Show commands
17
+ /exit Exit
18
+
19
+ Environment:
20
+ GEMINI_API_KEY Google Gemini API key
21
+ QWALL_WORKSPACE Optional workspace path
22
+ QWALL_MODEL Optional default model
23
+ `);
24
+ }
25
+ module.exports = { showHelp };
@@ -0,0 +1,26 @@
1
+ const readline = require("readline");
2
+ const { fetchModels } = require("../ai/models");
3
+ const config = require("../config");
4
+
5
+ function question(prompt) {
6
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
7
+ return new Promise(resolve => rl.question(prompt, answer => {
8
+ rl.close();
9
+ resolve(answer.trim());
10
+ }));
11
+ }
12
+
13
+ async function selectModel() {
14
+ const models = await fetchModels();
15
+ if (!models.length) throw new Error("No Gemini/Gemma models are available for this API key.");
16
+
17
+ console.log("\nAvailable models:");
18
+ models.forEach((m, i) => console.log(`${i + 1}. ${m.displayName} (${m.name})`));
19
+
20
+ const answer = await question(`Select model [1-${models.length}] (current: ${config.getModel() || "none"}): `);
21
+ const index = Number(answer) - 1;
22
+ if (!Number.isInteger(index) || !models[index]) throw new Error("Invalid model selection.");
23
+ return models[index].name;
24
+ }
25
+
26
+ module.exports = { selectModel };
@@ -0,0 +1,11 @@
1
+ function renderText(text) {
2
+ if (text) process.stdout.write(`\nQwall > ${text}\n`);
3
+ }
4
+
5
+ function renderTool(tool) {
6
+ console.log(`\n● Tool: ${tool.name}`);
7
+ if (tool.path) console.log(` File: ${tool.path}`);
8
+ if (tool.status) console.log(` ${tool.status}`);
9
+ }
10
+
11
+ module.exports = { renderText, renderTool };
@@ -0,0 +1,72 @@
1
+ const readline = require("readline");
2
+ const { ask } = require("../ai/chat");
3
+ const { renderText, renderTool } = require("./renderer");
4
+ const { selectModel } = require("./prompts");
5
+ const config = require("../config");
6
+ const { getWorkspace } = require("../workspace");
7
+
8
+ async function startRepl({ initialModel } = {}) {
9
+ let model = initialModel || config.getModel();
10
+ const workspace = getWorkspace();
11
+
12
+ console.log("╭────────────────────────────────────────╮");
13
+ console.log("│ Qwall Work │");
14
+ console.log("│ Gemini & Gemma AI CLI │");
15
+ console.log("╰────────────────────────────────────────╯");
16
+ console.log(`Model: ${model}`);
17
+ console.log(`Workspace: ${workspace}`);
18
+ console.log("Commands: /model /clear /help /exit");
19
+
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ prompt: "\nYou > "
24
+ });
25
+
26
+ let history = [];
27
+ rl.prompt();
28
+
29
+ for await (const line of rl) {
30
+ const input = line.trim();
31
+ if (!input) { rl.prompt(); continue; }
32
+
33
+ if (input === "/exit" || input === "/quit") break;
34
+ if (input === "/help") {
35
+ console.log("/model Select a Gemini/Gemma model");
36
+ console.log("/clear Clear conversation history");
37
+ console.log("/exit Exit Qwall Work");
38
+ rl.prompt();
39
+ continue;
40
+ }
41
+ if (input === "/clear") {
42
+ history = [];
43
+ console.log("Conversation cleared.");
44
+ rl.prompt();
45
+ continue;
46
+ }
47
+ if (input === "/model") {
48
+ model = await selectModel();
49
+ config.setModel(model);
50
+ console.log(`Model: ${model}`);
51
+ rl.prompt();
52
+ continue;
53
+ }
54
+
55
+ try {
56
+ const result = await ask({
57
+ model,
58
+ input,
59
+ history,
60
+ onTool: renderTool
61
+ });
62
+ renderText(result.text);
63
+ history = result.history;
64
+ } catch (error) {
65
+ console.error(`\nError: ${error.message}`);
66
+ }
67
+ rl.prompt();
68
+ }
69
+
70
+ rl.close();
71
+ }
72
+ module.exports = { startRepl };
@@ -0,0 +1,2 @@
1
+ const config = require("./index");
2
+ module.exports = { getApiKey: config.getApiKey, setApiKey: config.setApiKey };
@@ -0,0 +1,40 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+
5
+ const DIR = path.join(os.homedir(), ".qwall-work");
6
+ const FILE = path.join(DIR, "config.json");
7
+
8
+ function load() {
9
+ try { return JSON.parse(fs.readFileSync(FILE, "utf8")); }
10
+ catch { return {}; }
11
+ }
12
+ function save(data) {
13
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
14
+ fs.writeFileSync(FILE, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
15
+ }
16
+ function getApiKey() {
17
+ return process.env.GEMINI_API_KEY || load().apiKey || "";
18
+ }
19
+ function setApiKey(apiKey) {
20
+ const data = load(); data.apiKey = apiKey; save(data);
21
+ }
22
+ function getModel() {
23
+ return process.env.QWALL_MODEL || load().model || "gemini-2.5-flash";
24
+ }
25
+ function setModel(model) {
26
+ const data = load(); data.model = model; save(data);
27
+ }
28
+ async function interactive() {
29
+ const readline = require("readline");
30
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
31
+ const ask = p => new Promise(resolve => rl.question(p, resolve));
32
+ const current = getApiKey();
33
+ const key = await ask(`Gemini API key ${current ? "(configured, press Enter to keep)" : ""}: `);
34
+ if (key.trim()) setApiKey(key.trim());
35
+ const model = await ask(`Model [${getModel()}]: `);
36
+ if (model.trim()) setModel(model.trim());
37
+ rl.close();
38
+ console.log("Configuration saved.");
39
+ }
40
+ module.exports = { getApiKey, setApiKey, getModel, setModel, interactive };
@@ -0,0 +1,2 @@
1
+ const config = require("./index");
2
+ module.exports = { getModel: config.getModel, setModel: config.setModel };
@@ -0,0 +1,5 @@
1
+ const os = require("os");
2
+ const path = require("path");
3
+ function configDir() { return path.join(os.homedir(), ".qwall-work"); }
4
+ function configFile() { return path.join(configDir(), "config.json"); }
5
+ module.exports = { configDir, configFile };
@@ -0,0 +1,6 @@
1
+ module.exports = Object.freeze({
2
+ HELP: "help",
3
+ CONFIG: "config",
4
+ MODELS: "models",
5
+ VERSION: "version"
6
+ });
@@ -0,0 +1,3 @@
1
+ const VERSION = "1.0.0";
2
+ function getVersion() { return VERSION; }
3
+ module.exports = { VERSION, getVersion };
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ const { runCLI } = require("./cli/commands");
2
+
3
+ async function main() {
4
+ await runCLI(process.argv.slice(2));
5
+ }
6
+
7
+ module.exports = { main };
@@ -0,0 +1,9 @@
1
+ const { GoogleGenAI } = require("@google/genai");
2
+ const config = require("../../config");
3
+
4
+ function createGoogleClient() {
5
+ const key = config.getApiKey();
6
+ if (!key) throw new Error("GEMINI_API_KEY is not configured.");
7
+ return new GoogleGenAI({ apiKey: key });
8
+ }
9
+ module.exports = { createGoogleClient };
@@ -0,0 +1,11 @@
1
+ function normalizeGoogleError(error) {
2
+ const message = error?.message || String(error);
3
+ if (/api.?key|unauthorized|permission/i.test(message)) {
4
+ return new Error("Google API authentication failed. Check your Gemini API key.");
5
+ }
6
+ if (/quota|rate.?limit|resource.?exhausted/i.test(message)) {
7
+ return new Error("Google Gemini API quota or rate limit was reached.");
8
+ }
9
+ return new Error(message);
10
+ }
11
+ module.exports = { normalizeGoogleError };
@@ -0,0 +1,2 @@
1
+ const { fetchModels } = require("../../ai/models");
2
+ module.exports = { fetchModels };
@@ -0,0 +1,8 @@
1
+ function createContext({ workspace, model }) {
2
+ return {
3
+ workspace,
4
+ model,
5
+ createdAt: new Date().toISOString()
6
+ };
7
+ }
8
+ module.exports = { createContext };
@@ -0,0 +1,12 @@
1
+ function createSession() {
2
+ return { messages: [] };
3
+ }
4
+ function addMessage(session, message) {
5
+ session.messages.push(message);
6
+ return session;
7
+ }
8
+ function clearSession(session) {
9
+ session.messages.length = 0;
10
+ return session;
11
+ }
12
+ module.exports = { createSession, addMessage, clearSession };
@@ -0,0 +1,2 @@
1
+ const { createSession, addMessage, clearSession } = require("./history");
2
+ module.exports = { createSession, addMessage, clearSession };
@@ -0,0 +1,14 @@
1
+ const fs = require("fs/promises");
2
+ const { resolveSafePath } = require("../workspace/security");
3
+
4
+ async function editFile(relativePath, oldText, newText, workspace) {
5
+ const target = resolveSafePath(workspace, relativePath);
6
+ const content = await fs.readFile(target, "utf8");
7
+ if (!content.includes(oldText)) {
8
+ throw new Error(`Text to replace was not found in ${relativePath}.`);
9
+ }
10
+ const updated = content.replace(oldText, newText);
11
+ await fs.writeFile(target, updated, "utf8");
12
+ return { ok: true, path: relativePath };
13
+ }
14
+ module.exports = { editFile };
@@ -0,0 +1,7 @@
1
+ const { readFile } = require("./read-file");
2
+ const { writeFile } = require("./write-file");
3
+ const { editFile } = require("./edit-file");
4
+ const { listFiles } = require("./list-files");
5
+ const { openNano } = require("./nano");
6
+
7
+ module.exports = { readFile, writeFile, editFile, listFiles, openNano };
@@ -0,0 +1,12 @@
1
+ const fs = require("fs/promises");
2
+ const { resolveSafePath } = require("../workspace/security");
3
+
4
+ async function listFiles(relativePath, workspace) {
5
+ const target = resolveSafePath(workspace, relativePath);
6
+ const entries = await fs.readdir(target, { withFileTypes: true });
7
+ return entries.map(entry => ({
8
+ name: entry.name,
9
+ type: entry.isDirectory() ? "directory" : "file"
10
+ }));
11
+ }
12
+ module.exports = { listFiles };
@@ -0,0 +1,16 @@
1
+ const { spawn } = require("child_process");
2
+ const { resolveSafePath } = require("../workspace/security");
3
+
4
+ function openNano(relativePath, workspace) {
5
+ const target = resolveSafePath(workspace, relativePath);
6
+ return new Promise((resolve, reject) => {
7
+ const child = spawn("nano", [target], { stdio: "inherit" });
8
+ child.on("error", error => reject(new Error(`Could not start nano: ${error.message}`)));
9
+ child.on("exit", code => {
10
+ if (code === 0) resolve({ ok: true, path: relativePath });
11
+ else reject(new Error(`nano exited with code ${code}.`));
12
+ });
13
+ });
14
+ }
15
+
16
+ module.exports = { openNano };
@@ -0,0 +1,8 @@
1
+ const fs = require("fs/promises");
2
+ const { resolveSafePath } = require("../workspace/security");
3
+
4
+ async function readFile(relativePath, workspace) {
5
+ const target = resolveSafePath(workspace, relativePath);
6
+ return fs.readFile(target, "utf8");
7
+ }
8
+ module.exports = { readFile };
@@ -0,0 +1,11 @@
1
+ const fs = require("fs/promises");
2
+ const path = require("path");
3
+ const { resolveSafePath } = require("../workspace/security");
4
+
5
+ async function writeFile(relativePath, content, workspace) {
6
+ const target = resolveSafePath(workspace, relativePath);
7
+ await fs.mkdir(path.dirname(target), { recursive: true });
8
+ await fs.writeFile(target, String(content), "utf8");
9
+ return { ok: true, path: relativePath, bytes: Buffer.byteLength(String(content), "utf8") };
10
+ }
11
+ module.exports = { writeFile };
@@ -0,0 +1,8 @@
1
+ class QwallError extends Error {
2
+ constructor(message, code = "QWALL_ERROR") {
3
+ super(message);
4
+ this.name = "QwallError";
5
+ this.code = code;
6
+ }
7
+ }
8
+ module.exports = { QwallError };
@@ -0,0 +1,4 @@
1
+ function info(message) { console.log(`[Qwall] ${message}`); }
2
+ function warn(message) { console.warn(`[Qwall] ${message}`); }
3
+ function error(message) { console.error(`[Qwall] ${message}`); }
4
+ module.exports = { info, warn, error };
@@ -0,0 +1,3 @@
1
+ const path = require("path");
2
+ function normalizePath(value) { return path.normalize(value); }
3
+ module.exports = { normalizePath };
@@ -0,0 +1,7 @@
1
+ const os = require("os");
2
+ function platform() { return process.platform; }
3
+ function isWindows() { return process.platform === "win32"; }
4
+ function isLinux() { return process.platform === "linux"; }
5
+ function isMacOS() { return process.platform === "darwin"; }
6
+ function homeDir() { return os.homedir(); }
7
+ module.exports = { platform, isWindows, isLinux, isMacOS, homeDir };
@@ -0,0 +1,9 @@
1
+ const path = require("path");
2
+ const os = require("os");
3
+ const fs = require("fs");
4
+ function getWorkspace() {
5
+ const workspace = process.env.QWALL_WORKSPACE || path.join(process.cwd(), ".qwall-workspace");
6
+ fs.mkdirSync(workspace, { recursive: true });
7
+ return path.resolve(workspace);
8
+ }
9
+ module.exports = { getWorkspace };
@@ -0,0 +1,9 @@
1
+ const fs = require("fs/promises");
2
+ const { getWorkspace } = require("./index");
3
+
4
+ async function ensureWorkspace() {
5
+ const workspace = getWorkspace();
6
+ await fs.mkdir(workspace, { recursive: true });
7
+ return workspace;
8
+ }
9
+ module.exports = { ensureWorkspace };
@@ -0,0 +1,16 @@
1
+ const path = require("path");
2
+
3
+ function resolveSafePath(workspace, relativePath) {
4
+ if (typeof relativePath !== "string" || !relativePath.trim()) {
5
+ throw new Error("A file path is required.");
6
+ }
7
+ const root = path.resolve(workspace);
8
+ const target = path.resolve(root, relativePath);
9
+ const relative = path.relative(root, target);
10
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
11
+ throw new Error("Path escapes the Qwall Work workspace.");
12
+ }
13
+ return target;
14
+ }
15
+
16
+ module.exports = { resolveSafePath };