agent-orchestrator-kit 0.7.0 → 0.9.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.
@@ -0,0 +1,63 @@
1
+ function numOrNull(value) {
2
+ if (value == null || value === '') return null;
3
+ const n = Number(String(value).replace(/,/g, ''));
4
+ return Number.isFinite(n) ? n : null;
5
+ }
6
+
7
+ export function parseAmpUsageDetails(text) {
8
+ const raw = String(text || '');
9
+ const costMatch = /(?:^|\n)Cost:\s*\$([0-9]+(?:\.[0-9]+)?)/.exec(raw);
10
+ const totalMatch = /Total tokens:\s*([\d,]+)/i.exec(raw);
11
+ const inputMatch = /Input tokens:\s*([\d,]+)(?:\s*\(([\d,]+)\s*cache reads\))?/i.exec(raw);
12
+ const outputMatch = /Output tokens:\s*([\d,]+)/i.exec(raw);
13
+ const models = [];
14
+ const tableStart = raw.search(/##\s*Models\b/i);
15
+ if (tableStart >= 0) {
16
+ const section = raw.slice(tableStart);
17
+ const rowRe = /^\|\s*(?!Model\b|[-: ]+)([^|]+?)\s*\|\s*([\d,]+)\s*\|\s*([\d,]+)\s*\|\s*([\d,]+)\s*\|\s*\$([0-9]+(?:\.[0-9]+)?)\s*\|/gim;
18
+ let row;
19
+ while ((row = rowRe.exec(section))) {
20
+ models.push({
21
+ model: row[1].trim(),
22
+ requests: numOrNull(row[2]),
23
+ inputTokens: numOrNull(row[3]),
24
+ outputTokens: numOrNull(row[4]),
25
+ costUsd: numOrNull(row[5]),
26
+ });
27
+ }
28
+ }
29
+ return {
30
+ costUsd: costMatch ? numOrNull(costMatch[1]) : null,
31
+ totalTokens: totalMatch ? numOrNull(totalMatch[1]) : null,
32
+ inputTokens: inputMatch ? numOrNull(inputMatch[1]) : null,
33
+ cacheReadTokens: inputMatch && inputMatch[2] ? numOrNull(inputMatch[2]) : null,
34
+ outputTokens: outputMatch ? numOrNull(outputMatch[1]) : null,
35
+ models,
36
+ };
37
+ }
38
+
39
+ export function ampAgentMode(thread) {
40
+ if (!thread || typeof thread !== 'object') return null;
41
+ const direct = thread.agentMode || (thread.meta && thread.meta.agentMode);
42
+ if (direct == null || String(direct).trim() === '') return null;
43
+ return String(direct).trim();
44
+ }
45
+
46
+ function compactModel(value) {
47
+ return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '').replace(/(\d)p(\d)/g, '$1$2');
48
+ }
49
+
50
+ export function matchAmpUsageModel(displayName, sourceModels) {
51
+ const wanted = compactModel(displayName);
52
+ if (!wanted) return displayName;
53
+ let best = null;
54
+ for (const id of sourceModels || []) {
55
+ const compact = compactModel(id);
56
+ if (!compact) continue;
57
+ if (compact === wanted || compact.includes(wanted) || wanted.includes(compact.replace(/^accountsfireworkmodels/, ''))) {
58
+ best = id;
59
+ break;
60
+ }
61
+ }
62
+ return best || displayName;
63
+ }
@@ -0,0 +1,50 @@
1
+ function numOrNull(value) {
2
+ if (value == null || value === '') return null;
3
+ const n = Number(value);
4
+ return Number.isFinite(n) ? n : null;
5
+ }
6
+
7
+ const GROK_46 = {
8
+ inputPerM: 2,
9
+ cachedPerM: 0.5,
10
+ outputPerM: 6,
11
+ longInputPerM: 4,
12
+ longCachedPerM: 1,
13
+ longOutputPerM: 12,
14
+ longAt: 200000,
15
+ };
16
+
17
+ function ratesForModel(model) {
18
+ const id = String(model || '').toLowerCase();
19
+ if (!id) return null;
20
+ let rates = null;
21
+ if (id.includes('grok-4.6') || id.includes('grok-4-6')) rates = { ...GROK_46 };
22
+ else if (id.includes('grok-4.5') || id.includes('grok-4-5')) {
23
+ rates = { ...GROK_46, cachedPerM: 0.3, longCachedPerM: 0.6 };
24
+ } else {
25
+ return null;
26
+ }
27
+ if (id.includes('fast')) {
28
+ for (const key of ['inputPerM', 'cachedPerM', 'outputPerM', 'longInputPerM', 'longCachedPerM', 'longOutputPerM']) {
29
+ rates[key] *= 2;
30
+ }
31
+ }
32
+ return rates;
33
+ }
34
+
35
+ export function estimateCursorCostUsd({ model, inputTokens, outputTokens, cacheReadTokens } = {}) {
36
+ const rates = ratesForModel(model);
37
+ if (!rates) return null;
38
+ const input = numOrNull(inputTokens);
39
+ const output = numOrNull(outputTokens) ?? 0;
40
+ if (input == null && output == 0) return null;
41
+ const totalInput = input ?? 0;
42
+ const cached = Math.min(numOrNull(cacheReadTokens) ?? 0, totalInput);
43
+ const fresh = Math.max(0, totalInput - cached);
44
+ const long = totalInput >= rates.longAt;
45
+ const inputRate = long ? rates.longInputPerM : rates.inputPerM;
46
+ const cachedRate = long ? rates.longCachedPerM : rates.cachedPerM;
47
+ const outputRate = long ? rates.longOutputPerM : rates.outputPerM;
48
+ const usd = (fresh * inputRate + cached * cachedRate + output * outputRate) / 1e6;
49
+ return Math.round(usd * 10000) / 10000;
50
+ }
@@ -0,0 +1,99 @@
1
+ const DISPLAY_TIMEZONE = 'Europe/Kyiv';
2
+
3
+ function pad2(value) {
4
+ return String(value).padStart(2, '0');
5
+ }
6
+
7
+ export function parseFlexibleIso(value) {
8
+ if (value == null || value === '') return NaN;
9
+ if (typeof value === 'number' && Number.isFinite(value)) {
10
+ return value < 1e12 ? value * 1000 : value;
11
+ }
12
+ let raw = String(value).trim();
13
+ if (!raw) return NaN;
14
+ raw = raw.replace(/^(\d{4}-\d{2}-\d{2})[ ]+(\d{2}:)/, '$1T$2');
15
+ raw = raw.replace(/(\.\d{3})\d*\.000(?=Z$|[+-]\d{2}:?\d{2}$)/i, '$1');
16
+ raw = raw.replace(/(\.\d{3})\d+(?=Z$|[+-]\d{2}:?\d{2}$)/i, '$1');
17
+ raw = raw.replace(/([+-]\d{2})(\d{2})$/, '$1:$2');
18
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?$/.test(raw)) raw += 'Z';
19
+ const ms = Date.parse(raw);
20
+ return Number.isFinite(ms) ? ms : NaN;
21
+ }
22
+
23
+ export function formatUtcIso(value) {
24
+ const ms = parseFlexibleIso(value);
25
+ if (!Number.isFinite(ms)) return null;
26
+ return new Date(ms).toISOString();
27
+ }
28
+
29
+ export function nowUtcIso(now = Date.now()) {
30
+ return formatUtcIso(now);
31
+ }
32
+
33
+ function kyivOffset(ms) {
34
+ const d = new Date(ms);
35
+ const fmt = new Intl.DateTimeFormat('en-US', {
36
+ timeZone: DISPLAY_TIMEZONE,
37
+ timeZoneName: 'longOffset',
38
+ hour: '2-digit',
39
+ });
40
+ const name = fmt.formatToParts(d).find((part) => part.type === 'timeZoneName')?.value || '';
41
+ const match = /GMT([+-])(\d{1,2})(?::(\d{2}))?/.exec(name);
42
+ if (match) {
43
+ return `${match[1]}${pad2(match[2])}:${match[3] || '00'}`;
44
+ }
45
+ const utc = new Date(d.toLocaleString('en-US', { timeZone: 'UTC' })).getTime();
46
+ const kyiv = new Date(d.toLocaleString('en-US', { timeZone: DISPLAY_TIMEZONE })).getTime();
47
+ const minutes = Math.round((kyiv - utc) / 60000);
48
+ const sign = minutes >= 0 ? '+' : '-';
49
+ const abs = Math.abs(minutes);
50
+ return `${sign}${pad2(Math.floor(abs / 60))}:${pad2(abs % 60)}`;
51
+ }
52
+
53
+ function formatKyivIso(value) {
54
+ const ms = parseFlexibleIso(value);
55
+ if (!Number.isFinite(ms)) return null;
56
+ const d = new Date(ms);
57
+ const parts = Object.fromEntries(
58
+ new Intl.DateTimeFormat('en-US', {
59
+ timeZone: DISPLAY_TIMEZONE,
60
+ year: 'numeric',
61
+ month: '2-digit',
62
+ day: '2-digit',
63
+ hour: '2-digit',
64
+ minute: '2-digit',
65
+ second: '2-digit',
66
+ hourCycle: 'h23',
67
+ }).formatToParts(d).map((part) => [part.type, part.value]),
68
+ );
69
+ return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}.${String(ms % 1000).padStart(3, '0')}${kyivOffset(ms)}`;
70
+ }
71
+
72
+ export function formatKyivDisplay(value) {
73
+ const iso = formatKyivIso(value);
74
+ if (!iso) return '—';
75
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?([+-]\d{2}:\d{2})$/.exec(iso);
76
+ if (!match) return iso;
77
+ return `${match[3]}.${match[2]}.${match[1]} ${match[4]}:${match[5]}:${match[6]} (Київ ${match[7]})`;
78
+ }
79
+
80
+ export function isoOrNull(value) {
81
+ if (value == null || value === '') return null;
82
+ return formatUtcIso(value);
83
+ }
84
+
85
+ export function laterTimestamp(a, b) {
86
+ const left = parseFlexibleIso(a);
87
+ const right = parseFlexibleIso(b);
88
+ if (!Number.isFinite(left)) return b;
89
+ if (!Number.isFinite(right)) return a;
90
+ return left >= right ? a : b;
91
+ }
92
+
93
+ export function earlierTimestamp(a, b) {
94
+ const left = parseFlexibleIso(a);
95
+ const right = parseFlexibleIso(b);
96
+ if (!Number.isFinite(left)) return b;
97
+ if (!Number.isFinite(right)) return a;
98
+ return left <= right ? a : b;
99
+ }
@@ -0,0 +1,187 @@
1
+ import { existsSync, readFileSync, readlinkSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir as osHomedir } from 'os';
4
+ import { execFileSync } from 'child_process';
5
+
6
+ const VALID_PLATFORMS = new Set(['cursor', 'claude', 'amp']);
7
+ const AMP_TTY_MAX_AGE_MS = 2 * 60 * 60 * 1000;
8
+
9
+ function trim(value) {
10
+ return value == null ? '' : String(value).trim();
11
+ }
12
+
13
+ function envFlagOn(value) {
14
+ if (value == null) return false;
15
+ const normalized = String(value).trim().toLowerCase();
16
+ return normalized !== '' && normalized !== '0' && normalized !== 'false';
17
+ }
18
+
19
+ export function ampDataRoot(env = {}, homedir) {
20
+ if (env.AMP_DATA_DIR && String(env.AMP_DATA_DIR).trim()) return String(env.AMP_DATA_DIR).trim();
21
+ if (env.XDG_DATA_HOME && String(env.XDG_DATA_HOME).trim()) {
22
+ return join(String(env.XDG_DATA_HOME).trim(), 'amp');
23
+ }
24
+ return join(homedir || env.HOME || osHomedir(), '.local', 'share', 'amp');
25
+ }
26
+
27
+ export function ampThreadIdFromEnv(env = {}) {
28
+ for (const key of ['AMP_CURRENT_THREAD', 'AMP_THREAD_ID']) {
29
+ const value = trim(env[key]);
30
+ if (value) return value;
31
+ }
32
+ return '';
33
+ }
34
+
35
+ export function isUsableTtyPath(raw) {
36
+ const path = String(raw || '').replace(/^tty:/, '').trim();
37
+ if (!path.startsWith('/dev/')) return false;
38
+ if (path === '/dev/null' || path.startsWith('/dev/null')) return false;
39
+ return true;
40
+ }
41
+
42
+ export function currentTtyKey(env = {}, readlink = null) {
43
+ const forced = trim(env.AOK_TTY);
44
+ if (forced) {
45
+ const path = forced.startsWith('tty:') ? forced.slice(4) : forced;
46
+ return isUsableTtyPath(path) ? (forced.startsWith('tty:') ? forced : `tty:${path}`) : '';
47
+ }
48
+ try {
49
+ const fn = readlink || readlinkSync;
50
+ const raw = String(fn('/proc/self/fd/0') || '').trim();
51
+ return isUsableTtyPath(raw) ? `tty:${raw}` : '';
52
+ } catch {
53
+ return '';
54
+ }
55
+ }
56
+
57
+ const AMP_THREAD_ID_RE = /\bT-[0-9a-fA-F-]{8,}\b/g;
58
+
59
+ export function parseAmpThreadList(text) {
60
+ const ids = [];
61
+ for (const line of String(text || '').split('\n')) {
62
+ if (!line.trim() || /^Title\b/.test(line) || /^─/.test(line)) continue;
63
+ const matches = line.match(AMP_THREAD_ID_RE);
64
+ if (!matches || !matches.length) continue;
65
+ const id = matches[matches.length - 1];
66
+ if (!ids.includes(id)) ids.push(id);
67
+ }
68
+ return ids;
69
+ }
70
+
71
+ export function listRecentAmpThreadIds(options = {}) {
72
+ if (typeof options.listAmpThreads === 'function') {
73
+ try {
74
+ const out = options.listAmpThreads();
75
+ if (Array.isArray(out)) return out.map((id) => trim(id)).filter(Boolean);
76
+ return parseAmpThreadList(out);
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+ const env = options.env || {};
82
+ const bin = options.ampBin || trim(env.AOK_AMP_BIN) || 'amp';
83
+ if (bin !== 'amp' && !existsSync(bin)) return [];
84
+ try {
85
+ const text = execFileSync(bin, ['threads', 'list', '--limit', String(options.limit || 5)], {
86
+ encoding: 'utf-8',
87
+ timeout: options.timeoutMs != null ? Number(options.timeoutMs) : 15000,
88
+ env,
89
+ stdio: ['ignore', 'pipe', 'pipe'],
90
+ });
91
+ return parseAmpThreadList(text);
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ export function parentProcessComm(ppid = process.ppid, readFile = readFileSync) {
98
+ try {
99
+ return String(readFile(`/proc/${ppid}/comm`, 'utf-8')).trim();
100
+ } catch {
101
+ return '';
102
+ }
103
+ }
104
+
105
+ function looksLikeAmpProcess(comm) {
106
+ const name = String(comm || '').toLowerCase();
107
+ return name === 'amp' || name.startsWith('amp');
108
+ }
109
+
110
+ function isFreshTimestamp(value, now, maxAgeMs) {
111
+ if (value == null || value === '') return false;
112
+ const n = typeof value === 'number' ? (value < 1e12 ? value * 1000 : value) : Date.parse(String(value));
113
+ if (!Number.isFinite(n)) return false;
114
+ return now - n >= 0 && now - n <= maxAgeMs;
115
+ }
116
+
117
+ export function readAmpSessionHint(options = {}) {
118
+ const env = options.env || {};
119
+ const homedir = options.homedir || env.HOME;
120
+ const now = options.now != null ? Number(options.now) : Date.now();
121
+ const maxAgeMs = options.maxAgeMs != null ? Number(options.maxAgeMs) : AMP_TTY_MAX_AGE_MS;
122
+ const filePath = join(ampDataRoot(env, homedir), 'session.json');
123
+ if (!existsSync(filePath)) return { threadId: '', source: '' };
124
+ let data;
125
+ try {
126
+ data = JSON.parse(readFileSync(filePath, 'utf-8'));
127
+ } catch {
128
+ return { threadId: '', source: '' };
129
+ }
130
+ if (!data || typeof data !== 'object') return { threadId: '', source: '', lastThreadId: '' };
131
+ const lastThreadId = trim(data.lastThreadId);
132
+ const rawTty = options.ttyKey != null ? options.ttyKey : currentTtyKey(env, options.readlink);
133
+ const ttyKey = isUsableTtyPath(rawTty) ? (String(rawTty).startsWith('tty:') ? rawTty : `tty:${rawTty}`) : '';
134
+ const byTty = data.lastThreadByTerminal && ttyKey ? data.lastThreadByTerminal[ttyKey] : null;
135
+ if (byTty && trim(byTty.lastThreadId) && isFreshTimestamp(byTty.updatedAt, now, maxAgeMs)) {
136
+ return { threadId: trim(byTty.lastThreadId), source: 'amp-session-tty', lastThreadId };
137
+ }
138
+ return { threadId: '', source: '', lastThreadId };
139
+ }
140
+
141
+ export function detectSessionClient(options = {}) {
142
+ const env = options.env || {};
143
+ const ampId = ampThreadIdFromEnv(env);
144
+ if (ampId) {
145
+ return { platform: 'amp', threadId: ampId, source: 'amp-env' };
146
+ }
147
+
148
+ if (envFlagOn(env.CURSOR_AGENT) || trim(env.CURSOR_CONVERSATION_ID)) {
149
+ return { platform: 'cursor', threadId: null, source: 'cursor-env' };
150
+ }
151
+ if (envFlagOn(env.CLAUDECODE) || envFlagOn(env.CLAUDE_CODE) || trim(env.CLAUDE_CODE_ENTRYPOINT)) {
152
+ return { platform: 'claude', threadId: null, source: 'claude-env' };
153
+ }
154
+
155
+ const comm = options.parentComm != null ? options.parentComm : parentProcessComm();
156
+ const hint = readAmpSessionHint(options);
157
+ if (looksLikeAmpProcess(comm)) {
158
+ if (hint.threadId) {
159
+ return { platform: 'amp', threadId: hint.threadId, source: 'amp-parent' };
160
+ }
161
+ const listed = listRecentAmpThreadIds(options);
162
+ return {
163
+ platform: 'amp',
164
+ threadId: listed[0] || null,
165
+ source: listed[0] ? 'amp-threads-list' : 'amp-parent',
166
+ };
167
+ }
168
+ if (hint.threadId) {
169
+ return { platform: 'amp', threadId: hint.threadId, source: hint.source };
170
+ }
171
+
172
+ return { platform: null, threadId: null, source: 'none' };
173
+ }
174
+
175
+ export function resolveRestoreClient(options = {}) {
176
+ const env = options.env || {};
177
+ const detected = detectSessionClient(options);
178
+ const flag = trim(options.platform || env.AOK_PLATFORM).toLowerCase();
179
+ if (flag && VALID_PLATFORMS.has(flag)) {
180
+ return {
181
+ platform: flag,
182
+ threadId: detected.threadId,
183
+ source: options.platform ? 'flag' : 'aok-platform',
184
+ };
185
+ }
186
+ return detected;
187
+ }