Compare commits

...

2 Commits

Author SHA1 Message Date
seb
a194cf6b8b u 2026-10-17 02:59:37 +02:00
seb
59395f95d8 u 2026-10-17 02:47:20 +02:00
18 changed files with 4060 additions and 3215 deletions

26
API.md
View File

@@ -8,6 +8,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
> **Convention**: `PK` primary key · `?` nullable · `->tbl` foreign key · `UQ` unique · `s(N)` string · `txt` text · `i` int · `bi` bigint · `f` float · `d` decimal · `b` bit · `dt` datetime · `bin` binary · `=val` default
- **account**: `id PK, company_id->company, type:s(100), name:s(255)`
- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)`
- **articleattributevalue**: `id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)`
- **articleean**: `id PK, article_id->article, ean:s(50) UQ`
@@ -18,6 +19,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **articleproperty**: `id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0`
- **articlesupplier**: `id PK, article_id->article, entity_id->entity, net?:f, tax_id?->tax, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)`
- **articletext**: `id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)`
- **articletombstone**: `id PK, company_id->company, article_id?:i, sku?:s(100), name?:s(255), taxrate?:d(18,4), net?:f, deleted_at:dt`
- **attributename**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
- **attributevalue**: `id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)`
- **attributevaluepic**: `id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)`
@@ -25,13 +27,16 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **brandpic**: `id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)`
- **category**: `id PK, parent_id?->category, sort:i=0`
- **categoryarticle**: `id PK, category_id->category, article_id->article, UQ(category_id, article_id)`
- **categoryattributevalue**: `id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)`
- **categorypic**: `id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)`
- **categoryproperty**: `id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0`
- **categorytext**: `id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)`
- **company**: `id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup`
- **companyhistory**: `id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt`
- **companysetting**: `id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)`
- **companyuser**: `id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)`
- **customergroup**: `id PK, name:s(255), sort:i=0, company_id?->company`
- **deliverysummary**: `id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0`
- **entity**: `id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)`
- **entityaddress**: `id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0`
- **entitycontact**: `id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0`
@@ -44,16 +49,29 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt`
- **manifestarticle**: `id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0`
- **manifestattachment**: `id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)`
- **mcp_auth_code**: `code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt`
- **mcp_client**: `id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt`
- **mcp_token**: `id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt`
- **meta**: `key:s(255) PK, value:txt`
- **modelconversation**: `id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)='idle', started_by?:i`
- **modelmessage**: `id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt`
- **modelvfs**: `id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)`
- **numbers**: `id PK, company_id->company, type:s(100), prefix:s(50)='', padding:i=0, next:i=1, UQ(company_id, type)`
- **optionslist**: `id PK`
- **optionslistarticle**: `id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)`
- **ordersummary**: `id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0`
- **package**: `id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt`
- **payment**: `id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f`
- **pic**: `id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin`
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt`
- **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)`
- **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f`
- **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt`
- **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
- **quotesummary**: `id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0`
- **shipmentsummary**: `id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)='new'`
- **shipping**: `id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)`
- **subagentschedule**: `id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt`
- **tax**: `id PK, rate:d(18,4) UQ`
- **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)`
- **unittext**: `id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)`
@@ -363,5 +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<void>`
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
- [runner.js](API.runner.js.md)
- `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`
- [subagentschedule](API.subagentschedule.md)
- `create(googleAuthId: number, input: {prompt: string, cron: string, desc?: string, skills?: string|Array<string>, 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<void>`
- `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<Array<{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}>>`
- `update(googleAuthId: number, id: number, input: {prompt?: string, cron?: string, desc?: string|null, skills?: string|Array<string>|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}>`

View File

@@ -1,20 +0,0 @@
# runner.js
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `runner.js.runner`
*Source: `src/store/runner.js`*
Resolves the object a store method should query through. Converted store modules call `runner(sql, params)` or `runner.query(sql, params)`. During the incremental migration they may still be constructed with a raw `mssql` pool, or called with a raw `mssql` Transaction as `tx`. This adapter exposes the unified runner contract over both so mixed call sites keep working until every module is converted.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../providers/provider.js').DatabaseProvider` | |
| `[tx]` | `object` | Optional provider runner or legacy mssql Transaction. |
**Returns**
`((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`

130
API.subagentschedule.md Normal file
View File

@@ -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<string>` | 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<void>`
**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<Array<{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}>>`
## `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<string>|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.

View File

@@ -75,6 +75,8 @@ const INTERNAL_FILES = new Set([
'shape.js',
'createStore.js',
'manifestLines.js',
'runner.js',
'cron.js',
'index.js',
]);
@@ -321,6 +323,7 @@ function render() {
}
const SCHEMA_TABLES = [
['account', 'id PK, company_id->company, type:s(100), name:s(255)'],
['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'],
['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'],
['articleean', 'id PK, article_id->article, ean:s(50) UQ'],
@@ -331,6 +334,7 @@ const SCHEMA_TABLES = [
['articleproperty', 'id PK, article_id->article, property_id->propertyname, value:txt, sort:i=0'],
['articlesupplier', 'id PK, article_id->article, entity_id->entity, net?:f, tax_id?->tax, sku?:s(100), stock?:f, pu:f=1, sort:i=0, UQ(article_id, entity_id), UQ(article_id, sort)'],
['articletext', 'id PK, article_id->article, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(article_id, language_id)'],
['articletombstone', 'id PK, company_id->company, article_id?:i, sku?:s(100), name?:s(255), taxrate?:d(18,4), net?:f, deleted_at:dt'],
['attributename', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
['attributevalue', 'id PK, attribute_id->attributename, value:s(255), sort:i=0, UQ(attribute_id, value)'],
['attributevaluepic', 'id PK, attributevalue_id->attributevalue, pic_id->pic, sort:i=0, UQ(attributevalue_id, pic_id), UQ(attributevalue_id, sort)'],
@@ -338,13 +342,16 @@ const SCHEMA_TABLES = [
['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'],
['category', 'id PK, parent_id?->category, sort:i=0'],
['categoryarticle', 'id PK, category_id->category, article_id->article, UQ(category_id, article_id)'],
['categoryattributevalue', 'id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)'],
['categorypic', 'id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)'],
['categoryproperty', 'id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0'],
['categorytext', 'id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)'],
['company', 'id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup'],
['companyhistory', 'id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt'],
['companysetting', 'id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)'],
['companyuser', 'id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)'],
['customergroup', 'id PK, name:s(255), sort:i=0, company_id?->company'],
['deliverysummary', 'id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0'],
['entity', 'id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)'],
['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'],
['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'],
@@ -357,16 +364,29 @@ const SCHEMA_TABLES = [
['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'],
['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'],
['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'],
['mcp_auth_code', 'code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt'],
['mcp_client', 'id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt'],
['mcp_token', 'id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt'],
['meta', 'key:s(255) PK, value:txt'],
['modelconversation', 'id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)=\'idle\', started_by?:i'],
['modelmessage', 'id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt'],
['modelvfs', 'id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)'],
['numbers', 'id PK, company_id->company, type:s(100), prefix:s(50)=\'\', padding:i=0, next:i=1, UQ(company_id, type)'],
['optionslist', 'id PK'],
['optionslistarticle', 'id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)'],
['ordersummary', 'id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0'],
['package', 'id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt'],
['payment', 'id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f'],
['pic', 'id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin'],
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'],
['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'],
['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'],
['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'],
['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
['quotesummary', 'id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0'],
['shipmentsummary', 'id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)=\'new\''],
['shipping', 'id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)'],
['subagentschedule', 'id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt'],
['tax', 'id PK, rate:d(18,4) UQ'],
['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'],
['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'],

View File

@@ -394,7 +394,7 @@ const CLUSTERS = [
{
id: 'ai',
label: 'AI & Models',
tables: ['modelconversation', 'modelmessage', 'modelvfs'],
tables: ['modelconversation', 'modelmessage', 'modelvfs', 'subagentschedule'],
},
{
id: 'attributes',

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 396 KiB

View File

@@ -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),

View File

@@ -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
`;

View File

@@ -1376,6 +1376,26 @@ BEGIN
END
`;
export const INIT_SUBAGENTSCHEDULE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'subagentschedule')
BEGIN
CREATE TABLE subagentschedule (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
[desc] NVARCHAR(MAX) NULL,
prompt NVARCHAR(MAX) NOT NULL,
skills NVARCHAR(MAX) NULL,
lastagentnote NVARCHAR(MAX) NULL,
max_itr INT NOT NULL CONSTRAINT DF_subagentschedule_max_itr DEFAULT 15,
cron NVARCHAR(100) NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_subagentschedule_created_at DEFAULT SYSUTCDATETIME(),
lastrun_at DATETIME2 NULL,
CONSTRAINT FK_subagentschedule_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE INDEX IX_subagentschedule_company ON subagentschedule (company_id);
END
`;
/** DDL scripts in dependency order for schema init. */
export const SCHEMA_SCRIPTS = [
INIT_META,
@@ -1450,4 +1470,5 @@ export const SCHEMA_SCRIPTS = [
INIT_MCP_CLIENT,
INIT_MCP_AUTH_CODE,
INIT_MCP_TOKEN,
INIT_SUBAGENTSCHEDULE,
];

View File

@@ -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;

View File

@@ -1011,6 +1011,23 @@ CREATE TABLE IF NOT EXISTS mcp_token (
);
`;
export const INIT_SUBAGENTSCHEDULE = `
CREATE TABLE IF NOT EXISTS subagentschedule (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL,
"desc" TEXT,
prompt TEXT NOT NULL,
skills TEXT,
lastagentnote TEXT,
max_itr INTEGER NOT NULL DEFAULT 15,
cron TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
lastrun_at TEXT,
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS IX_subagentschedule_company ON subagentschedule (company_id);
`;
/** DDL scripts in dependency order for SQLite schema initialization. */
export const SCHEMA_SCRIPTS = [
INIT_META,
@@ -1082,4 +1099,5 @@ export const SCHEMA_SCRIPTS = [
INIT_MCP_CLIENT,
INIT_MCP_AUTH_CODE,
INIT_MCP_TOKEN,
INIT_SUBAGENTSCHEDULE,
];

View File

@@ -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<string>} [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);
};
}

View File

@@ -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;
}

View File

@@ -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<void>}
* @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');
}
};
}

View File

@@ -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;
};
}

View File

@@ -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<Array<{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}>>}
*/
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);
};
}

View File

@@ -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<string>|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);
};
}