larkin 0.5.3 → 0.5.5
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 +12 -0
- package/README.md +22 -1
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +1 -100
- package/dist/agent/agent-cli-capabilities.cjs +1 -1
- package/dist/agent/agent-cli-capabilities.mjs +1 -1
- package/dist/agent/config-cli-contract.cjs +3 -1
- package/dist/agent/config-cli-contract.mjs +3 -1
- package/dist/agent/context-prompt.cjs +16 -17
- package/dist/agent/context-prompt.mjs +15 -16
- package/dist/agent/inbox-audit-heartbeat.cjs +157 -37
- package/dist/agent/inbox-audit-heartbeat.mjs +122 -36
- package/dist/agent/missed-outbound-scan.cjs +326 -54
- package/dist/agent/missed-outbound-scan.mjs +319 -53
- package/dist/app/agent-cli.mjs +37 -6
- package/dist/app/agent-config.mjs +36 -5
- package/dist/app/cli.mjs +1 -0
- package/dist/dashboard/dashboard-config-controller.mjs +15 -0
- package/dist/dashboard/web/assets/dashboard.css +1 -1
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +63 -5
- package/dist/feishu/host-shell.mjs +66 -8
- package/dist/platform/config.cjs +173 -5
- package/dist/platform/config.mjs +167 -4
- package/dist/runtime/pi-state-dir.cjs +105 -0
- package/dist/runtime/pi-state-dir.mjs +94 -0
- package/dist/runtime/pi-tmux-extension.mjs +137 -0
- package/dist/runtime/pi-tmux-injection.mjs +49 -0
- package/dist/runtime/pi-tmux.bundle.js +600 -0
- package/dist/runtime/pi-tmux.mjs +408 -0
- package/dist/runtime/runtime-adapters.mjs +26 -113
- package/dist/runtime/runtime-host.cjs +3 -413
- package/dist/runtime/runtime-host.mjs +2 -412
- package/package.json +2 -8
- package/dist/runtime/pi-bash-timeout-extension.mjs +0 -60
- package/dist/runtime/pi-bash-timeout-injection.mjs +0 -54
- package/dist/runtime/pi-bash-timeout.bundle.js +0 -43
- package/dist/runtime/pi-extension-api.mjs +0 -1
- package/dist/runtime/pi-subagent-injection.mjs +0 -290
- package/dist/runtime/pi-subagent-ledger.cjs +0 -632
- package/dist/runtime/pi-subagent-ledger.mjs +0 -594
- package/dist/runtime/pi-subagent-record-watchdog-injection.mjs +0 -43
- package/dist/runtime/pi-subagent-record-watchdog.bundle.js +0 -183
- package/dist/runtime/pi-subagent-record-watchdog.mjs +0 -46
- package/dist/runtime/pi-subagents-notification.mjs +0 -175
- package/dist/runtime/pi-subagents.bundle.js +0 -9110
- package/dist/runtime/pi-supervised-command-extension.mjs +0 -72
- package/dist/runtime/pi-supervised-command.bundle.js +0 -4735
- package/dist/runtime/pi-supervised-command.mjs +0 -420
- package/patches/@tintinweb%2Fpi-subagents@0.14.3.patch +0 -945
package/dist/platform/config.mjs
CHANGED
|
@@ -8,11 +8,16 @@ import { CURRENT_RUNTIME_MODELS } from "../runtime/runtime-model-catalog.mjs";
|
|
|
8
8
|
import { fromUserRuntime, isAdapterRuntime, isUserRuntime, toUserRuntime } from "../runtime/user-runtime.mjs";
|
|
9
9
|
import processInspect from "./process-inspect.cjs";
|
|
10
10
|
export { RUNTIME_OPTIONS, fromUserRuntime, isAdapterRuntime, isUserRuntime, runtimeOptionOf, runtimeOptionTarget, toUserRuntime, } from "../runtime/user-runtime.mjs";
|
|
11
|
+
export const DEFAULT_INBOX_AUDIT_INTERVAL_MS = 15 * 60_000;
|
|
12
|
+
export const MIN_INBOX_AUDIT_INTERVAL_MS = 60_000;
|
|
13
|
+
export const MAX_INBOX_AUDIT_INTERVAL_MS = 24 * 60 * 60_000;
|
|
11
14
|
const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
|
|
12
15
|
const TOP_FIELDS_V4 = new Set(["version", "serverId", "mentionPolicy", "activeAgent", "agents"]);
|
|
16
|
+
const OPTIONAL_TOP_FIELDS_V4 = new Set(["inboxAudit"]);
|
|
13
17
|
const AGENT_FIELDS_V3 = new Set(["runtime", "model", "effort", "noMentionChats", "createdAt"]);
|
|
14
|
-
const AGENT_FIELDS_V4 = new Set(["runtime", "model", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
|
|
18
|
+
const AGENT_FIELDS_V4 = new Set(["runtime", "model", "effort", "mentionPolicy", "chatMentionPolicies", "inboxAudit", "createdAt"]);
|
|
15
19
|
const LEGACY_AGENT_FIELDS = new Set(["piDistribution", "runtimeOption"]);
|
|
20
|
+
const INBOX_AUDIT_FIELDS = new Set(["enabled", "intervalMs"]);
|
|
16
21
|
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
17
22
|
const CHAT_ID = /^oc_[A-Za-z0-9_-]+$/;
|
|
18
23
|
const PI_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
@@ -76,6 +81,85 @@ function assertPolicy(value, label) {
|
|
|
76
81
|
if (value !== "require" && value !== "free")
|
|
77
82
|
throw new Error(`${label} 只允许 require/free`);
|
|
78
83
|
}
|
|
84
|
+
export function assertInboxAuditInterval(value, label = "inbox-audit interval") {
|
|
85
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_INBOX_AUDIT_INTERVAL_MS || value > MAX_INBOX_AUDIT_INTERVAL_MS) {
|
|
86
|
+
throw new Error(`${label} 必须是 ${MIN_INBOX_AUDIT_INTERVAL_MS}–${MAX_INBOX_AUDIT_INTERVAL_MS} 毫秒`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function parseInboxAuditInterval(value) {
|
|
90
|
+
if (typeof value !== "string" || !value)
|
|
91
|
+
throw new Error("inbox-audit interval 不能为空");
|
|
92
|
+
let milliseconds;
|
|
93
|
+
if (/^[1-9]\d*$/.test(value))
|
|
94
|
+
milliseconds = Number(value);
|
|
95
|
+
else {
|
|
96
|
+
const match = /^([1-9]\d*)(m|h)$/.exec(value);
|
|
97
|
+
if (!match)
|
|
98
|
+
throw new Error("inbox-audit interval 只允许 15m/1h 或 60000–86400000 毫秒");
|
|
99
|
+
milliseconds = Number(match[1]) * (match[2] === "h" ? 3_600_000 : 60_000);
|
|
100
|
+
}
|
|
101
|
+
assertInboxAuditInterval(milliseconds);
|
|
102
|
+
return milliseconds;
|
|
103
|
+
}
|
|
104
|
+
export function formatInboxAuditInterval(intervalMs) {
|
|
105
|
+
if (intervalMs % 3_600_000 === 0)
|
|
106
|
+
return `${intervalMs / 3_600_000}h`;
|
|
107
|
+
if (intervalMs % 60_000 === 0)
|
|
108
|
+
return `${intervalMs / 60_000}m`;
|
|
109
|
+
return `${intervalMs}ms`;
|
|
110
|
+
}
|
|
111
|
+
function parseStoredInboxAudit(value, label) {
|
|
112
|
+
if (!isPlainObject(value))
|
|
113
|
+
throw new Error(`${label} 必须是普通 object`);
|
|
114
|
+
assertAllowedFields(value, INBOX_AUDIT_FIELDS, label);
|
|
115
|
+
const settings = {};
|
|
116
|
+
if (Object.hasOwn(value, "enabled")) {
|
|
117
|
+
if (typeof value.enabled !== "boolean")
|
|
118
|
+
throw new Error(`${label}.enabled 必须是 boolean`);
|
|
119
|
+
settings.enabled = value.enabled;
|
|
120
|
+
}
|
|
121
|
+
if (Object.hasOwn(value, "intervalMs")) {
|
|
122
|
+
assertInboxAuditInterval(value.intervalMs, `${label}.intervalMs`);
|
|
123
|
+
settings.intervalMs = value.intervalMs;
|
|
124
|
+
}
|
|
125
|
+
return settings;
|
|
126
|
+
}
|
|
127
|
+
function resolvedInboxAudit(settings) {
|
|
128
|
+
return {
|
|
129
|
+
enabled: settings?.enabled ?? false,
|
|
130
|
+
intervalMs: settings?.intervalMs ?? DEFAULT_INBOX_AUDIT_INTERVAL_MS,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function inboxAuditMutationFromCli(input) {
|
|
134
|
+
if (input.scope === "global") {
|
|
135
|
+
if (input.enabled !== "on" && input.enabled !== "off") {
|
|
136
|
+
throw new Error("用法: larkin config inbox-audit global <on|off> [--interval <15m|1h>]");
|
|
137
|
+
}
|
|
138
|
+
const mutation = {
|
|
139
|
+
kind: "set-global-inbox-audit", enabled: input.enabled === "on",
|
|
140
|
+
};
|
|
141
|
+
if (input.interval !== undefined) {
|
|
142
|
+
if (input.interval === "inherit")
|
|
143
|
+
throw new Error("全局 inbox-audit 不支持 interval=inherit");
|
|
144
|
+
mutation.intervalMs = parseInboxAuditInterval(input.interval);
|
|
145
|
+
}
|
|
146
|
+
return mutation;
|
|
147
|
+
}
|
|
148
|
+
if (input.scope === "agent") {
|
|
149
|
+
if (input.enabled !== "on" && input.enabled !== "off" && input.enabled !== "inherit") {
|
|
150
|
+
throw new Error("用法: larkin config inbox-audit agent <inherit|on|off> [--agent <App ID>] [--interval <15m|inherit>]");
|
|
151
|
+
}
|
|
152
|
+
const mutation = {
|
|
153
|
+
kind: "set-agent-inbox-audit", agentId: input.agentId,
|
|
154
|
+
enabled: input.enabled === "inherit" ? "inherit" : input.enabled === "on",
|
|
155
|
+
};
|
|
156
|
+
if (input.interval !== undefined) {
|
|
157
|
+
mutation.intervalMs = input.interval === "inherit" ? "inherit" : parseInboxAuditInterval(input.interval);
|
|
158
|
+
}
|
|
159
|
+
return mutation;
|
|
160
|
+
}
|
|
161
|
+
throw new Error("inbox-audit scope 只支持 global/agent");
|
|
162
|
+
}
|
|
79
163
|
function assertModel(runtime, model) {
|
|
80
164
|
if (model === "default")
|
|
81
165
|
return;
|
|
@@ -143,6 +227,8 @@ function validateStoredAgent(key, agent, version) {
|
|
|
143
227
|
}
|
|
144
228
|
if (version === 4 && Object.hasOwn(agent, "mentionPolicy"))
|
|
145
229
|
assertPolicy(agent.mentionPolicy, `Agent ${key}.mentionPolicy`);
|
|
230
|
+
if (version === 4 && Object.hasOwn(agent, "inboxAudit"))
|
|
231
|
+
parseStoredInboxAudit(agent.inboxAudit, `Agent ${key}.inboxAudit`);
|
|
146
232
|
if (version === 4 && Object.hasOwn(agent, "chatMentionPolicies")) {
|
|
147
233
|
if (!isPlainObject(agent.chatMentionPolicies))
|
|
148
234
|
throw new Error(`Agent ${key}.chatMentionPolicies 必须是普通 object`);
|
|
@@ -171,6 +257,7 @@ function hydratedStoredAgent(key, agent, version) {
|
|
|
171
257
|
model: agent.model,
|
|
172
258
|
...(typeof agent.effort === "string" ? { effort: agent.effort } : {}),
|
|
173
259
|
...(version === 4 && (agent.mentionPolicy === "require" || agent.mentionPolicy === "free") ? { mentionPolicy: agent.mentionPolicy } : {}),
|
|
260
|
+
...(version === 4 && Object.hasOwn(agent, "inboxAudit") ? { inboxAudit: parseStoredInboxAudit(agent.inboxAudit, `Agent ${key}.inboxAudit`) } : {}),
|
|
174
261
|
...(Object.keys(chatMentionPolicies).length ? { chatMentionPolicies, noMentionChats: Object.entries(chatMentionPolicies).filter(([, policy]) => policy === "free").map(([chatId]) => chatId) } : {}),
|
|
175
262
|
...(typeof agent.createdAt === "string" ? { createdAt: agent.createdAt } : {}),
|
|
176
263
|
};
|
|
@@ -184,6 +271,7 @@ export function hydrateAgent(key, agent, configDir) {
|
|
|
184
271
|
larkConfigDir: path.join(layout.agentStateDir(key), "lark-cli-config"),
|
|
185
272
|
...(agent.effort ? { effort: agent.effort } : {}),
|
|
186
273
|
...(agent.mentionPolicy ? { mentionPolicy: agent.mentionPolicy } : {}),
|
|
274
|
+
...(agent.inboxAudit ? { inboxAudit: { ...agent.inboxAudit } } : {}),
|
|
187
275
|
...(agent.chatMentionPolicies ? { chatMentionPolicies: { ...agent.chatMentionPolicies } } : {}),
|
|
188
276
|
...(agent.noMentionChats ? { noMentionChats: [...agent.noMentionChats] } : {}),
|
|
189
277
|
...(agent.createdAt ? { createdAt: agent.createdAt } : {}),
|
|
@@ -202,7 +290,7 @@ export function normalizeConfig(raw, configDir, { mint } = {}) {
|
|
|
202
290
|
if (version !== 3 && version !== 4)
|
|
203
291
|
throw new Error("不支持该配置格式:larkin 只接受 version=3/4");
|
|
204
292
|
const topFields = version === 3 ? TOP_FIELDS_V3 : TOP_FIELDS_V4;
|
|
205
|
-
assertAllowedFields(raw, topFields, "config");
|
|
293
|
+
assertAllowedFields(raw, version === 4 ? new Set([...TOP_FIELDS_V4, ...OPTIONAL_TOP_FIELDS_V4]) : topFields, "config");
|
|
206
294
|
for (const field of topFields)
|
|
207
295
|
if (!Object.hasOwn(raw, field))
|
|
208
296
|
throw new Error(`config 缺少必需字段 ${field}`);
|
|
@@ -221,8 +309,12 @@ export function normalizeConfig(raw, configDir, { mint } = {}) {
|
|
|
221
309
|
const agents = {};
|
|
222
310
|
for (const [key, agent] of Object.entries(raw.agents))
|
|
223
311
|
agents[key] = hydrateAgent(key, hydratedStoredAgent(key, agent, version), layout.root);
|
|
312
|
+
const inboxAudit = version === 4 && Object.hasOwn(raw, "inboxAudit")
|
|
313
|
+
? resolvedInboxAudit(parseStoredInboxAudit(raw.inboxAudit, "config.inboxAudit"))
|
|
314
|
+
: undefined;
|
|
224
315
|
return {
|
|
225
316
|
version: 4, serverId: raw.serverId, mentionPolicy: version === 4 ? raw.mentionPolicy : "require",
|
|
317
|
+
...(inboxAudit ? { inboxAudit } : {}),
|
|
226
318
|
configDir: layout.configDir, larkinHome: layout.larkinHome, larkConfigDir: resolveLarkConfigDir({}, layout.root), activeAgent: raw.activeAgent, agents,
|
|
227
319
|
};
|
|
228
320
|
}
|
|
@@ -448,7 +540,11 @@ export function selectAgent(config, env = process.env) {
|
|
|
448
540
|
return agents[config.activeAgent];
|
|
449
541
|
}
|
|
450
542
|
export function toStored(config) {
|
|
451
|
-
const out = {
|
|
543
|
+
const out = {
|
|
544
|
+
version: 4, serverId: config.serverId, mentionPolicy: config.mentionPolicy, activeAgent: config.activeAgent, agents: {},
|
|
545
|
+
};
|
|
546
|
+
if (config.inboxAudit)
|
|
547
|
+
out.inboxAudit = { enabled: config.inboxAudit.enabled, intervalMs: config.inboxAudit.intervalMs };
|
|
452
548
|
for (const [key, agent] of Object.entries(config.agents || {})) {
|
|
453
549
|
const stored = { runtime: agent.runtime, model: agent.model };
|
|
454
550
|
if (typeof agent.effort === "string" && agent.effort)
|
|
@@ -457,6 +553,9 @@ export function toStored(config) {
|
|
|
457
553
|
stored.mentionPolicy = agent.mentionPolicy;
|
|
458
554
|
if (agent.chatMentionPolicies && Object.keys(agent.chatMentionPolicies).length)
|
|
459
555
|
stored.chatMentionPolicies = { ...agent.chatMentionPolicies };
|
|
556
|
+
if (agent.inboxAudit && (agent.inboxAudit.enabled !== undefined || agent.inboxAudit.intervalMs !== undefined)) {
|
|
557
|
+
stored.inboxAudit = { ...agent.inboxAudit };
|
|
558
|
+
}
|
|
460
559
|
if (typeof agent.createdAt === "string" && agent.createdAt)
|
|
461
560
|
stored.createdAt = agent.createdAt;
|
|
462
561
|
out.agents[key] = stored;
|
|
@@ -483,6 +582,20 @@ export function resolveAgentGlobalMentionPolicy(config, agentId) {
|
|
|
483
582
|
return { effective: agent.mentionPolicy, source: "agent" };
|
|
484
583
|
return { effective: config.mentionPolicy, source: "global" };
|
|
485
584
|
}
|
|
585
|
+
export function resolveInboxAuditSchedule(config, agentId) {
|
|
586
|
+
const agent = config.agents[agentId];
|
|
587
|
+
if (!agent)
|
|
588
|
+
throw new Error(`Agent 不存在:${agentId}`);
|
|
589
|
+
const enabled = agent.inboxAudit?.enabled ?? config.inboxAudit?.enabled ?? false;
|
|
590
|
+
const intervalMs = agent.inboxAudit?.intervalMs ?? config.inboxAudit?.intervalMs ?? DEFAULT_INBOX_AUDIT_INTERVAL_MS;
|
|
591
|
+
assertInboxAuditInterval(intervalMs);
|
|
592
|
+
return {
|
|
593
|
+
enabled,
|
|
594
|
+
intervalMs,
|
|
595
|
+
enabledSource: agent.inboxAudit?.enabled !== undefined ? "agent" : config.inboxAudit ? "global" : "default",
|
|
596
|
+
intervalSource: agent.inboxAudit?.intervalMs !== undefined ? "agent" : config.inboxAudit?.intervalMs !== undefined ? "global" : "default",
|
|
597
|
+
};
|
|
598
|
+
}
|
|
486
599
|
function revision(bytes) {
|
|
487
600
|
return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
488
601
|
}
|
|
@@ -495,6 +608,7 @@ function agentSignature(config, agentId) {
|
|
|
495
608
|
schema: 2,
|
|
496
609
|
runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
|
|
497
610
|
globalMentionPolicy: config.mentionPolicy, agentMentionPolicy: agent.mentionPolicy ?? null, chatMentionPolicies: chats,
|
|
611
|
+
globalInboxAudit: config.inboxAudit ?? null, agentInboxAudit: agent.inboxAudit ?? null,
|
|
498
612
|
}));
|
|
499
613
|
}
|
|
500
614
|
export function runtimeConfigSignature(config, agentId) {
|
|
@@ -632,6 +746,19 @@ function applyMutation(config, mutation) {
|
|
|
632
746
|
config.mentionPolicy = mutation.value;
|
|
633
747
|
return { scope: "global", runtimeChange: false, affectedAgentIds: Object.keys(config.agents) };
|
|
634
748
|
}
|
|
749
|
+
if (mutation.kind === "set-global-inbox-audit") {
|
|
750
|
+
if (mutation.enabled === undefined && mutation.intervalMs === undefined)
|
|
751
|
+
throw new Error("inbox-audit 全局变更必须包含 enabled 或 interval");
|
|
752
|
+
const current = resolvedInboxAudit(config.inboxAudit);
|
|
753
|
+
if (mutation.enabled !== undefined)
|
|
754
|
+
current.enabled = mutation.enabled;
|
|
755
|
+
if (mutation.intervalMs !== undefined) {
|
|
756
|
+
assertInboxAuditInterval(mutation.intervalMs, "全局 inbox-audit interval");
|
|
757
|
+
current.intervalMs = mutation.intervalMs;
|
|
758
|
+
}
|
|
759
|
+
config.inboxAudit = current;
|
|
760
|
+
return { scope: "global", runtimeChange: false, affectedAgentIds: Object.keys(config.agents) };
|
|
761
|
+
}
|
|
635
762
|
if (!APP_ID.test(mutation.agentId) || !config.agents[mutation.agentId])
|
|
636
763
|
throw new Error(`Agent 不存在:${mutation.agentId}`);
|
|
637
764
|
const agent = config.agents[mutation.agentId];
|
|
@@ -644,6 +771,26 @@ function applyMutation(config, mutation) {
|
|
|
644
771
|
}
|
|
645
772
|
return { scope: "agent", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
|
|
646
773
|
}
|
|
774
|
+
if (mutation.kind === "set-agent-inbox-audit") {
|
|
775
|
+
if (mutation.enabled === undefined && mutation.intervalMs === undefined)
|
|
776
|
+
throw new Error("inbox-audit Agent 变更必须包含 enabled 或 interval");
|
|
777
|
+
const current = { ...(agent.inboxAudit || {}) };
|
|
778
|
+
if (mutation.enabled === "inherit")
|
|
779
|
+
delete current.enabled;
|
|
780
|
+
else if (mutation.enabled !== undefined)
|
|
781
|
+
current.enabled = mutation.enabled;
|
|
782
|
+
if (mutation.intervalMs === "inherit")
|
|
783
|
+
delete current.intervalMs;
|
|
784
|
+
else if (mutation.intervalMs !== undefined) {
|
|
785
|
+
assertInboxAuditInterval(mutation.intervalMs, "Agent inbox-audit interval");
|
|
786
|
+
current.intervalMs = mutation.intervalMs;
|
|
787
|
+
}
|
|
788
|
+
if (current.enabled === undefined && current.intervalMs === undefined)
|
|
789
|
+
delete agent.inboxAudit;
|
|
790
|
+
else
|
|
791
|
+
agent.inboxAudit = current;
|
|
792
|
+
return { scope: "agent", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
|
|
793
|
+
}
|
|
647
794
|
if (mutation.kind === "set-chat-mention") {
|
|
648
795
|
if (!CHAT_ID.test(mutation.chatId))
|
|
649
796
|
throw new Error(`群 ID 格式无效:${mutation.chatId}`);
|
|
@@ -1130,8 +1277,24 @@ export function safeConfigView(config, onlyAgentId, chatId, applyState) {
|
|
|
1130
1277
|
source: agent.mentionPolicy ? "agent" : "global",
|
|
1131
1278
|
...(chatId ? { chat: { chatId, override: agent.chatMentionPolicies?.[chatId] ?? "inherit", ...resolveMentionPolicy(config, agentId, chatId) } } : {}),
|
|
1132
1279
|
},
|
|
1280
|
+
inboxAudit: (() => {
|
|
1281
|
+
const resolved = resolveInboxAuditSchedule(config, agentId);
|
|
1282
|
+
return {
|
|
1283
|
+
override: {
|
|
1284
|
+
enabled: agent.inboxAudit?.enabled === undefined ? "inherit" : agent.inboxAudit.enabled ? "on" : "off",
|
|
1285
|
+
intervalMs: agent.inboxAudit?.intervalMs ?? "inherit",
|
|
1286
|
+
},
|
|
1287
|
+
effective: { enabled: resolved.enabled, intervalMs: resolved.intervalMs },
|
|
1288
|
+
source: { enabled: resolved.enabledSource, intervalMs: resolved.intervalSource },
|
|
1289
|
+
};
|
|
1290
|
+
})(),
|
|
1133
1291
|
chatMentionPolicies: { ...(agent.chatMentionPolicies || {}) },
|
|
1134
1292
|
apply: applyState?.agents?.[agentId] ?? { applyState: "unknown" },
|
|
1135
1293
|
}));
|
|
1136
|
-
|
|
1294
|
+
const inboxAudit = resolvedInboxAudit(config.inboxAudit);
|
|
1295
|
+
return {
|
|
1296
|
+
version: 4, mentionPolicy: config.mentionPolicy,
|
|
1297
|
+
inboxAudit: { enabled: inboxAudit.enabled, intervalMs: inboxAudit.intervalMs },
|
|
1298
|
+
persistedRevision: applyState?.persistedRevision ?? "unknown", agents,
|
|
1299
|
+
};
|
|
1137
1300
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.effectivePiStateDir = effectivePiStateDir;
|
|
7
|
+
exports.ensureOwnedTree = ensureOwnedTree;
|
|
8
|
+
exports.ensurePrivateDirectory = ensurePrivateDirectory;
|
|
9
|
+
exports.mkdirPrivateExclusive = mkdirPrivateExclusive;
|
|
10
|
+
exports.writePrivateExclusive = writePrivateExclusive;
|
|
11
|
+
exports.writePrivateAtomic = writePrivateAtomic;
|
|
12
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
/** Same implicit root the Pi adapter uses when `stateDir` is omitted. */
|
|
16
|
+
function effectivePiStateDir(input) {
|
|
17
|
+
return input.stateDir ?? node_path_1.default.join(input.workspaceDir, ".larkin");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 从 root 起逐层建 0700 子树。root 可以位于 /var、/tmp 等合法祖先 link 之后;
|
|
21
|
+
* 任一层 owned 组件自身是 symlink 则拒绝,不顺着 link 往外写。
|
|
22
|
+
*/
|
|
23
|
+
function ensureOwnedTree(root, parts = [], create = true) {
|
|
24
|
+
const resolved = node_path_1.default.resolve(root);
|
|
25
|
+
if (node_fs_1.default.existsSync(resolved)) {
|
|
26
|
+
const stat = node_fs_1.default.lstatSync(resolved);
|
|
27
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
28
|
+
throw new Error(`unsafe private directory: ${resolved}`);
|
|
29
|
+
}
|
|
30
|
+
else if (create) {
|
|
31
|
+
node_fs_1.default.mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
32
|
+
if (node_fs_1.default.lstatSync(resolved).isSymbolicLink() || !node_fs_1.default.lstatSync(resolved).isDirectory()) {
|
|
33
|
+
throw new Error(`unsafe private directory: ${resolved}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
throw new Error(`unsafe private directory: ${resolved}`);
|
|
38
|
+
}
|
|
39
|
+
let dir = resolved;
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
if (!part || part === "." || part === ".." || /[\\/]/.test(part))
|
|
42
|
+
throw new Error(`unsafe owned path component: ${part}`);
|
|
43
|
+
dir = node_path_1.default.join(dir, part);
|
|
44
|
+
try {
|
|
45
|
+
const existing = node_fs_1.default.lstatSync(dir);
|
|
46
|
+
if (existing.isSymbolicLink() || !existing.isDirectory())
|
|
47
|
+
throw new Error(`unsafe private directory: ${dir}`);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error.code !== "ENOENT")
|
|
51
|
+
throw error;
|
|
52
|
+
if (!create)
|
|
53
|
+
throw new Error(`unsafe private directory: ${dir}`);
|
|
54
|
+
node_fs_1.default.mkdirSync(dir, { mode: 0o700 });
|
|
55
|
+
if (node_fs_1.default.lstatSync(dir).isSymbolicLink() || !node_fs_1.default.lstatSync(dir).isDirectory())
|
|
56
|
+
throw new Error(`unsafe private directory: ${dir}`);
|
|
57
|
+
}
|
|
58
|
+
if (process.platform !== "win32")
|
|
59
|
+
node_fs_1.default.chmodSync(dir, 0o700);
|
|
60
|
+
}
|
|
61
|
+
return dir;
|
|
62
|
+
}
|
|
63
|
+
function ensurePrivateDirectory(directory) {
|
|
64
|
+
ensureOwnedTree(directory);
|
|
65
|
+
}
|
|
66
|
+
/** 独占新建 0700 目录;已存在或变成 symlink 都拒绝。 */
|
|
67
|
+
function mkdirPrivateExclusive(directory) {
|
|
68
|
+
node_fs_1.default.mkdirSync(directory, { mode: 0o700 });
|
|
69
|
+
const created = node_fs_1.default.lstatSync(directory);
|
|
70
|
+
if (created.isSymbolicLink() || !created.isDirectory())
|
|
71
|
+
throw new Error(`unsafe private directory: ${directory}`);
|
|
72
|
+
if (process.platform !== "win32")
|
|
73
|
+
node_fs_1.default.chmodSync(directory, 0o700);
|
|
74
|
+
}
|
|
75
|
+
/** 独占新建 0600 文件;已存在(含 symlink)时失败,不跟随写出。 */
|
|
76
|
+
function writePrivateExclusive(file, content) {
|
|
77
|
+
node_fs_1.default.writeFileSync(file, content, { mode: 0o600, flag: "wx" });
|
|
78
|
+
if (process.platform !== "win32")
|
|
79
|
+
node_fs_1.default.chmodSync(file, 0o600);
|
|
80
|
+
}
|
|
81
|
+
/** 原子写 0600 文件;目标或父目录已是 symlink 时拒绝,避免落到目录外。 */
|
|
82
|
+
function writePrivateAtomic(file, content) {
|
|
83
|
+
try {
|
|
84
|
+
if (node_fs_1.default.lstatSync(node_path_1.default.dirname(file)).isSymbolicLink())
|
|
85
|
+
throw new Error(`unsafe private directory: ${node_path_1.default.dirname(file)}`);
|
|
86
|
+
if (node_fs_1.default.lstatSync(file).isSymbolicLink())
|
|
87
|
+
throw new Error(`${node_path_1.default.basename(file)} must not be a symlink`);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code !== "ENOENT")
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const temporary = node_path_1.default.join(node_path_1.default.dirname(file), `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
|
|
94
|
+
try {
|
|
95
|
+
node_fs_1.default.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
96
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
97
|
+
node_fs_1.default.chmodSync(file, 0o600);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
try {
|
|
101
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
102
|
+
}
|
|
103
|
+
catch { /* renamed or absent */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** Same implicit root the Pi adapter uses when `stateDir` is omitted. */
|
|
5
|
+
export function effectivePiStateDir(input) {
|
|
6
|
+
return input.stateDir ?? path.join(input.workspaceDir, ".larkin");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 从 root 起逐层建 0700 子树。root 可以位于 /var、/tmp 等合法祖先 link 之后;
|
|
10
|
+
* 任一层 owned 组件自身是 symlink 则拒绝,不顺着 link 往外写。
|
|
11
|
+
*/
|
|
12
|
+
export function ensureOwnedTree(root, parts = [], create = true) {
|
|
13
|
+
const resolved = path.resolve(root);
|
|
14
|
+
if (fs.existsSync(resolved)) {
|
|
15
|
+
const stat = fs.lstatSync(resolved);
|
|
16
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
17
|
+
throw new Error(`unsafe private directory: ${resolved}`);
|
|
18
|
+
}
|
|
19
|
+
else if (create) {
|
|
20
|
+
fs.mkdirSync(resolved, { recursive: true, mode: 0o700 });
|
|
21
|
+
if (fs.lstatSync(resolved).isSymbolicLink() || !fs.lstatSync(resolved).isDirectory()) {
|
|
22
|
+
throw new Error(`unsafe private directory: ${resolved}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error(`unsafe private directory: ${resolved}`);
|
|
27
|
+
}
|
|
28
|
+
let dir = resolved;
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
if (!part || part === "." || part === ".." || /[\\/]/.test(part))
|
|
31
|
+
throw new Error(`unsafe owned path component: ${part}`);
|
|
32
|
+
dir = path.join(dir, part);
|
|
33
|
+
try {
|
|
34
|
+
const existing = fs.lstatSync(dir);
|
|
35
|
+
if (existing.isSymbolicLink() || !existing.isDirectory())
|
|
36
|
+
throw new Error(`unsafe private directory: ${dir}`);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT")
|
|
40
|
+
throw error;
|
|
41
|
+
if (!create)
|
|
42
|
+
throw new Error(`unsafe private directory: ${dir}`);
|
|
43
|
+
fs.mkdirSync(dir, { mode: 0o700 });
|
|
44
|
+
if (fs.lstatSync(dir).isSymbolicLink() || !fs.lstatSync(dir).isDirectory())
|
|
45
|
+
throw new Error(`unsafe private directory: ${dir}`);
|
|
46
|
+
}
|
|
47
|
+
if (process.platform !== "win32")
|
|
48
|
+
fs.chmodSync(dir, 0o700);
|
|
49
|
+
}
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
export function ensurePrivateDirectory(directory) {
|
|
53
|
+
ensureOwnedTree(directory);
|
|
54
|
+
}
|
|
55
|
+
/** 独占新建 0700 目录;已存在或变成 symlink 都拒绝。 */
|
|
56
|
+
export function mkdirPrivateExclusive(directory) {
|
|
57
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
58
|
+
const created = fs.lstatSync(directory);
|
|
59
|
+
if (created.isSymbolicLink() || !created.isDirectory())
|
|
60
|
+
throw new Error(`unsafe private directory: ${directory}`);
|
|
61
|
+
if (process.platform !== "win32")
|
|
62
|
+
fs.chmodSync(directory, 0o700);
|
|
63
|
+
}
|
|
64
|
+
/** 独占新建 0600 文件;已存在(含 symlink)时失败,不跟随写出。 */
|
|
65
|
+
export function writePrivateExclusive(file, content) {
|
|
66
|
+
fs.writeFileSync(file, content, { mode: 0o600, flag: "wx" });
|
|
67
|
+
if (process.platform !== "win32")
|
|
68
|
+
fs.chmodSync(file, 0o600);
|
|
69
|
+
}
|
|
70
|
+
/** 原子写 0600 文件;目标或父目录已是 symlink 时拒绝,避免落到目录外。 */
|
|
71
|
+
export function writePrivateAtomic(file, content) {
|
|
72
|
+
try {
|
|
73
|
+
if (fs.lstatSync(path.dirname(file)).isSymbolicLink())
|
|
74
|
+
throw new Error(`unsafe private directory: ${path.dirname(file)}`);
|
|
75
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
76
|
+
throw new Error(`${path.basename(file)} must not be a symlink`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code !== "ENOENT")
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
85
|
+
fs.renameSync(temporary, file);
|
|
86
|
+
fs.chmodSync(file, 0o600);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
try {
|
|
90
|
+
fs.unlinkSync(temporary);
|
|
91
|
+
}
|
|
92
|
+
catch { /* renamed or absent */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { createLarkinTmux, DEFAULT_BASH_WAIT_SECONDS, ForeignTmuxTaskError, formatTmuxTaskText, LARKIN_TMUX_COMPLETION_TYPE, tmuxAvailable, } from "./pi-tmux.mjs";
|
|
4
|
+
function resultOf(snapshot) {
|
|
5
|
+
return {
|
|
6
|
+
content: [{ type: "text", text: formatTmuxTaskText(snapshot) }],
|
|
7
|
+
details: {
|
|
8
|
+
taskId: snapshot.taskId,
|
|
9
|
+
status: snapshot.status,
|
|
10
|
+
exitCode: snapshot.exitCode,
|
|
11
|
+
output: snapshot.output,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function errorResult(message) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: message }],
|
|
18
|
+
details: { taskId: "", status: "failed", exitCode: null, output: message },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function registerLarkinTmuxExtension(pi) {
|
|
22
|
+
if (!tmuxAvailable())
|
|
23
|
+
return false;
|
|
24
|
+
const stateDir = process.env.LARKIN_STATE_DIR || `${process.cwd()}/.larkin`;
|
|
25
|
+
const agentId = process.env.LARKIN_AGENT_ID || "agent";
|
|
26
|
+
const instanceId = process.env.LARKIN_TMUX_INSTANCE_ID
|
|
27
|
+
|| process.env.LARKIN_RUNTIME_OBSERVATION_GENERATION
|
|
28
|
+
|| crypto.randomUUID();
|
|
29
|
+
const tmux = createLarkinTmux({ stateDir, agentId, instanceId, env: process.env });
|
|
30
|
+
const watchers = new Map();
|
|
31
|
+
const notified = new Set();
|
|
32
|
+
const stopWatchers = () => {
|
|
33
|
+
for (const timer of watchers.values())
|
|
34
|
+
clearInterval(timer);
|
|
35
|
+
watchers.clear();
|
|
36
|
+
};
|
|
37
|
+
const notifyOnce = (snapshot) => {
|
|
38
|
+
if (snapshot.status === "running" || notified.has(snapshot.taskId))
|
|
39
|
+
return;
|
|
40
|
+
notified.add(snapshot.taskId);
|
|
41
|
+
pi.sendMessage({
|
|
42
|
+
customType: LARKIN_TMUX_COMPLETION_TYPE,
|
|
43
|
+
content: formatTmuxTaskText(snapshot),
|
|
44
|
+
display: true,
|
|
45
|
+
details: {
|
|
46
|
+
taskId: snapshot.taskId,
|
|
47
|
+
exitCode: snapshot.exitCode,
|
|
48
|
+
output: snapshot.output,
|
|
49
|
+
},
|
|
50
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
51
|
+
};
|
|
52
|
+
const watch = (taskId) => {
|
|
53
|
+
if (watchers.has(taskId) || notified.has(taskId))
|
|
54
|
+
return;
|
|
55
|
+
const timer = setInterval(() => {
|
|
56
|
+
try {
|
|
57
|
+
const snapshot = tmux.peek(taskId);
|
|
58
|
+
if (snapshot.status === "running")
|
|
59
|
+
return;
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
watchers.delete(taskId);
|
|
62
|
+
notifyOnce(snapshot);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
clearInterval(timer);
|
|
66
|
+
watchers.delete(taskId);
|
|
67
|
+
}
|
|
68
|
+
}, 250);
|
|
69
|
+
watchers.set(taskId, timer);
|
|
70
|
+
};
|
|
71
|
+
pi.on("session_shutdown", () => {
|
|
72
|
+
stopWatchers();
|
|
73
|
+
});
|
|
74
|
+
pi.registerTool({
|
|
75
|
+
name: "bash",
|
|
76
|
+
label: "bash",
|
|
77
|
+
description: "Run a command in a detached tmux window. timeout is a wait window only and never hard-kills. background:true returns immediately.",
|
|
78
|
+
parameters: Type.Object({
|
|
79
|
+
command: Type.String(),
|
|
80
|
+
timeout: Type.Optional(Type.Number()),
|
|
81
|
+
background: Type.Optional(Type.Boolean()),
|
|
82
|
+
}),
|
|
83
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
84
|
+
const snapshot = tmux.start(params.command, ctx.cwd);
|
|
85
|
+
if (params.background === true) {
|
|
86
|
+
watch(snapshot.taskId);
|
|
87
|
+
return resultOf(tmux.peek(snapshot.taskId));
|
|
88
|
+
}
|
|
89
|
+
const timeout = typeof params.timeout === "number" && params.timeout >= 0 ? params.timeout : DEFAULT_BASH_WAIT_SECONDS;
|
|
90
|
+
const waited = await tmux.wait(snapshot.taskId, timeout, signal);
|
|
91
|
+
if (waited.status === "running")
|
|
92
|
+
watch(waited.taskId);
|
|
93
|
+
return resultOf(waited);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
pi.registerTool({
|
|
97
|
+
name: "tmux",
|
|
98
|
+
label: "tmux",
|
|
99
|
+
description: "List, peek, or kill Larkin-owned tmux tasks for this Agent. Foreign task IDs are rejected.",
|
|
100
|
+
parameters: Type.Object({
|
|
101
|
+
action: Type.Union([Type.Literal("list"), Type.Literal("peek"), Type.Literal("kill")]),
|
|
102
|
+
taskId: Type.Optional(Type.String()),
|
|
103
|
+
}),
|
|
104
|
+
async execute(_toolCallId, params) {
|
|
105
|
+
try {
|
|
106
|
+
if (params.action === "list") {
|
|
107
|
+
const tasks = tmux.list();
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: tasks.map(formatTmuxTaskText).join("\n---\n") || "no owned tmux tasks" }],
|
|
110
|
+
details: { taskId: "", status: "completed", exitCode: 0, output: "" },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!params.taskId)
|
|
114
|
+
return errorResult("taskId is required for peek and kill");
|
|
115
|
+
if (params.action === "peek")
|
|
116
|
+
return resultOf(tmux.peek(params.taskId));
|
|
117
|
+
const killed = tmux.kill(params.taskId);
|
|
118
|
+
notified.add(params.taskId);
|
|
119
|
+
const timer = watchers.get(params.taskId);
|
|
120
|
+
if (timer) {
|
|
121
|
+
clearInterval(timer);
|
|
122
|
+
watchers.delete(params.taskId);
|
|
123
|
+
}
|
|
124
|
+
return resultOf(killed);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof ForeignTmuxTaskError)
|
|
128
|
+
return errorResult(error.message);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
export default function larkinTmuxExtension(pi) {
|
|
136
|
+
registerLarkinTmuxExtension(pi);
|
|
137
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ensureOwnedTree, writePrivateAtomic } from "./pi-state-dir.mjs";
|
|
5
|
+
import { tmuxAvailable } from "./pi-tmux.mjs";
|
|
6
|
+
const BUNDLE_NAME = "pi-tmux.bundle.js";
|
|
7
|
+
export function materializeEmbeddedPiTmuxBundle(configDir) {
|
|
8
|
+
const embedded = globalThis.__LARKIN_EMBEDDED_PI_TMUX_BUNDLE__;
|
|
9
|
+
if (!embedded || !configDir)
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
const dir = ensureOwnedTree(path.resolve(configDir), ["providers", "pi", "extensions"]);
|
|
13
|
+
const target = path.join(dir, BUNDLE_NAME);
|
|
14
|
+
try {
|
|
15
|
+
if (fs.lstatSync(target).isSymbolicLink())
|
|
16
|
+
throw new Error("pi-tmux bundle must not be a symlink");
|
|
17
|
+
if (fs.readFileSync(target, "utf8") === embedded)
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== "ENOENT")
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
writePrivateAtomic(target, embedded);
|
|
25
|
+
return target;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new Error("Could not materialize the Larkin tmux extension", { cause: error });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function bundledPiTmuxExtensionPath(configDir) {
|
|
32
|
+
try {
|
|
33
|
+
const resolved = fileURLToPath(new URL(`./${BUNDLE_NAME}`, import.meta.url));
|
|
34
|
+
if (fs.existsSync(resolved))
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* fall through to embedded */
|
|
39
|
+
}
|
|
40
|
+
return materializeEmbeddedPiTmuxBundle(configDir);
|
|
41
|
+
}
|
|
42
|
+
export function resolvePiTmuxExtensionArg(input, resolveBundle = () => bundledPiTmuxExtensionPath(input.env.LARKIN_CONFIG_DIR), available = tmuxAvailable) {
|
|
43
|
+
if (input.platform === "win32" || !available(input.env, input.platform))
|
|
44
|
+
return null;
|
|
45
|
+
const bundle = resolveBundle();
|
|
46
|
+
if (!bundle)
|
|
47
|
+
throw new Error("Larkin tmux extension bundle is missing; rebuild or reinstall Larkin");
|
|
48
|
+
return bundle;
|
|
49
|
+
}
|