qaos 0.0.2 → 0.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/dist/app.js CHANGED
@@ -60,17 +60,28 @@ export default function App(args) {
60
60
  phase: 'completed',
61
61
  };
62
62
  case 'server_state': {
63
- const taskStatusMap = event.taskStatus ?? {};
64
- const updatedTasks = baseState.tasks.length > 0
65
- ? baseState.tasks.map(task => ({
66
- ...task,
67
- status: taskStatusMap[task.id] ?? task.status,
68
- }))
69
- : Object.keys(taskStatusMap).map(taskId => ({
70
- id: taskId,
71
- description: `Task ${taskId}`,
72
- status: taskStatusMap[taskId],
73
- }));
63
+ // Prefer the authoritative task list from the server (includes subtasks
64
+ // with metadata). Fall back to merging taskStatus into existing tasks
65
+ // for older server versions that don't send taskList.
66
+ let updatedTasks;
67
+ if (event.taskList && event.taskList.length > 0) {
68
+ updatedTasks = event.taskList;
69
+ }
70
+ else {
71
+ const taskStatusMap = event.taskStatus ?? {};
72
+ updatedTasks =
73
+ baseState.tasks.length > 0
74
+ ? baseState.tasks.map(task => ({
75
+ ...task,
76
+ status: taskStatusMap[task.id] ?? task.status,
77
+ }))
78
+ : Object.keys(taskStatusMap).map(taskId => ({
79
+ id: taskId,
80
+ description: `Task ${taskId}`,
81
+ status: taskStatusMap[taskId],
82
+ isSubtask: false,
83
+ }));
84
+ }
74
85
  return {
75
86
  ...baseState,
76
87
  thinking: event.thinking,
@@ -210,6 +221,7 @@ export default function App(args) {
210
221
  if (!runUiState)
211
222
  return React.createElement(Text, null, t('run.preparing'));
212
223
  if (runUiState.phase === 'error') {
224
+ setTimeout(() => process.exit(1), EXIT_DELAY_ERROR);
213
225
  return (React.createElement(RunError, { message: runUiState.errorMessage || t('auth.unknownError') }));
214
226
  }
215
227
  if (runUiState.phase === 'completed') {
@@ -66,7 +66,6 @@ export async function authenticate(args, t) {
66
66
  try {
67
67
  const method = args.method || 'ui ';
68
68
  const config = await configPromise;
69
- const serverUrl = config.API_SERVER_URL;
70
69
  const websiteUrl = config.WEBSITE_URL;
71
70
  if (method === 'api') {
72
71
  if (!args.token) {
@@ -76,7 +75,7 @@ export async function authenticate(args, t) {
76
75
  error: t('auth.usage'),
77
76
  };
78
77
  }
79
- const validationResult = await authService.validateToken(args.token, serverUrl);
78
+ const validationResult = await authService.validateToken(args.token, websiteUrl);
80
79
  if (!validationResult.valid) {
81
80
  return {
82
81
  success: false,
@@ -84,7 +83,7 @@ export async function authenticate(args, t) {
84
83
  error: validationResult.error || t('auth.invalidToken'),
85
84
  };
86
85
  }
87
- await authService.saveToken(args.token, serverUrl, validationResult.userId);
86
+ await authService.saveToken(args.token, websiteUrl, validationResult.userId);
88
87
  return {
89
88
  success: true,
90
89
  message: t('auth.successWithPath', {
@@ -111,10 +110,10 @@ export async function authenticate(args, t) {
111
110
  */
112
111
  export async function getAuthStatus() {
113
112
  const isAuthenticated = await authService.isAuthenticated();
114
- const serverUrl = await authService.getServerUrl();
113
+ const websiteUrl = await authService.getWebsiteUrl();
115
114
  return {
116
115
  authenticated: isAuthenticated,
117
- serverUrl: serverUrl || undefined,
116
+ serverUrl: websiteUrl || undefined,
118
117
  };
119
118
  }
120
119
  /**
@@ -5,8 +5,8 @@ export async function fetchRuns(options) {
5
5
  if (!token) {
6
6
  return { success: false, error: 'not_authenticated' };
7
7
  }
8
- const defaultServerUrl = (await configPromise).API_SERVER_URL;
9
- const serverUrl = (await authService.getServerUrl()) || defaultServerUrl;
8
+ const defaultServerUrl = (await configPromise).WEBSITE_URL;
9
+ const serverUrl = (await authService.getWebsiteUrl()) || defaultServerUrl;
10
10
  const url = new URL('/api/cli/runs', serverUrl);
11
11
  if (options.limit) {
12
12
  url.searchParams.set('limit', String(options.limit));
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
2
  import { Box, Text, measureElement } from 'ink';
3
- import { Badge, ProgressBar } from '@inkjs/ui';
3
+ import { Badge } from '@inkjs/ui';
4
4
  import Spinner from './spinner.js';
5
5
  import ThinkingAnimation from './thinking-animation.js';
6
6
  import { lightBlue, purple } from '../../constants/colors.js';
@@ -48,22 +48,22 @@ function AnimatedProgressBar({ value, isActive, total, }) {
48
48
  React.createElement(Text, { color: "magenta" }, SCAN_CHAR))) : (React.createElement(Text, { key: i, dimColor: true }, segment))))),
49
49
  emptyAfter > 0 && (React.createElement(Text, { dimColor: true }, REMAINING_CHAR.repeat(emptyAfter)))));
50
50
  }
51
- function renderTask(task, actions, isActive) {
51
+ function renderTask(task, actions, isActive, duration) {
52
52
  const color = STATUS_COLORS[task.status] || 'red';
53
- return (React.createElement(Box, { key: task.id, flexDirection: "column" },
54
- task.status === 'doing' && (React.createElement(Box, null,
53
+ const indent = task.isSubtask ? 2 : 0;
54
+ return (React.createElement(Box, { key: task.id, flexDirection: "column", marginLeft: indent },
55
+ task.status === 'doing' && (React.createElement(Box, { gap: 1 },
55
56
  React.createElement(Spinner, { color: color }),
56
- React.createElement(Text, { color: color, bold: true },
57
- ' ',
58
- task.description))),
57
+ React.createElement(Text, { color: color, bold: true }, task.description),
58
+ duration !== undefined && (React.createElement(Text, { dimColor: true }, duration)))),
59
59
  task.status === 'notStarted' && (React.createElement(Text, null,
60
60
  React.createElement(Text, { color: color }, "\u25CB"),
61
61
  ' ',
62
62
  React.createElement(Text, { color: color, dimColor: true }, task.description))),
63
63
  task.status === 'done' && (React.createElement(Text, null,
64
- React.createElement(Text, { color: color }, "\u2713"),
64
+ React.createElement(Text, { color: "green" }, "\u2713"),
65
65
  ' ',
66
- React.createElement(Text, { color: color }, task.description))),
66
+ React.createElement(Text, { color: "green" }, task.description))),
67
67
  task.status === 'doing' && isActive && actions.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, actions.map((action, index) => (React.createElement(Text, { key: `${action}-${index}`, dimColor: true },
68
68
  "\u2514\u2500 ",
69
69
  action)))))));
@@ -102,49 +102,33 @@ function formatDuration(seconds) {
102
102
  const s = seconds % 60;
103
103
  return s > 0 ? `${m}m ${s}s` : `${m}m`;
104
104
  }
105
- function renderQaosTask(task, actions, isActive, duration) {
106
- const color = STATUS_COLORS[task.status] || 'red';
107
- const indent = task.isSubtask ? 2 : 0;
108
- return (React.createElement(Box, { key: task.id, flexDirection: "column", marginLeft: indent },
109
- task.status === 'doing' && (React.createElement(Box, { gap: 1 },
110
- React.createElement(Spinner, { color: color }),
111
- React.createElement(Text, { color: color, bold: true }, task.description),
112
- duration !== undefined && (React.createElement(Text, { dimColor: true }, duration)))),
113
- task.status === 'notStarted' && (React.createElement(Text, null,
114
- React.createElement(Text, { color: color }, "\u25CB"),
115
- ' ',
116
- React.createElement(Text, { color: color, dimColor: true }, task.description))),
117
- task.status === 'done' && (React.createElement(Text, null,
118
- React.createElement(Text, { color: "green" }, "\u2713"),
119
- ' ',
120
- React.createElement(Text, { color: "green" }, task.description))),
121
- task.status === 'doing' && isActive && actions.length > 0 && (React.createElement(Box, { flexDirection: "column", marginLeft: 2 }, actions.map((action, index) => (React.createElement(Text, { key: `${action}-${index}`, dimColor: true },
122
- "\u2514\u2500 ",
123
- action)))))));
124
- }
125
- function QaosProgressSection({ qaos, actions }) {
105
+ /** Shared task list display: progress bars (top-level + subtasks), duration tracking, windowed list. */
106
+ function TaskSection({ tasks, actions }) {
126
107
  const { t } = useLanguage();
127
- const shortenedUrl = qaos.currentPageUrl ? shortenUrl(qaos.currentPageUrl) : null;
128
- const currentPage = shortenedUrl && shortenedUrl.trim() ? shortenedUrl : null;
129
- const activeTask = qaos.tasks.find(task => task.status === 'doing');
130
- // Local duration tracking tick every second
108
+ const topLevelTasks = tasks.filter(t => !t.isSubtask);
109
+ const tasksDone = topLevelTasks.filter(t => t.status === 'done').length;
110
+ const taskProgress = topLevelTasks.length > 0 ? tasksDone / topLevelTasks.length : 0;
111
+ // When a subtask is running, its parent is already 'done' (subtasks run after the
112
+ // parent completes). Find the active subtask first, then resolve its parent.
113
+ const activeSubtask = tasks.find(t => t.isSubtask && t.status === 'doing');
114
+ const activeTopLevelTask = activeSubtask
115
+ ? topLevelTasks.find(t => t.id === activeSubtask.parentTaskId)
116
+ : topLevelTasks.find(t => t.status === 'doing');
117
+ const activeTaskSubtasks = activeTopLevelTask
118
+ ? tasks.filter(t => t.isSubtask && t.parentTaskId === activeTopLevelTask.id)
119
+ : [];
120
+ const subtasksDone = activeTaskSubtasks.filter(t => t.status === 'done').length;
121
+ const subtaskProgress = activeTaskSubtasks.length > 0 ? subtasksDone / activeTaskSubtasks.length : 0;
122
+ // The active task for duration tracking is whichever task is currently 'doing'
123
+ const activeTask = tasks.find(t => t.status === 'doing');
124
+ // Duration tracking
131
125
  const [now, setNow] = useState(() => Date.now());
132
- const pageStartRef = useRef(null);
133
126
  const taskStartRef = useRef(null);
134
- const prevPageUrlRef = useRef(null);
135
127
  const prevTaskIdRef = useRef(null);
136
128
  useEffect(() => {
137
129
  const timer = setInterval(() => setNow(Date.now()), 1000);
138
130
  return () => clearInterval(timer);
139
131
  }, []);
140
- // Reset page start time when the current page URL changes
141
- useEffect(() => {
142
- if (qaos.currentPageUrl !== prevPageUrlRef.current) {
143
- prevPageUrlRef.current = qaos.currentPageUrl;
144
- pageStartRef.current = qaos.currentPageUrl !== null ? Date.now() : null;
145
- }
146
- }, [qaos.currentPageUrl]);
147
- // Reset task start time when the active task changes
148
132
  const activeTaskId = activeTask?.id ?? null;
149
133
  useEffect(() => {
150
134
  if (activeTaskId !== prevTaskIdRef.current) {
@@ -152,32 +136,14 @@ function QaosProgressSection({ qaos, actions }) {
152
136
  taskStartRef.current = activeTaskId !== null ? Date.now() : null;
153
137
  }
154
138
  }, [activeTaskId]);
155
- const pageDuration = pageStartRef.current !== null
156
- ? formatDuration(Math.max(0, Math.floor((now - pageStartRef.current) / 1000)))
157
- : undefined;
158
139
  const taskDuration = taskStartRef.current !== null
159
140
  ? formatDuration(Math.max(0, Math.floor((now - taskStartRef.current) / 1000)))
160
141
  : undefined;
161
- // QAOS Explore tasks are represented by the "Exploring X …" line — exclude
162
- // them from the task list and progress bars.
163
- const displayTasks = qaos.tasks.filter(t => !t.description.includes('[QAOS Explore]'));
164
- // Global tasks progress (top-level, exploration excluded)
165
- const topLevelTasks = displayTasks.filter(t => !t.isSubtask);
166
- const tasksDone = topLevelTasks.filter(t => t.status === 'done').length;
167
- const taskProgress = topLevelTasks.length > 0 ? tasksDone / topLevelTasks.length : 0;
168
- // Subtask progress scoped to the currently active top-level task
169
- const activeTopLevelTask = topLevelTasks.find(t => t.status === 'doing');
170
- const activeTaskSubtasks = activeTopLevelTask
171
- ? displayTasks.filter(t => t.isSubtask && t.parentTaskId === activeTopLevelTask.id)
172
- : [];
173
- const subtasksDone = activeTaskSubtasks.filter(t => t.status === 'done').length;
174
- const subtaskProgress = activeTaskSubtasks.length > 0 ? subtasksDone / activeTaskSubtasks.length : 0;
175
142
  // Re-sort into chronological display order: done → doing → notStarted
176
- // (server sends them as doing → notStarted → done, which is the wrong visual order)
177
143
  const sortedTasks = [
178
- ...displayTasks.filter(t => t.status === 'done'),
179
- ...displayTasks.filter(t => t.status === 'doing'),
180
- ...displayTasks.filter(t => t.status === 'notStarted'),
144
+ ...tasks.filter(t => t.status === 'done'),
145
+ ...tasks.filter(t => t.status === 'doing'),
146
+ ...tasks.filter(t => t.status === 'notStarted'),
181
147
  ];
182
148
  // Windowed task list: show at most 11 tasks centered on the active one
183
149
  const WINDOW_HALF = 5;
@@ -197,21 +163,11 @@ function QaosProgressSection({ qaos, actions }) {
197
163
  hiddenAfter = sortedTasks.length - endIdx;
198
164
  visibleTasks = sortedTasks.slice(startIdx, endIdx);
199
165
  }
200
- return (React.createElement(Box, { flexDirection: "column" },
201
- React.createElement(Text, { color: purple, bold: true }, t('run.pageExploration')),
202
- currentPage && (React.createElement(Box, { gap: 1, marginTop: 1 },
203
- React.createElement(Spinner, { color: lightBlue }),
204
- React.createElement(Text, { color: lightBlue },
205
- ' ',
206
- t('run.exploring'),
207
- " ",
208
- React.createElement(Text, { bold: true }, currentPage),
209
- " ..."),
210
- pageDuration !== undefined && (React.createElement(Text, { dimColor: true }, pageDuration)))),
166
+ return (React.createElement(React.Fragment, null,
211
167
  (topLevelTasks.length > 0 || activeTaskSubtasks.length > 0) && (React.createElement(Box, { flexDirection: "column", marginY: 1, gap: 0 },
212
168
  topLevelTasks.length > 0 && (React.createElement(Box, { gap: 1 },
213
169
  React.createElement(Text, { dimColor: true }, t('run.progressTasks')),
214
- React.createElement(AnimatedProgressBar, { value: taskProgress * 100, isActive: activeTask !== undefined && !activeTask.isSubtask, total: topLevelTasks.length }),
170
+ React.createElement(AnimatedProgressBar, { value: taskProgress * 100, isActive: activeTopLevelTask !== undefined, total: topLevelTasks.length }),
215
171
  React.createElement(Text, { dimColor: true },
216
172
  tasksDone,
217
173
  "/",
@@ -223,7 +179,7 @@ function QaosProgressSection({ qaos, actions }) {
223
179
  subtasksDone,
224
180
  "/",
225
181
  activeTaskSubtasks.length))))),
226
- qaos.tasks.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
182
+ sortedTasks.length > 0 && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
227
183
  hiddenBefore > 0 && (React.createElement(Text, { dimColor: true },
228
184
  ' ',
229
185
  "\u2191 ",
@@ -231,7 +187,7 @@ function QaosProgressSection({ qaos, actions }) {
231
187
  " more task",
232
188
  hiddenBefore > 1 ? 's' : '',
233
189
  " above")),
234
- visibleTasks.map(task => renderQaosTask(task, actions, task === activeTask, task === activeTask ? taskDuration : undefined)),
190
+ visibleTasks.map(task => renderTask(task, actions, task === activeTask, task === activeTask ? taskDuration : undefined)),
235
191
  hiddenAfter > 0 && (React.createElement(Text, { dimColor: true },
236
192
  ' ',
237
193
  "\u2193 ",
@@ -240,6 +196,40 @@ function QaosProgressSection({ qaos, actions }) {
240
196
  hiddenAfter > 1 ? 's' : '',
241
197
  " below"))))));
242
198
  }
199
+ function QaosProgressSection({ qaos, actions }) {
200
+ const { t } = useLanguage();
201
+ const shortenedUrl = qaos.currentPageUrl ? shortenUrl(qaos.currentPageUrl) : null;
202
+ const currentPage = shortenedUrl && shortenedUrl.trim() ? shortenedUrl : null;
203
+ // Page-level duration tracking
204
+ const [now, setNow] = useState(() => Date.now());
205
+ const pageStartRef = useRef(null);
206
+ const prevPageUrlRef = useRef(null);
207
+ useEffect(() => {
208
+ const timer = setInterval(() => setNow(Date.now()), 1000);
209
+ return () => clearInterval(timer);
210
+ }, []);
211
+ useEffect(() => {
212
+ if (qaos.currentPageUrl !== prevPageUrlRef.current) {
213
+ prevPageUrlRef.current = qaos.currentPageUrl;
214
+ pageStartRef.current = qaos.currentPageUrl !== null ? Date.now() : null;
215
+ }
216
+ }, [qaos.currentPageUrl]);
217
+ const pageDuration = pageStartRef.current !== null
218
+ ? formatDuration(Math.max(0, Math.floor((now - pageStartRef.current) / 1000)))
219
+ : undefined;
220
+ return (React.createElement(Box, { flexDirection: "column" },
221
+ React.createElement(Text, { color: purple, bold: true }, t('run.pageExploration')),
222
+ currentPage && (React.createElement(Box, { gap: 1, marginTop: 1 },
223
+ React.createElement(Spinner, { color: lightBlue }),
224
+ React.createElement(Text, { color: lightBlue },
225
+ ' ',
226
+ t('run.exploring'),
227
+ " ",
228
+ React.createElement(Text, { bold: true }, currentPage),
229
+ " ..."),
230
+ pageDuration !== undefined && (React.createElement(Text, { dimColor: true }, pageDuration)))),
231
+ React.createElement(TaskSection, { tasks: qaos.tasks, actions: actions })));
232
+ }
243
233
  export function RunConnecting({ uiState }) {
244
234
  const { t } = useLanguage();
245
235
  const badge = getConnectionBadge(uiState.connection, t);
@@ -266,14 +256,8 @@ export default function RunUi({ uiState }) {
266
256
  const badge = getConnectionBadge(uiState.connection, t);
267
257
  const thinkingText = uiState.thinking || 'Waiting for agent state...';
268
258
  const reportUrl = uiState.reportUrl;
269
- // Find the currently active (doing) task
270
- const activeTask = uiState.tasks.find(task => task.status === 'doing');
271
259
  const actions = uiState.actions.length > 0 ? uiState.actions : [];
272
260
  const isQaos = !!uiState.qaosProgress;
273
- // Calculate task completion (for non-QAOS mode)
274
- const completedTasks = uiState.tasks.filter(task => task.status === 'done').length;
275
- const totalTasks = uiState.tasks.length;
276
- const progress = totalTasks > 0 ? completedTasks / totalTasks : 0;
277
261
  return (React.createElement(Box, { borderStyle: "single", borderColor: "white", paddingX: 1, flexDirection: "column" },
278
262
  React.createElement(Badge, { color: badge.color }, badge.label),
279
263
  React.createElement(Box, { paddingX: 1, marginTop: 1 },
@@ -288,11 +272,5 @@ export default function RunUi({ uiState }) {
288
272
  React.createElement(Text, { color: "cyan", bold: true }, `\x1b]8;;${reportUrl}\x07${reportUrl}\x1b]8;;\x07`))),
289
273
  React.createElement(Box, { borderStyle: "round", borderColor: purple, paddingX: 1, flexDirection: "column", marginTop: 1 }, isQaos ? (React.createElement(QaosProgressSection, { qaos: uiState.qaosProgress, actions: actions })) : (React.createElement(React.Fragment, null,
290
274
  React.createElement(Text, { color: purple, bold: true }, t('run.tasks')),
291
- React.createElement(Box, { marginY: 1 },
292
- React.createElement(ProgressBar, { value: progress * 100 }),
293
- React.createElement(Text, { dimColor: true },
294
- completedTasks,
295
- "/",
296
- totalTasks)),
297
- uiState.tasks.length === 0 ? (React.createElement(Text, { color: "gray" }, t('run.noTasks'))) : (uiState.tasks.map(task => renderTask(task, actions, task === activeTask))))))));
275
+ uiState.tasks.length === 0 ? (React.createElement(Text, { color: "gray" }, t('run.noTasks'))) : (React.createElement(TaskSection, { tasks: uiState.tasks, actions: actions })))))));
298
276
  }
@@ -3,6 +3,7 @@ import { createSerializedDOMState, getLLMRepresentation, getInteractiveElementCo
3
3
  import { browserService } from './services/browser-service.js';
4
4
  import { BrowserActionService, } from './services/browser-action-service.js';
5
5
  import fs from 'fs';
6
+ import { randomUUID } from 'node:crypto';
6
7
  import { stateService } from './services/state-service.js';
7
8
  import { debug } from './utils/logger.js';
8
9
  import { getAuthStatus } from './commands/auth.js';
@@ -10,7 +11,6 @@ import { authService } from './services/auth-service.js';
10
11
  import { validateConfig } from './utils/config-validator.js';
11
12
  import { configPromise } from './config.js';
12
13
  const DEFAULT_TASK_DESCRIPTION = 'Task';
13
- const apiUrl = (await configPromise).API_SERVER_URL;
14
14
  function extractTasksFromConfig(configData) {
15
15
  const tasks = Array.isArray(configData.tasks)
16
16
  ? configData.tasks
@@ -25,6 +25,7 @@ function extractTasksFromConfig(configData) {
25
25
  id,
26
26
  description,
27
27
  status: 'notStarted',
28
+ isSubtask: false,
28
29
  };
29
30
  });
30
31
  }
@@ -130,13 +131,25 @@ export async function run(args, t) {
130
131
  args.onUiEvent?.({ type: 'error', message });
131
132
  throw new Error(message);
132
133
  }
134
+ // Assign UUIDs to any tasks missing an id
135
+ const cfg = rawConfigData;
136
+ if (Array.isArray(cfg['tasks'])) {
137
+ cfg['tasks'] = cfg['tasks'].map(task => ({
138
+ ...task,
139
+ id: typeof task['id'] === 'string' && task['id'].trim() !== ''
140
+ ? task['id']
141
+ : randomUUID(),
142
+ }));
143
+ }
133
144
  const configData = rawConfigData;
134
145
  const projectId = configData['projectId'];
135
- const serverUrl = await authService.getServerUrl();
146
+ const websiteUrl = (await configPromise).WEBSITE_URL;
147
+ const savedWebsiteUrl = await authService.getWebsiteUrl();
148
+ const validationBaseUrl = websiteUrl || savedWebsiteUrl;
136
149
  const apiToken = await authService.getAccessToken();
137
- if (serverUrl && apiToken) {
150
+ if (validationBaseUrl && apiToken) {
138
151
  try {
139
- const validateUrl = new URL(`/api/cli/projects/${encodeURIComponent(projectId)}/validate`, serverUrl);
152
+ const validateUrl = new URL(`/api/cli/projects/${encodeURIComponent(projectId)}/validate`, validationBaseUrl);
140
153
  const response = await fetch(validateUrl.toString(), {
141
154
  method: 'POST',
142
155
  headers: {
@@ -189,7 +202,7 @@ export async function run(args, t) {
189
202
  args.onUiEvent?.({ type: 'connection_status', status });
190
203
  },
191
204
  onRunStarted: data => {
192
- const appUrl = apiUrl;
205
+ const appUrl = websiteUrl;
193
206
  const projectId = configData['projectId'];
194
207
  const reportUrl = projectId && data.runId
195
208
  ? `${appUrl}/project/${projectId}/report/${data.runId}`
@@ -201,6 +214,7 @@ export async function run(args, t) {
201
214
  type: 'server_state',
202
215
  thinking: data.state.thinking,
203
216
  taskStatus: data.state.taskStatus,
217
+ taskList: data.state.taskList ?? undefined,
204
218
  actions: data.action?.map(action => formatActionSummary(action, t)) ?? [],
205
219
  qaosProgress: data.state.qaosProgress ?? undefined,
206
220
  });
@@ -116,6 +116,7 @@
116
116
  "errors.configMissingProjectId": "Config is missing required field \"projectId\" (must be a non-empty string).",
117
117
  "errors.configEnvFileMustBeString": "Config field \"envFile\" must be a string path.",
118
118
  "errors.configQaosModeNotBoolean": "Config field \"qaosMode\" must be a boolean (true or false).",
119
+ "errors.configMaxBudgetMustBePositiveNumber": "Config field \"maxBudget\" must be a positive number.",
119
120
  "errors.configQaosConfigRequired": "Config has \"qaosMode: true\" but is missing the required \"qaosConfig\" object.",
120
121
  "errors.configQaosStartUrlRequired": "Config \"qaosConfig.startUrl\" is required and must be a non-empty string.",
121
122
  "errors.configQaosMaxPagesMustBePositiveInt": "Config \"qaosConfig.maxPages\" must be a positive integer.",
@@ -130,6 +131,8 @@
130
131
  "errors.aiDecisionParseFailed": "The AI agent failed to produce a valid response after several retries. The run has been stopped.",
131
132
  "errors.projectNotFound": "Project \"{{projectId}}\" does not exist or you do not have access to it.",
132
133
  "errors.projectValidationFailed": "Could not verify project access: {{error}}",
134
+ "errors.configWebsiteOriginRequiredForFilesystem": "\"websiteOrigin\" is required when using filesystem paths (file:// URLs or local drive paths). Set it to the root directory of the website being tested (e.g. \"C:/my-site\").",
135
+ "errors.configWebsiteOriginMismatch": "All start URLs must begin with \"websiteOrigin\". Ensure every task \"startUrl\" (and \"qaosConfig.startUrl\") starts with the value of \"websiteOrigin\".",
133
136
 
134
137
  "actions.click": "click element {{index}}",
135
138
  "actions.input": "input {{index}}: \"{{text}}\"",
@@ -116,6 +116,7 @@
116
116
  "errors.configMissingProjectId": "Le champ obligatoire \"projectId\" est absent de la configuration (doit être une chaîne non vide).",
117
117
  "errors.configEnvFileMustBeString": "Le champ \"envFile\" de la configuration doit être un chemin de type chaîne.",
118
118
  "errors.configQaosModeNotBoolean": "Le champ \"qaosMode\" de la configuration doit être un booléen (true ou false).",
119
+ "errors.configMaxBudgetMustBePositiveNumber": "Le champ \"maxBudget\" de la configuration doit être un nombre positif.",
119
120
  "errors.configQaosConfigRequired": "La configuration a \"qaosMode: true\" mais l'objet \"qaosConfig\" requis est absent.",
120
121
  "errors.configQaosStartUrlRequired": "\"qaosConfig.startUrl\" est obligatoire et doit être une chaîne non vide.",
121
122
  "errors.configQaosMaxPagesMustBePositiveInt": "\"qaosConfig.maxPages\" doit être un entier positif.",
@@ -130,6 +131,8 @@
130
131
  "errors.aiDecisionParseFailed": "L'agent IA n'a pas réussi à produire une réponse valide après plusieurs tentatives. L'exécution a été arrêtée.",
131
132
  "errors.projectNotFound": "Le projet \"{{projectId}}\" n'existe pas ou vous n'y avez pas accès.",
132
133
  "errors.projectValidationFailed": "Impossible de vérifier l'accès au projet : {{error}}",
134
+ "errors.configWebsiteOriginRequiredForFilesystem": "\"websiteOrigin\" est requis lorsque des chemins de système de fichiers sont utilisés (URLs file:// ou chemins de lecteur local). Définissez-le sur le répertoire racine du site testé (ex. : \"C:/mon-site\").",
135
+ "errors.configWebsiteOriginMismatch": "Toutes les URLs de départ doivent commencer par \"websiteOrigin\". Assurez-vous que chaque \"startUrl\" (et \"qaosConfig.startUrl\") commence par la valeur de \"websiteOrigin\".",
133
136
 
134
137
  "actions.click": "cliquer sur l'élément {{index}}",
135
138
  "actions.input": "saisie {{index}} : \"{{text}}\"",
@@ -1,3 +1,3 @@
1
1
  export declare const WS_SERVER_URL = "ws://3.99.220.157:8000/ws/v1";
2
2
  export declare const API_SERVER_URL = "http://3.99.220.157:8000";
3
- export declare const WEBSITE_URL = "http://qaos.machdel.com";
3
+ export declare const WEBSITE_URL = "https://qaos.machdel.com";
@@ -1,3 +1,3 @@
1
1
  export const WS_SERVER_URL = 'ws://3.99.220.157:8000/ws/v1';
2
2
  export const API_SERVER_URL = 'http://3.99.220.157:8000';
3
- export const WEBSITE_URL = 'http://qaos.machdel.com';
3
+ export const WEBSITE_URL = 'https://qaos.machdel.com';
@@ -37,9 +37,9 @@ declare class AuthService {
37
37
  */
38
38
  getAccessToken(): Promise<string | null>;
39
39
  /**
40
- * Get the configured server URL
40
+ * Get the configured website URL
41
41
  */
42
- getServerUrl(): Promise<string | null>;
42
+ getWebsiteUrl(): Promise<string | null>;
43
43
  /**
44
44
  * Clear authentication (logout)
45
45
  */
@@ -114,9 +114,9 @@ class AuthService {
114
114
  return config?.accessToken || null;
115
115
  }
116
116
  /**
117
- * Get the configured server URL
117
+ * Get the configured website URL
118
118
  */
119
- async getServerUrl() {
119
+ async getWebsiteUrl() {
120
120
  const config = await this.loadToken();
121
121
  return config?.serverUrl || null;
122
122
  }
@@ -1,21 +1,11 @@
1
1
  import type { BrowserAction } from '../services/browser-action-service.js';
2
- export type TaskStatus = 'doing' | 'done' | 'notStarted';
3
- export interface QaosTaskInfo {
4
- id: string;
5
- description: string;
6
- status: TaskStatus;
7
- isSubtask: boolean;
8
- }
9
- export interface QaosProgress {
10
- pagesVisited: number;
11
- pagesRemaining: number;
12
- currentPageUrl: string | null;
13
- tasks: QaosTaskInfo[];
14
- }
2
+ import type { RunTask, RunTaskStatus, QaosProgress } from './run-ui-state.js';
3
+ export type { RunTask as QaosTaskInfo };
15
4
  export interface NextActionsResponse {
16
5
  state: {
17
6
  thinking: string;
18
- taskStatus: Record<string, TaskStatus>;
7
+ taskStatus: Record<string, RunTaskStatus>;
8
+ taskList: RunTask[] | null;
19
9
  qaosProgress: QaosProgress | null;
20
10
  };
21
11
  action: BrowserAction[];
@@ -5,11 +5,6 @@ export interface RunTask {
5
5
  id: string;
6
6
  description: string;
7
7
  status: RunTaskStatus;
8
- }
9
- export interface QaosTaskInfo {
10
- id: string;
11
- description: string;
12
- status: RunTaskStatus;
13
8
  isSubtask: boolean;
14
9
  parentTaskId?: string;
15
10
  }
@@ -17,7 +12,7 @@ export interface QaosProgress {
17
12
  pagesVisited: number;
18
13
  pagesRemaining: number;
19
14
  currentPageUrl: string | null;
20
- tasks: QaosTaskInfo[];
15
+ tasks: RunTask[];
21
16
  }
22
17
  export interface RunUiState {
23
18
  phase: RunPhase;
@@ -46,6 +41,7 @@ export type RunUiEvent = {
46
41
  type: 'server_state';
47
42
  thinking: string;
48
43
  taskStatus?: Record<string, RunTaskStatus>;
44
+ taskList?: RunTask[];
49
45
  actions?: string[];
50
46
  qaosProgress?: QaosProgress;
51
47
  } | {
@@ -1,3 +1,13 @@
1
+ function isFilesystem(url) {
2
+ try {
3
+ return new URL(url).protocol === 'file:' || /^[a-zA-Z]:[\\/]/.test(url);
4
+ }
5
+ catch { /* fall through */ }
6
+ return false;
7
+ }
8
+ function normalizePath(url) {
9
+ return url.replace(/\\/g, '/');
10
+ }
1
11
  /**
2
12
  * Validates the parsed config object.
3
13
  * Returns an array of translated error messages.
@@ -18,6 +28,11 @@ export function validateConfig(data, t) {
18
28
  errors.push(t('errors.configEnvFileMustBeString'));
19
29
  if (cfg['qaosMode'] !== undefined && typeof cfg['qaosMode'] !== 'boolean')
20
30
  errors.push(t('errors.configQaosModeNotBoolean'));
31
+ if (cfg['maxBudget'] !== undefined &&
32
+ (typeof cfg['maxBudget'] !== 'number' ||
33
+ !Number.isFinite(cfg['maxBudget']) ||
34
+ cfg['maxBudget'] <= 0))
35
+ errors.push(t('errors.configMaxBudgetMustBePositiveNumber'));
21
36
  const VALID_AGENTS = ['uiux', 'security'];
22
37
  if (cfg['qaosMode'] === true) {
23
38
  const qc = cfg['qaosConfig'];
@@ -51,6 +66,16 @@ export function validateConfig(data, t) {
51
66
  }));
52
67
  }
53
68
  }
69
+ if (typeof q['startUrl'] === 'string' &&
70
+ isFilesystem(q['startUrl']) &&
71
+ (typeof cfg['websiteOrigin'] !== 'string' || cfg['websiteOrigin'].trim() === ''))
72
+ errors.push(t('errors.configWebsiteOriginRequiredForFilesystem'));
73
+ if (typeof cfg['websiteOrigin'] === 'string' &&
74
+ cfg['websiteOrigin'].trim() !== '' &&
75
+ typeof q['startUrl'] === 'string' &&
76
+ q['startUrl'].trim() !== '' &&
77
+ !normalizePath(q['startUrl']).startsWith(normalizePath(cfg['websiteOrigin'].trim())))
78
+ errors.push(t('errors.configWebsiteOriginMismatch'));
54
79
  }
55
80
  }
56
81
  else {
@@ -61,8 +86,6 @@ export function validateConfig(data, t) {
61
86
  for (let i = 0; i < cfg['tasks'].length; i++) {
62
87
  const task = cfg['tasks'][i];
63
88
  const idx = i + 1;
64
- if (typeof task['id'] !== 'string' || task['id'].trim() === '')
65
- errors.push(t('errors.configTaskMissingId', { index: idx }));
66
89
  if (typeof task['description'] !== 'string' ||
67
90
  task['description'].trim() === '')
68
91
  errors.push(t('errors.configTaskMissingDescription', { index: idx }));
@@ -82,6 +105,19 @@ export function validateConfig(data, t) {
82
105
  }));
83
106
  }
84
107
  }
108
+ const validStartUrls = cfg['tasks']
109
+ .map((task) => task['startUrl'])
110
+ .filter((u) => typeof u === 'string' && u.trim() !== '');
111
+ if (validStartUrls.some(isFilesystem) &&
112
+ (typeof cfg['websiteOrigin'] !== 'string' || cfg['websiteOrigin'].trim() === ''))
113
+ errors.push(t('errors.configWebsiteOriginRequiredForFilesystem'));
114
+ if (typeof cfg['websiteOrigin'] === 'string' &&
115
+ cfg['websiteOrigin'].trim() !== '' &&
116
+ validStartUrls.length > 0) {
117
+ const wo = normalizePath(cfg['websiteOrigin'].trim());
118
+ if (validStartUrls.some((url) => !normalizePath(url).startsWith(wo)))
119
+ errors.push(t('errors.configWebsiteOriginMismatch'));
120
+ }
85
121
  }
86
122
  }
87
123
  return errors;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qaos",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "qaos": "dist/cli.js"