@agfpd/iapeer 0.4.16 → 0.4.18
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/CHANGELOG.md +32 -1
- package/package.json +1 -1
- package/src/cli/index.ts +141 -2
- package/src/identity/index.ts +14 -5
- package/src/launch/adapters/claude.ts +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.4.18] - 2026-06-22
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- `iapeer rename <old> <new>` now performs a **full folder rename** (extends 0.4.17's
|
|
18
|
+
personality-only rename). The personality is a self-healed mirror of `normalize(basename(cwd))`,
|
|
19
|
+
so a rename that kept the cwd would be reverted by profile-standard self-heal — the folder
|
|
20
|
+
must move. The verb now atomically moves the cwd folder (the per-cwd profile, `.mcp.json`,
|
|
21
|
+
native-memory, `CLAUDE.md`, and the `.git` clone ride it), moves the claude transcript slug
|
|
22
|
+
dir (`~/.claude/projects/<slug>`, keyed by `realpath(cwd)` — so claude history is preserved),
|
|
23
|
+
and updates the registry cwd + personality + per-cwd profile, with rollback of the fs moves on
|
|
24
|
+
failure. Best-effort afterwards: re-trust the new cwd for codex + clean the old, rewrite the
|
|
25
|
+
claude `.mcp.json` identity, purge the old identity's lifecycle markers. Refuses a LIVE peer
|
|
26
|
+
unless `--force`. Memory (operativka folder + author/index attribution) re-key is the memory
|
|
27
|
+
provider's separate step; codex resume-history (keyed by the cwd recorded inside each session
|
|
28
|
+
file, not a path-dir) does not carry to the new cwd.
|
|
29
|
+
|
|
30
|
+
## [0.4.17] - 2026-06-21
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `iapeer rename <old> <new>` — first-class peer-identity rename (parity with remove/create).
|
|
35
|
+
Renames the personality in **both** the registry and the per-cwd profile atomically and
|
|
36
|
+
**keeps the cwd**, so the claude transcript history (keyed by `realpath(cwd)`) survives.
|
|
37
|
+
Refuses a LIVE peer unless `--force` (a running session carries the old identity in its
|
|
38
|
+
env), and purges the old identity's lifecycle markers. Memory re-key (the personality-keyed
|
|
39
|
+
operativka folder + author/index attribution) is the memory provider's separate step;
|
|
40
|
+
native-memory is cwd-keyed and unaffected.
|
|
41
|
+
|
|
13
42
|
## [0.4.16] - 2026-06-21
|
|
14
43
|
|
|
15
44
|
### Fixed
|
|
@@ -245,7 +274,9 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
|
|
|
245
274
|
- **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
|
|
246
275
|
(timer = cron, watcher = event) are all first-class and addressed the same way.
|
|
247
276
|
|
|
248
|
-
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.
|
|
277
|
+
[Unreleased]: https://github.com/agfpd/iapeer/compare/v0.4.18...HEAD
|
|
278
|
+
[0.4.18]: https://github.com/agfpd/iapeer/compare/v0.4.17...v0.4.18
|
|
279
|
+
[0.4.17]: https://github.com/agfpd/iapeer/compare/v0.4.16...v0.4.17
|
|
249
280
|
[0.4.16]: https://github.com/agfpd/iapeer/compare/v0.4.15...v0.4.16
|
|
250
281
|
[0.4.15]: https://github.com/agfpd/iapeer/compare/v0.4.14...v0.4.15
|
|
251
282
|
[0.4.14]: https://github.com/agfpd/iapeer/compare/v0.4.13...v0.4.14
|
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// sentinel-marked always-on plist) are stop/start-able.
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from 'child_process'
|
|
14
|
-
import { existsSync, readFileSync } from 'fs'
|
|
14
|
+
import { existsSync, readFileSync, renameSync } from 'fs'
|
|
15
15
|
import { fileURLToPath } from 'url'
|
|
16
16
|
import { dirname, join } from 'path'
|
|
17
17
|
import {
|
|
@@ -53,7 +53,8 @@ import { getAdapter } from '../launch/index.ts'
|
|
|
53
53
|
import { hostRunDir } from '../launch/ptyHost.ts' // pty-only: attach via the supervisor client
|
|
54
54
|
import { runSupervisorClient } from '../supervisor/client.ts' // @xterm-free attach client (both reattach port-deps baked in)
|
|
55
55
|
import { cycleDaemon, isFoundationOwnedPlist, launchctlBootstrap, launchdLabel, launchdPlistPath } from '../launch/launchd.ts'
|
|
56
|
-
import { readPeerProfile, resolveCallerIdentity, resolveIdentity, writePeerProfileAtomic } from '../identity/index.ts'
|
|
56
|
+
import { readPeerProfile, renamePeer, resolveCallerIdentity, resolveIdentity, writePeerProfileAtomic } from '../identity/index.ts'
|
|
57
|
+
import { claudeProjectsRoot, transcriptSlug } from '../launch/adapters/claude.ts'
|
|
57
58
|
import { peerProfilePath } from '../storage/index.ts'
|
|
58
59
|
import {
|
|
59
60
|
isConformant,
|
|
@@ -827,6 +828,120 @@ export async function removePeerCli(
|
|
|
827
828
|
}
|
|
828
829
|
}
|
|
829
830
|
|
|
831
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
832
|
+
// rename — first-class peer-identity rename (parity with remove/create). Wraps
|
|
833
|
+
// renamePeer (registry + per-cwd profile, atomic, inside the lock) and KEEPS the
|
|
834
|
+
// cwd, so the claude transcript history (slug = realpath(cwd)) survives the rename.
|
|
835
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
836
|
+
|
|
837
|
+
export interface RenameOutcome {
|
|
838
|
+
oldPersonality: string
|
|
839
|
+
newPersonality: string
|
|
840
|
+
action: 'renamed' | 'absent' | 'target-exists' | 'target-cwd-exists' | 'refused-live'
|
|
841
|
+
reason?: string
|
|
842
|
+
oldCwd?: string
|
|
843
|
+
newCwd?: string
|
|
844
|
+
transcriptMoved?: boolean
|
|
845
|
+
purgedState?: string[]
|
|
846
|
+
sideEffects?: string[]
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* FULL folder rename of a peer (Arthur's invariant: personality == normalize(basename(cwd)),
|
|
851
|
+
* a self-healed mirror — so a rename MUST move the folder, or profile-standard self-heal
|
|
852
|
+
* reverts the personality back to the basename). Atomic core, best-effort side-effects:
|
|
853
|
+
* 1. mv the cwd folder oldCwd → dirname(oldCwd)/<new> (the per-cwd profile, .mcp.json,
|
|
854
|
+
* native-memory, CLAUDE.md, .git all ride it).
|
|
855
|
+
* 2. mv the claude transcript slug dir (~/.claude/projects/<oldSlug> → <newSlug>) — keyed
|
|
856
|
+
* by realpath(cwd), so this is how the claude history is NOT orphaned. (codex history
|
|
857
|
+
* is keyed by the cwd recorded INSIDE each ~/.codex/sessions jsonl, not a path-dir, so
|
|
858
|
+
* it does NOT carry — a documented limitation; codex starts fresh sessions at the new cwd.)
|
|
859
|
+
* 3. renamePeer(…, newCwd): atomic registry personality+cwd + per-cwd profile personality.
|
|
860
|
+
* On any failure in 1-3 the fs moves are rolled back (folder + transcript back). Then
|
|
861
|
+
* best-effort: codex re-trust the new cwd + clean the old, re-write the claude .mcp.json
|
|
862
|
+
* identity fallback, purge the old identity's lifecycle markers. Refuses a LIVE peer unless
|
|
863
|
+
* --force. Does NOT touch memory (operativka/author/index) — the provider re-keys that
|
|
864
|
+
* separately AFTER this rename; deliberately NO `remove`/unprovision (which would risk a
|
|
865
|
+
* memory purge). `claudeProjectsDir` is a test seam (default ~/.claude/projects).
|
|
866
|
+
*/
|
|
867
|
+
export async function renamePeerCli(
|
|
868
|
+
oldPersonality: string,
|
|
869
|
+
newPersonality: string,
|
|
870
|
+
opts: CliEnvOptions & { force?: boolean; claudeProjectsDir?: string } = {},
|
|
871
|
+
): Promise<RenameOutcome> {
|
|
872
|
+
const env = opts.env ?? process.env
|
|
873
|
+
const index = readPeersIndex({ env })
|
|
874
|
+
const peer = findPeer(index, oldPersonality)
|
|
875
|
+
if (!peer) return { oldPersonality, newPersonality, action: 'absent' }
|
|
876
|
+
if (findPeer(index, newPersonality)) {
|
|
877
|
+
return { oldPersonality, newPersonality, action: 'target-exists', reason: `"${newPersonality}" already exists` }
|
|
878
|
+
}
|
|
879
|
+
const oldCwd = peer.cwd
|
|
880
|
+
const newCwd = join(dirname(oldCwd), newPersonality)
|
|
881
|
+
if (existsSync(newCwd)) {
|
|
882
|
+
return { oldPersonality, newPersonality, action: 'target-cwd-exists', reason: `${newCwd} already exists — move or remove it first` }
|
|
883
|
+
}
|
|
884
|
+
const cfg = loadLifecycleConfig(env)
|
|
885
|
+
if (!opts.force) {
|
|
886
|
+
const liveRt = peer.runtimes.find(rt => isPeerLive(rt, oldPersonality, cfg.sockDir))
|
|
887
|
+
if (liveRt) {
|
|
888
|
+
return {
|
|
889
|
+
oldPersonality,
|
|
890
|
+
newPersonality,
|
|
891
|
+
action: 'refused-live',
|
|
892
|
+
reason: `"${oldPersonality}" is LIVE on ${liveRt} — its session + folder must be quiescent for the move; stop it first (\`iapeer stop ${oldPersonality}\`) or pass --force`,
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
// ── atomic core (1-3) with rollback ──────────────────────────────────────────
|
|
897
|
+
const projectsDir = opts.claudeProjectsDir ?? claudeProjectsRoot()
|
|
898
|
+
const oldSlug = transcriptSlug(oldCwd) // BEFORE the mv — realpath needs the live dir
|
|
899
|
+
const oldTx = join(projectsDir, oldSlug)
|
|
900
|
+
renameSync(oldCwd, newCwd) // 1. folder (atomic on one filesystem)
|
|
901
|
+
let transcriptMoved = false
|
|
902
|
+
let newTx: string | undefined
|
|
903
|
+
try {
|
|
904
|
+
const newSlug = transcriptSlug(newCwd) // AFTER the mv — realpath(newCwd) resolves
|
|
905
|
+
newTx = join(projectsDir, newSlug)
|
|
906
|
+
if (oldSlug !== newSlug && existsSync(oldTx) && !existsSync(newTx)) {
|
|
907
|
+
renameSync(oldTx, newTx) // 2. claude transcript slug dir
|
|
908
|
+
transcriptMoved = true
|
|
909
|
+
}
|
|
910
|
+
await renamePeer(oldPersonality, newPersonality, { env }, newCwd) // 3. atomic registry+profile
|
|
911
|
+
} catch (e) {
|
|
912
|
+
if (transcriptMoved && newTx) {
|
|
913
|
+
try { renameSync(newTx, oldTx) } catch { /* best-effort rollback */ }
|
|
914
|
+
}
|
|
915
|
+
try { renameSync(newCwd, oldCwd) } catch { /* best-effort rollback */ }
|
|
916
|
+
throw e
|
|
917
|
+
}
|
|
918
|
+
// ── best-effort side-effects (the core rename already committed) ──────────────
|
|
919
|
+
const sideEffects: string[] = []
|
|
920
|
+
if (peer.runtimes.includes('codex')) {
|
|
921
|
+
try {
|
|
922
|
+
const { preTrustCodexCwd, removeCodexCwdTrust } = await import('../launch/nativeMemory.ts')
|
|
923
|
+
preTrustCodexCwd(newCwd, env)
|
|
924
|
+
removeCodexCwdTrust(oldCwd, env)
|
|
925
|
+
const { removeCodexHooksTrustUnder } = await import('../launch/codexHooksTrust.ts')
|
|
926
|
+
removeCodexHooksTrustUnder(oldCwd, env)
|
|
927
|
+
sideEffects.push('codex-trust: re-trusted new cwd + cleaned old')
|
|
928
|
+
} catch (e) {
|
|
929
|
+
sideEffects.push(`codex-trust: failed (${e instanceof Error ? e.message : String(e)})`)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (peer.runtimes.includes('claude')) {
|
|
933
|
+
try {
|
|
934
|
+
const { writeClaudeMcpConfig, resolveDaemonMcpUrl } = await import('../init/index.ts')
|
|
935
|
+
writeClaudeMcpConfig(newCwd, newPersonality, resolveDaemonMcpUrl({ env }))
|
|
936
|
+
sideEffects.push('claude .mcp.json: identity rewritten')
|
|
937
|
+
} catch (e) {
|
|
938
|
+
sideEffects.push(`claude .mcp.json: failed (${e instanceof Error ? e.message : String(e)})`)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const purgedState = peer.runtimes.flatMap(rt => purgeIdentityState(cfg, buildProcessAddress(rt, oldPersonality)))
|
|
942
|
+
return { oldPersonality, newPersonality, action: 'renamed', oldCwd, newCwd, transcriptMoved, purgedState, sideEffects }
|
|
943
|
+
}
|
|
944
|
+
|
|
830
945
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
831
946
|
// send — manual IAP send fallback (contract Примитивы §send). Goes through the
|
|
832
947
|
// same router path as send_to_peer (resolve → deliver / wake), in-process so it
|
|
@@ -999,6 +1114,7 @@ const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
|
999
1114
|
desc: 'LAZY soft-reload: agentic peer comes up FRESH (re-reads doctrine) on its NEXT natural wake — no kill, no burst-wake; non-agentic runtimes skipped. --all = whole fleet. (eager: `new`/self-fresh)',
|
|
1000
1115
|
},
|
|
1001
1116
|
{ sig: 'remove <peer> [--force]', desc: 'delete a peer\'s registry record (locked writer); refuses a LIVE peer unless --force' },
|
|
1117
|
+
{ sig: 'rename <old> <new> [--force]', desc: 'rename a peer — moves the cwd folder + claude transcript + atomic registry/profile (personality = folder name); refuses a LIVE peer unless --force. Memory re-key is the provider\'s separate step.' },
|
|
1002
1118
|
{
|
|
1003
1119
|
sig: 'send <target> (--message <text> | --message-file <f|->) [--from <id>] [--attachment <p>]… [--topic <t>]',
|
|
1004
1120
|
desc: 'manual IAP send (fallback)',
|
|
@@ -1702,6 +1818,29 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1702
1818
|
else errOut(`remove: ${o.reason}\n`)
|
|
1703
1819
|
return o.action === 'refused-live' ? 1 : 0
|
|
1704
1820
|
}
|
|
1821
|
+
case 'rename': {
|
|
1822
|
+
// FULL folder rename (Arthur's invariant: personality == basename(cwd)): mv the
|
|
1823
|
+
// folder + the claude transcript slug dir + atomic registry cwd+personality, with
|
|
1824
|
+
// rollback; best-effort codex re-trust + .mcp.json rewrite + old-marker purge.
|
|
1825
|
+
// Refuses a LIVE peer unless --force. Memory + codex-history caveats are stated —
|
|
1826
|
+
// a silent data move reads as loss otherwise.
|
|
1827
|
+
if (!positionals[0] || !positionals[1]) {
|
|
1828
|
+
return argErr(errOut, 'rename needs old and new names — usage: iapeer rename <old> <new> [--force]')
|
|
1829
|
+
}
|
|
1830
|
+
const o = await renamePeerCli(positionals[0], positionals[1], { force: flags.force === true, env })
|
|
1831
|
+
if (o.action === 'renamed') {
|
|
1832
|
+
out(`renamed "${o.oldPersonality}" → "${o.newPersonality}"\n`)
|
|
1833
|
+
out(` folder: ${o.oldCwd} → ${o.newCwd}\n`)
|
|
1834
|
+
out(` claude transcript: ${o.transcriptMoved ? 'moved (history preserved)' : 'no slug dir to move'}\n`)
|
|
1835
|
+
if (o.sideEffects?.length) out(` ${o.sideEffects.join('\n ')}\n`)
|
|
1836
|
+
if (o.purgedState?.length) out(` lifecycle state purged (old identity): ${o.purgedState.join(', ')}\n`)
|
|
1837
|
+
out(
|
|
1838
|
+
`NOTE: memory (operativka folder + author/index) is personality-keyed — re-key it under "${o.newPersonality}" via the memory provider separately. codex resume-history for the old cwd does NOT carry (codex keys sessions on the in-file cwd). Wake "${o.newPersonality}" to verify identity + history.\n`,
|
|
1839
|
+
)
|
|
1840
|
+
} else if (o.action === 'absent') errOut(`rename: "${o.oldPersonality}" not registered\n`)
|
|
1841
|
+
else errOut(`rename: ${o.reason}\n`)
|
|
1842
|
+
return o.action === 'renamed' ? 0 : 1
|
|
1843
|
+
}
|
|
1705
1844
|
case 'send': {
|
|
1706
1845
|
// Message body from EITHER --message <text> OR --message-file <f> (f='-' →
|
|
1707
1846
|
// stdin). The runtime packages (telegram/notifier) + monitor deliver via
|
package/src/identity/index.ts
CHANGED
|
@@ -634,6 +634,7 @@ export async function renamePeer(
|
|
|
634
634
|
oldPersonality: string,
|
|
635
635
|
newPersonality: string,
|
|
636
636
|
options: PeersUpdateOptions = {},
|
|
637
|
+
newCwd?: string,
|
|
637
638
|
): Promise<PeersIndex> {
|
|
638
639
|
if (!isValidName(oldPersonality) || !isValidName(newPersonality)) {
|
|
639
640
|
throw new IapError('peers rename requires valid personalities')
|
|
@@ -647,22 +648,30 @@ export async function renamePeer(
|
|
|
647
648
|
if (index.peers.some(peer => peer.personality === newPersonality)) {
|
|
648
649
|
throw new IapError(`peer "${newPersonality}" already exists`)
|
|
649
650
|
}
|
|
650
|
-
|
|
651
|
+
// newCwd: the FOLDER was already moved by the caller (the cwd-move rename), so the
|
|
652
|
+
// profile now lives at newCwd; read/rewrite it THERE and update the registry cwd
|
|
653
|
+
// pointer atomically with the personality. Omitted → keep-cwd rename (profile at the
|
|
654
|
+
// registry's current cwd). The folder mv + transcript mv are the caller's side (cli),
|
|
655
|
+
// done BEFORE this atomic registry write (with rollback on failure).
|
|
656
|
+
const profileCwd = newCwd ?? target.cwd
|
|
657
|
+
const sourceProfile = readPeerProfile(profileCwd)
|
|
651
658
|
if (!sourceProfile) {
|
|
652
659
|
throw new IapError(
|
|
653
|
-
`peer "${oldPersonality}" cwd ${
|
|
660
|
+
`peer "${oldPersonality}" cwd ${profileCwd} has no ${IAPEER_DIR}/${PEER_PROFILE_FILE}; restore the cwd or remove the registry entry`,
|
|
654
661
|
)
|
|
655
662
|
}
|
|
656
663
|
if (sourceProfile.personality !== oldPersonality) {
|
|
657
664
|
throw new IapError(
|
|
658
|
-
`peer "${oldPersonality}" cwd ${
|
|
665
|
+
`peer "${oldPersonality}" cwd ${profileCwd} profile has personality "${sourceProfile.personality}", not "${oldPersonality}"; registry out of sync`,
|
|
659
666
|
)
|
|
660
667
|
}
|
|
661
|
-
writePeerProfileAtomic(
|
|
668
|
+
writePeerProfileAtomic(profileCwd, { ...sourceProfile, personality: newPersonality })
|
|
662
669
|
return {
|
|
663
670
|
...index,
|
|
664
671
|
peers: index.peers.map(peer =>
|
|
665
|
-
peer.personality === oldPersonality
|
|
672
|
+
peer.personality === oldPersonality
|
|
673
|
+
? { ...peer, personality: newPersonality, ...(newCwd ? { cwd: newCwd } : {}) }
|
|
674
|
+
: peer,
|
|
666
675
|
),
|
|
667
676
|
}
|
|
668
677
|
}, options)
|
|
@@ -89,7 +89,7 @@ function anyBootDialog(pane: string): boolean {
|
|
|
89
89
|
* mkdtemp temp cwd. realpath first so a symlinked cwd maps to the dir claude
|
|
90
90
|
* actually wrote (a stale path falls through to the original string).
|
|
91
91
|
*/
|
|
92
|
-
function transcriptSlug(workDir: string): string {
|
|
92
|
+
export function transcriptSlug(workDir: string): string {
|
|
93
93
|
let phys = workDir
|
|
94
94
|
try {
|
|
95
95
|
phys = realpathSync(workDir)
|
|
@@ -99,8 +99,15 @@ function transcriptSlug(workDir: string): string {
|
|
|
99
99
|
return phys.replace(/[^a-zA-Z0-9]/g, '-')
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
/** The base dir holding all claude project transcript slugs (~/.claude/projects).
|
|
103
|
+
* Exported so a cwd-move (peer folder rename) can relocate the slug dir from the
|
|
104
|
+
* SINGLE source of the slug encoding, never a re-implementation that could drift. */
|
|
105
|
+
export function claudeProjectsRoot(): string {
|
|
106
|
+
return join(homedir(), '.claude', 'projects')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function transcriptDir(workDir: string): string {
|
|
110
|
+
return join(claudeProjectsRoot(), transcriptSlug(workDir))
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
// ─────────────────────────────────────────────────────────────────────────────
|