@idleware/ringi 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +83 -2
- package/package.json +53 -4
- package/src/main/resources/profiles/a2a-1.json +43 -0
- package/src/main/resources/skill/ringi/SKILL.md +72 -0
- package/target/dts/app/agent.d.ts +104 -0
- package/target/dts/app/budget.d.ts +36 -0
- package/target/dts/app/context.d.ts +19 -0
- package/target/dts/app/hubs.d.ts +38 -0
- package/target/dts/app/overview.d.ts +25 -0
- package/target/dts/app/room.d.ts +76 -0
- package/target/dts/app/rooms.d.ts +26 -0
- package/target/dts/app/session.d.ts +60 -0
- package/target/dts/app/view.d.ts +64 -0
- package/target/dts/cli/agent.d.ts +10 -0
- package/target/dts/cli/args.d.ts +13 -0
- package/target/dts/cli/context.d.ts +6 -0
- package/target/dts/cli/index.d.ts +5 -0
- package/target/dts/cli/operator.d.ts +8 -0
- package/target/dts/cli/session.d.ts +7 -0
- package/target/dts/cli/shared.d.ts +20 -0
- package/target/dts/cli/skill.d.ts +4 -0
- package/target/dts/cli.d.ts +3 -0
- package/target/dts/domain/decisions.d.ts +65 -0
- package/target/dts/domain/index.d.ts +2 -0
- package/target/dts/domain/profiles.d.ts +39 -0
- package/target/dts/domain/signals.d.ts +37 -0
- package/target/dts/home/index.d.ts +21 -0
- package/target/dts/hub/index.d.ts +66 -0
- package/target/dts/hub/signals.d.ts +4 -0
- package/target/dts/index.d.ts +8 -0
- package/target/dts/lib/index.d.ts +18 -0
- package/target/dts/protocol/files.d.ts +9 -0
- package/target/dts/protocol/index.d.ts +72 -0
- package/target/dts/runtime/reader.d.ts +34 -0
- package/target/dts/runtime/session.d.ts +42 -0
- package/target/dts/runtime/watch.d.ts +17 -0
- package/target/dts/store/git/index.d.ts +64 -0
- package/target/dts/store/git/remote.d.ts +70 -0
- package/target/dts/store/index.d.ts +102 -0
- package/target/dts/store/kafka/index.d.ts +2 -0
- package/target/dts/store/temporal/index.d.ts +2 -0
- package/target/esm/app/agent.js +84 -0
- package/target/esm/app/budget.js +95 -0
- package/target/esm/app/context.js +16 -0
- package/target/esm/app/hubs.js +76 -0
- package/target/esm/app/overview.js +39 -0
- package/target/esm/app/room.js +127 -0
- package/target/esm/app/rooms.js +120 -0
- package/target/esm/app/session.js +97 -0
- package/target/esm/app/view.js +52 -0
- package/target/esm/cli/agent.js +40 -0
- package/target/esm/cli/args.js +61 -0
- package/target/esm/cli/context.js +8 -0
- package/target/esm/cli/index.js +125 -0
- package/target/esm/cli/operator.js +147 -0
- package/target/esm/cli/session.js +39 -0
- package/target/esm/cli/shared.js +143 -0
- package/target/esm/cli/skill.js +54 -0
- package/target/esm/cli.js +14 -0
- package/target/esm/domain/decisions.js +178 -0
- package/target/esm/domain/index.js +16 -0
- package/target/esm/domain/profiles.js +96 -0
- package/target/esm/domain/signals.js +129 -0
- package/target/esm/home/index.js +88 -0
- package/target/esm/hub/index.js +212 -0
- package/target/esm/hub/signals.js +27 -0
- package/target/esm/index.js +9 -0
- package/target/esm/lib/index.js +202 -0
- package/target/esm/protocol/files.js +19 -0
- package/target/esm/protocol/index.js +217 -0
- package/target/esm/runtime/reader.js +64 -0
- package/target/esm/runtime/session.js +112 -0
- package/target/esm/runtime/watch.js +304 -0
- package/target/esm/store/git/index.js +346 -0
- package/target/esm/store/git/remote.js +700 -0
- package/target/esm/store/index.js +15 -0
- package/target/esm/store/kafka/index.js +3 -0
- package/target/esm/store/temporal/index.js +3 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// room: what any participant (the operator or an agent) does in a room: write messages and read them, follow work,
|
|
2
|
+
// check signals. Each operation takes who acts and typed input, and returns data; interfaces print it.
|
|
3
|
+
import * as D from '../domain/decisions.js';
|
|
4
|
+
import { isOpen, tasksIn } from '../domain/profiles.js';
|
|
5
|
+
import * as Signals from '../domain/signals.js';
|
|
6
|
+
import { OPERATOR } from '../home/index.js';
|
|
7
|
+
import { HIDDEN, append } from '../hub/index.js';
|
|
8
|
+
import * as HubSignals from '../hub/signals.js';
|
|
9
|
+
import { fail, note, short } from '../lib/index.js';
|
|
10
|
+
import { checkCollisions } from '../protocol/files.js';
|
|
11
|
+
import { ID_RE, TOKEN_RE, checkOutgoing, compose, link, lookup, ordered, rootOf, under } from '../protocol/index.js';
|
|
12
|
+
import { linked, loadRoomUnlocked, loadState, readerLock, saveState } from '../runtime/reader.js';
|
|
13
|
+
import { taskView } from './view.js';
|
|
14
|
+
export const isRef = (R) => (x) => R.hub.store.isRef(x);
|
|
15
|
+
// A message from the actor. The operator may omit `to`: the room, or the assignees when there are any.
|
|
16
|
+
export const send = ({ R, me }, x) => {
|
|
17
|
+
const P = R.profile;
|
|
18
|
+
const { all } = linked(R, me);
|
|
19
|
+
const re = x.re ? lookup(all, x.re) : null;
|
|
20
|
+
let to = [...new Set(x.to ?? [])];
|
|
21
|
+
const assign = x.assign ? [...new Set(x.assign)] : null;
|
|
22
|
+
if (!to.length && x.reply && re)
|
|
23
|
+
to = re.from === me ? re.to : [re.from, ...(x.all ? re.to.filter(t => t !== me) : [])];
|
|
24
|
+
if (!to.length && assign)
|
|
25
|
+
to = assign;
|
|
26
|
+
if (!to.length && me === OPERATOR)
|
|
27
|
+
to = ['*'];
|
|
28
|
+
if (!to.length)
|
|
29
|
+
fail('--to is required: agent ids, comma-separated, or *');
|
|
30
|
+
for (const t of to)
|
|
31
|
+
if (t !== '*' && !ID_RE.test(t))
|
|
32
|
+
fail(`bad recipient ${t}`);
|
|
33
|
+
if (x.state && !P.states.includes(x.state))
|
|
34
|
+
fail(`--state must be one of profile ${P.name}: ${P.states.join(', ')}`);
|
|
35
|
+
if (x.state && !re)
|
|
36
|
+
fail('--state belongs to a reply inside a piece of work; pass --re <message>');
|
|
37
|
+
for (const a of assign ?? [])
|
|
38
|
+
if (!ID_RE.test(a))
|
|
39
|
+
fail(`bad assignee ${a}`);
|
|
40
|
+
if (x.roles && !x.roles.split(',').every(r => {
|
|
41
|
+
const parts = r.split('=').map(v => v.trim());
|
|
42
|
+
return parts.length === 2 && ID_RE.test(parts[0]) && TOKEN_RE.test(parts[1]);
|
|
43
|
+
}))
|
|
44
|
+
fail('--roles takes "<id>=<label>, …"; labels mean what the room profile says');
|
|
45
|
+
let grant = null;
|
|
46
|
+
if (x.grant) {
|
|
47
|
+
const d = lookup(all, x.grant);
|
|
48
|
+
const g = D.grantOf(all, R.authorityAt, d.id) ?? fail(`${short(d.id)} is not an approved decision with a budget`);
|
|
49
|
+
if (g.revoked)
|
|
50
|
+
fail(`grant ${short(g.id)} was revoked`);
|
|
51
|
+
grant = g.id;
|
|
52
|
+
}
|
|
53
|
+
const entries = x.files ?? [];
|
|
54
|
+
checkCollisions(entries.map(e => e.path));
|
|
55
|
+
const text = compose(x.text, [
|
|
56
|
+
['To', to.join(', ')], ['Re', re?.id], ['Kind', x.task && P.opens], ['State', x.state], ['Assign', assign?.join(', ')],
|
|
57
|
+
['Roles', x.roles], ['Grant', grant], ['Hop', re && String(re.hopEff + 1)],
|
|
58
|
+
]);
|
|
59
|
+
checkOutgoing(text, isRef(R));
|
|
60
|
+
const id = append(R, me, text, entries);
|
|
61
|
+
if (to.length === 1 && to[0] === '*')
|
|
62
|
+
note(`addressed to the whole room: this ${x.task ? P.opens : 'message'} wakes nobody; name recipients (--to or --assign) to start work`);
|
|
63
|
+
return { id, to, files: entries.length };
|
|
64
|
+
};
|
|
65
|
+
// --- reading ---
|
|
66
|
+
// Who marked a message how (read, taken), from the marks that pass their check.
|
|
67
|
+
export const marks = (R, all) => Signals.marksOn(Signals.check(R, all, R.hub.store.signals(R.s)));
|
|
68
|
+
export const message = ({ R, me }, ref) => {
|
|
69
|
+
const { all } = linked(R, me);
|
|
70
|
+
const m = lookup(all, ref);
|
|
71
|
+
return { all, m, replies: [...all.values()].filter(x => x.reId === m.id), marks: marks(R, all).get(m.id) };
|
|
72
|
+
};
|
|
73
|
+
export const thread = ({ R, me }, ref) => {
|
|
74
|
+
const { all } = linked(R, me);
|
|
75
|
+
return { all, msgs: under(all, rootOf(all, lookup(all, ref))) };
|
|
76
|
+
};
|
|
77
|
+
// Open work that concerns the actor (the operator: all open work); with `all`, every piece of work.
|
|
78
|
+
export const tasks = ({ R, me }, { all: everything = false } = {}) => {
|
|
79
|
+
const P = R.profile;
|
|
80
|
+
const { all } = linked(R, me);
|
|
81
|
+
let ts = tasksIn(all, P);
|
|
82
|
+
if (!everything)
|
|
83
|
+
ts = ts.filter(t => isOpen(t, P) && (me === OPERATOR || t.requester === me || t.assignees.includes(me) || (!t.assignees.length && (t.root.to.includes(me) || t.root.to.includes('*')))));
|
|
84
|
+
ts.sort((a, b) => a.at - b.at);
|
|
85
|
+
const m = marks(R, all);
|
|
86
|
+
return { tasks: ts, views: ts.map(t => taskView(t, m.get(t.root.id)?.taken ?? [])) };
|
|
87
|
+
};
|
|
88
|
+
export const log = ({ R, me }, { agent, all: everything = false, n = 20 } = {}) => {
|
|
89
|
+
const { all } = linked(R, me);
|
|
90
|
+
return ordered([...all.values()].filter(m => (!agent || m.from === agent) && (everything || !(m.kind && HIDDEN.has(m.kind))))).slice(-n);
|
|
91
|
+
};
|
|
92
|
+
// The files of a message: one named path, or all of them.
|
|
93
|
+
export const files = ({ R, me }, ref, path) => {
|
|
94
|
+
const { all } = linked(R, me);
|
|
95
|
+
const m = lookup(all, ref);
|
|
96
|
+
if (path && !m.files.some(f => f.path === path))
|
|
97
|
+
fail(`no ${path} in ${short(m.id)}`);
|
|
98
|
+
return { m, files: m.files.filter(f => !path || f.path === path).map(f => ({ path: f.path, content: R.hub.store.file(m.id, f.path) })) };
|
|
99
|
+
};
|
|
100
|
+
export const members = ({ R, me }) => {
|
|
101
|
+
const { logs, all } = linked(R, me);
|
|
102
|
+
const cards = new Map(ordered([...all.values()].filter(m => m.kind === 'card' && !m.bad)).map(m => [m.from, m]));
|
|
103
|
+
return Object.entries(R.members).map(([id, info]) => ({ id, role: info.role ?? 'member',
|
|
104
|
+
messages: (logs.get(id) ?? []).filter(m => !(m.kind && HIDDEN.has(m.kind))).length, card: cards.get(id)?.subject ?? null }));
|
|
105
|
+
};
|
|
106
|
+
// --- signals ---
|
|
107
|
+
// The room's signals, each checked against its message and against what this reader accepted before; `repair` first
|
|
108
|
+
// restores the missing signals of the actor's own messages.
|
|
109
|
+
export const signals = ({ R, me }, { repair = false } = {}) => readerLock(R, me, () => {
|
|
110
|
+
const { logs, st } = loadRoomUnlocked(R, me);
|
|
111
|
+
const all = link(logs);
|
|
112
|
+
let repaired = null;
|
|
113
|
+
if (repair) {
|
|
114
|
+
repaired = 0;
|
|
115
|
+
for (const m of logs.get(me) ?? [])
|
|
116
|
+
repaired += HubSignals.emit(R, me, all, m);
|
|
117
|
+
}
|
|
118
|
+
const records = R.hub.store.signals(R.s);
|
|
119
|
+
const checked = Signals.check(R, all, records);
|
|
120
|
+
const p = Signals.pins(records, checked, st.signals);
|
|
121
|
+
saveState(R, me, { ...loadState(R, me), signals: p.accepted });
|
|
122
|
+
return { all, checked, alarms: [...p.alarms, ...checked.filter(c => !c.ok).map(c => `signal ${c.ref}: ${c.why}`)], repaired };
|
|
123
|
+
});
|
|
124
|
+
// What interfaces need to show and store a message's files: their sizes, and a path safe to write under.
|
|
125
|
+
export { withSizes } from '../hub/index.js';
|
|
126
|
+
export { checkPath } from '../protocol/files.js';
|
|
127
|
+
//# sourceMappingURL=room.js.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// rooms: the operator's rooms in a hub: list, create, switch, admit and remove members, archive, delete, move to a
|
|
2
|
+
// newer contract, and accept a new baseline for a log after reviewing an alarm.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { DEFAULT_PROFILE, parseProfile } from '../domain/profiles.js';
|
|
7
|
+
import { HOME, OPERATOR, config, guard, locked, setConfig } from '../home/index.js';
|
|
8
|
+
import { RESERVED_ROOMS, ROOM_MD, controlChange, profileText, room as roomOf, roomNames } from '../hub/index.js';
|
|
9
|
+
import { Fail, fail, note } from '../lib/index.js';
|
|
10
|
+
import { ID_RE, compose } from '../protocol/index.js';
|
|
11
|
+
import { loadState, readerLock, saveState } from '../runtime/reader.js';
|
|
12
|
+
import { allSessions, liveness } from '../runtime/session.js';
|
|
13
|
+
// The operator's terminal context picks the room unless one is named.
|
|
14
|
+
export const roomIn = (H, name) => roomOf(H, name ?? config().room);
|
|
15
|
+
// A room the store offers but that does not open (a history under another name) is an alarm, not a row.
|
|
16
|
+
const opens = (H, n) => {
|
|
17
|
+
try {
|
|
18
|
+
return roomOf(H, n);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
if (!(e instanceof Fail))
|
|
22
|
+
throw e;
|
|
23
|
+
note(`ALARM ${e.message}`);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export const rooms = (H, { archived = false } = {}) => {
|
|
28
|
+
const cur = config().room;
|
|
29
|
+
const view = (n, R) => {
|
|
30
|
+
const ss = allSessions().filter(s => s.hub === H.name && s.genesis === R.genesis);
|
|
31
|
+
return { name: n, current: n === cur && !R.archived, archived: R.archived, members: Object.keys(R.members).length,
|
|
32
|
+
active: ss.filter(s => liveness(s) === 'active').length, profile: R.profile.name, genesis: R.genesis };
|
|
33
|
+
};
|
|
34
|
+
const live = roomNames(H).flatMap(n => { const R = opens(H, n); return R ? [view(n, R)] : []; });
|
|
35
|
+
const gone = archived ? roomNames(H, true).flatMap(n => { const R = opens(H, n); return R ? [view(n, R)] : []; }) : [];
|
|
36
|
+
return [...live, ...gone];
|
|
37
|
+
};
|
|
38
|
+
// A room carries its profile from the start: later changes to the hub's profiles do not reach it.
|
|
39
|
+
export const create = (H, name, { profile } = {}) => {
|
|
40
|
+
if (!ID_RE.test(name) || RESERVED_ROOMS.has(name))
|
|
41
|
+
fail(`usage: ringi room create <id> [--profile <name>] (an id like fix-badge; not ${[...RESERVED_ROOMS].join(', ')})`);
|
|
42
|
+
const p = profile ?? H.contract.profile ?? DEFAULT_PROFILE;
|
|
43
|
+
const text = profileText(H, p) ?? fail(`hub ${H.name} carries no profile ${p}; add it with ringi hub amend -m "<why>" --attach <file>:profiles/${p}.json`);
|
|
44
|
+
parseProfile(text, `profile ${p}`);
|
|
45
|
+
locked(`hub-${H.name}`, () => {
|
|
46
|
+
const files = { 'ROOM.md': ROOM_MD(name), 'members.json': JSON.stringify({ [OPERATOR]: { role: 'root' } }, null, 2) + '\n', 'profile.json': text };
|
|
47
|
+
// A signed room's roster: who may sign its logs, starting with the root.
|
|
48
|
+
if (H.signed)
|
|
49
|
+
files.allowed_signers = H.store.rootRoster?.() ?? fail(`hub ${H.name}: its store cannot start a signed roster`);
|
|
50
|
+
// The genesis names the room; a random id makes it unique, so a room deleted and created again is a different room.
|
|
51
|
+
H.store.createRoom(name, OPERATOR, compose(`room ${name}`, [['To', '*'], ['Kind', 'room'], ['Room-Id', crypto.randomBytes(16).toString('hex')]]), files);
|
|
52
|
+
});
|
|
53
|
+
setConfig({ hub: H.name, room: name });
|
|
54
|
+
return { profile: p };
|
|
55
|
+
};
|
|
56
|
+
export const use = (H, name) => {
|
|
57
|
+
const R = roomOf(H, name);
|
|
58
|
+
setConfig({ hub: H.name, room: R.name });
|
|
59
|
+
return R;
|
|
60
|
+
};
|
|
61
|
+
// Removing, archiving or deleting in a signed hub needs revocation published to every replica: not in this version.
|
|
62
|
+
const notSigned = (H, what) => { if (H.signed)
|
|
63
|
+
fail(`room ${what} is not supported on signed hubs yet: a local change would come back with the next fetch`); };
|
|
64
|
+
export const admit = (H, R, who, key) => {
|
|
65
|
+
if (!H.signed || !H.store.admit)
|
|
66
|
+
fail('room admit is for signed hubs; on a local hub the operator\'s join adds members');
|
|
67
|
+
locked(`hub-${H.name}`, () => H.store.admit(R.s, who, key));
|
|
68
|
+
};
|
|
69
|
+
export const remove = (H, R, who) => {
|
|
70
|
+
notSigned(H, 'remove');
|
|
71
|
+
if (!R.members[who])
|
|
72
|
+
fail(`${who} is not a member of ${R.name}`);
|
|
73
|
+
if (who === OPERATOR)
|
|
74
|
+
fail('the operator stays in every room');
|
|
75
|
+
controlChange(R, OPERATOR, `remove ${who}`, m => { delete m[who]; });
|
|
76
|
+
};
|
|
77
|
+
export const archive = (H, R) => {
|
|
78
|
+
notSigned(H, 'archive');
|
|
79
|
+
if (R.archived)
|
|
80
|
+
fail(`${R.name} is already archived`);
|
|
81
|
+
const name = `${R.name}@${R.genesis.slice(0, 8)}`;
|
|
82
|
+
locked(`hub-${H.name}`, () => H.store.archiveRoom(R.s, name));
|
|
83
|
+
if (config().room === R.name)
|
|
84
|
+
setConfig({ room: null });
|
|
85
|
+
return name;
|
|
86
|
+
};
|
|
87
|
+
export const erase = (H, R) => {
|
|
88
|
+
notSigned(H, 'delete');
|
|
89
|
+
locked(`hub-${H.name}`, () => H.store.deleteRoom(R.s));
|
|
90
|
+
guard(() => fs.rmSync(path.join(HOME, 'readers', H.name, R.genesis), { recursive: true, force: true }));
|
|
91
|
+
if (config().room === R.name)
|
|
92
|
+
setConfig({ room: null });
|
|
93
|
+
};
|
|
94
|
+
// The room follows the hub's current contract from now on; nothing written before is reinterpreted.
|
|
95
|
+
export const rebase = (H, R) => controlChange(R, OPERATOR, `contract ${H.contract.version}`, () => { }, [['Contract', H.main]]);
|
|
96
|
+
// The operator accepts a new baseline for one log, for every reader of the room, after reviewing an alarm. A signed
|
|
97
|
+
// store resets its transport pin too, keeping the displaced head as evidence; signatures are never waived.
|
|
98
|
+
export const forgetLog = (H, R, id) => {
|
|
99
|
+
if (!ID_RE.test(id))
|
|
100
|
+
fail('usage: ringi forget <room>/<log>');
|
|
101
|
+
if (H.signed)
|
|
102
|
+
H.store.forgetLog?.(R.s, id);
|
|
103
|
+
const dir = path.join(HOME, 'readers', H.name, R.genesis);
|
|
104
|
+
let n = 0;
|
|
105
|
+
for (const f of fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith('.json')) : []) {
|
|
106
|
+
const reader = f.slice(0, -5);
|
|
107
|
+
readerLock(R, reader, () => {
|
|
108
|
+
const st = loadState(R, reader);
|
|
109
|
+
if (st.heads?.[id]) {
|
|
110
|
+
delete st.heads[id];
|
|
111
|
+
st.alarms = (st.alarms ?? []).filter(a => !a.startsWith(`log ${id} `) && !a.startsWith(`log ${id}:`));
|
|
112
|
+
saveState(R, reader, st);
|
|
113
|
+
n++;
|
|
114
|
+
}
|
|
115
|
+
H.store.unpin(R.s, reader, id);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return n;
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=rooms.js.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Session use cases preserve the operator's identity, seat and delegated scope.
|
|
2
|
+
import { OPERATOR, ensureHome, locked } from '../home/index.js';
|
|
3
|
+
import { controlChange, openHub, room as roomOf } from '../hub/index.js';
|
|
4
|
+
import { ID_RE } from '../protocol/index.js';
|
|
5
|
+
import { allSessions, bound, dropSession, liveness, ownSession, parseSelector, pidAlive, readSession, saveSession, seatHolder, whoAmI } from '../runtime/session.js';
|
|
6
|
+
import { fail, note, stamp } from '../lib/index.js';
|
|
7
|
+
import { inbox } from './agent.js';
|
|
8
|
+
import { tasks } from './room.js';
|
|
9
|
+
export { bound } from '../runtime/session.js';
|
|
10
|
+
export const briefing = (s) => {
|
|
11
|
+
const actor = bound();
|
|
12
|
+
return { session: s, actor, tasks: tasks(actor).views, inbox: inbox(actor) };
|
|
13
|
+
};
|
|
14
|
+
export const join = ({ hub, room, agent: id, scope }) => {
|
|
15
|
+
const me = whoAmI();
|
|
16
|
+
const agent = id ?? me.product ?? fail('--as <id> is required: this session\'s product is unknown');
|
|
17
|
+
if (!ID_RE.test(agent) || agent === OPERATOR)
|
|
18
|
+
fail(`bad agent id ${agent}`);
|
|
19
|
+
const H = openHub(hub), R = roomOf(H, room);
|
|
20
|
+
if (R.archived)
|
|
21
|
+
fail(`${room} is archived`);
|
|
22
|
+
if (H.signed) {
|
|
23
|
+
if (!H.store.joinKey)
|
|
24
|
+
fail(`hub ${H.name}: its store cannot make keys`);
|
|
25
|
+
const key = H.store.joinKey(R.s, agent);
|
|
26
|
+
if (!key.admitted)
|
|
27
|
+
return { agent, hub: H.name, room: R.name, session: null, key };
|
|
28
|
+
}
|
|
29
|
+
locked('sessions', () => {
|
|
30
|
+
const holder = seatHolder(H.name, R.genesis, agent, me.handle);
|
|
31
|
+
if (holder)
|
|
32
|
+
fail(`${room}/${agent} is held by another session (${holder.handle}, ${liveness(holder)}). A seat ends only with leave, or the operator's takeover: "continue ${room}/${agent}" in this chat`);
|
|
33
|
+
const mine = ownSession(me.handle);
|
|
34
|
+
if (mine?.product === 'watch' && mine.pid && pidAlive(mine.pid))
|
|
35
|
+
fail('this chat handed its seat to a live watcher; stop or leave it before joining another room');
|
|
36
|
+
if (mine && mine.handle !== me.handle)
|
|
37
|
+
dropSession(mine.handle);
|
|
38
|
+
const same = !!mine && mine.hub === H.name && mine.genesis === R.genesis && mine.agent === agent;
|
|
39
|
+
if (!R.members[agent]) {
|
|
40
|
+
if (H.signed)
|
|
41
|
+
fail(`${agent} is not in the roster of ${room}; the root admits its key first`);
|
|
42
|
+
controlChange(R, OPERATOR, `join ${agent}`, m => { m[agent] = { role: 'member', since: stamp() }; });
|
|
43
|
+
}
|
|
44
|
+
if (mine && !same)
|
|
45
|
+
note(`left ${mine.hub}/${mine.room} as ${mine.agent}: one room per session`);
|
|
46
|
+
saveSession({ handle: me.handle, product: me.product, hub: H.name, room: R.name, genesis: R.genesis, agent,
|
|
47
|
+
scope: scope ?? (same ? mine.scope : null), state: 'active', since: same ? mine.since : stamp(), lastSeen: stamp(), pid: me.pid });
|
|
48
|
+
});
|
|
49
|
+
return { agent, hub: H.name, room: R.name, session: readSession(me.handle), key: null };
|
|
50
|
+
};
|
|
51
|
+
export const resume = (selector) => {
|
|
52
|
+
const me = whoAmI();
|
|
53
|
+
ensureHome();
|
|
54
|
+
return locked('sessions', () => {
|
|
55
|
+
let target;
|
|
56
|
+
if (!selector) {
|
|
57
|
+
const mine = ownSession(me.handle);
|
|
58
|
+
if (!mine) {
|
|
59
|
+
const cands = allSessions().filter(s => liveness(s) !== 'active').sort((a, b) => Number(b.product === me.product) - Number(a.product === me.product));
|
|
60
|
+
if (!cands.length)
|
|
61
|
+
fail('nothing to continue: this chat was never joined, and no paused or gone session waits. The operator says "join <room> as <id>"');
|
|
62
|
+
fail(`this chat has no binding of its own. Say which one to take over:\n${cands.map(s => ` ringi continue ${s.hub}:${s.room}/${s.agent} (${liveness(s)}, ${s.product ?? '?'}, ${s.handle}${s.scope ? `, scope: ${s.scope}` : ''})`).join('\n')}`);
|
|
63
|
+
}
|
|
64
|
+
target = mine;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const sel = parseSelector(selector);
|
|
68
|
+
const hits = allSessions().filter(s => s.room === sel.room && s.agent === sel.agent && (!sel.hub || s.hub === sel.hub));
|
|
69
|
+
if (hits.length > 1)
|
|
70
|
+
fail(`several hubs have ${sel.room}/${sel.agent}; name one: <hub>:${sel.room}/${sel.agent}`);
|
|
71
|
+
target = hits[0] ?? fail(`no session holds ${selector}; the operator joins this chat with ringi join`);
|
|
72
|
+
}
|
|
73
|
+
if (target.handle !== me.handle) {
|
|
74
|
+
if (liveness(target) === 'active' || (target.product === 'watch' && target.pid && pidAlive(target.pid)))
|
|
75
|
+
fail(`${target.room}/${target.agent} is held by a live session (${target.handle}); it is not taken over`);
|
|
76
|
+
dropSession(target.handle);
|
|
77
|
+
note(`took over ${target.hub}/${target.room}/${target.agent} from ${target.handle}`);
|
|
78
|
+
}
|
|
79
|
+
const session = { ...target, handle: me.handle, product: target.handle === me.handle ? target.product : me.product, state: 'active',
|
|
80
|
+
since: target.handle === me.handle ? target.since : stamp(), lastSeen: stamp(), pid: me.pid ?? (target.handle === me.handle ? target.pid : null),
|
|
81
|
+
takenFrom: target.handle === me.handle ? target.takenFrom : undefined };
|
|
82
|
+
saveSession(session);
|
|
83
|
+
return session;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
export const stop = () => locked('sessions', () => {
|
|
87
|
+
const s = ownSession(whoAmI().handle) ?? fail('this session is in no room');
|
|
88
|
+
const stopped = { ...s, state: 'stopped', lastSeen: stamp() };
|
|
89
|
+
saveSession(stopped);
|
|
90
|
+
return stopped;
|
|
91
|
+
});
|
|
92
|
+
export const leave = () => locked('sessions', () => {
|
|
93
|
+
const s = ownSession(whoAmI().handle) ?? fail('this session is in no room');
|
|
94
|
+
dropSession(s.handle);
|
|
95
|
+
return s;
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// view: how messages, work, proposals and grants are shown: as text framed for people and models, and as the JSON
|
|
2
|
+
// shapes every interface (the CLI's --json, a control panel) returns.
|
|
3
|
+
import { bytes, when } from '../lib/index.js';
|
|
4
|
+
import { KNOWN } from '../protocol/index.js';
|
|
5
|
+
const id12 = (x) => x.slice(0, 12);
|
|
6
|
+
// Trailers a store writes about its own records (the git store's list of attached paths): shown as `Files:`, not again.
|
|
7
|
+
const STORED = new Set(['ringi-files']);
|
|
8
|
+
// Frames a message for a model: sender and how it is known, id, addressing, hop; the body is the peer's content.
|
|
9
|
+
export const render = (all, m, { max = 0, files = () => m.files } = {}) => {
|
|
10
|
+
const lines = [`=== ${id12(m.id)} · ${m.from} (${m.trust}) → ${m.to.join(', ') || '(nobody)'} · ${when(m.time)}`];
|
|
11
|
+
if (m.re || m.trailers.re) {
|
|
12
|
+
const p = m.reId ? all.get(m.reId) : undefined;
|
|
13
|
+
lines.push(`Re: ${id12(m.reId ?? m.re ?? m.trailers.re[0])}${p ? ` (${p.from}) ${p.subject}` : ' (not available)'}`);
|
|
14
|
+
}
|
|
15
|
+
if (m.bad)
|
|
16
|
+
lines.push(`Malformed: ${m.bad} (not delivered, not part of tasks)`);
|
|
17
|
+
if (m.kind)
|
|
18
|
+
lines.push(`Kind: ${m.kind}`);
|
|
19
|
+
if (m.state)
|
|
20
|
+
lines.push(`State: ${m.state}`);
|
|
21
|
+
if (m.assign)
|
|
22
|
+
lines.push(`Assign: ${m.assign.join(', ')}`);
|
|
23
|
+
if (m.roles)
|
|
24
|
+
lines.push(`Roles: ${Object.entries(m.roles).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
25
|
+
if (m.hopEff)
|
|
26
|
+
lines.push(`Hop: ${m.hop}${m.hopEff !== m.hop ? ` (effective ${m.hopEff})` : ''}`);
|
|
27
|
+
for (const [k, v] of Object.entries(m.trailers))
|
|
28
|
+
if (!KNOWN.has(k) && !STORED.has(k))
|
|
29
|
+
lines.push(`${k[0].toUpperCase()}${k.slice(1)}: ${v.join(', ')}`);
|
|
30
|
+
const fl = files();
|
|
31
|
+
if (fl.length)
|
|
32
|
+
lines.push(`Files: ${fl.map(f => `${f.path} (${bytes(f.size ?? 0)})`).join(', ')}`);
|
|
33
|
+
if (m.refused.length)
|
|
34
|
+
lines.push(`Refused: ${m.refused.map(f => `${f.path} (${f.why ?? `mode ${f.mode}`})`).join(', ')}`);
|
|
35
|
+
lines.push(`Subject: ${m.subject}`);
|
|
36
|
+
const body = max && m.body.length > max ? `${m.body.slice(0, max)}\n[… ${m.body.length - max} more chars: read ${id12(m.id)}]` : m.body;
|
|
37
|
+
if (body)
|
|
38
|
+
lines.push('', body);
|
|
39
|
+
lines.push(`=== end ${id12(m.id)}`);
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
};
|
|
42
|
+
export const oneLine = (m) => `${id12(m.id)} ${when(m.time).slice(5)} ${m.from} → ${m.to.join(',') || '-'} ${[m.kind, m.state].filter(Boolean).map(x => `[${x}] `).join('')}${m.subject}`;
|
|
43
|
+
export const asJson = (m) => ({ id: m.id, from: m.from, trust: m.trust, to: m.to, re: m.re, kind: m.kind, state: m.state,
|
|
44
|
+
assign: m.assign, roles: m.roles, hop: m.hop, hopEff: m.hopEff, bad: m.bad, time: new Date(m.time * 1000).toISOString(), subject: m.subject,
|
|
45
|
+
body: m.body, files: m.files.map(f => ({ path: f.path, size: f.size })), refused: m.refused, trailers: m.trailers });
|
|
46
|
+
export const taskView = (t, taken) => ({ id: t.root.id, subject: t.root.subject, requester: t.requester,
|
|
47
|
+
assignees: t.assignees, roles: t.roles, taken, grant: t.grant, state: t.state, by: t.by, at: new Date(t.at * 1000).toISOString() });
|
|
48
|
+
export const proposalView = (x) => ({ id: x.proposal.id, subject: x.proposal.subject, from: x.proposal.from,
|
|
49
|
+
decider: x.decider, state: x.state, budget: x.budget, stamps: x.stamps, missing: x.missing });
|
|
50
|
+
export const grantView = (x, usage) => ({ grant: x.grant.id, proposal: x.proposal.id,
|
|
51
|
+
subject: x.proposal.subject, budget: x.grant.budget, revoked: x.grant.revoked, usage });
|
|
52
|
+
//# sourceMappingURL=view.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Agent interface: parse options, call the app, render its result.
|
|
2
|
+
import * as Agent from '../app/agent.js';
|
|
3
|
+
import { asJson, render } from '../app/view.js';
|
|
4
|
+
import { fail, short, when } from '../lib/index.js';
|
|
5
|
+
import { dump, flag, list, opt, out, textOf } from './args.js';
|
|
6
|
+
const options = (o) => ({ peek: flag(o, 'peek'), limit: opt(o, 'limit') ? Number(opt(o, 'limit')) : undefined });
|
|
7
|
+
export const showInbox = ({ R, me, s }, res, o) => {
|
|
8
|
+
if (flag(o, 'json'))
|
|
9
|
+
return dump(res.msgs.map(asJson));
|
|
10
|
+
if (!res.msgs.length)
|
|
11
|
+
return out('inbox: nothing new');
|
|
12
|
+
out(`inbox: ${res.msgs.length} new for ${me} in ${R.hub.name}/${R.name} (senders ${R.hub.signed ? 'verified by signature' : 'asserted, not verified'})${s?.scope ? `\nyour scope: ${s.scope}` : ''}`);
|
|
13
|
+
for (const m of res.msgs)
|
|
14
|
+
out(`\n${render(res.all, m, { max: 4000 })}`);
|
|
15
|
+
if (res.rest)
|
|
16
|
+
out(`\n(${res.rest} more: run ringi agent inbox again)`);
|
|
17
|
+
};
|
|
18
|
+
export const inbox = (a, o) => showInbox(a, Agent.inbox(a, options(o)), o);
|
|
19
|
+
export const take = (a, o) => {
|
|
20
|
+
const { m, made } = Agent.take(a, o._[0] ?? fail('usage: ringi agent take <work>'));
|
|
21
|
+
out(`${made ? 'took' : 'already took'} ${short(m.id)}: ${m.subject}`);
|
|
22
|
+
};
|
|
23
|
+
export const whoami = (a) => {
|
|
24
|
+
const { session: s, profile, members } = Agent.whoami(a);
|
|
25
|
+
out(`${s.agent} in ${s.hub}/${s.room} (genesis ${short(s.genesis)}, profile ${profile}), session ${s.handle}, ${s.state}\nscope: ${s.scope ?? 'none recorded; ask the operator what to accept'}\nmembers: ${members.join(', ')}`);
|
|
26
|
+
};
|
|
27
|
+
export const card = (a, o) => out(`card ${short(Agent.card(a, textOf(o)))}`);
|
|
28
|
+
export const ping = (a, o) => {
|
|
29
|
+
const r = Agent.ping(a, { to: o._[0] ?? fail('usage: ringi agent ping <id> [-m <note>] [--next <s>] [--re <msg>]'),
|
|
30
|
+
text: list(o, 'm').length ? list(o, 'm').join('\n\n') : undefined, next: opt(o, 'next') ? Number(opt(o, 'next')) : undefined, re: opt(o, 're') });
|
|
31
|
+
out(`ping ${short(r.id)} to ${r.to}${r.previous ? (r.previous.answeredAt !== null ? `; ${r.to} answered the previous one at ${when(r.previous.answeredAt)}` : `; no pong to the previous one (${short(r.previous.id)})`) : ''}`);
|
|
32
|
+
};
|
|
33
|
+
export const wait = async (a, o) => {
|
|
34
|
+
const r = await Agent.wait(a, { ...options(o), max: opt(o, 'max') === undefined ? undefined : Number(opt(o, 'max')) });
|
|
35
|
+
if (r.inbox)
|
|
36
|
+
return showInbox(a, r.inbox, o);
|
|
37
|
+
out(`wait: nothing for ${a.me} in ${Math.round(r.seconds)}s`);
|
|
38
|
+
process.exitCode = 3;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// args: the command line: options, message text, files to attach, and printing to stdout.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fail } from '../lib/index.js';
|
|
5
|
+
import { checkPath } from '../app/room.js';
|
|
6
|
+
export const out = (s = '') => { process.stdout.write(s.endsWith('\n') ? s : `${s}\n`); };
|
|
7
|
+
export const dump = (v) => out(JSON.stringify(v, null, 2));
|
|
8
|
+
const BOOLS = new Set(['task', 'peek', 'all', 'json', 'help', 'once', 'project', 'follow', 'short', 'yes', 'require-grant', 'md', 'archived', 'signed', 'repair', 'handoff']);
|
|
9
|
+
const MULTI = new Set(['m', 'attach', 'set']);
|
|
10
|
+
const SHORT = { m: 'm', F: 'file', n: 'n', o: 'out', h: 'help' };
|
|
11
|
+
export const parseArgs = (argv) => {
|
|
12
|
+
const o = { _: [] };
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const a = argv[i];
|
|
15
|
+
let key;
|
|
16
|
+
let val;
|
|
17
|
+
if (a === '--') {
|
|
18
|
+
o._.push(...argv.slice(i + 1));
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
if (a.startsWith('--'))
|
|
22
|
+
[key, val] = a.slice(2).split(/=(.*)/s);
|
|
23
|
+
else if (/^-[a-zA-Z]$/.test(a))
|
|
24
|
+
key = SHORT[a[1]] ?? fail(`unknown option ${a}`);
|
|
25
|
+
else {
|
|
26
|
+
o._.push(a);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (BOOLS.has(key)) {
|
|
30
|
+
o[key] = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (val === undefined)
|
|
34
|
+
val = argv[++i] ?? fail(`--${key} needs a value`);
|
|
35
|
+
if (MULTI.has(key))
|
|
36
|
+
o[key] = [...(o[key] ?? []), val];
|
|
37
|
+
else
|
|
38
|
+
o[key] = val;
|
|
39
|
+
}
|
|
40
|
+
return o;
|
|
41
|
+
};
|
|
42
|
+
export const opt = (o, key) => typeof o[key] === 'string' ? o[key] : undefined;
|
|
43
|
+
export const list = (o, key) => Array.isArray(o[key]) ? o[key] : [];
|
|
44
|
+
export const flag = (o, key) => o[key] === true;
|
|
45
|
+
export const textOf = (o) => {
|
|
46
|
+
const file = opt(o, 'file');
|
|
47
|
+
const text = list(o, 'm').length ? list(o, 'm').join('\n\n') : file ? fs.readFileSync(file === '-' ? 0 : file, 'utf8') : '';
|
|
48
|
+
return text.replace(/\r\n/g, '\n').trim() || fail('empty message: pass -m "<subject>" [-m "<body>"] or -F <file>');
|
|
49
|
+
};
|
|
50
|
+
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap(e => e.name === '.git' ? [] : e.isDirectory() ? walk(path.join(d, e.name)) : e.isFile() ? [path.join(d, e.name)] : []);
|
|
51
|
+
// `<src>[:<dest>]` → entries; a directory source expands to its regular files.
|
|
52
|
+
export const attachments = (spec) => {
|
|
53
|
+
const i = spec.indexOf(':');
|
|
54
|
+
const src = path.resolve(i < 0 ? spec : spec.slice(0, i));
|
|
55
|
+
const dest = checkPath((i < 0 ? `files/${path.basename(src)}` : spec.slice(i + 1)).replace(/^\/+|\/+$/g, ''));
|
|
56
|
+
const mode = (f) => fs.statSync(f).mode & 0o111 ? '100755' : '100644';
|
|
57
|
+
if (!fs.statSync(src).isDirectory())
|
|
58
|
+
return [{ path: dest, file: src, mode: mode(src) }];
|
|
59
|
+
return walk(src).map(f => ({ path: checkPath(`${dest}/${path.relative(src, f).split(path.sep).join('/')}`), file: f, mode: mode(f) }));
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=args.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// context: who runs a command and in which room, from the command line.
|
|
2
|
+
import * as Context from '../app/context.js';
|
|
3
|
+
import { words } from '../lib/index.js';
|
|
4
|
+
import { opt } from './args.js';
|
|
5
|
+
export { isRef } from '../app/room.js';
|
|
6
|
+
export const parseTo = (v) => [...new Set(words(v))];
|
|
7
|
+
export const operatorCtx = (o) => Context.operator({ hub: opt(o, 'hub'), room: opt(o, 'room') });
|
|
8
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// ringi: a distributed agent coordinator (docs/spec/hub-1.md). This file is the command surface: what the operator, a session
|
|
2
|
+
// and an agent may run, and how each command maps onto hubs, rooms, sessions and the watcher.
|
|
3
|
+
import { about, agent } from '../app/context.js';
|
|
4
|
+
import { fail, words } from '../lib/index.js';
|
|
5
|
+
import { flag, opt, out, parseArgs } from './args.js';
|
|
6
|
+
import { watch } from '../app/agent.js';
|
|
7
|
+
import { card, inbox, ping, take, wait, whoami } from './agent.js';
|
|
8
|
+
import { operatorCtx } from './context.js';
|
|
9
|
+
import { forget, hub, roomCmd, sessions, where } from './operator.js';
|
|
10
|
+
import { join, leave, resume, stop } from './session.js';
|
|
11
|
+
import { cat, decide, get, grants, log, profileCmd, proposals, propose, read, reply, send, signals, stamp_, tasks, thread, who } from './shared.js';
|
|
12
|
+
import { skill } from './skill.js';
|
|
13
|
+
// --- commands ---
|
|
14
|
+
// [command, who runs it, what it does]: the one table behind `ringi help` and COMMANDS.md.
|
|
15
|
+
export const TABLE = [
|
|
16
|
+
['hub [list]', 'operator', 'the hubs; `local` exists by default; * marks the current one'],
|
|
17
|
+
['hub create <name> [--signed [--url <git url>]]', 'operator', 'another hub: asserted senders, or signed with a root key made on this machine; --url publishes it'],
|
|
18
|
+
['hub add <name> <git url> --root <public key|file> [--genesis <sha>]', 'operator', 'a replica of a signed hub; the root key comes from you, out of band, never from the hub'],
|
|
19
|
+
['hub switch <name>', 'operator', 'change the terminal\'s current hub; agents stay where they are'],
|
|
20
|
+
['hub amend -m <subject> [--set <key>=<value>]... [--authority <file>] [--attach <file>:profiles/<name>.json]...', 'operator', 'refine the shared contract: version, capabilities, default profile, authority table, profiles'],
|
|
21
|
+
['room list [--archived]', 'operator', 'rooms of the current hub'],
|
|
22
|
+
['room create <id> [--profile <name>]', 'operator', 'a room for one piece of work, following a profile; becomes the current room'],
|
|
23
|
+
['room switch <id>', 'operator', 'change the terminal\'s current room; agents stay where they are'],
|
|
24
|
+
['room admit <agent> <public key|file>', 'operator', 'signed hubs: admit the key an agent made on its own machine'],
|
|
25
|
+
['room remove <agent>', 'operator', 'local hubs: take an agent out of the current room'],
|
|
26
|
+
['room archive <id>', 'operator', 'local hubs: read-only from now on; wakes nobody; its sessions end'],
|
|
27
|
+
['room delete <id> --yes', 'operator', 'local hubs: remove a room\'s history for good'],
|
|
28
|
+
['room rebase-contract <id>', 'operator', 'move a room onto the hub\'s current contract'],
|
|
29
|
+
['where [-n n] [--short] [--follow]', 'operator', 'hub, room, sessions (active, stopped, gone), open work and proposals, latest messages'],
|
|
30
|
+
['send -m <subject> [-m <body>]... [--to <ids>] [--task] [--re <msg>] [--state <s>] [--assign <ids>] [--roles <id>=<label>,...] [--grant <decision>] [--attach <src>[:<dest>]]...', 'operator', 'a message as operator; without --to it goes to the room and wakes nobody; --task opens work of the room\'s profile'],
|
|
31
|
+
['propose -m <subject> [-m <body>] --decider <id> [--reviewers <ids>] [--due 2h|<ISO>] [--budget runs=<n>,run-seconds=<n>,concurrency=<n>] [--supersedes <proposal>]', 'both', 'a proposal; it circulates to the reviewers and the decider'],
|
|
32
|
+
['stamp <proposal> approve|object|abstain -m <reason>', 'both', 'a named reviewer\'s stamp on that exact revision'],
|
|
33
|
+
['decide <proposal> approved|rejected|revoked -m <reason>', 'both', 'the named decider, with authority; an approval with a budget is a grant'],
|
|
34
|
+
['proposals [--all] | grants', 'both', 'open proposals with their stamps; grants with their usage'],
|
|
35
|
+
['tasks [--all] | log [-n n] | read <msg> | thread <msg> | cat <msg> [<path>] | get <msg> [-o <dir>] | who', 'both', 'reading the room (agents: ringi agent …)'],
|
|
36
|
+
['signals [--all] [--repair]', 'both', 'what happened (signals) and where attention is (read and taken marks), each checked; --repair restores your missing signals'],
|
|
37
|
+
['profile', 'both', 'the room\'s profile: what a piece of work is, its states, who may move it'],
|
|
38
|
+
['sessions | sessions forget <handle>', 'operator', 'the chats the CLI remembers; drop a stale one'],
|
|
39
|
+
['forget <room>/<log>', 'operator', 'after a changed-history alarm: accept a new baseline for that log'],
|
|
40
|
+
['watch [<hub>:]<room>/<agent> --exec <cmd> [--handoff] [--scope <text>] [--once] [--require-grant] [--from <ids>] [--max-hop n] [--max-runs n] [--window s] [--interval s]', 'operator', 'headless turns for one agent: answers pings, spends grants, writes usage receipts; --handoff, run from the agent\'s own chat, moves its seat and scope to the watcher; exits with the failed turn\'s status'],
|
|
41
|
+
['skill install [--project] | uninstall | status | show', 'operator', 'the skill for Claude Code, Codex and Cursor'],
|
|
42
|
+
['join <hub> <room> [--as <id>] [--scope <text>]', 'session', 'bind this chat to a room as an agent, with what the operator delegates'],
|
|
43
|
+
['continue [[<hub>:]<room>/<agent>]', 'session', 'resume this chat\'s binding, also after a restart; naming one takes over a paused or gone one'],
|
|
44
|
+
['stop', 'session', 'pause: keep the seat, react to nothing'],
|
|
45
|
+
['leave', 'session', 'release the seat'],
|
|
46
|
+
['agent whoami', 'agent', 'identity, room, profile, scope'],
|
|
47
|
+
['agent inbox [--peek] [--limit n]', 'agent', 'what is new for you; marks it seen and leaves a read mark on each (--peek: neither)'],
|
|
48
|
+
['agent take <work>', 'agent', 'a taken mark: you are on it; others see it in tasks and read. It changes no state'],
|
|
49
|
+
['agent send … | reply <msg> -m … [--state <s>] [--assign <ids>] [--roles …] [--grant <decision>] [--attach …] [--all]', 'agent', 'write; answer the message you act on'],
|
|
50
|
+
['agent propose | stamp | decide | proposals | grants', 'agent', 'decisions, as the joined agent'],
|
|
51
|
+
['agent tasks | log | read | thread | cat | get | who | signals | profile', 'agent', 'reading, as the joined agent'],
|
|
52
|
+
['agent ping <id> [-m <note>] [--next <s>] | agent wait [--max <s>] | agent card -m <text>', 'agent', 'heartbeats; hold the line only when the operator asks; who you are'],
|
|
53
|
+
];
|
|
54
|
+
const helpText = () => {
|
|
55
|
+
const groups = [['operator', 'operator: in a terminal, in the current hub and room'], ['session', 'session: in a chat, on the operator\'s words'], ['agent', 'agent: during a turn, in the session\'s own room'], ['both', 'operator in a terminal, or an agent as ringi agent …']];
|
|
56
|
+
const width = 56;
|
|
57
|
+
const line = ([cmd, , what]) => cmd.length > width ? ` ringi ${cmd}\n ${' '.repeat(width + 6)} ${what}` : ` ringi ${cmd.padEnd(width)} ${what}`;
|
|
58
|
+
return [`ringi ${about().version}: distributed agent coordinator. Home: ${about().home} (RINGI_HOME).`,
|
|
59
|
+
...groups.flatMap(([w, title]) => ['', title, ...TABLE.filter(r => r[1] === w).map(line)]),
|
|
60
|
+
'', 'message ids are store revisions (git: commit shas); any unique prefix of 4+ characters works. States and roles come from the room\'s profile: ringi profile'].join('\n');
|
|
61
|
+
};
|
|
62
|
+
export const helpMarkdown = () => ['# ringi commands', '',
|
|
63
|
+
'One table. **operator**: the human in a terminal, in the current hub and room. **session**: run in a chat on the operator\'s words. **agent**: what an agent runs during a turn, as `ringi agent …`, always in its session\'s own room. **both**: the operator directly, or an agent under `ringi agent`. Generated by `ringi help --md`; a test keeps this file in sync.', '',
|
|
64
|
+
'| command | who | what |', '|---|---|---|',
|
|
65
|
+
...TABLE.map(([cmd, w, what]) => `| \`ringi ${cmd.replace(/\|/g, '\\|')}\` | ${w} | ${what} |`), '',
|
|
66
|
+
'Message ids are store revisions (git: commit shas), any unique prefix of 4+ characters. States and roles come from the room\'s profile (`ringi profile`).', ''].join('\n');
|
|
67
|
+
const SHARED = { send, reply, read, thread, tasks, log, cat, get, who, signals, profile: profileCmd, propose, stamp: stamp_, decide, proposals, grants };
|
|
68
|
+
const AGENT = { ...SHARED, inbox, whoami, status: whoami, card, ping, wait, take };
|
|
69
|
+
// A reader that stops early (`ringi log | head`) closes the pipe: that ends the output, not the command's work.
|
|
70
|
+
const quietPipe = (e) => { if (e.code === 'EPIPE')
|
|
71
|
+
process.exit(process.exitCode ?? 0);
|
|
72
|
+
else
|
|
73
|
+
throw e; };
|
|
74
|
+
export const main = async (argv = process.argv.slice(2)) => {
|
|
75
|
+
if (!process.stdout.listeners('error').includes(quietPipe))
|
|
76
|
+
process.stdout.on('error', quietPipe);
|
|
77
|
+
const [cmd = 'help', ...rest] = argv;
|
|
78
|
+
const o = cmd === 'agent' ? { _: [] } : parseArgs(rest);
|
|
79
|
+
if (['--version', '-v', 'version'].includes(cmd)) {
|
|
80
|
+
const b = about();
|
|
81
|
+
return out(`ringi ${b.version}; hub contract ${b.contract}; capabilities ${b.capabilities.join(' ')}`);
|
|
82
|
+
}
|
|
83
|
+
if (['help', '--help', '-h'].includes(cmd))
|
|
84
|
+
return flag(o, 'md') ? void process.stdout.write(helpMarkdown()) : out(helpText());
|
|
85
|
+
if (cmd === 'skill')
|
|
86
|
+
return skill(o);
|
|
87
|
+
if (cmd === 'agent') {
|
|
88
|
+
const [sub = 'whoami', ...more] = rest;
|
|
89
|
+
const fn = AGENT[sub] ?? fail(`unknown: ringi agent ${sub}; see ringi help`);
|
|
90
|
+
return fn(agent(), parseArgs(more));
|
|
91
|
+
}
|
|
92
|
+
if (cmd === 'hub')
|
|
93
|
+
return hub(o);
|
|
94
|
+
if (cmd === 'room')
|
|
95
|
+
return roomCmd(o);
|
|
96
|
+
if (cmd === 'where')
|
|
97
|
+
return where(o);
|
|
98
|
+
if (cmd === 'sessions')
|
|
99
|
+
return sessions(o);
|
|
100
|
+
if (cmd === 'forget')
|
|
101
|
+
return forget(o);
|
|
102
|
+
if (cmd === 'watch') {
|
|
103
|
+
const num = (k) => opt(o, k) === undefined ? undefined : Number(opt(o, k));
|
|
104
|
+
process.exitCode = await watch({ seat: o._[0] ?? fail('usage: ringi watch <room>/<agent> --exec <cmd> [--scope <text>] [--once] [--require-grant] …'),
|
|
105
|
+
exec: opt(o, 'exec') ?? fail('--exec <cmd> is required: the command that starts a headless turn'),
|
|
106
|
+
hub: opt(o, 'hub'), scope: opt(o, 'scope'), once: flag(o, 'once'), requireGrant: flag(o, 'require-grant'), handoff: flag(o, 'handoff'),
|
|
107
|
+
interval: num('interval'), from: opt(o, 'from') ? words(opt(o, 'from')) : undefined, maxHop: num('max-hop'),
|
|
108
|
+
maxRuns: num('max-runs'), window: num('window') }, out);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (cmd === 'join')
|
|
112
|
+
return join(o);
|
|
113
|
+
if (cmd === 'continue')
|
|
114
|
+
return resume(o);
|
|
115
|
+
if (cmd === 'stop')
|
|
116
|
+
return stop();
|
|
117
|
+
if (cmd === 'leave')
|
|
118
|
+
return leave();
|
|
119
|
+
if (SHARED[cmd])
|
|
120
|
+
return SHARED[cmd](operatorCtx(o), o);
|
|
121
|
+
if (['inbox', 'whoami', 'ping', 'wait', 'card'].includes(cmd))
|
|
122
|
+
fail(`${cmd} is an agent command: ringi agent ${cmd}`);
|
|
123
|
+
fail(`unknown command ${cmd}; see ringi help`);
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=index.js.map
|