ftown-bridge 0.19.8 → 0.19.9

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/pi-extension/ftown.js +119 -117
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftown-bridge",
3
- "version": "0.19.8",
3
+ "version": "0.19.9",
4
4
  "description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -211,7 +211,36 @@ export function registerFtownPiExtension(pi, options = {}) {
211
211
  throw new Error(`Session not found: ${normalized}`);
212
212
  }
213
213
 
214
+ function hasOwn(value, key) {
215
+ return value !== null && typeof value === 'object'
216
+ && Object.prototype.hasOwnProperty.call(value, key);
217
+ }
218
+
219
+ function requireString(params, field, label = field) {
220
+ const value = params?.[field];
221
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${label} is required`);
222
+ }
223
+
224
+ function requireProperty(params, field, label = field) {
225
+ if (!hasOwn(params, field)) throw new Error(`${label} is required`);
226
+ }
227
+
228
+ function requireOperation(params, allowed) {
229
+ if (!allowed.includes(params?.operation)) {
230
+ throw new Error(`operation must be one of: ${allowed.join(', ')}`);
231
+ }
232
+ }
233
+
234
+ function validateMailParams(params) {
235
+ requireOperation(params, ['send', 'read']);
236
+ if (params.operation === 'send') {
237
+ requireString(params, 'target');
238
+ requireString(params, 'body');
239
+ }
240
+ }
241
+
214
242
  async function runMail(params) {
243
+ validateMailParams(params);
215
244
  if (params.operation === 'read') {
216
245
  if (!ftownSessionId) throw new Error('FTOWN_SESSION_ID is unavailable');
217
246
  const query = new URLSearchParams({ wait: '0' });
@@ -261,31 +290,19 @@ export function registerFtownPiExtension(pi, options = {}) {
261
290
  label: 'ftown mail',
262
291
  description: 'Send durable mail to another ftown session, or read this session inbox.',
263
292
  parameters: {
264
- anyOf: [
265
- {
266
- type: 'object',
267
- properties: {
268
- operation: { const: 'send' },
269
- target: { type: 'string', description: 'Session id/name, or "parent".' },
270
- body: { type: 'string', minLength: 1 },
271
- type: { enum: ['message', 'task', 'result', 'escalation'] },
272
- threadId: { type: 'string' },
273
- },
274
- required: ['operation', 'target', 'body'],
275
- additionalProperties: false,
276
- },
277
- {
278
- type: 'object',
279
- properties: {
280
- operation: { const: 'read' },
281
- peek: { type: 'boolean', description: 'Do not mark messages delivered.' },
282
- all: { type: 'boolean', description: 'Include already-delivered messages.' },
283
- limit: { type: 'integer', minimum: 1, maximum: 100 },
284
- },
285
- required: ['operation'],
286
- additionalProperties: false,
287
- },
288
- ],
293
+ type: 'object',
294
+ properties: {
295
+ operation: { type: 'string', enum: ['send', 'read'] },
296
+ target: { type: 'string', description: 'Session id/name, or "parent".' },
297
+ body: { type: 'string', minLength: 1 },
298
+ type: { type: 'string', enum: ['message', 'task', 'result', 'escalation'] },
299
+ threadId: { type: 'string' },
300
+ peek: { type: 'boolean', description: 'Do not mark messages delivered.' },
301
+ all: { type: 'boolean', description: 'Include already-delivered messages.' },
302
+ limit: { type: 'integer', minimum: 1, maximum: 100 },
303
+ },
304
+ required: ['operation'],
305
+ additionalProperties: false,
289
306
  },
290
307
  async execute(toolCallId, params) {
291
308
  try {
@@ -322,6 +339,9 @@ export function registerFtownPiExtension(pi, options = {}) {
322
339
  });
323
340
 
324
341
  async function runSessions(params) {
342
+ requireOperation(params, ['list', 'archive', 'get', 'usage', 'running', 'screen', 'grep']);
343
+ if (!['list', 'archive'].includes(params.operation)) requireString(params, 'target');
344
+ if (params.operation === 'grep') requireString(params, 'pattern');
325
345
  if (params.operation === 'archive') {
326
346
  return requestJson('/api/archive', { method: 'GET' });
327
347
  }
@@ -369,30 +389,19 @@ export function registerFtownPiExtension(pi, options = {}) {
369
389
  label: 'ftown sessions',
370
390
  description: 'List or inspect ftown sessions, running state, archive, token usage, terminal screen, and terminal log matches.',
371
391
  parameters: {
372
- anyOf: [
373
- {
374
- type: 'object', properties: { operation: { enum: ['list', 'archive'] } },
375
- required: ['operation'], additionalProperties: false,
376
- },
377
- ...['get', 'usage', 'running'].map((operation) => ({
378
- type: 'object', properties: { operation: { const: operation }, target: targetProperty },
379
- required: ['operation', 'target'], additionalProperties: false,
380
- })),
381
- {
382
- type: 'object',
383
- properties: { operation: { const: 'screen' }, target: targetProperty, ...pageProperties },
384
- required: ['operation', 'target'], additionalProperties: false,
385
- },
386
- {
387
- type: 'object',
388
- properties: {
389
- operation: { const: 'grep' }, target: targetProperty,
390
- pattern: { type: 'string', minLength: 1 }, ...pageProperties,
391
- context: { type: 'integer', minimum: 0, maximum: 10 },
392
- },
393
- required: ['operation', 'target', 'pattern'], additionalProperties: false,
392
+ type: 'object',
393
+ properties: {
394
+ operation: {
395
+ type: 'string',
396
+ enum: ['list', 'archive', 'get', 'usage', 'running', 'screen', 'grep'],
394
397
  },
395
- ],
398
+ target: targetProperty,
399
+ pattern: { type: 'string', minLength: 1 },
400
+ ...pageProperties,
401
+ context: { type: 'integer', minimum: 0, maximum: 10 },
402
+ },
403
+ required: ['operation'],
404
+ additionalProperties: false,
396
405
  },
397
406
  async execute(_toolCallId, params) {
398
407
  try {
@@ -422,6 +431,7 @@ export function registerFtownPiExtension(pi, options = {}) {
422
431
  type: 'object',
423
432
  properties: {
424
433
  shell: {
434
+ type: 'string',
425
435
  enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell', 'zai', 'kimi', 'deepseek', 'fireworks'],
426
436
  },
427
437
  prompt: { type: 'string', minLength: 1 },
@@ -467,6 +477,10 @@ export function registerFtownPiExtension(pi, options = {}) {
467
477
  });
468
478
 
469
479
  async function runSessionManage(params) {
480
+ requireOperation(params, ['stop', 'remove', 'revive', 'rename', 'reparent']);
481
+ requireString(params, 'target');
482
+ if (params.operation === 'rename') requireString(params, 'name');
483
+ if (params.operation === 'reparent') requireProperty(params, 'parent');
470
484
  if (params.operation === 'revive') {
471
485
  const payload = await requestJson('/api/archive', { method: 'GET' });
472
486
  const archived = Array.isArray(payload?.archived) ? payload.archived : [];
@@ -517,28 +531,17 @@ export function registerFtownPiExtension(pi, options = {}) {
517
531
  label: 'manage ftown session',
518
532
  description: 'Stop, rename, reparent, remove, or revive an ftown session.',
519
533
  parameters: {
520
- anyOf: [
521
- ...['stop', 'remove', 'revive'].map((operation) => ({
522
- type: 'object', properties: { operation: { const: operation }, target: targetProperty },
523
- required: ['operation', 'target'], additionalProperties: false,
524
- })),
525
- {
526
- type: 'object',
527
- properties: {
528
- operation: { const: 'rename' }, target: targetProperty,
529
- name: { type: 'string', minLength: 1 },
530
- },
531
- required: ['operation', 'target', 'name'], additionalProperties: false,
532
- },
533
- {
534
- type: 'object',
535
- properties: {
536
- operation: { const: 'reparent' }, target: targetProperty,
537
- parent: { type: ['string', 'null'], description: 'Parent id/name; null clears it.' },
538
- },
539
- required: ['operation', 'target', 'parent'], additionalProperties: false,
534
+ type: 'object',
535
+ properties: {
536
+ operation: {
537
+ type: 'string', enum: ['stop', 'remove', 'revive', 'rename', 'reparent'],
540
538
  },
541
- ],
539
+ target: targetProperty,
540
+ name: { type: 'string', minLength: 1 },
541
+ parent: { type: ['string', 'null'], description: 'Parent id/name; null clears it.' },
542
+ },
543
+ required: ['operation', 'target'],
544
+ additionalProperties: false,
542
545
  },
543
546
  async execute(toolCallId, params) {
544
547
  try {
@@ -567,41 +570,54 @@ export function registerFtownPiExtension(pi, options = {}) {
567
570
  }
568
571
 
569
572
  const loopScheduleProperty = {
570
- anyOf: [
571
- {
572
- type: 'object',
573
- properties: {
574
- kind: { const: 'interval' },
575
- everyMs: { type: 'integer', minimum: 1000 },
576
- },
577
- required: ['kind', 'everyMs'],
578
- additionalProperties: false,
579
- },
580
- {
581
- type: 'object',
582
- properties: {
583
- kind: { const: 'cron' },
584
- expression: { type: 'string', minLength: 1 },
585
- tz: { type: 'string', minLength: 1 },
586
- },
587
- required: ['kind', 'expression'],
588
- additionalProperties: false,
589
- },
590
- ],
573
+ type: 'object',
574
+ properties: {
575
+ kind: { type: 'string', enum: ['interval', 'cron'] },
576
+ everyMs: { type: 'integer', minimum: 1000 },
577
+ expression: { type: 'string', minLength: 1 },
578
+ tz: { type: 'string', minLength: 1 },
579
+ },
580
+ required: ['kind'],
581
+ additionalProperties: false,
591
582
  };
592
583
  const loopDraftProperties = {
593
584
  name: { type: 'string', minLength: 1 },
594
585
  task: { type: 'string', minLength: 1 },
595
586
  schedule: loopScheduleProperty,
596
- shell: { enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell'] },
587
+ shell: { type: 'string', enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell'] },
597
588
  workdir: { type: 'string' },
598
589
  model: { type: 'string' },
599
590
  enabled: { type: 'boolean' },
600
- overlapPolicy: { enum: ['skip', 'allow'] },
591
+ overlapPolicy: { type: 'string', enum: ['skip', 'allow'] },
601
592
  retention: { type: ['integer', 'null'], minimum: 0 },
602
593
  maxRuntimeMs: { type: 'integer', minimum: 1000 },
603
594
  group: { type: 'string' },
604
595
  };
596
+ const loopDraftFields = Object.keys(loopDraftProperties);
597
+
598
+ function validateLoopSchedule(schedule) {
599
+ requireProperty({ schedule }, 'schedule');
600
+ if (schedule === null || typeof schedule !== 'object') throw new Error('schedule is required');
601
+ requireOperation({ operation: schedule.kind }, ['interval', 'cron']);
602
+ if (schedule.kind === 'interval') requireProperty(schedule, 'everyMs', 'schedule.everyMs');
603
+ if (schedule.kind === 'cron') requireString(schedule, 'expression', 'schedule.expression');
604
+ }
605
+
606
+ function validateLoopParams(params) {
607
+ requireOperation(params, ['list', 'get', 'runs', 'run_now', 'delete', 'create', 'update']);
608
+ if (['get', 'runs', 'run_now', 'delete', 'update'].includes(params.operation)) {
609
+ requireString(params, 'target');
610
+ }
611
+ if (params.operation === 'create') {
612
+ requireString(params, 'name');
613
+ requireString(params, 'task');
614
+ requireProperty(params, 'schedule');
615
+ }
616
+ if (params.operation === 'update' && !loopDraftFields.some((field) => hasOwn(params, field))) {
617
+ throw new Error('At least one field to update is required');
618
+ }
619
+ if (hasOwn(params, 'schedule')) validateLoopSchedule(params.schedule);
620
+ }
605
621
 
606
622
  function loopBody(params, create = false) {
607
623
  const body = {};
@@ -626,35 +642,21 @@ export function registerFtownPiExtension(pi, options = {}) {
626
642
  label: 'ftown loops',
627
643
  description: 'List, inspect, create, update, delete, or run scheduled ftown loops.',
628
644
  parameters: {
629
- anyOf: [
630
- {
631
- type: 'object', properties: { operation: { const: 'list' } },
632
- required: ['operation'], additionalProperties: false,
633
- },
634
- ...['get', 'runs', 'run_now', 'delete'].map((operation) => ({
635
- type: 'object',
636
- properties: { operation: { const: operation }, target: { type: 'string' } },
637
- required: ['operation', 'target'], additionalProperties: false,
638
- })),
639
- {
640
- type: 'object',
641
- properties: { operation: { const: 'create' }, ...loopDraftProperties },
642
- required: ['operation', 'name', 'task', 'schedule'],
643
- additionalProperties: false,
644
- },
645
- {
646
- type: 'object',
647
- properties: {
648
- operation: { const: 'update' }, target: { type: 'string' }, ...loopDraftProperties,
649
- },
650
- required: ['operation', 'target'],
651
- minProperties: 3,
652
- additionalProperties: false,
645
+ type: 'object',
646
+ properties: {
647
+ operation: {
648
+ type: 'string',
649
+ enum: ['list', 'get', 'runs', 'run_now', 'delete', 'create', 'update'],
653
650
  },
654
- ],
651
+ target: { type: 'string' },
652
+ ...loopDraftProperties,
653
+ },
654
+ required: ['operation'],
655
+ additionalProperties: false,
655
656
  },
656
657
  async execute(toolCallId, params) {
657
658
  try {
659
+ validateLoopParams(params);
658
660
  if (params.operation === 'create') {
659
661
  return await executeOnce('ftown_loops', toolCallId, async () =>
660
662
  toolResult(await requestJson('/api/loops', {