@agfpd/iapeer 0.4.17 → 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 +19 -1
- package/package.json +1 -1
- package/src/cli/index.ts +88 -24
- package/src/identity/index.ts +14 -5
- package/src/launch/adapters/claude.ts +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,23 @@ 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
|
+
|
|
13
30
|
## [0.4.17] - 2026-06-21
|
|
14
31
|
|
|
15
32
|
### Added
|
|
@@ -257,7 +274,8 @@ Initial public release — the foundation core of the iapeer multi-agent ecosyst
|
|
|
257
274
|
- **Heterogeneous peers** — AI agents (Claude, Codex), humans (Telegram), and services
|
|
258
275
|
(timer = cron, watcher = event) are all first-class and addressed the same way.
|
|
259
276
|
|
|
260
|
-
[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
|
|
261
279
|
[0.4.17]: https://github.com/agfpd/iapeer/compare/v0.4.16...v0.4.17
|
|
262
280
|
[0.4.16]: https://github.com/agfpd/iapeer/compare/v0.4.15...v0.4.16
|
|
263
281
|
[0.4.15]: https://github.com/agfpd/iapeer/compare/v0.4.14...v0.4.15
|
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 {
|
|
@@ -54,6 +54,7 @@ import { hostRunDir } from '../launch/ptyHost.ts' // pty-only: attach via the su
|
|
|
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
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,
|
|
@@ -836,27 +837,37 @@ export async function removePeerCli(
|
|
|
836
837
|
export interface RenameOutcome {
|
|
837
838
|
oldPersonality: string
|
|
838
839
|
newPersonality: string
|
|
839
|
-
action: 'renamed' | 'absent' | 'target-exists' | 'refused-live'
|
|
840
|
+
action: 'renamed' | 'absent' | 'target-exists' | 'target-cwd-exists' | 'refused-live'
|
|
840
841
|
reason?: string
|
|
841
|
-
|
|
842
|
+
oldCwd?: string
|
|
843
|
+
newCwd?: string
|
|
844
|
+
transcriptMoved?: boolean
|
|
842
845
|
purgedState?: string[]
|
|
846
|
+
sideEffects?: string[]
|
|
843
847
|
}
|
|
844
848
|
|
|
845
849
|
/**
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
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).
|
|
855
866
|
*/
|
|
856
867
|
export async function renamePeerCli(
|
|
857
868
|
oldPersonality: string,
|
|
858
869
|
newPersonality: string,
|
|
859
|
-
opts: CliEnvOptions & { force?: boolean } = {},
|
|
870
|
+
opts: CliEnvOptions & { force?: boolean; claudeProjectsDir?: string } = {},
|
|
860
871
|
): Promise<RenameOutcome> {
|
|
861
872
|
const env = opts.env ?? process.env
|
|
862
873
|
const index = readPeersIndex({ env })
|
|
@@ -865,6 +876,11 @@ export async function renamePeerCli(
|
|
|
865
876
|
if (findPeer(index, newPersonality)) {
|
|
866
877
|
return { oldPersonality, newPersonality, action: 'target-exists', reason: `"${newPersonality}" already exists` }
|
|
867
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
|
+
}
|
|
868
884
|
const cfg = loadLifecycleConfig(env)
|
|
869
885
|
if (!opts.force) {
|
|
870
886
|
const liveRt = peer.runtimes.find(rt => isPeerLive(rt, oldPersonality, cfg.sockDir))
|
|
@@ -873,13 +889,57 @@ export async function renamePeerCli(
|
|
|
873
889
|
oldPersonality,
|
|
874
890
|
newPersonality,
|
|
875
891
|
action: 'refused-live',
|
|
876
|
-
reason: `"${oldPersonality}" is LIVE on ${liveRt} — its
|
|
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`,
|
|
877
893
|
}
|
|
878
894
|
}
|
|
879
895
|
}
|
|
880
|
-
|
|
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
|
+
}
|
|
881
941
|
const purgedState = peer.runtimes.flatMap(rt => purgeIdentityState(cfg, buildProcessAddress(rt, oldPersonality)))
|
|
882
|
-
return { oldPersonality, newPersonality, action: 'renamed',
|
|
942
|
+
return { oldPersonality, newPersonality, action: 'renamed', oldCwd, newCwd, transcriptMoved, purgedState, sideEffects }
|
|
883
943
|
}
|
|
884
944
|
|
|
885
945
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1054,7 +1114,7 @@ const VERBS: ReadonlyArray<{ sig: string; desc: string }> = [
|
|
|
1054
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)',
|
|
1055
1115
|
},
|
|
1056
1116
|
{ sig: 'remove <peer> [--force]', desc: 'delete a peer\'s registry record (locked writer); refuses a LIVE peer unless --force' },
|
|
1057
|
-
{ sig: 'rename <old> <new> [--force]', desc: 'rename a peer
|
|
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.' },
|
|
1058
1118
|
{
|
|
1059
1119
|
sig: 'send <target> (--message <text> | --message-file <f|->) [--from <id>] [--attachment <p>]… [--topic <t>]',
|
|
1060
1120
|
desc: 'manual IAP send (fallback)',
|
|
@@ -1759,19 +1819,23 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
|
|
|
1759
1819
|
return o.action === 'refused-live' ? 1 : 0
|
|
1760
1820
|
}
|
|
1761
1821
|
case 'rename': {
|
|
1762
|
-
//
|
|
1763
|
-
// the transcript
|
|
1764
|
-
//
|
|
1765
|
-
//
|
|
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.
|
|
1766
1827
|
if (!positionals[0] || !positionals[1]) {
|
|
1767
1828
|
return argErr(errOut, 'rename needs old and new names — usage: iapeer rename <old> <new> [--force]')
|
|
1768
1829
|
}
|
|
1769
1830
|
const o = await renamePeerCli(positionals[0], positionals[1], { force: flags.force === true, env })
|
|
1770
1831
|
if (o.action === 'renamed') {
|
|
1771
|
-
out(`renamed "${o.oldPersonality}" → "${o.newPersonality}"
|
|
1772
|
-
|
|
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`)
|
|
1773
1837
|
out(
|
|
1774
|
-
`NOTE: memory
|
|
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`,
|
|
1775
1839
|
)
|
|
1776
1840
|
} else if (o.action === 'absent') errOut(`rename: "${o.oldPersonality}" not registered\n`)
|
|
1777
1841
|
else errOut(`rename: ${o.reason}\n`)
|
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
|
// ─────────────────────────────────────────────────────────────────────────────
|