@tealbrick/avm 0.2.5 → 0.2.7
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/ROUTINES.md +15 -1
- package/bin/routine-host.mjs +39 -0
- package/dist/client/routines-eve.js +4 -3
- package/dist/client/routines-recovery.d.ts +9 -0
- package/dist/client/routines-recovery.js +23 -0
- package/dist/client/routines.d.ts +10 -0
- package/dist/client/routines.js +35 -4
- package/dist/client/wake.d.ts +2 -0
- package/dist/client/wake.js +3 -3
- package/dist/extension/_manifest.json +3 -3
- package/package.json +12 -4
package/ROUTINES.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Shared host routines (Eve 0.
|
|
1
|
+
# Shared host routines (Eve 0.66.0)
|
|
2
2
|
|
|
3
3
|
The AVM package owns the host store, typed client and opt-in Eve routine tool.
|
|
4
4
|
The host store is authoritative. Neither the desktop nor Eve edits a second
|
|
@@ -83,3 +83,17 @@ remove the corresponding old static schedule from discovery, then build/restart
|
|
|
83
83
|
once. Never leave both static and dynamic execution enabled for the same job.
|
|
84
84
|
Subsequent edits need no rebuild or restart. Desktop reads dynamic rows from
|
|
85
85
|
management discovery and hides the dispatcher implementation from its list.
|
|
86
|
+
|
|
87
|
+
## Deployment recovery and standing delegation (0.2.7)
|
|
88
|
+
|
|
89
|
+
Use `startRoutineRecovery` from `@tealbrick/avm/routines-recovery` in the host
|
|
90
|
+
service. It reconciles at startup and on a bounded timer without guest calls.
|
|
91
|
+
Keep its stop handle for shutdown. `reconcile` is an internal host-store action;
|
|
92
|
+
the example HTTP bridge does not expose it to the guest.
|
|
93
|
+
|
|
94
|
+
Optional `delegation()` is re-read for native app-auth CRUD calls. It binds the
|
|
95
|
+
exact scope, expiry, allowed operations, name prefix and wake permission.
|
|
96
|
+
`authorizeExecution()` independently gates each claim batch. The supplied host
|
|
97
|
+
service requires an external execution grant and fails closed without one.
|
|
98
|
+
`misfireGraceMs` controls the explicit late-start window. No backlog burst or
|
|
99
|
+
uncertain side-effect retry is added. See the root DEPLOYMENT-FIXES.md contract.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run only on the AVMM host. Bind its worker bridge IP, not a public interface.
|
|
3
|
+
import {createServer} from 'node:http';
|
|
4
|
+
import {execFile} from 'node:child_process';
|
|
5
|
+
import {promisify} from 'node:util';
|
|
6
|
+
import {timingSafeEqual} from 'node:crypto';
|
|
7
|
+
import {readFileSync,lstatSync} from 'node:fs';
|
|
8
|
+
import {startRoutineRecovery} from '@tealbrick/avm/routines-recovery';
|
|
9
|
+
import {createRoutineStore} from '@tealbrick/avm/routines';
|
|
10
|
+
import {createAvmmWakeAdapter} from '@tealbrick/avm/wake';
|
|
11
|
+
const configPath=process.env.TEAL_WAKE_CONFIG;if(!configPath)throw Error('routine_host_config_required');
|
|
12
|
+
const configStat=lstatSync(configPath);if(!configStat.isFile()||configStat.isSymbolicLink()||(configStat.mode&0o077))throw Error('routine_host_config_permissions');
|
|
13
|
+
const config=JSON.parse(readFileSync(process.env.TEAL_WAKE_CONFIG,'utf8'));
|
|
14
|
+
if(typeof config.token!=='string'||config.token.length<32||!config.routineScope||!config.routineStore||!config.executionGrantFile)throw Error('routine_host_enrollment_required');
|
|
15
|
+
const run=promisify(execFile);
|
|
16
|
+
const adapter=createAvmmWakeAdapter({allowed:true,timezone:'UTC',credentialsRefreshOnRequest:config.credentialsRefreshOnRequest===true,worker:config.worker,ownerKey:config.ownerKey,transport:{async run(argv,{signal}={}){try{const r=await run('/usr/local/bin/avm',argv,{signal,maxBuffer:1048576});return {exitCode:0,...r};}catch{return {exitCode:1,stdout:'',stderr:''};}}}});
|
|
17
|
+
const routines=config.routineScope?createRoutineStore({path:config.routineStore,scope:config.routineScope,misfireGraceMs:config.misfireGraceMs,wake:adapter,delegation:async()=>{try{return JSON.parse(readFileSync(config.delegationFile,'utf8'));}catch{return null;}},authorizeExecution:async()=>{try{const grant=JSON.parse(readFileSync(config.executionGrantFile,'utf8'));return grant.enabled===true&&grant.expiresAt>Date.now()&&Object.entries(config.routineScope).every(([key,value])=>grant.scope?.[key]===value);}catch{return false;}}}):null;
|
|
18
|
+
const recovery=routines?startRoutineRecovery(routines,{onError:()=>console.error('routine_wake_reconciliation_pending')}):null;
|
|
19
|
+
process.once('SIGTERM',()=>{recovery?.stop();server.close();});
|
|
20
|
+
const server=createServer(async(req,res)=>{
|
|
21
|
+
const expected=Buffer.from(`Bearer ${config.token}`), supplied=Buffer.from(req.headers.authorization??'');
|
|
22
|
+
if(req.socket.remoteAddress!==config.workerIp||supplied.length!==expected.length||!timingSafeEqual(supplied,expected)){res.writeHead(403).end();return;}
|
|
23
|
+
try{
|
|
24
|
+
let result;
|
|
25
|
+
if(req.method==='GET'&&req.url==='/status') result=await adapter.status();
|
|
26
|
+
else if(req.method==='POST'&&req.url==='/routines'&&routines){
|
|
27
|
+
let body='';for await(const chunk of req){body+=chunk;if(body.length>20000)throw Error('too_large');}
|
|
28
|
+
const {action,input}=JSON.parse(body);if(!['status','apply','claim','accepted','complete','failed','delegated-status','delegated-apply'].includes(action))throw Error('unsupported_operation');result=await routines.call(action,input);
|
|
29
|
+
}
|
|
30
|
+
else if(req.method==='POST'&&req.url==='/reconcile'){
|
|
31
|
+
if(routines)throw Error('shared_registry_owns_wake');
|
|
32
|
+
let body='';for await(const chunk of req){body+=chunk;if(body.length>20000)throw Error('too_large');}
|
|
33
|
+
const routines=JSON.parse(body);
|
|
34
|
+
if(!Array.isArray(routines)||routines.length>50||routines.some(r=>!r||typeof r.name!=='string'||!/^[a-z0-9][a-z0-9-]{0,47}$/.test(r.name)||typeof r.cron!=='string'))throw Error('invalid_routines');
|
|
35
|
+
await adapter.reconcile(routines);result={ok:true};
|
|
36
|
+
}else{res.writeHead(404).end();return;}
|
|
37
|
+
res.writeHead(200,{'content-type':'application/json'}).end(JSON.stringify(result));
|
|
38
|
+
}catch(error){const allowed=['revision_conflict','schedule_exists','schedule_not_found','wake_unavailable','unsupported_wake_cron','request_id_conflict'];res.writeHead(409,{'content-type':'application/json'}).end(JSON.stringify({ok:false,error:allowed.includes(error?.message)?error.message:'host_operation_failed'}));}
|
|
39
|
+
}).listen(config.port,config.bindIp);
|
|
@@ -6,14 +6,15 @@ const input = z.object({ operation: z.enum(['list', 'create', 'update', 'pause',
|
|
|
6
6
|
export function routineTool(options) {
|
|
7
7
|
return defineTool({ description: 'Manage your host-persisted routines shared with desktop. List first to get current revisions. Create needs a unique lowercase hyphenated name, five-field UTC cron and instruction. Update/pause/resume/remove require the revision returned by list. Wake is optional and off by default. Runs deliver into the owner’s desktop chat. requestId must be a stable UUID for each intended change; reuse it only for an identical retry. Do not claim execution from a saved routine.', inputSchema: input, async execute(args, ctx) {
|
|
8
8
|
const auth = ctx.session.auth.current;
|
|
9
|
-
|
|
9
|
+
const delegated = auth?.authenticator === 'app' && auth.principalType === 'runtime' && auth.principalId === 'eve:app';
|
|
10
|
+
if (!delegated && (auth?.authenticator !== 'tealbrick' || auth.principalType !== 'user' || auth.principalId !== options.scope.owner || auth.attributes.org !== options.scope.org || auth.attributes.agentGrantLevel !== 'manage'))
|
|
10
11
|
throw Error('routine_manage_permission_required');
|
|
11
12
|
if (args.operation === 'list')
|
|
12
|
-
return options.bridge.call('status');
|
|
13
|
+
return options.bridge.call(delegated ? 'delegated-status' : 'status');
|
|
13
14
|
if (!args.requestId)
|
|
14
15
|
throw Error('request_id_required');
|
|
15
16
|
const { operation, ...fields } = args;
|
|
16
|
-
return options.bridge.call('apply', { ...fields, operation: operation === 'create' ? 'upsert' : operation === 'remove' ? 'delete' : 'update', ...(operation === 'pause' ? { enabled: false } : operation === 'resume' ? { enabled: true } : {}) });
|
|
17
|
+
return options.bridge.call(delegated ? 'delegated-apply' : 'apply', { ...fields, operation: operation === 'create' ? 'upsert' : operation === 'remove' ? 'delete' : 'update', ...(operation === 'pause' ? { enabled: false } : operation === 'resume' ? { enabled: true } : {}) });
|
|
17
18
|
} });
|
|
18
19
|
}
|
|
19
20
|
/** Install once; all recurrence definitions are read from the host at execution time. */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RoutineBridge } from './routines.js';
|
|
2
|
+
/** Host lifecycle worker: recovery never depends on a running guest. */
|
|
3
|
+
export declare function startRoutineRecovery(bridge: RoutineBridge, options?: {
|
|
4
|
+
intervalMs?: number;
|
|
5
|
+
onError?: (error: unknown) => void;
|
|
6
|
+
}): {
|
|
7
|
+
ready: Promise<void>;
|
|
8
|
+
stop(): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Host lifecycle worker: recovery never depends on a running guest. */
|
|
2
|
+
export function startRoutineRecovery(bridge, options = {}) {
|
|
3
|
+
const interval = options.intervalMs ?? 15000;
|
|
4
|
+
if (!Number.isInteger(interval) || interval < 10 || interval > 300000)
|
|
5
|
+
throw Error('invalid_recovery_interval');
|
|
6
|
+
let stopped = false;
|
|
7
|
+
let timer;
|
|
8
|
+
const tick = async () => {
|
|
9
|
+
try {
|
|
10
|
+
await bridge.call('reconcile');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
options.onError?.(error);
|
|
14
|
+
}
|
|
15
|
+
if (!stopped) {
|
|
16
|
+
timer = setTimeout(tick, interval);
|
|
17
|
+
timer.unref();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const ready = tick();
|
|
21
|
+
return { ready, stop() { stopped = true; if (timer)
|
|
22
|
+
clearTimeout(timer); } };
|
|
23
|
+
}
|
|
@@ -31,6 +31,13 @@ export interface RoutineSnapshot {
|
|
|
31
31
|
wake: Record<string, unknown>;
|
|
32
32
|
dynamic: true;
|
|
33
33
|
}
|
|
34
|
+
export interface RoutineDelegation {
|
|
35
|
+
scope: RoutineScope;
|
|
36
|
+
expiresAt: number;
|
|
37
|
+
operations: string[];
|
|
38
|
+
namePrefix: string;
|
|
39
|
+
allowWake: boolean;
|
|
40
|
+
}
|
|
34
41
|
export interface RoutineBridge {
|
|
35
42
|
call(action: string, input?: Record<string, unknown>): Promise<any>;
|
|
36
43
|
}
|
|
@@ -40,6 +47,9 @@ export declare function createRoutineStore(options: {
|
|
|
40
47
|
path: string;
|
|
41
48
|
scope: RoutineScope;
|
|
42
49
|
now?: () => number;
|
|
50
|
+
misfireGraceMs?: number;
|
|
51
|
+
delegation?: () => Promise<RoutineDelegation | null>;
|
|
52
|
+
authorizeExecution?: () => Promise<boolean>;
|
|
43
53
|
wake?: {
|
|
44
54
|
status(): Promise<any>;
|
|
45
55
|
validate(rows: {
|
package/dist/client/routines.js
CHANGED
|
@@ -26,6 +26,9 @@ export function createRoutineStore(options) {
|
|
|
26
26
|
throw Error('scope_mismatch');
|
|
27
27
|
const save = (s) => db.prepare('UPDATE state SET json=? WHERE id=1').run(JSON.stringify(s));
|
|
28
28
|
const now = options.now ?? Date.now;
|
|
29
|
+
const grace = options.misfireGraceMs ?? 120000;
|
|
30
|
+
if (!Number.isInteger(grace) || grace < 0 || grace > 86400000)
|
|
31
|
+
throw Error('invalid_misfire_grace');
|
|
29
32
|
const tx = (fn) => { db.exec('BEGIN IMMEDIATE'); try {
|
|
30
33
|
const value = fn();
|
|
31
34
|
db.exec('COMMIT');
|
|
@@ -39,18 +42,44 @@ export function createRoutineStore(options) {
|
|
|
39
42
|
const writeRun = (r) => db.prepare('INSERT OR REPLACE INTO runs VALUES (?,?)').run(r.id, JSON.stringify(r));
|
|
40
43
|
const reconcile = async () => { const s = read(); if (!s.dirty)
|
|
41
44
|
return; if (!options.wake)
|
|
42
|
-
throw Error('wake_unavailable'); await options.wake.reconcile(s.routines.filter(r => !r.deleted && r.enabled && r.wake)); tx(() => { const current = read(); current.dirty =
|
|
45
|
+
throw Error('wake_unavailable'); await options.wake.reconcile(s.routines.filter(r => !r.deleted && r.enabled && r.wake)); tx(() => { const current = read(); current.dirty = JSON.stringify(current.routines) !== JSON.stringify(s.routines); save(current); }); };
|
|
43
46
|
let queue = Promise.resolve();
|
|
44
47
|
const call = async (action, input = {}) => {
|
|
48
|
+
if (action === 'reconcile') {
|
|
49
|
+
await reconcile();
|
|
50
|
+
return { ok: true, pending: read().dirty };
|
|
51
|
+
}
|
|
52
|
+
if (action === 'delegated-status' || action === 'delegated-apply') {
|
|
53
|
+
const grant = await options.delegation?.();
|
|
54
|
+
const operation = action === 'delegated-status' ? 'list' : String(input.operation);
|
|
55
|
+
if (!grant || !Object.entries(options.scope).every(([key, value]) => grant.scope?.[key] === value) || !Number.isFinite(grant.expiresAt) || grant.expiresAt <= now() || !grant.namePrefix || !grant.operations.includes(operation))
|
|
56
|
+
throw Error('routine_delegation_required');
|
|
57
|
+
if (action === 'delegated-apply') {
|
|
58
|
+
if (typeof input.name !== 'string' || !input.name.startsWith(grant.namePrefix))
|
|
59
|
+
throw Error('routine_delegation_scope');
|
|
60
|
+
const existing = read().routines.find(r => r.name === input.name);
|
|
61
|
+
if (!grant.allowWake && (input.wake === true || existing?.wake))
|
|
62
|
+
throw Error('routine_delegation_wake_denied');
|
|
63
|
+
}
|
|
64
|
+
if (action === 'delegated-status') {
|
|
65
|
+
const result = await call('status');
|
|
66
|
+
return { ...result, routines: result.routines.filter((r) => r.name.startsWith(grant.namePrefix)), runs: result.runs.filter((r) => r.name.startsWith(grant.namePrefix)) };
|
|
67
|
+
}
|
|
68
|
+
action = 'apply';
|
|
69
|
+
}
|
|
45
70
|
if (action === 'status')
|
|
46
|
-
return { scope: options.scope, dynamic: true, routines: read().routines.filter(r => !r.deleted), runs: runs(), wake: { ...(await options.wake?.status() ?? { supported: false }), pending: read().dirty, managed: read().routines.filter(r => !r.deleted).map(r => r.name), routines: read().routines.filter(r => !r.deleted && r.wake).map(r => r.name) } };
|
|
71
|
+
return { executionGranted: options.authorizeExecution ? await options.authorizeExecution() : null, scope: options.scope, dynamic: true, routines: read().routines.filter(r => !r.deleted), runs: runs(), wake: { ...(await options.wake?.status() ?? { supported: false }), pending: read().dirty, managed: read().routines.filter(r => !r.deleted).map(r => r.name), routines: read().routines.filter(r => !r.deleted && r.wake).map(r => r.name) } };
|
|
47
72
|
if (action === 'apply') {
|
|
48
73
|
const patch = change.parse(input), digest = createHash('sha256').update(JSON.stringify(patch)).digest('hex');
|
|
49
74
|
const old = db.prepare('SELECT digest,json FROM receipts WHERE id=?').get(patch.requestId);
|
|
50
75
|
if (old) {
|
|
51
76
|
if (old.digest !== digest)
|
|
52
77
|
throw Error('request_id_conflict');
|
|
53
|
-
|
|
78
|
+
try {
|
|
79
|
+
await reconcile();
|
|
80
|
+
}
|
|
81
|
+
catch { /* Report current persisted state on retries. */ }
|
|
82
|
+
return { ...JSON.parse(old.json), wakePending: read().dirty };
|
|
54
83
|
}
|
|
55
84
|
const before = read(), prior = before.routines.find(r => r.name === patch.name);
|
|
56
85
|
if (patch.operation === 'upsert' && prior && !prior.deleted)
|
|
@@ -80,6 +109,8 @@ export function createRoutineStore(options) {
|
|
|
80
109
|
return { ok: true, dynamic: true, saved: true, routine: row };
|
|
81
110
|
}
|
|
82
111
|
if (action === 'claim') {
|
|
112
|
+
if (options.authorizeExecution && !await options.authorizeExecution())
|
|
113
|
+
return { jobs: [], blocked: 'routine_execution_not_granted' };
|
|
83
114
|
try {
|
|
84
115
|
await reconcile();
|
|
85
116
|
}
|
|
@@ -100,7 +131,7 @@ export function createRoutineStore(options) {
|
|
|
100
131
|
const due = row.nextRunAt;
|
|
101
132
|
row.nextRunAt = nextRoutineTime(row.cron, at);
|
|
102
133
|
const id = createHash('sha256').update(JSON.stringify([s.scope, row.name, row.revision, due])).digest('hex');
|
|
103
|
-
const run = { id, name: row.name, revision: row.revision, dueAt: due, status: at - due >
|
|
134
|
+
const run = { id, name: row.name, revision: row.revision, dueAt: due, status: at - due > grace ? 'missed' : 'claimed', updatedAt: at };
|
|
104
135
|
if (!db.prepare('SELECT id FROM runs WHERE id=?').get(id)) {
|
|
105
136
|
writeRun(run);
|
|
106
137
|
if (run.status === 'claimed')
|
package/dist/client/wake.d.ts
CHANGED
|
@@ -21,10 +21,12 @@ export declare function createAvmmWakeAdapter(options: {
|
|
|
21
21
|
status(): Promise<{
|
|
22
22
|
supported: boolean;
|
|
23
23
|
reason: string;
|
|
24
|
+
wakeRequired?: undefined;
|
|
24
25
|
provider?: undefined;
|
|
25
26
|
timezone?: undefined;
|
|
26
27
|
} | {
|
|
27
28
|
supported: boolean;
|
|
29
|
+
wakeRequired: boolean;
|
|
28
30
|
provider: string;
|
|
29
31
|
timezone: string;
|
|
30
32
|
reason: string;
|
package/dist/client/wake.js
CHANGED
|
@@ -58,13 +58,13 @@ export function createAvmmWakeAdapter(options) {
|
|
|
58
58
|
if (!Array.isArray(rows))
|
|
59
59
|
throw Error();
|
|
60
60
|
const worker = await run(['worker', 'status', options.worker]);
|
|
61
|
-
if (worker.mode !== 'durable' || !Number.isFinite(worker.idle_timeout_seconds) || worker.idle_timeout_seconds <= lead)
|
|
61
|
+
if (worker.mode !== 'durable' || !Number.isFinite(worker.idle_timeout_seconds) || (worker.idle_timeout_seconds !== 0 && worker.idle_timeout_seconds <= lead))
|
|
62
62
|
throw Error();
|
|
63
63
|
if (worker.auth?.codex === 'shared' && (!options.credentialsRefreshOnRequest || !worker.capabilities?.includes('model:codex-subscription')))
|
|
64
64
|
throw Error();
|
|
65
65
|
if (!Array.isArray(worker.capabilities) || !worker.capabilities.includes(`harness:${options.harness ?? 'eve'}`))
|
|
66
66
|
throw Error();
|
|
67
|
-
return { supported: true, provider: 'avmm', timezone: 'UTC', reason: 'Minute/hour intervals, daily, weekdays and weekly routines. Host wakes only; Eve executes.' };
|
|
67
|
+
return { supported: true, wakeRequired: worker.idle_timeout_seconds !== 0, provider: 'avmm', timezone: 'UTC', reason: 'Minute/hour intervals, daily, weekdays and weekly routines. Host wakes only; Eve executes.' };
|
|
68
68
|
}
|
|
69
69
|
catch {
|
|
70
70
|
return { supported: false, reason: 'Host scheduler, worker capability or idle timeout could not be verified.' };
|
|
@@ -80,7 +80,7 @@ export function createAvmmWakeAdapter(options) {
|
|
|
80
80
|
const existing = await run(['schedule', 'ls']);
|
|
81
81
|
const worker = await run(['worker', 'status', options.worker]);
|
|
82
82
|
const authCaps = worker.auth?.codex === 'shared' ? ['--cap', 'model:codex-subscription'] : [];
|
|
83
|
-
for (const routine of routines) {
|
|
83
|
+
for (const routine of worker.idle_timeout_seconds === 0 ? [] : routines) {
|
|
84
84
|
const key = createHash('sha256').update(routine.name).digest('hex').slice(0, 16);
|
|
85
85
|
const plans = planWake(routine.cron, Date.now(), lead);
|
|
86
86
|
for (const [i, p] of plans.entries()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tealbrick/avm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Typed host-owned AgentVM transport and opt-in Eve capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"README.md",
|
|
10
10
|
"PROVENANCE.md",
|
|
11
11
|
"LICENSE",
|
|
12
|
-
"ROUTINES.md"
|
|
12
|
+
"ROUTINES.md",
|
|
13
|
+
"bin"
|
|
13
14
|
],
|
|
14
15
|
"engines": {
|
|
15
16
|
"node": ">=24"
|
|
@@ -46,6 +47,10 @@
|
|
|
46
47
|
"./routines-eve": {
|
|
47
48
|
"types": "./dist/client/routines-eve.d.ts",
|
|
48
49
|
"import": "./dist/client/routines-eve.js"
|
|
50
|
+
},
|
|
51
|
+
"./routines-recovery": {
|
|
52
|
+
"types": "./dist/client/routines-recovery.d.ts",
|
|
53
|
+
"import": "./dist/client/routines-recovery.js"
|
|
49
54
|
}
|
|
50
55
|
},
|
|
51
56
|
"eve": {
|
|
@@ -65,11 +70,11 @@
|
|
|
65
70
|
},
|
|
66
71
|
"devDependencies": {
|
|
67
72
|
"@types/node": "^24.0.0",
|
|
68
|
-
"eve": "0.
|
|
73
|
+
"eve": "0.66.0",
|
|
69
74
|
"typescript": "^5.9.3"
|
|
70
75
|
},
|
|
71
76
|
"peerDependencies": {
|
|
72
|
-
"eve": ">=0.
|
|
77
|
+
"eve": ">=0.66.0 <0.67.0"
|
|
73
78
|
},
|
|
74
79
|
"peerDependenciesMeta": {
|
|
75
80
|
"eve": {
|
|
@@ -79,5 +84,8 @@
|
|
|
79
84
|
"publishConfig": {
|
|
80
85
|
"access": "public",
|
|
81
86
|
"registry": "https://registry.npmjs.org"
|
|
87
|
+
},
|
|
88
|
+
"bin": {
|
|
89
|
+
"tealbrick-routine-host": "bin/routine-host.mjs"
|
|
82
90
|
}
|
|
83
91
|
}
|