cowork-cli 0.2.8 → 1.1.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
@@ -54,7 +54,7 @@ cwk "Explain the data flow in the engine/ models"
54
54
  ## ✨ Features that Matter
55
55
 
56
56
  - **Zero-Whitespace UI**: High-density terminal output designed for professionals. No fluff, no headers, just data.
57
- - **Interactive Feedback**: The AI can now ask you clarifying questions via the `askUser` tool when it needs more context.
57
+ - **Interactive Feedback**: The AI can request clarifications via the `askUser` tool or trigger an interactive `[ Yes ] No` toggle using `askConfirm`.
58
58
  - **Smart Discovery**: Built-in `searchText`, `findFile`, and `projectTree` tools that respect your `.gitignore`.
59
59
  - **Surgical I/O**: Read entire files or specific line ranges (`readFileChunk`) with automatic binary detection.
60
60
  - **Piping Support**: Pipe logs or diffs directly into `cwk` for instant analysis.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cowork-cli",
3
- "version": "0.2.8",
3
+ "version": "1.1.0",
4
4
  "description": "work with cowork",
5
5
  "bin": {
6
6
  "cwk": "bin/cli.js"
@@ -35,4 +35,4 @@
35
35
  "ipaddr.js": "^2.4.0",
36
36
  "openai": "^6.38.0"
37
37
  }
38
- }
38
+ }
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "accents": {
3
- "orangex": "#D97757",
4
- "greyx": "#808080",
5
- "resetx": "#FFFFFF"
6
- },
7
- "systemPrompt": "You are a concise Developer Analyst. Use available tools proactively to investigate and process the user query. Provide a direct answer based on your findings. ENVIRONMENT: Terminal. STRICT RULES: 1. NO conversational filler or pleasantries. 2. NO Markdown formatting (do not use asterisks, backticks, or hashes). 3. NO XML or HTML tags. 4. Output ONLY structured, concise plain text. 5. Use simple spacing and indentation for structure. CWD: ${folder}. Year: ${year}."
3
+ "main": "#7BA5DA",
4
+ "tool": "#F2CF6E",
5
+ "data": "#C2C6C5",
6
+ "success": "#7AC391",
7
+ "error": "#E07070",
8
+ "dim": "#606060",
9
+ "header": "#A37ACC"
10
+ }
8
11
  }
@@ -0,0 +1,33 @@
1
+ # IDENTITY
2
+ You are Cowork (cwk), an interactive CLI tool acting as a Full Engineering Read-Only Analyst.
3
+
4
+ # CORE MANDATES
5
+ - ONLY perform exactly what the user explicitly says.
6
+ - NO unsolicited investigations, side tasks, or extra work of any kind.
7
+ - Provide accurate data with absolute minimum token usage unless "deep work" is requested.
8
+
9
+ # CONSTRAINTS (Terminal Environment)
10
+ - NO conversational filler, pleasantries, preambles, or postambles.
11
+ - NO Markdown (no *, `, #, or code blocks). NO XML/HTML tags.
12
+ - Output ONLY structured, concise plain text with simple spacing/indentation.
13
+ - Cite code locations using: file_path:line_number.
14
+
15
+ # EXECUTION & STYLE
16
+ - Answer directly without elaboration or explanation.
17
+ - Use fewer than 4 lines (excluding tool use). One-word answers are preferred.
18
+ - Use all tools efficiently (parallel/sequential) to minimize context usage.
19
+ - Verify dependencies and mimic local code style; never assume availability.
20
+
21
+ # EXAMPLES
22
+ user: files in src/?
23
+ assistant:
24
+ src/foo.js
25
+ src/bar.js
26
+
27
+ user: where is foo?
28
+ assistant:
29
+ src/foo.js:12
30
+
31
+ # CONTEXT
32
+ Working directory: ${folder}
33
+ Year: ${year}
@@ -10,7 +10,7 @@ function clientLoader() {
10
10
  const config = loadConfig();
11
11
 
12
12
  if (!validateConfig(config)) {
13
- throw new Error("Configuration missing or invalid. Please configure your ~/.env file (run 'btw --help' for details).");
13
+ throw new Error("Configuration missing or invalid. Please configure your ~/.env file (run 'cwk --help' for details).");
14
14
  }
15
15
 
16
16
  // Normalize baseURL: remove trailing slashes as the SDK appends paths starting with /
@@ -1,6 +1,6 @@
1
1
  import { toolDefinitions, dispatchTool } from '../tools/index.js';
2
2
  import { logger } from '../../utils/logger.js';
3
- import { spinner } from '../../utils/ui.js';
3
+ import { ui } from '../../utils/ui.js';
4
4
  import { outputFormatted } from '../../utils/outputFormatter.js';
5
5
 
6
6
  /**
@@ -46,9 +46,9 @@ export default class BaseModel {
46
46
  turn++;
47
47
 
48
48
  try {
49
- spinner.start("Thinking");
49
+ ui.think();
50
50
  const response = await this._getCompletion();
51
- spinner.stop();
51
+ ui.stop();
52
52
 
53
53
  const message = response.choices[0].message;
54
54
 
@@ -71,7 +71,7 @@ export default class BaseModel {
71
71
  await this._processToolCalls(message.tool_calls);
72
72
 
73
73
  } catch (err) {
74
- spinner.stop();
74
+ ui.stop();
75
75
  // Deep error logging for API failures
76
76
  if (err.status) {
77
77
  logger.error(`[API Error] Status: ${err.status}`);
@@ -146,9 +146,9 @@ export default class BaseModel {
146
146
  const jitter = Math.random() * 500;
147
147
  const finalDelay = delay + jitter;
148
148
 
149
- spinner.update(`Error ${err.status}. Retrying in ${(finalDelay/1000).toFixed(1)}s`);
149
+ ui.update(`Error ${err.status}. Retrying in ${(finalDelay/1000).toFixed(1)}s`);
150
150
  await new Promise(resolve => setTimeout(resolve, finalDelay));
151
- spinner.update("Thinking");
151
+ ui.update('Thinking');
152
152
  continue;
153
153
  }
154
154
  throw err;
@@ -194,25 +194,21 @@ export default class BaseModel {
194
194
  else if (name === 'readFileChunk') displayArg = `${args.filePath} [L${args.startLine}-${args.endLine}]`;
195
195
  else displayArg = args.url || args.filePath || args.dirPath || args.path || args.pattern || JSON.stringify(args);
196
196
 
197
- const displayStr = displayArg.length > 60 ? displayArg.slice(0, 57) + "..." : displayArg;
198
-
199
- // NOTE: 'askUser' is interactive and handles its own semantic logging ([asking])
200
- // to maintain terminal focus and avoid conflicts with the global spinner.
201
- if (name !== 'askUser') {
202
- logger.secondary(`[${label}] ${displayStr}`);
203
- spinner.start(`[${label}] working`);
197
+ // ui.start() handles terminal-aware truncation internally.
198
+ if (name !== 'askUser' && name !== 'askConfirm') {
199
+ ui.start(label, displayArg);
204
200
  }
205
201
 
206
202
  const result = await dispatchTool(name, args);
207
203
 
208
- if (name !== 'askUser') {
209
- spinner.stop();
204
+ if (name !== 'askUser' && name !== 'askConfirm') {
205
+ ui.stop();
210
206
  }
211
207
 
212
208
  this.addMessage('tool', result, { tool_call_id: toolCall.id });
213
209
 
214
210
  } catch (err) {
215
- spinner.stop();
211
+ ui.stop();
216
212
  const errorMsg = err.message;
217
213
  logger.error(`[FAILED] ${name}: ${errorMsg}`);
218
214
 
package/src/engine/run.js CHANGED
@@ -6,7 +6,6 @@ import DefaultModel from './models/default.js';
6
6
  import GeminiModel from './models/gemini.js';
7
7
 
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const CONFIG_PATH = path.join(__dirname, '../configs/config.json');
10
9
 
11
10
  /**
12
11
  * Executes a chat completion query using the appropriate model handler.
@@ -21,17 +20,17 @@ export default async function runQuery(client, config, query) {
21
20
  }
22
21
 
23
22
  try {
24
- // 1. Load and format system prompt from internal config
23
+ // 1. Load and format system prompt from internal sys.txt
25
24
  let systemPrompt = null;
26
25
  try {
27
- const internalConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
28
- if (internalConfig.systemPrompt) {
29
- systemPrompt = internalConfig.systemPrompt
26
+ const promptPath = path.join(__dirname, '../configs/sys.txt');
27
+ if (fs.existsSync(promptPath)) {
28
+ systemPrompt = fs.readFileSync(promptPath, 'utf8')
30
29
  .replace('${folder}', process.cwd())
31
- .replace('${year}', new Date().getFullYear());
30
+ .replace(/\${year}/g, new Date().getFullYear());
32
31
  }
33
32
  } catch (e) {
34
- // Fallback if config is missing - proceed without system prompt
33
+ // Fallback if prompt is missing - proceed without system prompt
35
34
  }
36
35
 
37
36
  const isGemini = config.model_type.toLowerCase() === 'gemini';
@@ -0,0 +1,34 @@
1
+ import { ui } from '../../utils/ui.js';
2
+
3
+ /**
4
+ * askConfirm tool — asks the user a yes/no question via the terminal.
5
+ *
6
+ * Accepted inputs (case-insensitive):
7
+ * yes → y, yes
8
+ * no → n, no
9
+ * cancel → Ctrl+C (treated as no, with dismissed flag)
10
+ *
11
+ * Any other input is silently erased and the prompt re-appears.
12
+ *
13
+ * @param {Object} args
14
+ * @param {string} args.question The yes/no question to ask.
15
+ * @returns {Promise<string>} JSON: { confirmed } or { confirmed, dismissed }
16
+ */
17
+ export default async function askConfirm({ question }) {
18
+ // 1. Input validation
19
+ if (!question || typeof question !== 'string' || question.trim().length === 0) {
20
+ return JSON.stringify({ error: "'question' must be a non-empty string.", dismissed: true });
21
+ }
22
+
23
+ // 2. TTY guard — cannot prompt in pipes or CI environments
24
+ if (!process.stdin.isTTY) {
25
+ return JSON.stringify({
26
+ error: 'stdin is not a TTY. Cannot prompt in non-interactive environments.',
27
+ dismissed: true,
28
+ });
29
+ }
30
+
31
+ // 3. Delegate entirely to UIEngine — it owns all terminal state
32
+ const result = await ui.confirm(question);
33
+ return JSON.stringify(result);
34
+ }
@@ -1,62 +1,32 @@
1
- import readlinePromises from 'readline/promises';
2
- import readline from 'readline';
3
- import { stdin as input, stdout as output } from 'process';
4
- import { formatSecondary } from '../../utils/logger.js';
1
+ import { ui } from '../../utils/ui.js';
5
2
 
6
3
  /**
7
4
  * Implementation of the askUser tool.
8
- * Asks the user a single question via the terminal.
9
- *
5
+ * Delegates rendering and input handling to the UIEngine singleton.
6
+ *
10
7
  * @param {Object} args
11
- * @param {string} args.question The question to ask the user.
12
- * @returns {Promise<string>} JSON string with answer or error.
8
+ * @param {string} args.question The question to ask the user.
9
+ * @returns {Promise<string>} JSON string: { answer } or { error, dismissed }.
13
10
  */
14
11
  export default async function askUser({ question }) {
15
- // 1. Input Validation
16
- if (!question) {
17
- return "Error: 'question' parameter is required.";
18
- }
19
-
20
- if (typeof question !== 'string' || question.trim().length === 0) {
21
- return "Error: 'question' must be a non-empty string.";
12
+ // 1. Input validation
13
+ if (!question || typeof question !== 'string' || question.trim().length === 0) {
14
+ return JSON.stringify({ error: "'question' must be a non-empty string.", dismissed: true });
22
15
  }
23
16
 
24
- // 2. TTY Check: Ensure we have a terminal to interact with
25
- if (!input.isTTY) {
26
- return "Error: stdin is not a TTY. Cannot prompt user in non-interactive environments.";
17
+ // 2. TTY check
18
+ if (!process.stdin.isTTY) {
19
+ return JSON.stringify({ error: 'stdin is not a TTY. Cannot prompt in non-interactive environments.', dismissed: true });
27
20
  }
28
21
 
29
- const rl = readlinePromises.createInterface({ input, output });
30
-
31
- return new Promise((resolve) => {
32
- // 3. Graceful Signal Handling
33
- rl.on('SIGINT', () => {
34
- rl.close();
35
- process.stdout.write(formatSecondary(` cancelled\n`));
36
- resolve(JSON.stringify({ error: "user cancelled the request", dismissed: true }));
37
- });
38
-
39
- const doAsk = async () => {
40
- try {
41
- const answer = await rl.question(formatSecondary(`[asking] ${question} -> `));
42
- const trimmed = answer.trim();
43
-
44
- if (trimmed.length === 0) {
45
- // Move cursor up and clear line reliably using Node's readline methods
46
- readline.moveCursor(process.stdout, 0, -1);
47
- readline.clearLine(process.stdout, 0);
48
- readline.cursorTo(process.stdout, 0);
49
- doAsk();
50
- } else {
51
- rl.close();
52
- resolve(JSON.stringify({ answer: trimmed }));
53
- }
54
- } catch (err) {
55
- rl.close();
56
- resolve(JSON.stringify({ error: `System Error: ${err.message}`, dismissed: true }));
57
- }
58
- };
59
-
60
- doAsk();
61
- });
22
+ try {
23
+ const answer = await ui.ask(question);
24
+ return JSON.stringify({ answer });
25
+ } catch (err) {
26
+ // ui.ask() rejects with { cancelled: true } on SIGINT, or an Error otherwise.
27
+ if (err && err.cancelled) {
28
+ return JSON.stringify({ error: 'User cancelled the request.', dismissed: true });
29
+ }
30
+ return JSON.stringify({ error: `System Error: ${err.message}`, dismissed: true });
31
+ }
62
32
  }
@@ -8,6 +8,7 @@ import listTools from './listTools.js';
8
8
  import findFile from './findFile.js';
9
9
  import findDir from './findDir.js';
10
10
  import askUser from './askUser.js';
11
+ import askConfirm from './askConfirm.js';
11
12
 
12
13
  export const toolDefinitions = [
13
14
  {
@@ -148,7 +149,7 @@ export const toolDefinitions = [
148
149
  type: "function",
149
150
  function: {
150
151
  name: "askUser",
151
- description: "Ask the user a question via the terminal and get a text response.",
152
+ description: "Ask the user an open-ended question via the terminal and get a free-text response.",
152
153
  parameters: {
153
154
  type: "object",
154
155
  properties: {
@@ -157,6 +158,20 @@ export const toolDefinitions = [
157
158
  required: ["question"]
158
159
  }
159
160
  }
161
+ },
162
+ {
163
+ type: "function",
164
+ function: {
165
+ name: "askConfirm",
166
+ description: "Ask the user a yes/no confirmation question. Use this instead of askUser when only a boolean decision is needed. Returns { confirmed: true } for yes, { confirmed: false } for no, and { confirmed: false, dismissed: true } if the user cancels with Ctrl+C.",
167
+ parameters: {
168
+ type: "object",
169
+ properties: {
170
+ question: { type: "string", description: "The yes/no question to ask the user." }
171
+ },
172
+ required: ["question"]
173
+ }
174
+ }
160
175
  }
161
176
  ];
162
177
 
@@ -170,7 +185,8 @@ const toolImplementations = {
170
185
  listTools,
171
186
  findFile,
172
187
  findDir,
173
- askUser
188
+ askUser,
189
+ askConfirm
174
190
  };
175
191
 
176
192
  /**
@@ -17,10 +17,10 @@ export const loadConfig = () => {
17
17
  const envConfig = dotenv.parse(content);
18
18
 
19
19
  const config = {
20
- model_name: envConfig.CWK_MODEL_NAME || envConfig.BTW_MODEL_NAME || envConfig.MODEL_NAME,
21
- model_url: envConfig.CWK_MODEL_URL || envConfig.BTW_MODEL_URL || envConfig.MODEL_URL,
22
- model_api_key: envConfig.CWK_MODEL_API_KEY || envConfig.BTW_MODEL_API_KEY || envConfig.MODEL_API_KEY,
23
- model_type: envConfig.CWK_MODEL_TYPE || envConfig.BTW_MODEL_TYPE || envConfig.MODEL_TYPE
20
+ model_name: envConfig.CWK_MODEL_NAME || envConfig.MODEL_NAME,
21
+ model_url: envConfig.CWK_MODEL_URL || envConfig.MODEL_URL,
22
+ model_api_key: envConfig.CWK_MODEL_API_KEY || envConfig.MODEL_API_KEY,
23
+ model_type: envConfig.CWK_MODEL_TYPE || envConfig.MODEL_TYPE
24
24
  };
25
25
 
26
26
  // Remove undefined/empty values