@pmate/merry-scheduler 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/index.mjs +54 -0
- package/manifest.json +113 -0
- package/mcp.mjs +18 -0
- package/package.json +18 -4
- package/tools.mjs +23 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Scheduler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Install @pmate/merry-scheduler and run merry-scheduler --help. Operators use pmate:factory authentication for owned Agent management. New tasks require an original authorized schedule request: an immutable command input reference or a Merry message reference read with a pmate:merry session. Agents use their own exact pmate:merry session and a source reference. No active Runtime Run is required. Source proof is verified by the server; user-provided transcripts are not accepted. Each job remains bound to its owning Agent and original requester conversation. API default https://issue-api.pmate.chat; PMATE_SCHEDULER_API_URL, PMATE_SCHEDULER_NAMESPACE, PMATE_SCHEDULER_TOPIC and PMATE_SCHEDULER_APP configure the independent client. Content JSON accepts prompt and timing only. List/get are read-only. Create/update/pause/resume/cancel use stable idempotency keys; updates also require expectedRevision. Conflict means read current state and reassess. Cancellation deactivates a job and retains history. Due jobs run as normal Runtime inputs on the owning Agent, without Master Agent dispatch.
|
|
4
|
+
|
|
5
|
+
Run `merry-scheduler --help` for arguments. MCP: `node <plugin-directory>/mcp.mjs`.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {readFileSync,statSync,realpathSync} from 'node:fs'
|
|
3
|
+
import {execFileSync} from 'node:child_process'
|
|
4
|
+
import {pathToFileURL} from 'node:url'
|
|
5
|
+
export const HELP=`Usage: merry-scheduler list --agent ID [--source JSON]
|
|
6
|
+
merry-scheduler get --agent ID --id ID [--source JSON]
|
|
7
|
+
merry-scheduler create --agent ID --file JSON --idempotency-key KEY [--source JSON]
|
|
8
|
+
merry-scheduler update --agent ID --id ID --expected-revision N --file JSON --idempotency-key KEY [--source JSON]
|
|
9
|
+
merry-scheduler pause|resume|cancel --agent ID --id ID --expected-revision N --idempotency-key KEY [--source JSON]
|
|
10
|
+
Content files contain prompt and/or timing. Source files contain verifiable conversation references.
|
|
11
|
+
The target defaults to PMATE_SCHEDULER_AGENT_ID or PCS_AGENT_ID.
|
|
12
|
+
No Runtime is required; uses an exact-app pmate CLI session and the Scheduler API.`
|
|
13
|
+
function jsonFile(file){if(statSync(file).size>128000)throw Error('Scheduler file is too large');const text=readFileSync(file,'utf8');if(Buffer.byteLength(text)>128000)throw Error('Scheduler file is too large');return JSON.parse(text)}
|
|
14
|
+
export function parse(args,env={}){
|
|
15
|
+
if(args.length===1&&['help','--help'].includes(args[0]))return{help:true}
|
|
16
|
+
const operation=args[0],fields={list:['agent','source'],get:['agent','id','source'],create:['agent','file','idempotency-key','source'],update:['agent','id','expected-revision','file','idempotency-key','source'],pause:['agent','id','expected-revision','idempotency-key','source'],resume:['agent','id','expected-revision','idempotency-key','source'],cancel:['agent','id','expected-revision','idempotency-key','source']}[operation]
|
|
17
|
+
if(!fields)throw Error(HELP)
|
|
18
|
+
const values={}
|
|
19
|
+
for(let i=1;i<args.length;i+=2){const key=args[i]?.slice(2),value=args[i+1];if(!args[i]?.startsWith('--')||!fields.includes(key)||Object.hasOwn(values,key)||!value?.trim())throw Error(HELP);values[key]=value}
|
|
20
|
+
const agent=values.agent??env.PMATE_SCHEDULER_AGENT_ID??env.PCS_AGENT_ID
|
|
21
|
+
if(!/^0x[a-f0-9]{64}$/.test(agent??''))throw Error('Explicit Agent Profile ID required (--agent)')
|
|
22
|
+
const body={}
|
|
23
|
+
for(const key of ['id','idempotency-key'])if(values[key])body[key==='id'?'id':'idempotencyKey']=values[key]
|
|
24
|
+
if(values['expected-revision']){if(!/^[1-9][0-9]*$/.test(values['expected-revision'])||!Number.isSafeInteger(Number(values['expected-revision'])))throw Error('Positive revision required');body.expectedRevision=Number(values['expected-revision'])}
|
|
25
|
+
if(operation!=='list'&&operation!=='create'&&!body.id||!['list','get'].includes(operation)&&!body.idempotencyKey||['update','pause','resume','cancel'].includes(operation)&&!body.expectedRevision)throw Error(HELP)
|
|
26
|
+
if(['create','update'].includes(operation)){
|
|
27
|
+
if(!values.file)throw Error(HELP)
|
|
28
|
+
const patch=jsonFile(values.file)
|
|
29
|
+
if(!patch||typeof patch!=='object'||Array.isArray(patch)||!Object.keys(patch).length||Object.keys(patch).some(key=>!['prompt','timing'].includes(key)))throw Error('Scheduler content accepts only prompt and timing')
|
|
30
|
+
Object.assign(body,patch)
|
|
31
|
+
}
|
|
32
|
+
if(values.source)body.source=jsonFile(values.source)
|
|
33
|
+
else if(env.MERRY_RUNTIME_INPUT_ID)body.source={kind:'command',inputId:env.MERRY_RUNTIME_INPUT_ID}
|
|
34
|
+
if(operation==='create'&&!body.source)throw Error('An authorized source is required (--source JSON)')
|
|
35
|
+
return{agent,operation,body}
|
|
36
|
+
}
|
|
37
|
+
export function readSession(app,env){try{return JSON.parse(execFileSync('pmate',['session','read','--app',app],{env,encoding:'utf8',timeout:15000,maxBuffer:32768,stdio:['ignore','pipe','pipe']}))}catch{throw Error(`Unable to read session; run pmate login --app ${app}`)}}
|
|
38
|
+
export async function scheduler(args,{env=process.env,fetcher=fetch,session=readSession}={}){
|
|
39
|
+
const parsed=parse(args,env);if(parsed.help)return HELP
|
|
40
|
+
const {agent,operation,body}=parsed,app=env.PMATE_SCHEDULER_APP??(env.PCS_AGENT_ID?'pmate:merry':'pmate:factory')
|
|
41
|
+
const identity=await session(app,env)
|
|
42
|
+
if(!identity?.token||!identity.accountId||!identity.profileId||!Number.isFinite(Date.parse(identity.expiresAt))||Date.parse(identity.expiresAt)<=Date.now())throw Error(`Session missing or expired; run pmate login --app ${app}`)
|
|
43
|
+
if(env.PCS_AGENT_ID&&identity.profileId!==env.PCS_AGENT_ID)throw Error('Scheduler session does not match the executing Agent')
|
|
44
|
+
const base=new URL(env.PMATE_SCHEDULER_API_URL??env.PCS_ISSUES_API_URL??'https://issue-api.pmate.chat')
|
|
45
|
+
if(base.protocol!=='https:'&&!(base.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(base.hostname)))throw Error('Scheduler API requires HTTPS or loopback')
|
|
46
|
+
const namespace=env.PMATE_SCHEDULER_NAMESPACE??'pcs-agent',topic=env.PMATE_SCHEDULER_TOPIC??'merry'
|
|
47
|
+
const url=new URL(`/v2/topics/${encodeURIComponent(namespace)}/${encodeURIComponent(topic)}/agents/${encodeURIComponent(agent)}/schedules/${operation}`,base)
|
|
48
|
+
const response=await fetcher(url,{method:'POST',headers:{authorization:`Bearer ${identity.token}`,'x-pmate-app':app,'content-type':'application/json',...(env.PCS_AGENT_ID?{'x-pcs-agent-id':env.PCS_AGENT_ID}:{})},body:JSON.stringify(body),signal:AbortSignal.timeout(30000),redirect:'error'})
|
|
49
|
+
let result;try{result=await response.json()}catch{throw Error(`Scheduler API returned invalid JSON (${response.status}); mutation outcome may be uncertain`)}
|
|
50
|
+
if(!response.ok||!result.success)throw Object.assign(Error(result.message??`Scheduler operation failed (${response.status})`),{httpCode:response.status})
|
|
51
|
+
return result.data
|
|
52
|
+
}
|
|
53
|
+
export default async function invoke(_host,args){return scheduler(args)}
|
|
54
|
+
if(process.argv[1]&&import.meta.url===pathToFileURL(realpathSync(process.argv[1])).href){try{const result=await scheduler(process.argv.slice(2));console.log(typeof result==='string'?result:JSON.stringify(result))}catch(error){console.error(error.message);process.exitCode=1}}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "scheduler",
|
|
4
|
+
"kind": "plugin",
|
|
5
|
+
"name": "Scheduler",
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "Independent authenticated Agent schedule management; no Runtime required",
|
|
8
|
+
"docs": [
|
|
9
|
+
{
|
|
10
|
+
"name": "host-instructions",
|
|
11
|
+
"content": "Scheduler is an independent plugin. Read scheduler help before CRUD. Use the exact authenticated identity, a verified source reference, current revisions and stable idempotency keys. Runtime is only needed later to execute due tasks in their owning Agent environment."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "scheduler",
|
|
15
|
+
"content": "Install @pmate/merry-scheduler and run merry-scheduler --help. Operators use pmate:factory authentication for owned Agent management. New tasks require an original authorized schedule request: an immutable command input reference or a Merry message reference read with a pmate:merry session. Agents use their own exact pmate:merry session and a source reference. No active Runtime Run is required. Source proof is verified by the server; user-provided transcripts are not accepted. Each job remains bound to its owning Agent and original requester conversation. API default https://issue-api.pmate.chat; PMATE_SCHEDULER_API_URL, PMATE_SCHEDULER_NAMESPACE, PMATE_SCHEDULER_TOPIC and PMATE_SCHEDULER_APP configure the independent client. Content JSON accepts prompt and timing only. List/get are read-only. Create/update/pause/resume/cancel use stable idempotency keys; updates also require expectedRevision. Conflict means read current state and reassess. Cancellation deactivates a job and retains history. Due jobs run as normal Runtime inputs on the owning Agent, without Master Agent dispatch."
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"configSchema": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {},
|
|
21
|
+
"additionalProperties": false
|
|
22
|
+
},
|
|
23
|
+
"executables": [
|
|
24
|
+
{
|
|
25
|
+
"name": "scheduler",
|
|
26
|
+
"entry": "./index.mjs"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "mcp",
|
|
30
|
+
"entry": "./mcp.mjs"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"services": [],
|
|
34
|
+
"defaultForAgents": true,
|
|
35
|
+
"commands": [
|
|
36
|
+
{
|
|
37
|
+
"name": "schedule",
|
|
38
|
+
"description": "管理我的定时任务",
|
|
39
|
+
"usage": "/schedule <任务安排或修改要求>",
|
|
40
|
+
"arguments": {
|
|
41
|
+
"type": "text",
|
|
42
|
+
"required": true,
|
|
43
|
+
"placeholder": "描述任务内容、时间或需要修改的计划"
|
|
44
|
+
},
|
|
45
|
+
"execution": {
|
|
46
|
+
"type": "agent",
|
|
47
|
+
"executor": "target-agent",
|
|
48
|
+
"inference": {
|
|
49
|
+
"mode": "explicit",
|
|
50
|
+
"engine": "codex",
|
|
51
|
+
"model": "gpt-6.1-sol",
|
|
52
|
+
"reasoningEffort": "medium"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"subcommands": [
|
|
56
|
+
{
|
|
57
|
+
"name": "show",
|
|
58
|
+
"description": "查看我的定时任务",
|
|
59
|
+
"usage": "/schedule show [查看范围]",
|
|
60
|
+
"arguments": {
|
|
61
|
+
"type": "text",
|
|
62
|
+
"required": false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "create",
|
|
67
|
+
"description": "创建定时任务",
|
|
68
|
+
"usage": "/schedule create <任务要求>",
|
|
69
|
+
"arguments": {
|
|
70
|
+
"type": "text",
|
|
71
|
+
"required": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "update",
|
|
76
|
+
"description": "修改定时任务",
|
|
77
|
+
"usage": "/schedule update <任务要求>",
|
|
78
|
+
"arguments": {
|
|
79
|
+
"type": "text",
|
|
80
|
+
"required": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "pause",
|
|
85
|
+
"description": "暂停定时任务",
|
|
86
|
+
"usage": "/schedule pause <任务要求>",
|
|
87
|
+
"arguments": {
|
|
88
|
+
"type": "text",
|
|
89
|
+
"required": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "resume",
|
|
94
|
+
"description": "恢复定时任务",
|
|
95
|
+
"usage": "/schedule resume <任务要求>",
|
|
96
|
+
"arguments": {
|
|
97
|
+
"type": "text",
|
|
98
|
+
"required": true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "cancel",
|
|
103
|
+
"description": "取消定时任务",
|
|
104
|
+
"usage": "/schedule cancel <任务要求>",
|
|
105
|
+
"arguments": {
|
|
106
|
+
"type": "text",
|
|
107
|
+
"required": true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
package/mcp.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {schedulerTools,callSchedulerTool} from './tools.mjs'
|
|
3
|
+
let buffer=''
|
|
4
|
+
const decoder=new TextDecoder()
|
|
5
|
+
for await(const chunk of process.stdin){
|
|
6
|
+
buffer+=decoder.decode(chunk,{stream:true})
|
|
7
|
+
for(;;){const end=buffer.indexOf('\n');if(end<0)break;const line=buffer.slice(0,end);buffer=buffer.slice(end+1);if(!line.trim())continue
|
|
8
|
+
let request
|
|
9
|
+
try{request=JSON.parse(line);if(request.id===undefined)continue;let result
|
|
10
|
+
if(request.method==='initialize')result={protocolVersion:'2024-11-05',capabilities:{tools:{}},serverInfo:{name:'scheduler',version:'1'}}
|
|
11
|
+
else if(request.method==='tools/list')result={tools:schedulerTools}
|
|
12
|
+
else if(request.method==='tools/call'){try{result={content:[{type:'text',text:JSON.stringify(await callSchedulerTool(request.params.name,request.params.arguments??{}))}]}}catch(error){const status=error.httpCode;result={isError:true,content:[{type:'text',text:status===409?'Scheduler conflict: read the current entry before reassessing; a reused key must keep the identical payload.':status===403?'Scheduler operation is not authorized for this request.':status===404?'Scheduler entry was not found.':status===400?'Invalid scheduler arguments. Read scheduler_help.':'Scheduler operation failed or its outcome is uncertain. Retry only the same operation with the same idempotency key.'}]}}}
|
|
13
|
+
else if(request.method==='ping')result={}
|
|
14
|
+
else{process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,error:{code:-32601,message:'Method not found'}})+'\n');continue}
|
|
15
|
+
process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,result})+'\n')
|
|
16
|
+
}catch{process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request?.id??null,error:{code:-32603,message:'Invalid scheduler protocol request'}})+'\n')}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmate/merry-scheduler",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Independent authenticated Agent schedule management; no Runtime required",
|
|
6
|
+
"bin": {
|
|
7
|
+
"merry-scheduler": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"mcp.mjs",
|
|
12
|
+
"tools.mjs",
|
|
13
|
+
"manifest.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"license": "UNLICENSED"
|
|
20
|
+
}
|
package/tools.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {mkdtempSync,writeFileSync,rmSync} from 'node:fs'
|
|
2
|
+
import {join} from 'node:path'
|
|
3
|
+
import {tmpdir} from 'node:os'
|
|
4
|
+
import {scheduler,HELP} from './index.mjs'
|
|
5
|
+
const text={type:'string'},source={type:'object',description:'Verified command or original Merry message reference; never a transcript'},timing={type:'object',description:'Validated once, interval or calendar timing'}
|
|
6
|
+
const properties={agentId:text,id:text,expectedRevision:{type:'integer',minimum:1},prompt:text,timing,source,idempotencyKey:text}
|
|
7
|
+
const definitions=[['help',[],[]],['list',['agentId','source'],[]],['get',['agentId','id','source'],['id']],['create',['agentId','prompt','timing','source','idempotencyKey'],['prompt','timing','idempotencyKey']],['update',['agentId','id','expectedRevision','prompt','timing','source','idempotencyKey'],['id','expectedRevision','idempotencyKey']],...['pause','resume','cancel'].map(op=>[op,['agentId','id','expectedRevision','source','idempotencyKey'],['id','expectedRevision','idempotencyKey']])]
|
|
8
|
+
export const schedulerTools=definitions.map(([operation,fields,required])=>({name:`scheduler_${operation}`,description:operation==='help'?'Read the independent Scheduler plugin help':`Manage authorized Agent schedules: ${operation}`,inputSchema:{type:'object',properties:Object.fromEntries(fields.map(k=>[k,properties[k]])),required,additionalProperties:false}}))
|
|
9
|
+
export async function callSchedulerTool(name,value,ports={}){
|
|
10
|
+
const tool=schedulerTools.find(t=>t.name===name)
|
|
11
|
+
if(!tool||!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).some(k=>!Object.hasOwn(tool.inputSchema.properties,k))||tool.inputSchema.required.some(k=>value[k]===undefined))throw Error('Invalid scheduler arguments')
|
|
12
|
+
if(name==='scheduler_help')return HELP
|
|
13
|
+
const args=[name.slice(10)],flags={agentId:'agent',expectedRevision:'expected-revision',idempotencyKey:'idempotency-key'}
|
|
14
|
+
for(const [key,v]of Object.entries(value))if(!['prompt','timing','source'].includes(key))args.push(`--${flags[key]??key}`,String(v))
|
|
15
|
+
let directory
|
|
16
|
+
try{
|
|
17
|
+
const files=[]
|
|
18
|
+
if(value.source!==undefined)files.push(['source',value.source])
|
|
19
|
+
if(['scheduler_create','scheduler_update'].includes(name))files.push(['file',Object.fromEntries(Object.entries(value).filter(([k])=>['prompt','timing'].includes(k)))])
|
|
20
|
+
if(files.length){directory=mkdtempSync(join(tmpdir(),'merry-scheduler-'));for(const [flag,body]of files){const file=join(directory,`${flag}.json`);writeFileSync(file,JSON.stringify(body),{mode:0o600});args.push(`--${flag}`,file)}}
|
|
21
|
+
return await scheduler(args,ports)
|
|
22
|
+
}finally{if(directory)rmSync(directory,{recursive:true,force:true})}
|
|
23
|
+
}
|