martty 0.3.2-beta.1 → 0.3.2

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.
@@ -20,12 +20,17 @@ import { downloadFile } from './download.js'
20
20
  export const ACP_REGISTRY_URL = 'https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json'
21
21
 
22
22
  export const OPENMA_PI_ACP = '@openma/pi-acp@latest'
23
+ export const OPENMA_PI_ACP_REPOSITORY = 'https://github.com/openma-ai/pi-acp'
23
24
 
24
25
  function preferMarttyPiAcp(records) {
25
26
  return records.map((record) => record.id === 'pi-acp' ? {
26
27
  ...record,
27
- label: 'Pi (OpenMA)',
28
+ label: 'pi-acp',
28
29
  version: 'latest',
30
+ // The official catalog also lists an unrelated unprefixed pi-acp package;
31
+ // Martty only ever launches @openma/pi-acp, and the repository URL is how
32
+ // users tell the two entries apart.
33
+ repository: OPENMA_PI_ACP_REPOSITORY,
29
34
  distributions: [{
30
35
  type: 'npx', command: 'npx', args: [OPENMA_PI_ACP], env: {},
31
36
  }],
@@ -144,6 +149,7 @@ export function normalizeAcpRegistry(value, options = {}) {
144
149
  ...(typeof agent.icon === 'string' ? { icon: agent.icon } : {}),
145
150
  version: agent.version,
146
151
  description: typeof agent.description === 'string' ? agent.description : '',
152
+ ...(typeof agent.repository === 'string' ? { repository: agent.repository } : {}),
147
153
  distributions,
148
154
  }]
149
155
  })
@@ -41,6 +41,15 @@ function findDescription(entry) {
41
41
  return `found locally · configure · ${commandText(entry)}`
42
42
  }
43
43
 
44
+ // Registry entries carry their repository like the catalog's name /
45
+ // description / repository triple; the URL distinguishes same-named
46
+ // harnesses (e.g. @openma/pi-acp from the unrelated registry pi-acp).
47
+ function describeCandidate(entry) {
48
+ const detail = findDescription(entry)
49
+ const repository = entry.registryRecord?.repository
50
+ return repository === undefined ? detail : `${detail} · ${repository}`
51
+ }
52
+
44
53
  export function apply(ctx, options = {}) {
45
54
  const settingsPath = options.settingsPath
46
55
  let registryPromise
@@ -477,7 +486,7 @@ export function apply(ctx, options = {}) {
477
486
  openSelect({ id: 'harness-find', title: failure ? 'Add Harness · Registry offline' : pending ? 'Add Harness · checking Registry…' : 'Add Harness', searchable: true,
478
487
  value: entries[0]?.id ?? REFRESH,
479
488
  options: [
480
- ...entries.map((entry) => ({ value: entry.id, label: entry.label, description: findDescription(entry),
489
+ ...entries.map((entry) => ({ value: entry.id, label: entry.label, description: describeCandidate(entry),
481
490
  ...currentOption(entry),
482
491
  group: locallyAvailable(entry) ? 'Installed / configured' : entry.status === 'Checking locally' ? 'Catalog' : 'Not downloaded' })),
483
492
  { value: REFRESH, label: failure ? 'Retry Registry' : 'Refresh Registry', description: failure ? errorText(failure) : 'Recheck the catalog and local programs' },
package/lib/harnesses.js CHANGED
@@ -16,15 +16,20 @@ import {
16
16
  installRegistryBinary,
17
17
  managedBinaryPath,
18
18
  normalizeAcpRegistry,
19
- OPENMA_PI_ACP,
20
19
  readAcpRegistrySnapshot,
21
20
  } from './harness-registry.js'
21
+ import {
22
+ migrateLegacyAcpAgents,
23
+ readCustomAcpAgents,
24
+ removeCustomAcpAgent,
25
+ upsertCustomAcpAgent,
26
+ } from '@openma/common/acp-harnesses/custom-agents'
22
27
 
23
28
  export { fetchAcpRegistry } from './harness-registry.js'
24
29
 
25
30
  const HARNESS_ID = /^[a-z0-9][a-z0-9-]*$/
26
- const PI_ACP_LATEST = OPENMA_PI_ACP
27
- const PI_ACP_SPEC = /^(?:@openma\/)?pi-acp(?:@.+)?$/
31
+ const PI_ACP_ID = 'pi-acp'
32
+ const PI_ACP_LEGACY_LABEL = 'Pi (OpenMA)'
28
33
  const ANSI = Object.freeze({ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', cyan: '\x1b[36m', green: '\x1b[32m' })
29
34
 
30
35
  // Kept as a source-compatible empty export. The source of truth is the
@@ -366,27 +371,50 @@ function validateHarness(value) {
366
371
  }
367
372
  const isNpx = /^npx(?:\.(?:cmd|bat|exe))?$/i.test(path.win32.basename(value.command))
368
373
  const normalizedArgs = isNpx
369
- ? [...args]
370
- .filter((arg) => arg !== '--yes' && arg !== '--prefer-offline')
371
- // A pinned pi-acp spec (any old `pi-acp@x` or `@openma/pi-acp@x` entry)
372
- // always resolves `latest` at runtime instead — harness upgrades ship
373
- // through npm, never through a Martty release.
374
- .map((arg) => PI_ACP_SPEC.test(arg) ? PI_ACP_LATEST : arg)
374
+ ? [...args].filter((arg) => arg !== '--yes' && arg !== '--prefer-offline')
375
375
  : [...args]
376
+ const label = typeof value.label === 'string' && value.label.trim().length > 0
377
+ ? value.label
378
+ : value.id
376
379
  return {
377
380
  id: value.id,
378
- label: typeof value.label === 'string' && value.label.trim().length > 0
379
- ? value.label
380
- : value.id,
381
+ // Configs saved when the pi harness was branded "Pi (OpenMA)" keep
382
+ // working; only the stale default display label migrates to the id.
383
+ label: value.id === PI_ACP_ID && label === PI_ACP_LEGACY_LABEL ? PI_ACP_ID : label,
381
384
  command: value.command,
382
385
  args: normalizedArgs,
383
386
  ...(env !== undefined ? { env: { ...env } } : {}),
384
387
  }
385
388
  }
386
389
 
387
- function configuredHarnesses(settings) {
388
- if (!Array.isArray(settings.harnesses)) return []
389
- return settings.harnesses.map(validateHarness)
390
+ // Harness definitions live in <oma>/agents.json, the shared store from
391
+ // @openma/common — Backchat reads and writes the same file. Legacy rows
392
+ // still inside settings.json (and Backchat's config.toml overrides) keep
393
+ // reading through the shared module; the first shared write migrates
394
+ // harnesses[] into config.toml and strips the key so removals stick.
395
+ function settingsHome(settingsPath) {
396
+ return path.dirname(settingsPath)
397
+ }
398
+
399
+ function migrateStoredLabel(agent) {
400
+ // Configs saved when the pi harness was branded "Pi (OpenMA)" keep
401
+ // working; only the stale default display label migrates to the id.
402
+ return agent.id === PI_ACP_ID && agent.label === PI_ACP_LEGACY_LABEL
403
+ ? { ...agent, label: PI_ACP_ID }
404
+ : agent
405
+ }
406
+
407
+ function configuredHarnesses(settingsPath) {
408
+ // enabled=false is Backchat's disable flag: the definition stays stored
409
+ // but a disabled agent must not be selectable or launchable here.
410
+ return readCustomAcpAgents(settingsHome(settingsPath))
411
+ .filter((agent) => agent.enabled !== false)
412
+ .map(migrateStoredLabel)
413
+ }
414
+
415
+ function settingsWithoutHarnesses(settings) {
416
+ const { harnesses: _legacyHarnesses, ...rest } = settings
417
+ return rest
390
418
  }
391
419
 
392
420
  function withoutLegacyActive(settings) {
@@ -409,36 +437,37 @@ function persistedDefaultId(settings) {
409
437
 
410
438
  export function upsertHarness(settingsPath, harness) {
411
439
  const next = validateHarness(harness)
412
- const settings = readSettings(settingsPath)
413
- const harnesses = configuredHarnesses(settings)
414
- const index = harnesses.findIndex(({ id }) => id === next.id)
415
- if (index === -1) harnesses.push(next)
416
- else harnesses[index] = next
417
- writeSettings(settingsPath, { ...withoutLegacyActive(settings), harnesses })
440
+ // The shared write migrates legacy harnesses[] into config.toml and
441
+ // strips the key; read settings after it so a stale copy cannot race
442
+ // the migrated file back in.
443
+ upsertCustomAcpAgent(settingsHome(settingsPath), next)
444
+ const settings = withoutLegacyActive(readSettings(settingsPath))
445
+ writeSettings(settingsPath, settingsWithoutHarnesses(settings))
418
446
  return next
419
447
  }
420
448
 
421
449
  export function setDefaultHarness(settingsPath, id) {
422
- const settings = readSettings(settingsPath)
423
- const harnesses = configuredHarnesses(settings)
450
+ const harnesses = configuredHarnesses(settingsPath)
424
451
  if (!harnesses.some((harness) => harness.id === id)) {
425
452
  throw new Error(`unknown harness ${JSON.stringify(id)}`)
426
453
  }
427
- writeSettings(settingsPath, { ...withoutLegacyActive(settings), harnesses, defaultHarness: id })
454
+ const settings = withoutLegacyActive(readSettings(settingsPath))
455
+ migrateLegacyAcpAgents(settingsHome(settingsPath))
456
+ writeSettings(settingsPath, { ...settingsWithoutHarnesses(settings), defaultHarness: id })
428
457
  }
429
458
 
430
459
  export function savedHarnesses(settingsPath) {
431
- return configuredHarnesses(readSettings(settingsPath))
460
+ return configuredHarnesses(settingsPath)
432
461
  }
433
462
 
434
463
  export function removeHarnessConfiguration(settingsPath, expected) {
435
- const settings = withoutLegacyActive(readSettings(settingsPath))
436
- const harnesses = configuredHarnesses(settings)
437
- const current = harnesses.find(({ id }) => id === expected.id)
464
+ const current = configuredHarnesses(settingsPath).find(({ id }) => id === expected.id)
438
465
  if (JSON.stringify(current) !== JSON.stringify(expected)) throw new Error('Harness configuration changed; review removal again')
439
- settings.harnesses = harnesses.filter(({ id }) => id !== expected.id)
440
- if (settings.defaultHarness === expected.id) delete settings.defaultHarness
441
- writeSettings(settingsPath, settings)
466
+ removeCustomAcpAgent(settingsHome(settingsPath), expected.id)
467
+ const settings = withoutLegacyActive(readSettings(settingsPath))
468
+ const rest = settingsWithoutHarnesses(settings)
469
+ if (rest.defaultHarness === expected.id) delete rest.defaultHarness
470
+ writeSettings(settingsPath, rest)
442
471
  }
443
472
 
444
473
  // Kept as a source-compatible alias for integrations compiled against 0.2.30.
@@ -449,7 +478,7 @@ export function selectedHarness(settingsPath) {
449
478
  const settings = readSettings(settingsPath)
450
479
  const id = persistedDefaultId(settings)
451
480
  if (typeof id !== 'string') return undefined
452
- return configuredHarnesses(settings).find((harness) => harness.id === id)
481
+ return configuredHarnesses(settingsPath).find((harness) => harness.id === id)
453
482
  }
454
483
 
455
484
  function executableFile(command) {
@@ -529,6 +558,7 @@ function registryRecords(options = {}) {
529
558
  : record.id,
530
559
  version: typeof record.version === 'string' ? record.version : 'unversioned',
531
560
  description: typeof record.description === 'string' ? record.description : '',
561
+ ...(typeof record.repository === 'string' ? { repository: record.repository } : {}),
532
562
  distributions,
533
563
  }]
534
564
  }
@@ -1004,7 +1034,7 @@ export function discoverHarnesses(settingsPath, options = {}) {
1004
1034
  }
1005
1035
 
1006
1036
  function discoverHarnessEntries(settingsPath, options, registryEntries) {
1007
- const configured = configuredHarnesses(readSettings(settingsPath))
1037
+ const configured = configuredHarnesses(settingsPath)
1008
1038
  .map((harness) => ({ ...harness, source: 'configured' }))
1009
1039
  const defaults = (options.defaults ?? []).map((entry) => ({
1010
1040
  ...validateHarness(entry),
@@ -1071,7 +1101,7 @@ function searchScore(entry, query = '') {
1071
1101
  }
1072
1102
 
1073
1103
  function findHarnessCandidates(settingsPath, options = {}, query = '') {
1074
- const configured = configuredHarnesses(readSettings(settingsPath)).map((entry) => ({
1104
+ const configured = configuredHarnesses(settingsPath).map((entry) => ({
1075
1105
  ...entry,
1076
1106
  source: 'configured',
1077
1107
  status: 'Configured',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "martty",
3
- "version": "0.3.2-beta.1",
3
+ "version": "0.3.2",
4
4
  "description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,7 +77,8 @@
77
77
  "dependencies": {
78
78
  "@deepseek-ai/cordis": "^4.0.1",
79
79
  "@lobehub/icons-static-svg": "1.95.0",
80
- "@openma/deepseek-harness-acp": "0.4.37",
80
+ "@openma/common": "^0.9.2",
81
+ "@openma/deepseek-harness-acp": "0.4.38",
81
82
  "@resvg/resvg-js": "2.6.2",
82
83
  "cross-spawn": "^7.0.6",
83
84
  "node-downloader-helper": "2.1.11"
@@ -97,10 +98,10 @@
97
98
  "agent-client-protocol"
98
99
  ],
99
100
  "optionalDependencies": {
100
- "@openma/martty-darwin-arm64": "0.3.2-beta.1",
101
- "@openma/martty-darwin-x64": "0.3.2-beta.1",
102
- "@openma/martty-linux-x64": "0.3.2-beta.1",
103
- "@openma/martty-linux-arm64": "0.3.2-beta.1",
104
- "@openma/martty-win32-x64": "0.3.2-beta.1"
101
+ "@openma/martty-darwin-arm64": "0.3.2",
102
+ "@openma/martty-darwin-x64": "0.3.2",
103
+ "@openma/martty-linux-x64": "0.3.2",
104
+ "@openma/martty-linux-arm64": "0.3.2",
105
+ "@openma/martty-win32-x64": "0.3.2"
105
106
  }
106
107
  }