From a194cf6b8b4382a04d3cc02c4b3abbd90b7db2bf Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 17 Oct 2026 02:59:37 +0200 Subject: [PATCH] u --- API.md | 6 + API.subagentschedule.md | 130 ++++++++++++++++ generate-api-doc.js | 1 + src/index.js | 13 ++ .../mssql/queries/subagentschedule.js | 88 +++++++++++ .../sqlite3/queries/subagentschedule.js | 12 ++ src/store/subagentschedule/create.js | 118 +++++++++++++++ src/store/subagentschedule/cron.js | 99 ++++++++++++ src/store/subagentschedule/delete.js | 34 +++++ src/store/subagentschedule/getById.js | 47 ++++++ src/store/subagentschedule/list.js | 40 +++++ src/store/subagentschedule/update.js | 142 ++++++++++++++++++ 12 files changed, 730 insertions(+) create mode 100644 API.subagentschedule.md create mode 100644 src/providers/mssql/queries/subagentschedule.js create mode 100644 src/providers/sqlite3/queries/subagentschedule.js create mode 100644 src/store/subagentschedule/create.js create mode 100644 src/store/subagentschedule/cron.js create mode 100644 src/store/subagentschedule/delete.js create mode 100644 src/store/subagentschedule/getById.js create mode 100644 src/store/subagentschedule/list.js create mode 100644 src/store/subagentschedule/update.js diff --git a/API.md b/API.md index ec5bc93..83e1e6d 100644 --- a/API.md +++ b/API.md @@ -381,3 +381,9 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `create(googleAuthId: number, input: {accountId: number, manifestId: number, type: string, amount: number}, tx?: object): Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise>` +- [subagentschedule](API.subagentschedule.md) + - `create(googleAuthId: number, input: {prompt: string, cron: string, desc?: string, skills?: string|Array, lastagentnote?: string, max_itr?: number}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `update(googleAuthId: number, id: number, input: {prompt?: string, cron?: string, desc?: string|null, skills?: string|Array|null, lastagentnote?: string|null, max_itr?: number, lastrun_at?: string|null}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>` diff --git a/API.subagentschedule.md b/API.subagentschedule.md new file mode 100644 index 0000000..6944022 --- /dev/null +++ b/API.subagentschedule.md @@ -0,0 +1,130 @@ +# subagentschedule + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `subagentschedule.create` + +*Source: `src/store/subagentschedule/create.js`* + +Creates a new subagent schedule for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Schedule input. | +| `input.prompt` | `string` | The task prompt for the subagent. | +| `input.cron` | `string` | Standard 5-field cron timetable string. | +| `[input.desc]` | `string` | Optional schedule description. | +| `[input.skills]` | `string|Array` | Optional skill names or JSON string. | +| `[input.lastagentnote]` | `string` | Optional initial agent note. | +| `[input.max_itr=15]` | `number` | Optional max iterations per run. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>` + +**Throws** + +`Error` — If prompt is missing, cron is invalid, or max_itr is invalid. + +**Example** + +```js +const schedule = await store.subagentschedule.create(googleAuthId, { + desc: 'Nightly inventory audit', + prompt: 'Check stock discrepancies across all warehouses and summarize.', + cron: '0 2 * * *', + skills: ['stock', 'reporting'], + max_itr: 20, +}); +``` + +## `subagentschedule.delete` + +*Source: `src/store/subagentschedule/delete.js`* + +Deletes a subagent schedule by id for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The schedule ID to delete. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If schedule not found. + +## `subagentschedule.getById` + +*Source: `src/store/subagentschedule/getById.js`* + +Retrieves a single subagent schedule by id for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The schedule ID. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>` + +## `subagentschedule.list` + +*Source: `src/store/subagentschedule/list.js`* + +Lists all subagent schedules for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise>` + +## `subagentschedule.update` + +*Source: `src/store/subagentschedule/update.js`* + +Updates an existing subagent schedule for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The schedule ID to update. | +| `input` | `object` | Update payload. | +| `[input.prompt]` | `string` | Optional updated task prompt. | +| `[input.cron]` | `string` | Optional updated cron expression. | +| `[input.desc]` | `string|null` | Optional updated description. | +| `[input.skills]` | `string|Array|null` | Optional updated skills. | +| `[input.lastagentnote]` | `string|null` | Optional updated agent note. | +| `[input.max_itr]` | `number` | Optional updated max iterations. | +| `[input.lastrun_at]` | `string|null` | Optional updated last run timestamp. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>` + +**Throws** + +`Error` — If schedule not found, prompt is empty, cron is invalid, or max_itr is invalid. diff --git a/generate-api-doc.js b/generate-api-doc.js index 8decc2a..479da89 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -76,6 +76,7 @@ const INTERNAL_FILES = new Set([ 'createStore.js', 'manifestLines.js', 'runner.js', + 'cron.js', 'index.js', ]); diff --git a/src/index.js b/src/index.js index 204ade8..f22ee0a 100644 --- a/src/index.js +++ b/src/index.js @@ -183,6 +183,11 @@ import modelvfsRead from './store/modelvfs/read.js'; import modelvfsList from './store/modelvfs/list.js'; import modelvfsDelete from './store/modelvfs/delete.js'; import modelvfsReset from './store/modelvfs/reset.js'; +import subagentscheduleCreate from './store/subagentschedule/create.js'; +import subagentscheduleGetById from './store/subagentschedule/getById.js'; +import subagentscheduleList from './store/subagentschedule/list.js'; +import subagentscheduleUpdate from './store/subagentschedule/update.js'; +import subagentscheduleDelete from './store/subagentschedule/delete.js'; import accountCreate from './store/account/create.js'; import accountList from './store/account/list.js'; import accountDelete from './store/account/delete.js'; @@ -584,6 +589,14 @@ export default class LibDashStore { add: modelmessageAdd(provider), }; + this.subagentschedule = { + create: subagentscheduleCreate(provider), + getById: subagentscheduleGetById(provider), + list: subagentscheduleList(provider), + update: subagentscheduleUpdate(provider), + delete: subagentscheduleDelete(provider), + }; + this.labeltemplate = { create: labeltemplateCreate(provider), getById: labeltemplateGetById(provider), diff --git a/src/providers/mssql/queries/subagentschedule.js b/src/providers/mssql/queries/subagentschedule.js new file mode 100644 index 0000000..5d02951 --- /dev/null +++ b/src/providers/mssql/queries/subagentschedule.js @@ -0,0 +1,88 @@ +export const SUBAGENTSCHEDULE_COLUMNS = ` + id, + company_id, + [desc], + prompt, + skills, + lastagentnote, + max_itr, + cron, + created_at, + lastrun_at +`; + +export const LIST_BY_COMPANY = ` +SELECT +${SUBAGENTSCHEDULE_COLUMNS} +FROM subagentschedule +WHERE company_id = @companyId +ORDER BY id ASC +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT +${SUBAGENTSCHEDULE_COLUMNS} +FROM subagentschedule +WHERE id = @id AND company_id = @companyId +`; + +export const CREATE = ` +INSERT INTO subagentschedule ( + company_id, + [desc], + prompt, + skills, + lastagentnote, + max_itr, + cron +) +OUTPUT + INSERTED.id, + INSERTED.company_id, + INSERTED.[desc], + INSERTED.prompt, + INSERTED.skills, + INSERTED.lastagentnote, + INSERTED.max_itr, + INSERTED.cron, + INSERTED.created_at, + INSERTED.lastrun_at +VALUES ( + @companyId, + @desc, + @prompt, + @skills, + @lastagentnote, + @maxItr, + @cron +) +`; + +export const UPDATE = ` +UPDATE subagentschedule +SET + [desc] = @desc, + prompt = @prompt, + skills = @skills, + lastagentnote = @lastagentnote, + max_itr = @maxItr, + cron = @cron, + lastrun_at = @lastrunAt +OUTPUT + INSERTED.id, + INSERTED.company_id, + INSERTED.[desc], + INSERTED.prompt, + INSERTED.skills, + INSERTED.lastagentnote, + INSERTED.max_itr, + INSERTED.cron, + INSERTED.created_at, + INSERTED.lastrun_at +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM subagentschedule +WHERE id = @id AND company_id = @companyId +`; diff --git a/src/providers/sqlite3/queries/subagentschedule.js b/src/providers/sqlite3/queries/subagentschedule.js new file mode 100644 index 0000000..28ecc14 --- /dev/null +++ b/src/providers/sqlite3/queries/subagentschedule.js @@ -0,0 +1,12 @@ +import * as source from '../../mssql/queries/subagentschedule.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'subagentschedule'); +export const { + CREATE, + DELETE_BY_COMPANY_AND_ID, + GET_BY_COMPANY_AND_ID, + LIST_BY_COMPANY, + SUBAGENTSCHEDULE_COLUMNS, + UPDATE, +} = queries; diff --git a/src/store/subagentschedule/create.js b/src/store/subagentschedule/create.js new file mode 100644 index 0000000..6d408ca --- /dev/null +++ b/src/store/subagentschedule/create.js @@ -0,0 +1,118 @@ +import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/subagentschedule.js'; +import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/subagentschedule.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import { isValidCron } from './cron.js'; + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + desc: row.desc ?? null, + prompt: row.prompt, + skills: row.skills ?? null, + lastagentnote: row.lastagentnote ?? null, + max_itr: row.max_itr, + cron: row.cron, + created_at: row.created_at, + lastrun_at: row.lastrun_at ?? null, + }; +} + +function optionalString(value) { + if (value == null || value === '') return null; + const s = String(value).trim(); + return s || null; +} + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates a new subagent schedule for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Schedule input. + * @param {string} input.prompt - The task prompt for the subagent. + * @param {string} input.cron - Standard 5-field cron timetable string. + * @param {string} [input.desc] - Optional schedule description. + * @param {string|Array} [input.skills] - Optional skill names or JSON string. + * @param {string} [input.lastagentnote] - Optional initial agent note. + * @param {number} [input.max_itr=15] - Optional max iterations per run. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>} + * @throws {Error} If prompt is missing, cron is invalid, or max_itr is invalid. + * + * @example + * const schedule = await store.subagentschedule.create(googleAuthId, { + * desc: 'Nightly inventory audit', + * prompt: 'Check stock discrepancies across all warehouses and summarize.', + * cron: '0 2 * * *', + * skills: ['stock', 'reporting'], + * max_itr: 20, + * }); + */ + return async (googleAuthId, input = {}, tx) => { + const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : ''; + if (!prompt) { + throw new Error('Prompt required'); + } + + const cron = typeof input.cron === 'string' ? input.cron.trim() : ''; + if (!cron) { + throw new Error('Cron expression required'); + } + if (!isValidCron(cron)) { + throw new Error('Invalid cron expression'); + } + + let maxItr = 15; + if (input.max_itr !== undefined && input.max_itr !== null) { + const parsed = Number(input.max_itr); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error('Invalid max iterations: must be a positive integer'); + } + maxItr = parsed; + } + + let skills = null; + if (Array.isArray(input.skills)) { + skills = JSON.stringify(input.skills); + } else if (input.skills != null) { + skills = optionalString(input.skills); + } + + const desc = optionalString(input.desc); + const lastagentnote = optionalString(input.lastagentnote); + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(createSql, { + companyId, + desc, + prompt, + skills, + lastagentnote, + maxItr, + cron, + }); + + const row = result.rows[0] ?? { + id: result.insertId, + company_id: companyId, + desc, + prompt, + skills, + lastagentnote, + max_itr: maxItr, + cron, + created_at: new Date().toISOString(), + lastrun_at: null, + }; + + return shape(row); + }; +} diff --git a/src/store/subagentschedule/cron.js b/src/store/subagentschedule/cron.js new file mode 100644 index 0000000..87a81c3 --- /dev/null +++ b/src/store/subagentschedule/cron.js @@ -0,0 +1,99 @@ +const CRON_NICKNAMES = new Set([ + '@yearly', + '@annually', + '@monthly', + '@weekly', + '@daily', + '@midnight', + '@hourly', +]); + +const MONTH_NAMES = { + jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, + jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12, +}; + +const DAY_NAMES = { + sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, +}; + +const FIELD_RANGES = [ + { name: 'minute', min: 0, max: 59 }, + { name: 'hour', min: 0, max: 23 }, + { name: 'day of month', min: 1, max: 31 }, + { name: 'month', min: 1, max: 12, names: MONTH_NAMES }, + { name: 'day of week', min: 0, max: 7, names: DAY_NAMES }, +]; + +function parseCronValue(val, min, max, names) { + if (names && typeof val === 'string') { + const lower = val.toLowerCase(); + if (lower in names) return names[lower]; + } + if (!/^\d+$/.test(val)) return null; + const num = parseInt(val, 10); + if (num < min || num > max) return null; + return num; +} + +function validateField(part, min, max, names) { + if (!part) return false; + const items = part.split(','); + if (items.length === 0) return false; + + for (const item of items) { + if (!item) return false; + const slashParts = item.split('/'); + if (slashParts.length > 2) return false; + if (slashParts.length === 2) { + const step = slashParts[1]; + if (!/^\d+$/.test(step)) return false; + const stepNum = parseInt(step, 10); + if (stepNum <= 0 || stepNum > max) return false; + } + + const base = slashParts[0]; + if (base === '*' || base === '?') { + continue; + } + + const dashParts = base.split('-'); + if (dashParts.length > 2) return false; + if (dashParts.length === 2) { + const start = parseCronValue(dashParts[0], min, max, names); + const end = parseCronValue(dashParts[1], min, max, names); + if (start === null || end === null || start > end) return false; + continue; + } + + const val = parseCronValue(base, min, max, names); + if (val === null) return false; + } + + return true; +} + +/** + * Validates a standard 5-part cron timetable string or standard nickname. + * + * @param {string} cron - The cron expression to validate. + * @returns {boolean} True if the cron expression is syntactically valid. + */ +export function isValidCron(cron) { + if (typeof cron !== 'string') return false; + const trimmed = cron.trim(); + if (!trimmed) return false; + if (CRON_NICKNAMES.has(trimmed.toLowerCase())) return true; + + const parts = trimmed.split(/\s+/); + if (parts.length !== 5) return false; + + for (let i = 0; i < 5; i++) { + const part = parts[i]; + const { min, max, names } = FIELD_RANGES[i]; + if (!validateField(part, min, max, names)) { + return false; + } + } + return true; +} diff --git a/src/store/subagentschedule/delete.js b/src/store/subagentschedule/delete.js new file mode 100644 index 0000000..db206e5 --- /dev/null +++ b/src/store/subagentschedule/delete.js @@ -0,0 +1,34 @@ +import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/subagentschedule.js'; +import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/subagentschedule.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function remove(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes a subagent schedule by id for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The schedule ID to delete. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} + * @throws {Error} If schedule not found. + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + throw new Error('Subagent schedule not found'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(deleteSql, { id: numericId, companyId }); + if (result.rowCount === 0) { + throw new Error('Subagent schedule not found'); + } + }; +} diff --git a/src/store/subagentschedule/getById.js b/src/store/subagentschedule/getById.js new file mode 100644 index 0000000..45f99a2 --- /dev/null +++ b/src/store/subagentschedule/getById.js @@ -0,0 +1,47 @@ +import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/subagentschedule.js'; +import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/subagentschedule.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + desc: row.desc ?? null, + prompt: row.prompt, + skills: row.skills ?? null, + lastagentnote: row.lastagentnote ?? null, + max_itr: row.max_itr, + cron: row.cron, + created_at: row.created_at, + lastrun_at: row.lastrun_at ?? null, + }; +} + +export default function getById(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; + + /** + * Retrieves a single subagent schedule by id for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The schedule ID. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>} + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + return null; + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(getSql, { id: numericId, companyId }); + const row = result.rows[0]; + return row ? shape(row) : null; + }; +} diff --git a/src/store/subagentschedule/list.js b/src/store/subagentschedule/list.js new file mode 100644 index 0000000..1c3559b --- /dev/null +++ b/src/store/subagentschedule/list.js @@ -0,0 +1,40 @@ +import { LIST_BY_COMPANY as MSSQL_LIST } from '../../providers/mssql/queries/subagentschedule.js'; +import { LIST_BY_COMPANY as SQLITE_LIST } from '../../providers/sqlite3/queries/subagentschedule.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + desc: row.desc ?? null, + prompt: row.prompt, + skills: row.skills ?? null, + lastagentnote: row.lastagentnote ?? null, + max_itr: row.max_itr, + cron: row.cron, + created_at: row.created_at, + lastrun_at: row.lastrun_at ?? null, + }; +} + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listSql = isSqlite ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists all subagent schedules for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise>} + */ + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(listSql, { companyId }); + return result.rows.map(shape); + }; +} diff --git a/src/store/subagentschedule/update.js b/src/store/subagentschedule/update.js new file mode 100644 index 0000000..6965bcc --- /dev/null +++ b/src/store/subagentschedule/update.js @@ -0,0 +1,142 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID, + UPDATE as MSSQL_UPDATE, +} from '../../providers/mssql/queries/subagentschedule.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID, + UPDATE as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/subagentschedule.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import { isValidCron } from './cron.js'; + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + desc: row.desc ?? null, + prompt: row.prompt, + skills: row.skills ?? null, + lastagentnote: row.lastagentnote ?? null, + max_itr: row.max_itr, + cron: row.cron, + created_at: row.created_at, + lastrun_at: row.lastrun_at ?? null, + }; +} + +function optionalString(value) { + if (value == null || value === '') return null; + const s = String(value).trim(); + return s || null; +} + +export default function update(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Updates an existing subagent schedule for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The schedule ID to update. + * @param {object} input - Update payload. + * @param {string} [input.prompt] - Optional updated task prompt. + * @param {string} [input.cron] - Optional updated cron expression. + * @param {string|null} [input.desc] - Optional updated description. + * @param {string|Array|null} [input.skills] - Optional updated skills. + * @param {string|null} [input.lastagentnote] - Optional updated agent note. + * @param {number} [input.max_itr] - Optional updated max iterations. + * @param {string|null} [input.lastrun_at] - Optional updated last run timestamp. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>} + * @throws {Error} If schedule not found, prompt is empty, cron is invalid, or max_itr is invalid. + */ + return async (googleAuthId, id, input = {}, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + throw new Error('Subagent schedule not found'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const existing = await runner.query(getSql, { id: numericId, companyId }); + const current = existing.rows[0]; + if (!current) { + throw new Error('Subagent schedule not found'); + } + + let prompt = current.prompt; + if (input.prompt !== undefined) { + const cleaned = typeof input.prompt === 'string' ? input.prompt.trim() : ''; + if (!cleaned) { + throw new Error('Prompt required'); + } + prompt = cleaned; + } + + let cron = current.cron; + if (input.cron !== undefined) { + const cleaned = typeof input.cron === 'string' ? input.cron.trim() : ''; + if (!cleaned) { + throw new Error('Cron expression required'); + } + if (!isValidCron(cleaned)) { + throw new Error('Invalid cron expression'); + } + cron = cleaned; + } + + let maxItr = current.max_itr; + if (input.max_itr !== undefined && input.max_itr !== null) { + const parsed = Number(input.max_itr); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error('Invalid max iterations: must be a positive integer'); + } + maxItr = parsed; + } + + let skills = current.skills; + if (input.skills !== undefined) { + if (Array.isArray(input.skills)) { + skills = JSON.stringify(input.skills); + } else { + skills = optionalString(input.skills); + } + } + + const desc = input.desc !== undefined ? optionalString(input.desc) : (current.desc ?? null); + const lastagentnote = input.lastagentnote !== undefined ? optionalString(input.lastagentnote) : (current.lastagentnote ?? null); + const lastrunAt = input.lastrun_at !== undefined ? optionalString(input.lastrun_at) : (current.lastrun_at ?? null); + + const result = await runner.query(updateSql, { + id: numericId, + companyId, + desc, + prompt, + skills, + lastagentnote, + maxItr, + cron, + lastrunAt, + }); + + const row = result.rows[0] ?? { + id: numericId, + company_id: companyId, + desc, + prompt, + skills, + lastagentnote, + max_itr: maxItr, + cron, + created_at: current.created_at, + lastrun_at: lastrunAt, + }; + + return shape(row); + }; +}