@plannotator/cline 0.0.0-stage → 0.28.51
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/README.md +26 -2
- package/dist/index.js +1610 -0
- package/package.json +42 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1610 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
3
|
+
import { dirname, join as join4 } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// inbox.ts
|
|
7
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
8
|
+
|
|
9
|
+
// ../../packages/shared/config.ts
|
|
10
|
+
import { isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
|
|
11
|
+
|
|
12
|
+
// ../../packages/shared/data-dir.ts
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { isAbsolute, join, resolve } from "path";
|
|
16
|
+
function getPlannotatorDataDir() {
|
|
17
|
+
const home = homedir();
|
|
18
|
+
const envDir = process.env.PLANNOTATOR_DATA_DIR?.trim();
|
|
19
|
+
if (envDir) {
|
|
20
|
+
if (envDir === "~")
|
|
21
|
+
return home;
|
|
22
|
+
if (envDir.startsWith("~/") || envDir.startsWith("~\\")) {
|
|
23
|
+
return join(home, envDir.slice(2));
|
|
24
|
+
}
|
|
25
|
+
return resolve(envDir);
|
|
26
|
+
}
|
|
27
|
+
return getDefaultPlannotatorDataDir(home);
|
|
28
|
+
}
|
|
29
|
+
var DATA_DIR_MARKERS = [
|
|
30
|
+
"history",
|
|
31
|
+
"sessions",
|
|
32
|
+
"drafts",
|
|
33
|
+
"inbox",
|
|
34
|
+
"claude-code-mod",
|
|
35
|
+
"install-flags.json"
|
|
36
|
+
];
|
|
37
|
+
function holdsPlannotatorData(dir) {
|
|
38
|
+
return DATA_DIR_MARKERS.some((entry) => existsSync(join(dir, entry)));
|
|
39
|
+
}
|
|
40
|
+
function xdgPlannotatorDataDir() {
|
|
41
|
+
const xdgDataHome = process.env.XDG_DATA_HOME?.trim();
|
|
42
|
+
return xdgDataHome && isAbsolute(xdgDataHome) ? join(xdgDataHome, "plannotator") : null;
|
|
43
|
+
}
|
|
44
|
+
function getDefaultPlannotatorDataDir(home = homedir()) {
|
|
45
|
+
const legacyDir = join(home, ".plannotator");
|
|
46
|
+
const xdgDir = xdgPlannotatorDataDir();
|
|
47
|
+
if (existsSync(legacyDir)) {
|
|
48
|
+
if (xdgDir && existsSync(xdgDir) && holdsPlannotatorData(xdgDir) && !holdsPlannotatorData(legacyDir)) {
|
|
49
|
+
return xdgDir;
|
|
50
|
+
}
|
|
51
|
+
return legacyDir;
|
|
52
|
+
}
|
|
53
|
+
return xdgDir ?? legacyDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ../../packages/shared/config.ts
|
|
57
|
+
import {
|
|
58
|
+
readFileSync,
|
|
59
|
+
writeFileSync,
|
|
60
|
+
mkdirSync,
|
|
61
|
+
existsSync as existsSync2,
|
|
62
|
+
openSync,
|
|
63
|
+
writeSync,
|
|
64
|
+
closeSync,
|
|
65
|
+
statSync,
|
|
66
|
+
unlinkSync,
|
|
67
|
+
renameSync,
|
|
68
|
+
realpathSync
|
|
69
|
+
} from "fs";
|
|
70
|
+
|
|
71
|
+
// ../../packages/core/favicon.ts
|
|
72
|
+
var FAVICON_PNG_BASE64 = [
|
|
73
|
+
"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAYhklEQVR42u2be7hdZX3nP7/3XWvty7mfkxu5UUJuJAFFiVIU",
|
|
74
|
+
"QS2d4gUrTuIUSisdBkZQEerz1Gr1cNqq1YoOF9vn6eA4M0adSWxBvFC1ToowyCVcDEkgGELI/SQ5J+e6L+vy/uaPtfY+e+9z",
|
|
75
|
+
"DgSVTv/ofp717GSdtdf7/m7f3/WFV/HT34/RfryZ/qabsdqP2bwh/X6pZ17NPcqr9WLtx8gADuCZu/o6lqqsNLHrdcr4aC54",
|
|
76
|
+
"fuF/PHq88fnjd/UtbIuT5cZKu3PeyMl277lFVx450fquf5UMUBD6EdYi7ERZi8hGkhO39S5uN+ZPrdrf9YSFeEACseOkWv1h",
|
|
77
|
+
"VYLPuGSyI2+8T0hsLvY92jEKiRDBMJ7cPVGRP+/96JH9W/vxLl6LshMh/dZfB1N+JQaoImzByEaS1r+d/GLf29vz5hueb+dT",
|
|
78
|
+
"VeJYVUEB8Q1CwRKWNAL1goKIlh2J4gBVxfgWoWiIYxmMVC4v3nD4oZm0DOBXYYT8OlT8B7eRu7CtZ4VWbV+MOUniutrV/NA3",
|
|
79
|
+
"UogijQQ8JFtLFRBVcFawquBUEwQjSO0pVFVRYj9v/ShmuBTopZrEHeIzX5yMJL63s/eaoy8C6AasbJkuhFeNATXid/TTfuac",
|
|
80
|
+
"vo95Tq4EWe5ZIY7BKc4XMbFTZySVkjYuppqZjmh2Q0AQabYrAKeqvjUSo3g+YAEnxLErqTU/HI/4dN9HB3foZuxMmvhrZ0CN",
|
|
81
|
+
"+CO3zz+jj+QeP2fPoeJIElBwgpiMRsWk7xeROtGNDAABSXW+kQE1ZqW6AppxyKFONGWNEbGmKCSOiUqcbGz/yIn7fhmwlFds",
|
|
82
|
+
"87cgQ7297R1qHglydnVcSkJEPKi5K8lEqqIZgdKoAY2MyJ7XmTYm09bO+NBwS0l8X7zYUCoZ73Wd1x9+jn7klTDBvBLi991C",
|
|
83
|
+
"jlvQgpM/CYpmdVxyoYgE6XtkSm6qktKpdeKntFqa+K4t0pAGRtVhs64rLTwSvCjS2PNNMYiSLwDQi6+bsb9WDWi1r+qX5x4K",
|
|
84
|
+
"rCyIY0UQo41kKM1SbxClam1BbcDD5q1IE2N0CjvRFiBp0DYBZ4hOtJvFtfhC+zHcgsrMCnbqDKjZ1dBXepbknX2fh1xkYnmP",
|
|
85
|
+
"1BGsQcrarL6ZZbfEDFqnXKThdylapL/VmfRjGj4iSPY+UWNFYqOP4OvPQtx3Oj449M81zX0pJrwUA0Qzeyrd0fcxH/NJzzfd",
|
|
86
|
+
"OIgrrkm1Z9JnkWYbl7pMa5FTuvo0Jknd4KdvsmHRVmVQBC9nqAVbEfrtku9f1/2fDg6/FBNmZUDNt058qe+Otg7vQzrhSBxx",
|
|
87
|
+
"ptWWFkG1Aj2SInhq1NKKZtNNpkkdMnORVokzTZskw0UVQQUnqg4V8TqNjSr6+Gih+61zDuyenM0cZk1CZAvJxG1zr2pr8z4U",
|
|
88
|
+
"j7kodqgIXo34mgpKzZXNgNxTFGidOVojVGkEONK4pwEb0CaGtoYI2giNtS/FgHiI2mg0Cf28vL6tNPJ5GcCxZWZaZca4XuEX",
|
|
89
|
+
"txMsdXN25TzzG1GkmAZmaQNkTflszSAp25w0aWxqr5lTb9VvaQBEadmMSgswNuJFg1lJE7zWeK2qIslETlb2fHBw30xxwnSu",
|
|
90
|
+
"bMaIoHOinvU5a5bFkSLTnpMWvNYmOxWZDn6pSUjd9lv/KDLj7YyJDR6iLnRtWlnRpjsCog7n5Y3vR3rJbPROZ8DO9I05Y9fg",
|
|
91
|
+
"oWng2ajSDURktquzKVYW2k8FQJq5Qmlx7joNS7VBWVpX0FYQ0elr1xVNVMWx4hUFQtqPwUhQd+IiDSinrc66Oc+Zwd01gkMK",
|
|
92
|
+
"AQ1uoMWFNt3QZg2gBVObQiqdzWuKGCScrbAy7ebjC1PXV411X12jlOkOftZQTprSnBmLBzU+qs648VYTmuZydbrg6yZUA1Od",
|
|
93
|
+
"Ylsk7pAM4Fg4o5VND3pOfLn3rC7PbpZE1jqnmIY0dfbIRKcHatO8XYOvnCHzU5kZlWnykDqjX5BGQ6k7H1FjRfHkxEQiV3Xf",
|
|
94
|
+
"eORHrUAorYnO4Y723jk2/3iQs0ujinNGxJxKSYiXjjhncUD6sq8VfamYcFowMQ0bFFE/EIlhfDJw53Zde3wvt0wlTFPEbUk5",
|
|
95
|
+
"02NynwoKZmlUcpERMVrz4TpTJtcIVoLDkGBIsA2Xl35L7f8GpwaXPZ9eNr3UkOjU7x2WOLumnjVNVjlbiFS7bQSJQhd5AR25",
|
|
96
|
+
"qhkQSUt2zdlEFio+c1dfx7Ixed4zZo5TVbRB+hlouQboERSj6VYhAYkBN3XV4910J/V/N2mA1JP+ZpDVqf+rZgmnAc0u8VOG",
|
|
97
|
+
"YVAMU0GnIupSNylSL7z4BqrOjZZ7ojN6rh4dqdGclqxvSXczZ1JWelbmulgVwYBmHDeZHEMgzAhVwAeTZ1KLjLkuRrSb0aSL",
|
|
98
|
+
"Ee1kJOlg1HUxru1MuiJlClQ1R5Ug1RJN35uCl8NqghVHIBEBEXlKtEmJdjNJlxmnw0zQKeN0yyg95iSdMka7mcTXElAFdaSJ",
|
|
99
|
+
"gI/DJ8YDBSMOQSVO0MA33ePVwnIY3bZlIwZIUgasRehXY2Vh0RDjUEVFnBgCUwIJgSKDbgH7kyXsTZaxN1rKvmgxh5IFDCW9",
|
|
100
|
+
"jCQdlJI8kaaLp8QJ1oBn0m9rUkWoXVIXvqAKiSqJA+cgUUhcejlVVB2iCb7EFKRMh1eix46ywJ5gqX+YM7z9LPf2coa3j4Xm",
|
|
101
|
+
"MHkdBrFE2l43XTFgEhPQ32827BqAjGWws18ZENdzJ/vjcF5swDpRDcyYbE/Wc2/lnTxWeQ17q0sZSrqJyeFZQ1ugdBeU7rxj",
|
|
102
|
+
"cQG6itBVgI68oZgzFAIh8IWcJ/iewbNSJ9w0hH6aqb9LC6Q4B7FTwkipxko5dJSqMFkVxsqWkcmAkVInI5WF/LwsPFgSqrGA",
|
|
103
|
+
"xrTLOEv8I7yusJPLC9/hzd79xFpUa0QiNdXetsN7GRhw9PcbGEglz4C4df3bV1dk3l33dLz//LWyzcQmJ1+Y/Bh/O/KHhK7A",
|
|
104
|
+
"gk7H4i7Hoh5Y1Gs5rdujs82jveARWEklZARrTC3ayRKcKeysF3gUGnNCoTG+b/brIo35QlYlysKbMHaUqwnj5YThiZjBkYQD",
|
|
105
|
+
"Q44XTsCLIwbnHNf2fINPtX0Gn7I+7N7qNo59/cHT4t1XP/qZ81+gX40Ht3BR/9b8gbj77xe2xWsWyAGHjeSz4wN8cfg6fvO0",
|
|
106
|
+
"Ud7xOsPKhUXmd3l05oWJinJ0IoXDaphQVUtHZw7noFpVojDCudTVGmMwRk7RMWqTR3Xq6gRbawlyPrkAkgRGR8t41lLMp0JY",
|
|
107
|
+
"PDePFcFpqjFHhqrc++gotx28Bl8SPtX+p7Ik2SfzA71oOJ7/rYv6t775/lqYf07/IyvDaNHuIuPu23OukJOuVy49+i3OW1jh",
|
|
108
|
+
"xncv4LzFHp0+WFHECHtOJDxxKGZeX45vbvo+/7Dpbtaddw7nvHYNa9atYsmShRSLHmIgrEIYRs2VHpklimyJ9QsFH9+HKIKx",
|
|
109
|
+
"sTJ7n9/H9qd28eTjT3P0yFH+4q8+wW8sW0K1EmZRey1yV/KBIYyV275zlJ0ncvx4wfuIndErRv6HS9RovrN0+pN/ctZhAVj1",
|
|
110
|
+
"Z8+s8pLcM6Hmpd2OaZtMyOHqPG56dy9/8IYinbkUnKwRjk8oDx9IUFU8TxgbneCpJ3ewbdvTPP/cC1QqFdrbiqxcs4Izly1h",
|
|
111
|
+
"3TlrWLL0NJLEpX6nodCBzhwbaabuO3Y+x/PPPc/ze/bz3LN7SJKE3r5u1p29mvPe+FpWr16OMXam8gNJorQXLU+9UOLO+0Y4",
|
|
112
|
+
"vXiM4aRLq9ouHpWq9aPlTw+sPJh1bkNU8pKTiIorMu7a6S3GnD7Hp91PVc5aGCkrjxxMcKpYIySJ0tXdxW9feiGXvfdCDh6a",
|
|
113
|
+
"4Ibr/ow9T+5izzN7KLQVedfl/47rrr+S0mSCGKm7+KYicr0Gl2KDZy0nR8b4+n/fwjO7nocjx6Crnbdf9lt88tPXY71Us6qV",
|
|
114
|
+
"uG4irTGQsRDFjnldPh05x+HqAjyT4EsEDeGNaa3JepkvFmPwbbpha2GsAj/bn5A4xTNSr0vHccz4WIUTQ1Vu/fzfcN171/PG",
|
|
115
|
+
"i1+Hdcqys1bw+x/YQLWSIMZMF7rOlLul75w7t4vrbvgA689dweduvYE5i+bxk83f4yu3b6JaVibGyynx0pCsthRg0gZKRhcR",
|
|
116
|
+
"BldL1qSFAUEdo5tKCpnvnKzCQ/sTqklKfFMy6JTurjwPPPAkZ84J+MuPX8n1v/92EqfsfOwJHrj/UQpFLwVFmUJ1dPZ6n1Ml",
|
|
117
|
+
"lzP8r2/eww3vfxMfv/49/Nab1iJewD/+4CccOXKCfD6Happ1qk6V3IXpdas0Q5zKFUSmctW0jWUjASto0rSpKIGhEjx8IKEc",
|
|
118
|
+
"p8Q71WmFSevBE9u2E0YxAA9t2w3WIsby+GNPNRcu9OU7MJ61jI6E7Nj+LLlcnjhJ2PWLQ1DMM3lylB1PP0sul7o5bWqmzJZs",
|
|
119
|
+
"KTWaDQmoeg0MUPEwB2NNDoReD0ZjRRUjaRDys/0xE6HiZ5KXFgQThGoIpYlx7v7+o1y4cYC7Nt+P15ZHRTg2eIJqVTFipny/",
|
|
120
|
+
"zp6Tq6Yub2RklLBS4aN/uYnzL+9n+44XCIo5iB1HDg82F5pbaiKN5bKaaYsqRhMXen0k6M6E8Bj9asyGDVvMroF1E0vzB3/v",
|
|
121
|
+
"DG93ZUT7JFZb620hAlZkhpxLmhgtgIsTHnxoV5pdZTiRJMnL1uB1BhyIogTrWQYHT/L4tt14+SCz+RQjWnsnU0CqJCL4zuG7",
|
|
122
|
+
"JGOIo+o8HaXHnGl3jC/vOHbFroF1YT/gbV6zUQXhR31vGSnH8/37cu/gC6MfkYh5SK3QILO7b1XFWOjq7kLUkW8vEMUxIoIk",
|
|
123
|
+
"ju6eLnxfiEKlVlrQrDaojZ0kmao1Js7R1lYkn8tRLVXwC7mMkanL6+3tbmzfNICe4kQQhfVHjjLY3cFB20aCz+LgsNzccQe/",
|
|
124
|
+
"U/xxPvAHQ0nLH5jMDDhWXrSq4Im9vPA1d27wFEOVPGEUZ6CpM0qvMcRdtXp56n9rlSERNHGsXL0c61HHDpflocWcoT2fXoXA",
|
|
125
|
+
"pJvPaohxFNPT28HS0xej1TBbR1CnSCHHqtUriMJGc5R6nO1EWHf8BL3lChihUo05GRW4KP8Al+U2JYHn+cerp68AhbWIYW0q",
|
|
126
|
+
"UE/DkjqDcwU5K3iesQrsPx4SeIKbBbhEFGMMlXLMBW9+Pb1LFhJOlPADn7BcId/bxcVvu4BK2WHEkKiS9w1Jojy+Z4J7Hxvl",
|
|
127
|
+
"e9tGeHrfROqLPJNlfoq1wqXvfBsaRmls4PtEo2Oc89p1rFx9BpVKtR79iSoqEFnL2qFhFk1MUrIek4U8+45UqcTKKn8vjjZR",
|
|
128
|
+
"B3mpTk6B4M6UAZUg3hVGcWjw5NLcj7TLjrN1R4nYpb7UzQjiqeJFUUxPTwc3/vG1FNvaqAydxDOGD910DUuWzKdaDXFAwTfs",
|
|
129
|
+
"Plzhc3cP8YV/jPn7R0tsebTCX93nuPW7wxwYCsn7BhXD5GTIxW97I++66nKisUkqwyc5bdnp3HDj1VPpbWbzsUkLNWcfP8GS",
|
|
130
|
+
"8XEAhjuKHBafB3aVOS0Y4mL/n9VIIGEcTU76+gwAG7JcoNYHLH1p7ncLee9dVE/Gn5r8a+/OE1fyB+ur/O4F8yhV4owZM4yy",
|
|
131
|
+
"CDjnKBZz7HvxKM9u38UZq85k5crTKU1WUYS2vGH3wQpf+sEoRSb4SNd/5fxgG06En8YXccfQ1eQCj49d1sNpPQHl0OFZ8Hyf",
|
|
132
|
+
"J5/YwfDxYV7/xnPp6e6gWg0xJrX1yBjaooizj51gbqlMbISR9iLPLprHpgeOs2V7G5+ecxs3Fz4b0zbHq1TibxZuHLyy1vKX",
|
|
133
|
+
"eh9gAB26rfesTvUe8zUujLoCv3fym/JYaR0bXlPl0vV9FHOGKFbihMwstKEBojin5HIBQU4Iq5qpqaEtbxgaj/j8PcOUy1U2",
|
|
134
|
+
"z72as70HQdvTd9gJtlYv56rBO1gx13HTZXPJeUI5TDPKYjGHtVAux8RxjDEmJV6EheMTvObYiTRgay8w1NXODpvnO48M8d2d",
|
|
135
|
+
"BS7t/L98teOPNO8bicSMTPp6bvd/HnyxVhi1AAP3o2s3YNffWj5202/nn/at//4ik7w9/39kp76Ge188g937x8FFtAVCMQOu",
|
|
136
|
+
"nC/4nuBbwQp4VkATojBGcOQDS94X9h8v83c/Psme0XbumPsJ3uJ9n7IuQLEkEhC6Llb4T1D0Pb5x/BKOHRvizAUBfR0+vjUk",
|
|
137
|
+
"cUQUJlgB30tD9MAaAl8oWBjuLPJsVxePked7L8Zs+ukkD+8v8u7OrdzZeSPttqqJ50cTqpf1fGjwyVuYKo1LY0eYDbiRL/e9",
|
|
138
|
+
"tdOTf4pDq4GpmCo+d1Wv5Wsj/4F94SLa88KS7oQlPTC/S+htN3TkJWVGVvFRIIpheNKx40DIQ3sNUeL4i+7P8kf5v6NKF7Y+",
|
|
139
|
+
"cJJuwSEEMskXyx/nr09+kM5cwpuWKasX+XS3GQKbPhc7iGKlEinjFeXYJBwZUw4Nw9FRKFeVZbmDXNO5iatz/w2rSiw5Z3LK",
|
|
140
|
+
"WJJc3PPhEw82zjZKQ1PEkwHisVv7bu0o2JujUhKD9Yw4rBljWBexNXwrW8sX8nR1FUeTeYwmRRICxBisae7POk39eZEJLmjb",
|
|
141
|
+
"zoeLd3KB/SlV6cTO2DZKw+xAJvhxfCl/M3Et2ybXUKGItQZrqL/X1QvGCQEhXWaCRcEgZ+ee5S3Bg1zk/5RuOUKsnSgGcLHf",
|
|
142
|
+
"Yb1Kmc8Wbjz6yRqtUzXBho9V01XrbwsOp0Kc9NIro7wv9w3el/sWVe1hkPkcSeZxPJ7DSe1lzLVT0TwhPhZHmymxwBzlbH8n",
|
|
143
|
+
"K2UXBDHYTnKhEsZp4bM+WQEYlCAAgk4uie/jEn8rO7vWsitcxaCbz4QrIkDeVClQodOM02uGmGuHWGCPM9ccw9OxbBahSFV7",
|
|
144
|
+
"sLipKlNaYO1spXcaA5wwmJXZ69GZJSHCw7keBPAlZCl7WWqfAy/J+gBN3c+pxqgLcEGBf/i55ZkjynVvipnXpc2DgALEwoFh",
|
|
145
|
+
"4asPC284vZtL1yasrf6ctYXHmievmjoyJosOfRLnE2oqu7TVkjTP4RjA6OHZGZAFRLG4n5DYT6Rjq1O9PCOKZNli2r3J19Nm",
|
|
146
|
+
"bejmNlZ6RcCh5I3j0Bh8+vsBA1stF57heO0Cx4L21Pb3j8K2Q8K2/QZi4WtXRiBKmTaMa5/uc+sZ3lQtUVTTfL81QUgr0KKh",
|
|
147
|
+
"EnvxPzXSOmNzdMta5F375zxUyJs3hBUXm3QIsnXco6l7O1WVaWmPZu5RHXg5+MUgbHrS477dsGfYMhGmTOrOK2vmOt69Rtl4",
|
|
148
|
+
"TsLiHiEOG9ecaQiptb0oM7aZVYn8ovjlsm4t3nzsbbM2Rxu7w8O39pzdbu1DvpH2KNQIEZu9rj76KZnkJZvMqHdnG9WuYYRO",
|
|
149
|
+
"FfxAwHcQQ7UilDIGdOTA5jVV06oQxWBMg2Y1zZrqzElJQ16QjWEokPg58SPHyGgSnz/npqFpk6Qy21zg6BfnXlD0dZMXmDOI",
|
|
150
|
+
"FBeljQsaZiamBN+wgfq01/RkN+3+pKG1Z2rlqhTVY5d+W6FpLkNnmsWbVkZq1jwrgvgCgSEO3Z7JRK7o/ujRx2aaEZoxRd+8",
|
|
151
|
+
"AbtxC8mLn+vqmV/MXW/Q94rjLByFjFaR2Yu6p9T+Vm0eBGodlUkryPoSP24BxZRZKghqZFI93aUidx/yw78987qTo7MNUsup",
|
|
152
|
+
"nAcAOHnH/Iu7ErfVJSSZoGYgrxEFGyZGWsdIWwcKT2W2QF9mxyneJIiYEeQ359w0+MhstJzyqGw2NGHZhcoWkvKtvffk27z3",
|
|
153
|
+
"xJMaSlpJbZabtoJgyxzczDp96szQlmHkhpEdRUO/0waViWRT4abjV+lmLDsRbiH5ZUdlpx1NGVw8b25vWR/wfVkRl1ycwZxp",
|
|
154
|
+
"GJFSQTxVdZD1vl8CtRVVaZ6JnrV1pKlHFdBEkPrEHekxG/E7rY1Cni6Z4KKurfvHWHNqZ4rklRyBGxjA7b+1d9F837srsPxO",
|
|
155
|
+
"tq2pQMMKrqwYgTjWBCN2ujlkZUtVZw0mPTKDE2rHK5o5ldXqne8bm7oSpooTkPbcRQgd9w4lXLPwxqPHX25A+pc/MNFgSxO3",
|
|
156
|
+
"z3+np/rvjdOzUYpqZcgpP6tituRd/Jlch70kHnNJRoBpwANFVf28eGHFhSA2yImNqi7ODFmaA1OM12VNZVy/Han5L76JNxpY",
|
|
157
|
+
"L9CtMKlGtqu1/7tww5EfNQrq1Tsyo03jwfVxLBqaDUf+5/y2nhH9Sk74QzwgzqQmqZZgIYrZVYrNByxRZ96ar3q+nE6cHplD",
|
|
158
|
+
"swjXgyQSDVW+UnjTopvlvMej2daslS1PVfK/+qmx2qmMjTgBzXDCsBatpZpjt8+5MMBeYRLW41wfUFbLc5Fn7t1e9b91wR8f",
|
|
159
|
+
"LAO8+Lmunjkd+au8hHdIrMsUzalwVK19uGr4eveHj26rr9kAbNl5Rdu45v+vj0ybhWyZzNyxeU2ATo88W5/bvBn73A+W51pH",
|
|
160
|
+
"93WGIzP8a/9kZ389bSm8TLuniPbjNTIjY6L3ap8d/pdjRmvv8qUnaYV/+/zb51/k8/8ATqSGLedi1bsAAAAASUVORK5CYII="
|
|
161
|
+
].join("");
|
|
162
|
+
var FAVICON_PNG_DATA_URL = `data:image/png;base64,${FAVICON_PNG_BASE64}`;
|
|
163
|
+
var FAVICON_PNG_BYTES = Uint8Array.from(atob(FAVICON_PNG_BASE64), (character) => character.charCodeAt(0));
|
|
164
|
+
var FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
165
|
+
<image width="64" height="64" href="${FAVICON_PNG_DATA_URL}"/>
|
|
166
|
+
</svg>`;
|
|
167
|
+
var CLASSIC_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
168
|
+
<rect width="64" height="64" rx="14" fill="#070b14"/>
|
|
169
|
+
<rect x="12" y="28" width="40" height="14" rx="3" fill="#E0BA55" opacity="0.35"/>
|
|
170
|
+
<text x="32" y="46" text-anchor="middle" font-family="Inter,system-ui,sans-serif" font-weight="800" font-size="42" fill="white">P</text>
|
|
171
|
+
</svg>`;
|
|
172
|
+
var CLASSIC_FAVICON_DATA_URL = `data:image/svg+xml;base64,${btoa(CLASSIC_FAVICON_SVG)}`;
|
|
173
|
+
// ../../packages/shared/inbox/connection.ts
|
|
174
|
+
function inboxClaimName(replyId, session) {
|
|
175
|
+
return `${replyId}.${session.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120)}`;
|
|
176
|
+
}
|
|
177
|
+
var INBOX_TOOL_NAME = "plannotator_inbox";
|
|
178
|
+
var INBOX_TOOL_ACTIONS = [
|
|
179
|
+
"send_message",
|
|
180
|
+
"read_thread",
|
|
181
|
+
"search",
|
|
182
|
+
"resolve_message",
|
|
183
|
+
"snooze_thread",
|
|
184
|
+
"submit_guide",
|
|
185
|
+
"get_guide_brief",
|
|
186
|
+
"card",
|
|
187
|
+
"steps"
|
|
188
|
+
];
|
|
189
|
+
var INBOX_REMOVED_ACTION = "wait_for_reply";
|
|
190
|
+
var INBOX_REMOVED_ACTION_TEXT = "action_removed: The Plannotator Inbox no longer has wait_for_reply. Go on with your work or end your turn. If the reply does not arrive in this session by itself, call read_thread with the thread_id later.";
|
|
191
|
+
var INBOX_FILLED_ARGUMENTS = ["project_path", "agent_session", "agent_host", "agent_name"];
|
|
192
|
+
var INBOX_AGENT_PROJECT_PATH = "project_path";
|
|
193
|
+
var INBOX_AGENT_PROJECT_PATH_DESCRIPTION = "The repository or folder this is about; defaults to the folder your session started in. Pass it only when the message is about another repository or folder.";
|
|
194
|
+
function inboxProjectPathShapeRefusal(value) {
|
|
195
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
196
|
+
return "project_path: a folder path (absolute, or relative to your working folder).";
|
|
197
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
198
|
+
return "project_path: no control characters or line breaks.";
|
|
199
|
+
if (value.length > 4096)
|
|
200
|
+
return "project_path: at most 4096 characters.";
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
var INBOX_WAKES_META_KEY = "ai.plannotator/inbox-wakes";
|
|
204
|
+
var INBOX_WORKING_FOLDER_META_KEY = "ai.plannotator/working-folder";
|
|
205
|
+
function inboxToolCallParams(call, options) {
|
|
206
|
+
const meta = {
|
|
207
|
+
...options.wakes ? { [INBOX_WAKES_META_KEY]: true } : {},
|
|
208
|
+
...options.workingFolder ? { [INBOX_WORKING_FOLDER_META_KEY]: options.workingFolder } : {}
|
|
209
|
+
};
|
|
210
|
+
return { name: call.name, arguments: call.arguments, ...Object.keys(meta).length > 0 ? { _meta: meta } : {} };
|
|
211
|
+
}
|
|
212
|
+
var INBOX_REPLY_TO_CHECK = "Before reply_to into a thread you have not read in this context window, call read_thread with no id and match the subject.";
|
|
213
|
+
var INBOX_REF_LINK_RULE = "A bare #123 links to this project's repository. For an issue or PR in another repo, write owner/repo#123 or its full URL (in a GitLab project, a GitHub one needs its URL).";
|
|
214
|
+
var INBOX_PEEK_META_KEY = "ai.plannotator/inbox-peek";
|
|
215
|
+
var INBOX_SESSION_THREADS_MAX = 5;
|
|
216
|
+
var SESSION_THREAD_TEXT_MAX = 80;
|
|
217
|
+
function inboxSessionThreadsParams(projectPath, session) {
|
|
218
|
+
return {
|
|
219
|
+
name: "read_thread",
|
|
220
|
+
arguments: { project_path: projectPath, agent_session: session, limit: INBOX_SESSION_THREADS_MAX },
|
|
221
|
+
_meta: { [INBOX_PEEK_META_KEY]: true }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function inboxHealthPeeks(health, serverSession) {
|
|
225
|
+
const value = health && typeof health === "object" ? health : null;
|
|
226
|
+
return !!value && value.serverSession === serverSession && value.peek === true;
|
|
227
|
+
}
|
|
228
|
+
var INBOX_SESSION_THREADS_MORE = 'More: read_thread with no id lists every thread (asked_by "anyone", include_resolved); search finds older ones by words (all_projects).';
|
|
229
|
+
function inboxSessionThreadsText(result, toolName = INBOX_TOOL_NAME) {
|
|
230
|
+
const structured = result?.structuredContent;
|
|
231
|
+
if (!structured || !Array.isArray(structured.threads))
|
|
232
|
+
return null;
|
|
233
|
+
const oneLine = (value) => {
|
|
234
|
+
if (typeof value !== "string")
|
|
235
|
+
return null;
|
|
236
|
+
const flat = value.replace(/[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g, "").replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, " ").replace(/\s+/g, " ").trim();
|
|
237
|
+
if (!flat)
|
|
238
|
+
return null;
|
|
239
|
+
return JSON.stringify(flat.length > SESSION_THREAD_TEXT_MAX ? `${flat.slice(0, SESSION_THREAD_TEXT_MAX - 1)}…` : flat);
|
|
240
|
+
};
|
|
241
|
+
const time = (value) => typeof value === "string" && /^\d{4}-\d\d-\d\dT\d\d:\d\d/.test(value) ? `${value.slice(0, 10)} ${value.slice(11, 16)}Z` : null;
|
|
242
|
+
const lines = [];
|
|
243
|
+
for (const row of structured.threads.slice(0, INBOX_SESSION_THREADS_MAX)) {
|
|
244
|
+
const thread = row;
|
|
245
|
+
const id = thread?.thread_id;
|
|
246
|
+
if (typeof id !== "string" || !/^msg_[0-9A-Za-z]{26}$/.test(id))
|
|
247
|
+
continue;
|
|
248
|
+
const parts = [`- ${id}`, oneLine(thread.subject) ?? "(no subject)"];
|
|
249
|
+
const name = oneLine(thread.thread_name);
|
|
250
|
+
if (name)
|
|
251
|
+
parts.push(`name ${name}`);
|
|
252
|
+
const questions = thread.questions;
|
|
253
|
+
const open = typeof questions?.open === "number" && questions.open > 0 ? questions.open : 0;
|
|
254
|
+
if (open > 0)
|
|
255
|
+
parts.push(`${open} open question${open === 1 ? "" : "s"}`);
|
|
256
|
+
if (thread.unread === true)
|
|
257
|
+
parts.push("the person wrote something you have not read");
|
|
258
|
+
const steps = thread.steps;
|
|
259
|
+
if (typeof steps?.done === "number" && typeof steps.total === "number" && steps.total > 0)
|
|
260
|
+
parts.push(`steps ${steps.done}/${steps.total}`);
|
|
261
|
+
const at = time(thread.last_at);
|
|
262
|
+
if (at)
|
|
263
|
+
parts.push(`last ${thread.last_author === "person" ? "the person" : "an agent"} ${at}`);
|
|
264
|
+
lines.push(parts.join(" · "));
|
|
265
|
+
}
|
|
266
|
+
if (lines.length === 0)
|
|
267
|
+
return null;
|
|
268
|
+
const total = typeof structured.total === "number" && structured.total > lines.length ? structured.total : lines.length;
|
|
269
|
+
const count = total > lines.length ? `${lines.length} of ${total}, newest first` : `${lines.length}`;
|
|
270
|
+
const check = INBOX_REPLY_TO_CHECK.replace("call read_thread", `call ${toolName} read_thread`);
|
|
271
|
+
return [`Your open Plannotator Inbox threads in this project (${count}). ${check} ${INBOX_SESSION_THREADS_CHANNEL}`, ...lines, INBOX_SESSION_THREADS_MORE].join(`
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
274
|
+
var INBOX_BRIDGE_POLL_PATH = "/api/inbox/bridge/poll";
|
|
275
|
+
var INBOX_BRIDGE_EVENT_PATH = "/api/inbox/bridge/event";
|
|
276
|
+
function parseInboxToolList(result) {
|
|
277
|
+
const listed = result && typeof result === "object" ? result.tools : undefined;
|
|
278
|
+
if (!Array.isArray(listed))
|
|
279
|
+
return [];
|
|
280
|
+
const byName = new Map;
|
|
281
|
+
for (const item of listed) {
|
|
282
|
+
if (!item || typeof item !== "object")
|
|
283
|
+
continue;
|
|
284
|
+
const tool = item;
|
|
285
|
+
if (typeof tool.name !== "string" || !INBOX_TOOL_ACTIONS.includes(tool.name))
|
|
286
|
+
continue;
|
|
287
|
+
const schema = tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : { type: "object" };
|
|
288
|
+
byName.set(tool.name, { name: tool.name, description: typeof tool.description === "string" ? tool.description : "", inputSchema: schema });
|
|
289
|
+
}
|
|
290
|
+
return INBOX_TOOL_ACTIONS.flatMap((name) => {
|
|
291
|
+
const tool = byName.get(name);
|
|
292
|
+
return tool ? [tool] : [];
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function propertiesOf(tool) {
|
|
296
|
+
const properties = tool.inputSchema.properties;
|
|
297
|
+
return properties && typeof properties === "object" ? properties : {};
|
|
298
|
+
}
|
|
299
|
+
function isFilled(name) {
|
|
300
|
+
return INBOX_FILLED_ARGUMENTS.includes(name);
|
|
301
|
+
}
|
|
302
|
+
function leadOf(description) {
|
|
303
|
+
const line = description.split(`
|
|
304
|
+
`, 1)[0] ?? "";
|
|
305
|
+
const stop = line.search(/\.\s/);
|
|
306
|
+
return (stop >= 0 ? line.slice(0, stop + 1) : line).trim();
|
|
307
|
+
}
|
|
308
|
+
var INBOX_CHANNEL_RULE = "A decision in the Inbox stays there: when the person writes in the chat, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Ask in the chat only about something new they raised there that needs no pictures or files, or when they ask.";
|
|
309
|
+
var INBOX_SESSION_THREADS_CHANNEL = "Their decisions stay in these threads: when the person writes in the chat, say what waits and link the thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar).";
|
|
310
|
+
var INBOX_USE_RULE = "Use it for a decision only they can make (a :::question block; go on with other work), finished long work (a short summary with its pictures inline, files in `attachments`), or when they ask.";
|
|
311
|
+
var INBOX_THREAD_RULE = "One thread per work stream, unless the person says otherwise.";
|
|
312
|
+
var INBOX_FOLLOW_THROUGH_RULE = "Be proactive about Inbox updates and about replying to the person through the Inbox when you think they would want you to. Don't withhold important updates or decisions, and don't batch updates to send later: proactivity is better than procrastination.";
|
|
313
|
+
var INBOX_SUBAGENT_RULE = "If another agent started you, report to it instead.";
|
|
314
|
+
var INBOX_AGENT_RULES = `${INBOX_USE_RULE} ${INBOX_THREAD_RULE} ${INBOX_CHANNEL_RULE} ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_SUBAGENT_RULE}`;
|
|
315
|
+
var INBOX_TOOL_LEAD = `The Plannotator Inbox on this machine: message the person, get their answer later; nothing blocks. ${INBOX_AGENT_RULES} Set \`action\` to a tool below, with its fields.`;
|
|
316
|
+
var INBOX_TOOL_WAKE_NOTE = "A reply arrives in this session by itself once it is idle, headed `Plannotator Inbox: <title> (<reply id>)`: end your turn or keep working. To answer it, send_message with reply_to set to that reply id, never one from memory.";
|
|
317
|
+
var INBOX_TOOL_HELP_NOTE = "Every field and question syntax: `plannotator inbox --help`.";
|
|
318
|
+
var INBOX_TOOL_BOARD_OFF_NOTE = "The Board (cards for work that outlasts a reply) is off until the person turns it on; see `plannotator card --help`.";
|
|
319
|
+
var INBOX_STEPS_LEAD = "The person's checklist for this thread, only for multi-step work you're both aligned on. High-level steps, a few plain words each; never your own to-do list. The whole list each call. status: todo, doing, done, waiting (on the person), dropped (say why in note). Update each step as it starts and ends; notifies no one.";
|
|
320
|
+
var INBOX_STEPS_ACTION_LINE = "person's checklist for multi-step work you're aligned on (see the steps field).";
|
|
321
|
+
var INBOX_STEPS_FIELD_DESCRIPTION = `steps: ${INBOX_STEPS_LEAD} No thread yet: send it with the send_message that starts one.`;
|
|
322
|
+
function inboxStepsWakeLine(steps) {
|
|
323
|
+
return `Your checklist here: ${steps.done} of ${steps.total} done${steps.doing ? `; doing: ${steps.doing}` : ""}. If you're still working on this, keep it current.`;
|
|
324
|
+
}
|
|
325
|
+
function wakeSteps(value) {
|
|
326
|
+
if (!value || typeof value !== "object")
|
|
327
|
+
return null;
|
|
328
|
+
const { done, total, doing } = value;
|
|
329
|
+
const count = (n) => typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= 12;
|
|
330
|
+
if (!count(done) || !count(total) || total === 0 || done > total)
|
|
331
|
+
return null;
|
|
332
|
+
const step = typeof doing === "string" ? doing.replace(/[\p{Cc}\p{Zl}\p{Zp}]+/gu, " ").replace(/\s+/g, " ").trim().slice(0, 80) : "";
|
|
333
|
+
return { done, total, doing: step || null };
|
|
334
|
+
}
|
|
335
|
+
var INBOX_AGENT_ACTIONS_NO_LINE = ["snooze_thread"];
|
|
336
|
+
var INBOX_AGENT_ACTION_LEADS = {
|
|
337
|
+
send_message: "Message the person (markdown), in your session's open thread (see `thread`).",
|
|
338
|
+
resolve_message: "Archive a message's thread, only when nothing in it is left for the person to read or answer.",
|
|
339
|
+
read_thread: 'Read a thread (thread_id); with no id, list your threads (asked_by "anyone": all).',
|
|
340
|
+
search: "Find messages by words, older ones too (read-only).",
|
|
341
|
+
get_guide_brief: "How to write a guided review, with a worked submit_guide call.",
|
|
342
|
+
card: "A Board card for work that outlasts this reply: create it, then move it and say where it is as the work moves.",
|
|
343
|
+
steps: INBOX_STEPS_ACTION_LINE
|
|
344
|
+
};
|
|
345
|
+
var INBOX_PICTURES_RULE = "Pictures go in the body where you write about them, ``, never only in `attachments`; do not also attach one the body shows. Visual options are picture choices, one picture each, inside the choice (not in the text above the question): `- [ ] A · Label —  what it means`; the label comes back.";
|
|
346
|
+
var INBOX_ATTACHMENTS_PICTURES_LINE = "Screenshots and pictures go in the body instead (``); attach what the person should open or click through.";
|
|
347
|
+
var INBOX_AGENT_FIELD_DESCRIPTIONS = {
|
|
348
|
+
send_message: {
|
|
349
|
+
body: [
|
|
350
|
+
"The message, markdown. Ask what only the person can decide as a question block, which renders as an answerable card:",
|
|
351
|
+
"",
|
|
352
|
+
":::question",
|
|
353
|
+
"Where should the edit that loses a sync conflict go?",
|
|
354
|
+
"",
|
|
355
|
+
"Context in one to three plain sentences: what it affects, what you know, why you recommend one. The person switches in cold.",
|
|
356
|
+
"",
|
|
357
|
+
"- [ ] Nowhere — silent loss, as today",
|
|
358
|
+
"- [ ] Local only, purged after 30 days — cheap",
|
|
359
|
+
"- [ ] Server-side per user — needs a retention policy",
|
|
360
|
+
"",
|
|
361
|
+
"Recommended: Local only, purged after 30 days",
|
|
362
|
+
"Holds up: sync release",
|
|
363
|
+
":::",
|
|
364
|
+
"",
|
|
365
|
+
'Your recommendation goes on the `Recommended:` line, never in a choice label. Keep the choices in their natural order; never move yours first. `:::question-multi` takes several choices, `:::question-text` free text. The person can always answer Other, add a note or skip, so never add an Other choice yourself. `Stopped: <why>` only when you cannot go on until it is answered (never `Stopped: no`); `Holds up: <work>` names work that waits while you go on. Ask only what you cannot decide alone, about 8 a round at most. Answers come back in the reply under "Answers to your questions".',
|
|
366
|
+
"",
|
|
367
|
+
INBOX_PICTURES_RULE,
|
|
368
|
+
"",
|
|
369
|
+
"Optional lines after the question; most questions need none of these: `Stakes: <what it affects>` (give one with every Stopped or Holds up), `Shows: ` or `Shows: [name](file)` (the one picture or file it is about), `Why: <why your recommendation>` on its own line, `Reversible: no — <why>` only when it cannot be undone.",
|
|
370
|
+
"",
|
|
371
|
+
'Only for a true blocker the person must go and do outside this chat, that you cannot do yourself (approve a deploy, sign in, touch a security key, merge a PR you cannot merge), write a `:::action` block: first line the order, then why. Each command the person must run goes in the why in its own fenced code block (```), one block per command in the order to run them, so each gets a copy button. Optional: `Where: <url>`, `Stopped:`, `Watching: <what you watch>`. Never for a decision or a question, for what you can do yourself, or as a checklist or status update; most messages need none. Done or Can\'t do it comes back under "Action items you asked for".',
|
|
372
|
+
"",
|
|
373
|
+
"Blocks fenced as `mermaid` or `dot` draw as diagrams; one fenced as `html` draws as a live, sandboxed page in the message (chart, SVG, mock; https libraries load). Leave its background transparent; use var(--foreground), var(--chart-1)…var(--chart-5). Let content set its height: no vh units or height:100% on html or body.",
|
|
374
|
+
"",
|
|
375
|
+
INBOX_REF_LINK_RULE
|
|
376
|
+
].join(`
|
|
377
|
+
`),
|
|
378
|
+
reply_to: `A message id: post this as a reply in that message's thread. ${INBOX_REPLY_TO_CHECK}`,
|
|
379
|
+
thread: "A short thread name, one line, to keep separate work apart: the same name in this project is the same open thread, across sessions. Without it, your messages join your session's open thread. Ignored with reply_to, except when you are asked to name a thread the person started: then it names that thread, once.",
|
|
380
|
+
supersedes: `Your own questions in this thread that are still open (read_thread state open or picked) and no longer apply: a newer message replaces them, or the reply settled them in words. "<message id>/<key>" for one, "<message id>" for all of that message's; send with reply_to into their thread. With one question in this message it replaces them, otherwise they read "No longer applies".`,
|
|
381
|
+
done: 'Your own open action items (`:::action`) in this thread that you saw done yourself, e.g. the deploy you watch is Ready: "<message id>/<key>" (key a-…); send with reply_to into their thread. The card says you saw it done; nothing is sent to the person.',
|
|
382
|
+
attachments: `Files to show the person beside the message, absolute or relative to project_path: a plan, an HTML mock, code, a video, any file. ${INBOX_ATTACHMENTS_PICTURES_LINE} Link one from the body (\`[the mock](mock.html)\`) to open it beside the thread. Their comments on the files come back in the reply. Folders and .env files are refused.`
|
|
383
|
+
},
|
|
384
|
+
read_thread: {
|
|
385
|
+
thread_id: "A thread id. read_thread shows its open questions, the newest messages in full, and earlier ones one line each."
|
|
386
|
+
},
|
|
387
|
+
submit_guide: {
|
|
388
|
+
guide: "The guide in get_guide_brief's output_schema, as an object or its JSON.",
|
|
389
|
+
snapshot: "Instead of guide and patch: a complete Plannotator guide snapshot, as an object or its JSON."
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var INBOX_REMINDER_TEXT = `Plannotator Inbox is on: when you finish long work, or need a decision only the person can make, send it with plannotator_inbox send_message (each decision as a \`:::question\` block) and keep going. ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_THREAD_RULE} Their reply comes back here. A decision in the Inbox stays there: when the person writes here, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Run \`plannotator inbox --help\` for how it works.`;
|
|
393
|
+
var INBOX_REMINDER_TEXT_NO_WAKE = `Plannotator Inbox is on: when you finish long work, or need a decision only the person can make, send it with plannotator_inbox send_message (each decision as a \`:::question\` block) and keep going. ${INBOX_FOLLOW_THROUGH_RULE} ${INBOX_THREAD_RULE} Their reply does not come back here by itself: read the thread later with read_thread. A decision in the Inbox stays there: when the person writes here, say what waits and link its thread, but never also ask it in the chat with a blocking question tool (AskUserQuestion or similar). Run \`plannotator inbox --help\` for how it works.`;
|
|
394
|
+
var INBOX_BRIEFING_NEVER_RUN = "Never run `plannotator inbox` just to send: send_message starts a stopped Inbox itself, and bare `plannotator inbox` opens the window for the person.";
|
|
395
|
+
var INBOX_BRIEFING_FROM_NOW_ON = "For the rest of this session, use the Inbox as above: for a decision the person is not here to make, and to hand over long work you finished.";
|
|
396
|
+
var INBOX_COMMAND_BRIEFING = [
|
|
397
|
+
"The person opened their Plannotator Inbox with /plannotator-inbox and has its address. The Plannotator Inbox is one window on this computer where you leave the person messages, `:::question` blocks they answer with a click, and files. They answer when they can. Nothing blocks: you send, then keep working or end your turn.",
|
|
398
|
+
"",
|
|
399
|
+
`- ${INBOX_USE_RULE}`,
|
|
400
|
+
`- ${INBOX_THREAD_RULE}`,
|
|
401
|
+
"- Send with a `plannotator_inbox` tool when you have one (it can be listed with a prefix and may need loading through tool search first): `action` `send_message`, with a markdown `body`. Otherwise call `send_message` on the Inbox's MCP server, `plannotator inbox mcp`.",
|
|
402
|
+
"- send_message's result says whether the person's reply arrives in this session by itself. When it does, do not wait or poll for it: end your turn or go on with other work. When it does not, go on with your work and read the thread later with read_thread.",
|
|
403
|
+
`- ${INBOX_CHANNEL_RULE}`,
|
|
404
|
+
`- ${INBOX_FOLLOW_THROUGH_RULE}`,
|
|
405
|
+
`- ${INBOX_BRIEFING_NEVER_RUN}`,
|
|
406
|
+
"- Before your first send, run `plannotator inbox --help` and read it: every field, the full question syntax, and how to connect the MCP server.",
|
|
407
|
+
"- The `plannotator` skill, where you have it, has more.",
|
|
408
|
+
"",
|
|
409
|
+
`Opening the Inbox asks for no work by itself. ${INBOX_BRIEFING_FROM_NOW_ON}`
|
|
410
|
+
].join(`
|
|
411
|
+
`);
|
|
412
|
+
function inboxAgentTool(tools, options = {}) {
|
|
413
|
+
if (tools.length === 0)
|
|
414
|
+
return null;
|
|
415
|
+
const description = [
|
|
416
|
+
INBOX_TOOL_LEAD,
|
|
417
|
+
"",
|
|
418
|
+
...tools.filter((tool) => !INBOX_AGENT_ACTIONS_NO_LINE.includes(tool.name)).map((tool) => `- ${tool.name}: ${INBOX_AGENT_ACTION_LEADS[tool.name] ?? leadOf(tool.description)}`),
|
|
419
|
+
...options.wakes === false ? [] : ["", INBOX_TOOL_WAKE_NOTE],
|
|
420
|
+
"",
|
|
421
|
+
...tools.some((tool) => tool.name === "card") ? [] : [INBOX_TOOL_BOARD_OFF_NOTE],
|
|
422
|
+
INBOX_TOOL_HELP_NOTE
|
|
423
|
+
].join(`
|
|
424
|
+
`);
|
|
425
|
+
const properties = {
|
|
426
|
+
action: { type: "string", enum: tools.map((tool) => tool.name), description: "Which Inbox tool to call." }
|
|
427
|
+
};
|
|
428
|
+
const usedBy = new Map;
|
|
429
|
+
for (const tool of tools) {
|
|
430
|
+
for (const [name, schema] of Object.entries(propertiesOf(tool))) {
|
|
431
|
+
if (isFilled(name) && name !== INBOX_AGENT_PROJECT_PATH || name === "action")
|
|
432
|
+
continue;
|
|
433
|
+
const users = usedBy.get(name);
|
|
434
|
+
if (users) {
|
|
435
|
+
users.push(tool.name);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
usedBy.set(name, [tool.name]);
|
|
439
|
+
if (name === INBOX_AGENT_PROJECT_PATH) {
|
|
440
|
+
properties[name] = { type: "string", description: INBOX_AGENT_PROJECT_PATH_DESCRIPTION };
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const own = INBOX_AGENT_FIELD_DESCRIPTIONS[tool.name]?.[name];
|
|
444
|
+
properties[name] = { ...schema, description: (own ?? (typeof schema.description === "string" ? schema.description : "")).trim() };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
for (const [name, users] of usedBy) {
|
|
448
|
+
const schema = properties[name];
|
|
449
|
+
properties[name] = { ...schema, description: `(${users.join(", ")}) ${schema.description}`.trim() };
|
|
450
|
+
}
|
|
451
|
+
return { name: INBOX_TOOL_NAME, description, inputSchema: { type: "object", properties, required: ["action"], additionalProperties: false } };
|
|
452
|
+
}
|
|
453
|
+
function inboxToolCall(input, tools, filled) {
|
|
454
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
455
|
+
return { error: "Invalid plannotator_inbox call: expected an object with an action." };
|
|
456
|
+
const { action, ...rest } = input;
|
|
457
|
+
if (action === INBOX_REMOVED_ACTION)
|
|
458
|
+
return { error: INBOX_REMOVED_ACTION_TEXT };
|
|
459
|
+
const tool = tools.find((candidate) => candidate.name === action);
|
|
460
|
+
if (!tool) {
|
|
461
|
+
return {
|
|
462
|
+
error: `Invalid plannotator_inbox call: action must be one of ${tools.map((candidate) => candidate.name).join(", ")}${typeof action === "string" && INBOX_TOOL_ACTIONS.includes(action) ? ` (this Plannotator Inbox has no ${action}; update Plannotator for it)` : ""}.`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const own = propertiesOf(tool);
|
|
466
|
+
const args = {};
|
|
467
|
+
let projectPath;
|
|
468
|
+
for (const [name, value] of Object.entries(rest)) {
|
|
469
|
+
if (value === undefined)
|
|
470
|
+
continue;
|
|
471
|
+
if (name === INBOX_AGENT_PROJECT_PATH && name in own) {
|
|
472
|
+
const refusal = inboxProjectPathShapeRefusal(value);
|
|
473
|
+
if (refusal)
|
|
474
|
+
return { error: `Invalid plannotator_inbox call: ${refusal}` };
|
|
475
|
+
projectPath = value;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (!(name in own) || isFilled(name)) {
|
|
479
|
+
const fields = Object.keys(own).filter((field) => !isFilled(field) || field === INBOX_AGENT_PROJECT_PATH);
|
|
480
|
+
return { error: `Invalid plannotator_inbox call: ${tool.name} takes no "${name}" (its fields: ${fields.join(", ") || "none"}).` };
|
|
481
|
+
}
|
|
482
|
+
args[name] = value;
|
|
483
|
+
}
|
|
484
|
+
for (const name of INBOX_FILLED_ARGUMENTS)
|
|
485
|
+
if (name in own)
|
|
486
|
+
args[name] = filled[name];
|
|
487
|
+
return projectPath === undefined ? { name: tool.name, arguments: args } : { name: tool.name, arguments: args, projectPath };
|
|
488
|
+
}
|
|
489
|
+
var INBOX_TEXT_COMPLETE_META_KEY = "ai.plannotator/text-complete";
|
|
490
|
+
function inboxToolResultText(result) {
|
|
491
|
+
const value = result && typeof result === "object" ? result : {};
|
|
492
|
+
const text = Array.isArray(value.content) ? value.content.map((part) => part && typeof part === "object" && typeof part.text === "string" ? part.text : "").filter(Boolean).join(`
|
|
493
|
+
`) : "";
|
|
494
|
+
const meta = value._meta && typeof value._meta === "object" ? value._meta : {};
|
|
495
|
+
const complete = meta[INBOX_TEXT_COMPLETE_META_KEY] === true;
|
|
496
|
+
const structured = !complete && value.structuredContent && typeof value.structuredContent === "object" ? `
|
|
497
|
+
|
|
498
|
+
${JSON.stringify(value.structuredContent)}` : "";
|
|
499
|
+
return { text: `${text}${value.isError === true ? "" : structured}`.trim(), isError: value.isError === true };
|
|
500
|
+
}
|
|
501
|
+
function mcpAnswerOf(text) {
|
|
502
|
+
const candidates = /^\s*(event:|data:|:)/m.test(text) ? text.split(/\r?\n\r?\n/).map((block) => block.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join(`
|
|
503
|
+
`)).filter((data) => data.trim()) : [text];
|
|
504
|
+
for (const candidate of candidates) {
|
|
505
|
+
try {
|
|
506
|
+
const value = JSON.parse(candidate);
|
|
507
|
+
if (value && typeof value === "object" && (("result" in value) || ("error" in value)))
|
|
508
|
+
return value;
|
|
509
|
+
} catch {}
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
var INBOX_MAX_REQUEST_BYTES = 128 * 1024 * 1024;
|
|
514
|
+
function inboxRequestTooLargeMessage(bytes) {
|
|
515
|
+
const size = bytes === undefined ? "" : ` (${bytes} bytes)`;
|
|
516
|
+
return `The message is too large for the Plannotator Inbox${size}: one request may carry at most ${INBOX_MAX_REQUEST_BYTES} bytes (128 MiB). Send a smaller message, or attach the content as a file.`;
|
|
517
|
+
}
|
|
518
|
+
function mcpAnswerOfResponse(status, text) {
|
|
519
|
+
return mcpAnswerOf(text) ?? (status === 413 ? { error: { message: inboxRequestTooLargeMessage() } } : null);
|
|
520
|
+
}
|
|
521
|
+
function parseInboxRegistry(text) {
|
|
522
|
+
if (!text)
|
|
523
|
+
return null;
|
|
524
|
+
try {
|
|
525
|
+
const value = JSON.parse(text);
|
|
526
|
+
if (value.v === 1 && typeof value.pid === "number" && typeof value.port === "number" && Number.isInteger(value.port) && value.port > 0 && value.port < 65536 && typeof value.token === "string" && value.token.length >= 32 && typeof value.serverSession === "string" && typeof value.url === "string") {
|
|
527
|
+
return { pid: value.pid, port: value.port, token: value.token, serverSession: value.serverSession, url: value.url };
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
var INBOX_TAGS_POLL_FIELD = "tags";
|
|
533
|
+
var INBOX_TAGGED_INSTRUCTION = "The person tagged you in a Plannotator Inbox thread that another session owns. Their reply follows as they wrote it; parts of it may be meant for the other session, not for you.";
|
|
534
|
+
function inboxTaggedLine(owner, id) {
|
|
535
|
+
return `The thread belongs to another session (${owner}); you have not seen it. Read it first with read_thread (message_id ${id}). Answer only what is addressed to you, in the same thread: plannotator_inbox send_message with reply_to ${id}.`;
|
|
536
|
+
}
|
|
537
|
+
function inboxAlsoTaggedLine(names) {
|
|
538
|
+
const list = names.length <= 1 ? names[0] ?? "another session" : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
539
|
+
return `This reply also tags ${list} (other sessions); each gets it too. Leave what is addressed to them to them.`;
|
|
540
|
+
}
|
|
541
|
+
var INBOX_ADDED_INSTRUCTION = "The person added you to a Plannotator Inbox thread that another session owns: you work on it alongside that session, and their later replies in it come to you too. Their reply follows as they wrote it; parts of it may be meant for the other session.";
|
|
542
|
+
function inboxAddedLine(owner, id) {
|
|
543
|
+
return `The thread's owner is ${owner}. If you have not read the thread yet, read it first with read_thread (message_id ${id}). Answer your part in the same thread: plannotator_inbox send_message with reply_to ${id}.`;
|
|
544
|
+
}
|
|
545
|
+
var INBOX_HANDOFF_INSTRUCTION = "The person handed you this Plannotator Inbox thread: it is yours now, and their later replies in it come to you. Their reply follows as they wrote it.";
|
|
546
|
+
function inboxHandoffLine(from, id) {
|
|
547
|
+
return `Another session (${from}) had this thread before; you have not seen it. Read it first with read_thread (message_id ${id}), then answer in it: plannotator_inbox send_message with reply_to ${id}.`;
|
|
548
|
+
}
|
|
549
|
+
function inboxHandedAwayLine(to) {
|
|
550
|
+
return `The person handed this thread to another session (${to}); their later replies in it go there. Stop work on it, and write in it only if they ask you to.`;
|
|
551
|
+
}
|
|
552
|
+
var INBOX_WAKE_AGENT_NAMES = ["Claude Code", "Pi", "OpenCode", "Codex", "Cursor", "Grok Build", "Qwen Code"];
|
|
553
|
+
function wakeName(value) {
|
|
554
|
+
if (typeof value !== "string")
|
|
555
|
+
return "";
|
|
556
|
+
const name = value.replace(/[\u0000-\u001f\u007f-\u009f\s]+/g, " ").trim();
|
|
557
|
+
return INBOX_WAKE_AGENT_NAMES.includes(name) ? name : "";
|
|
558
|
+
}
|
|
559
|
+
var INBOX_SEND_NOW_POLL_FIELD = "send_now";
|
|
560
|
+
var INBOX_STEER_POLLS_POLL_FIELD = "steer_polls";
|
|
561
|
+
function parseInboxBridgeCommands(text) {
|
|
562
|
+
try {
|
|
563
|
+
const body = JSON.parse(text);
|
|
564
|
+
if (!Array.isArray(body.commands))
|
|
565
|
+
return [];
|
|
566
|
+
return body.commands.filter((command) => {
|
|
567
|
+
if (!command || typeof command !== "object")
|
|
568
|
+
return false;
|
|
569
|
+
const c = command;
|
|
570
|
+
return (c.type === "reply" || c.type === "message") && typeof c.id === "string" && typeof c.thread_id === "string" && typeof c.body === "string";
|
|
571
|
+
});
|
|
572
|
+
} catch {
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
var INBOX_WAKE_INSTRUCTION = "This is a user reply from Plannotator Inbox. Their reply follows as they wrote it: it is their answer to you. When they need to hear back, answer in the same thread: plannotator_inbox send_message with reply_to set to the id in parentheses on the line above.";
|
|
577
|
+
var INBOX_REASK_INSTRUCTION = "The person asked, from the Plannotator Inbox, for the open questions in this thread to be asked again, together in one new message. The questions and what to do with each follow. Answer in the same thread: plannotator_inbox send_message with reply_to set to the id in parentheses on the line above.";
|
|
578
|
+
var reaskWords = (items) => items === "actions" ? { what: "open action items", they: "The action items" } : items === "both" ? { what: "open questions and action items", they: "They" } : { what: "open questions", they: "The questions" };
|
|
579
|
+
function inboxReaskInstruction(items, one = false) {
|
|
580
|
+
if (one && items !== "both") {
|
|
581
|
+
const what2 = items === "actions" ? "action item" : "question";
|
|
582
|
+
return `The person asked, from the Plannotator Inbox, for the open ${what2} in this thread to be asked again, in one new message. The ${what2} and what to do with it follow. Answer in the same thread: plannotator_inbox send_message with reply_to set to the id in parentheses on the line above.`;
|
|
583
|
+
}
|
|
584
|
+
if (items !== "actions" && items !== "both")
|
|
585
|
+
return INBOX_REASK_INSTRUCTION;
|
|
586
|
+
const { what, they } = reaskWords(items);
|
|
587
|
+
return `The person asked, from the Plannotator Inbox, for the ${what} in this thread to be asked again, together in one new message. ${they} and what to do with each follow. Answer in the same thread: plannotator_inbox send_message with reply_to set to the id in parentheses on the line above.`;
|
|
588
|
+
}
|
|
589
|
+
function inboxReaskRedirectedLine(asker, id, items, one = false) {
|
|
590
|
+
if (one && items !== "both") {
|
|
591
|
+
const what = items === "actions" ? "This action item" : "This question";
|
|
592
|
+
return `${what} was asked by another ${asker} session, which has ended; the person picked you to take it over. Read the thread first with read_thread (message_id ${id}), then ask it again as below if it is still needed.`;
|
|
593
|
+
}
|
|
594
|
+
const these = items === "actions" ? "These action items" : items === "both" ? "These questions and action items" : "These questions";
|
|
595
|
+
return `${these} were asked by another ${asker} session, which has ended; the person picked you to take them over. Read the thread first with read_thread (message_id ${id}), then ask the ones still needed again as below.`;
|
|
596
|
+
}
|
|
597
|
+
var INBOX_MESSAGE_INSTRUCTION = "The person wrote to you from the Plannotator Inbox. Their message follows as they wrote it: it is from them, not from the Inbox. When they need to hear back, answer in the same thread: plannotator_inbox send_message with reply_to set to the id in parentheses on the line above.";
|
|
598
|
+
var INBOX_NEW_THREAD_INSTRUCTION = "The person started a new thread with you in the Plannotator Inbox: an idea or a request of their own, not an answer to something you asked. Their message follows as they wrote it. It may be unrelated to what you were doing; say how you will handle it before you start. When they need to hear back, answer in the same thread: plannotator_inbox send_message with reply_to set to the id in parentheses on the line above.";
|
|
599
|
+
var INBOX_NAME_THREAD_LINE = "Name this thread in your reply: pass `thread` with a few words saying what it is about. The first name you give sets it, once.";
|
|
600
|
+
function inboxContinuesLine(id) {
|
|
601
|
+
return `This continues a thread the person started with another session; you have not seen it. Read it first with read_thread (message_id ${id}), then answer in it with reply_to ${id}.`;
|
|
602
|
+
}
|
|
603
|
+
function inboxRedirectedLine(asker, id) {
|
|
604
|
+
return `This reply was written to another ${asker} session that has ended. Read the thread with read_thread to see what it asked, then answer with reply_to ${id}.`;
|
|
605
|
+
}
|
|
606
|
+
function inboxAckLine(messageId) {
|
|
607
|
+
return `The person marked your message \uD83D\uDC4D Looks good (${messageId}). No reply needed unless you have a question.`;
|
|
608
|
+
}
|
|
609
|
+
function wakeThreadId(value) {
|
|
610
|
+
return typeof value === "string" && /^msg_[0-9A-Za-z]{1,64}$/.test(value) ? value : "";
|
|
611
|
+
}
|
|
612
|
+
var INBOX_WAKE_CLOSING_LINE = "[for context: read_thread (opt: set limit) or search · help: plannotator inbox --help]";
|
|
613
|
+
function inboxWakeAgo(at, now) {
|
|
614
|
+
const then = typeof at === "string" ? Date.parse(at) : Number.NaN;
|
|
615
|
+
if (!Number.isFinite(then))
|
|
616
|
+
return "";
|
|
617
|
+
const minutes = Math.floor((now - then) / 60000);
|
|
618
|
+
if (minutes < 1)
|
|
619
|
+
return "just now";
|
|
620
|
+
if (minutes < 2)
|
|
621
|
+
return "a minute ago";
|
|
622
|
+
if (minutes < 60)
|
|
623
|
+
return `${minutes} minutes ago`;
|
|
624
|
+
const hours = Math.floor(minutes / 60);
|
|
625
|
+
if (hours < 2)
|
|
626
|
+
return "an hour ago";
|
|
627
|
+
if (hours < 24)
|
|
628
|
+
return `${hours} hours ago`;
|
|
629
|
+
const days = Math.floor(hours / 24);
|
|
630
|
+
return days < 2 ? "a day ago" : `${days} days ago`;
|
|
631
|
+
}
|
|
632
|
+
var wakeCount = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
633
|
+
var wakePlural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
634
|
+
function inboxWakeDetailsLine(threadId, context, options) {
|
|
635
|
+
const thread = wakeThreadId(threadId);
|
|
636
|
+
if (!thread || !context || typeof context !== "object")
|
|
637
|
+
return "";
|
|
638
|
+
const { messages, open_questions, open_actions, last_agent_at } = context;
|
|
639
|
+
const count = wakeCount(messages);
|
|
640
|
+
const questions = wakeCount(open_questions);
|
|
641
|
+
const actions = wakeCount(open_actions);
|
|
642
|
+
if (count === null || questions === null || actions === null)
|
|
643
|
+
return "";
|
|
644
|
+
const open = [];
|
|
645
|
+
if (questions > 0)
|
|
646
|
+
open.push(`${wakePlural(questions, "question")} open${options.reask ? "" : `, don't re-ask ${questions === 1 ? "it" : "them"}`}`);
|
|
647
|
+
if (actions > 0)
|
|
648
|
+
open.push(`${wakePlural(actions, "action item")} open`);
|
|
649
|
+
const state = open.join(", ") || (options.queued ? "" : "nothing open");
|
|
650
|
+
const ago = inboxWakeAgo(last_agent_at, options.now);
|
|
651
|
+
return `[Thread ${thread}: ${wakePlural(count, "message")}${state ? `, ${state}` : ""}${ago ? `. Your last message was ${ago}` : ""}]`;
|
|
652
|
+
}
|
|
653
|
+
function wakeQuote(value) {
|
|
654
|
+
if (typeof value !== "string")
|
|
655
|
+
return "";
|
|
656
|
+
const flat = value.replace(/[\p{Cc}\p{Cs}\p{Zl}\p{Zp}]+/gu, " ").replace(/\p{Cf}+/gu, "").replace(/[[\]]/g, "").replace(/"/g, "'").replace(/\s+/g, " ").trim();
|
|
657
|
+
const chars = [...flat];
|
|
658
|
+
return chars.length > 120 ? `${chars.slice(0, 120).join("").trimEnd()}…` : flat;
|
|
659
|
+
}
|
|
660
|
+
function inboxWakeReplyingLine(target, now) {
|
|
661
|
+
if (!target || typeof target !== "object")
|
|
662
|
+
return "";
|
|
663
|
+
const { id, at, quote, questions, actions } = target;
|
|
664
|
+
const message = wakeThreadId(id);
|
|
665
|
+
const ago = inboxWakeAgo(at, now);
|
|
666
|
+
if (!message || !ago)
|
|
667
|
+
return "";
|
|
668
|
+
const head = `Replying to your message from ${ago} (${message})`;
|
|
669
|
+
const words = wakeQuote(quote);
|
|
670
|
+
if (words)
|
|
671
|
+
return `[${head}: "${words}"]`;
|
|
672
|
+
const asked = [];
|
|
673
|
+
if ((wakeCount(questions) ?? 0) > 0)
|
|
674
|
+
asked.push(wakePlural(questions, "question"));
|
|
675
|
+
if ((wakeCount(actions) ?? 0) > 0)
|
|
676
|
+
asked.push(wakePlural(actions, "action item"));
|
|
677
|
+
return `[${head}${asked.length > 0 ? `, with ${asked.join(" and ")}` : ""}]`;
|
|
678
|
+
}
|
|
679
|
+
function inboxWakeCommand(command) {
|
|
680
|
+
if (!command.context || typeof command.context !== "object")
|
|
681
|
+
return command;
|
|
682
|
+
const { still_open_line: line, still_open_at: at, ...context } = command.context;
|
|
683
|
+
if (typeof line !== "string" || line === "")
|
|
684
|
+
return command;
|
|
685
|
+
if (!inboxWakeDetailsLine(command.thread_id, context, { now: 0 }) || !wakeCount(context.open_questions))
|
|
686
|
+
return { ...command, context };
|
|
687
|
+
if (typeof at !== "number" || !Number.isSafeInteger(at) || at < 0 || !command.body.startsWith(line, at))
|
|
688
|
+
return { ...command, context };
|
|
689
|
+
const end = at + line.length;
|
|
690
|
+
const alone = (at === 0 || command.body.slice(at - 2, at) === `
|
|
691
|
+
|
|
692
|
+
`) && (end === command.body.length || command.body.slice(end, end + 2) === `
|
|
693
|
+
|
|
694
|
+
`);
|
|
695
|
+
if (!alone)
|
|
696
|
+
return { ...command, context };
|
|
697
|
+
const body = [command.body.slice(0, Math.max(0, at - 2)), command.body.slice(end + 2)].filter((part) => part !== "").join(`
|
|
698
|
+
|
|
699
|
+
`);
|
|
700
|
+
return { ...command, context, body: body === "" ? command.body : body };
|
|
701
|
+
}
|
|
702
|
+
function inboxWakeText(command, now = Date.now(), wording = {}) {
|
|
703
|
+
if (command.tidy === true) {
|
|
704
|
+
return `Plannotator Inbox: ${(command.subject ?? "").replace(/\s+/g, " ").trim() || "Tidy"} (${command.id})
|
|
705
|
+
${command.body}
|
|
706
|
+
|
|
707
|
+
${INBOX_WAKE_CLOSING_LINE}`;
|
|
708
|
+
}
|
|
709
|
+
if (typeof command.ack_of === "string" && command.ack_of !== "") {
|
|
710
|
+
const ackSubject = (command.subject ?? "").replace(/\s+/g, " ").trim() || "a message";
|
|
711
|
+
const thread2 = wakeThreadId(command.thread_id);
|
|
712
|
+
return `Plannotator Inbox: ${ackSubject} (${command.id})
|
|
713
|
+
${inboxAckLine(command.ack_of)}${thread2 ? `
|
|
714
|
+
|
|
715
|
+
[thread ${thread2}]` : ""}`;
|
|
716
|
+
}
|
|
717
|
+
const steps = wakeSteps(command.steps);
|
|
718
|
+
const closing = `${steps ? `
|
|
719
|
+
|
|
720
|
+
${inboxStepsWakeLine(steps)}` : ""}
|
|
721
|
+
|
|
722
|
+
${INBOX_WAKE_CLOSING_LINE}`;
|
|
723
|
+
const { body, context } = inboxWakeCommand(command);
|
|
724
|
+
const details = inboxWakeDetailsLine(command.thread_id, context, { now, reask: command.reask === true, queued: wording.queued === true });
|
|
725
|
+
const target = details ? inboxWakeReplyingLine(context.replying_to, now) : "";
|
|
726
|
+
const thread = wakeThreadId(command.thread_id);
|
|
727
|
+
const facts = details ? `
|
|
728
|
+
${details}${target ? `
|
|
729
|
+
${target}` : ""}` : thread ? `
|
|
730
|
+
[Thread ${thread}]` : "";
|
|
731
|
+
const subject = (command.subject ?? "").replace(/\s+/g, " ").trim() || (command.type === "message" ? "a message" : "a reply");
|
|
732
|
+
if (command.handed_away && typeof command.handed_away === "object") {
|
|
733
|
+
const to = wakeName(command.handed_away.to) || "another agent";
|
|
734
|
+
return `Plannotator Inbox: ${subject} (${command.id})
|
|
735
|
+
${inboxHandedAwayLine(to)}${facts}${closing}`;
|
|
736
|
+
}
|
|
737
|
+
if (command.handoff && typeof command.handoff === "object") {
|
|
738
|
+
const from = wakeName(command.handoff.from) || "another agent";
|
|
739
|
+
return `Plannotator Inbox: ${subject} (${command.id})
|
|
740
|
+
${INBOX_HANDOFF_INSTRUCTION}
|
|
741
|
+
${inboxHandoffLine(from, command.id)}${facts}
|
|
742
|
+
|
|
743
|
+
${body}${closing}`;
|
|
744
|
+
}
|
|
745
|
+
if (command.tagged && typeof command.tagged === "object") {
|
|
746
|
+
const owner = wakeName(command.tagged.owner) || "another agent";
|
|
747
|
+
if (command.added === true) {
|
|
748
|
+
return `Plannotator Inbox: ${subject} (${command.id})
|
|
749
|
+
${INBOX_ADDED_INSTRUCTION}
|
|
750
|
+
${inboxAddedLine(owner, command.id)}${facts}
|
|
751
|
+
|
|
752
|
+
${body}${closing}`;
|
|
753
|
+
}
|
|
754
|
+
return `Plannotator Inbox: ${subject} (${command.id})
|
|
755
|
+
${INBOX_TAGGED_INSTRUCTION}
|
|
756
|
+
${inboxTaggedLine(owner, command.id)}${facts}
|
|
757
|
+
|
|
758
|
+
${body}${closing}`;
|
|
759
|
+
}
|
|
760
|
+
const others = Array.isArray(command.also_tagged) ? command.also_tagged.filter((name) => typeof name === "string" && name.trim() !== "").map((name) => wakeName(name) || "another agent").slice(0, 8) : [];
|
|
761
|
+
const also = others.length > 0 ? `
|
|
762
|
+
${inboxAlsoTaggedLine(others)}` : "";
|
|
763
|
+
const reaskItems = command.reask_items === "actions" || command.reask_items === "both" ? command.reask_items : undefined;
|
|
764
|
+
const reaskOne = command.reask_one === true;
|
|
765
|
+
const instruction = command.type === "message" ? command.new_thread === true ? INBOX_NEW_THREAD_INSTRUCTION : INBOX_MESSAGE_INSTRUCTION : command.reask === true ? inboxReaskInstruction(reaskItems, reaskOne) : INBOX_WAKE_INSTRUCTION;
|
|
766
|
+
const asker = command.type !== "message" && typeof command.redirected_from === "string" && command.redirected_from.trim() !== "" ? wakeName(command.redirected_from) || "agent" : "";
|
|
767
|
+
const redirected = asker ? `
|
|
768
|
+
${command.reask === true ? inboxReaskRedirectedLine(asker, command.id, reaskItems, reaskOne) : inboxRedirectedLine(asker, command.id)}` : command.continues_thread === true ? `
|
|
769
|
+
${inboxContinuesLine(command.id)}` : "";
|
|
770
|
+
const naming = command.name_thread === true ? `
|
|
771
|
+
${INBOX_NAME_THREAD_LINE}` : "";
|
|
772
|
+
return `Plannotator Inbox: ${subject} (${command.id})
|
|
773
|
+
${instruction}${redirected}${also}${naming}${facts}
|
|
774
|
+
|
|
775
|
+
${body}${closing}`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ../../packages/shared/config.ts
|
|
779
|
+
function getConfigDir() {
|
|
780
|
+
return getPlannotatorDataDir();
|
|
781
|
+
}
|
|
782
|
+
function getConfigPath() {
|
|
783
|
+
return join2(getConfigDir(), "config.json");
|
|
784
|
+
}
|
|
785
|
+
function loadConfig(options = {}) {
|
|
786
|
+
try {
|
|
787
|
+
const configPath = getConfigPath();
|
|
788
|
+
if (!existsSync2(configPath))
|
|
789
|
+
return {};
|
|
790
|
+
const raw = readFileSync(configPath, "utf-8").replace(/^\uFEFF/, "");
|
|
791
|
+
const parsed = JSON.parse(raw);
|
|
792
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
793
|
+
} catch (e) {
|
|
794
|
+
if (!options.quiet)
|
|
795
|
+
process.stderr.write(`[plannotator] Warning: failed to read config.json: ${e}
|
|
796
|
+
`);
|
|
797
|
+
return {};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function parseConfigBoolean(value) {
|
|
801
|
+
if (typeof value === "boolean")
|
|
802
|
+
return value;
|
|
803
|
+
if (typeof value === "string") {
|
|
804
|
+
const v = value.trim().toLowerCase();
|
|
805
|
+
if (v === "true" || v === "1")
|
|
806
|
+
return true;
|
|
807
|
+
if (v === "false" || v === "0")
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
var AGENT_TOOL_DEFAULTS = {
|
|
813
|
+
"claude-code": true,
|
|
814
|
+
pi: false,
|
|
815
|
+
"oh-my-pi": true,
|
|
816
|
+
opencode: false,
|
|
817
|
+
dsh: false
|
|
818
|
+
};
|
|
819
|
+
var INBOX_TOOL_DEFAULTS = {
|
|
820
|
+
"claude-code": AGENT_TOOL_DEFAULTS["claude-code"],
|
|
821
|
+
pi: AGENT_TOOL_DEFAULTS.pi,
|
|
822
|
+
opencode: AGENT_TOOL_DEFAULTS.opencode,
|
|
823
|
+
dsh: AGENT_TOOL_DEFAULTS.dsh,
|
|
824
|
+
cline: false
|
|
825
|
+
};
|
|
826
|
+
function parseInboxToolEnv(env = process.env) {
|
|
827
|
+
const v = env.PLANNOTATOR_INBOX_TOOL?.trim().toLowerCase();
|
|
828
|
+
if (v === "1" || v === "true" || v === "on")
|
|
829
|
+
return true;
|
|
830
|
+
if (v === "0" || v === "false" || v === "off" || v === "disabled")
|
|
831
|
+
return false;
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
function configuredInboxTool(config, host) {
|
|
835
|
+
const value = config.inboxTool;
|
|
836
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
837
|
+
return parseConfigBoolean(value[host]);
|
|
838
|
+
}
|
|
839
|
+
return parseConfigBoolean(value);
|
|
840
|
+
}
|
|
841
|
+
function resolveInboxTool(config, env, host) {
|
|
842
|
+
return parseInboxToolEnv(env) ?? configuredInboxTool(config, host) ?? INBOX_TOOL_DEFAULTS[host];
|
|
843
|
+
}
|
|
844
|
+
var warnedInvalidGuideShareUrls = new Set;
|
|
845
|
+
var warnedInvalidPublicUrls = new Set;
|
|
846
|
+
var warnedInvalidUrlHosts = new Set;
|
|
847
|
+
var warnedBitbucketApiUrls = new Set;
|
|
848
|
+
|
|
849
|
+
// ../../packages/shared/inbox/agent-link.ts
|
|
850
|
+
import { spawn } from "node:child_process";
|
|
851
|
+
import { randomUUID } from "node:crypto";
|
|
852
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync as renameSync2, rmSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
853
|
+
import { homedir as homedir2 } from "node:os";
|
|
854
|
+
import { isAbsolute as isAbsolute3, join as join3, resolve as resolve3 } from "node:path";
|
|
855
|
+
var INBOX_DISCOVER_TIMEOUT_MS = 2000;
|
|
856
|
+
var INBOX_SESSION_THREADS_TIMEOUT_MS = 1500;
|
|
857
|
+
var INBOX_POLL_WAIT_MS = 20000;
|
|
858
|
+
var INBOX_RETRY_MS = { first: 5000, max: 60000 };
|
|
859
|
+
var INBOX_TICK_MS = 1000;
|
|
860
|
+
var START_TIMEOUT_MS = 30000;
|
|
861
|
+
var INBOX_LEASE_EVERY_MS = 5000;
|
|
862
|
+
var INBOX_LEASE_STALE_MS = 20000;
|
|
863
|
+
var INBOX_STATE_EVERY_TICKS = 5;
|
|
864
|
+
var INBOX_INSTALL_TEXT = "The Plannotator Inbox is not running, and there is no plannotator command on this machine to start it: install Plannotator (https://plannotator.ai/docs/getting-started/installation/), then call again.";
|
|
865
|
+
var INBOX_STILL_MISSING_TEXT = "The Plannotator Inbox is not running (no plannotator command on PATH).";
|
|
866
|
+
function inboxRegistryFileOf(dataDir) {
|
|
867
|
+
return join3(dataDir, "inbox", "inbox.json");
|
|
868
|
+
}
|
|
869
|
+
function rememberedToolsFileOf(dataDir) {
|
|
870
|
+
return join3(dataDir, "inbox", "connection-tools.json");
|
|
871
|
+
}
|
|
872
|
+
function claimDirOf(dataDir, replyId, session) {
|
|
873
|
+
return join3(dataDir, "inbox", "claims", inboxClaimName(replyId, session));
|
|
874
|
+
}
|
|
875
|
+
function inboxLeaseFileOf(dataDir, host, sessionId) {
|
|
876
|
+
return join3(dataDir, "inbox", "leases", `${host}-${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
877
|
+
}
|
|
878
|
+
function parseLease(text) {
|
|
879
|
+
try {
|
|
880
|
+
const value = JSON.parse(text ?? "");
|
|
881
|
+
if (typeof value.at !== "number")
|
|
882
|
+
return null;
|
|
883
|
+
return {
|
|
884
|
+
owner: typeof value.owner === "string" && value.owner ? value.owner : null,
|
|
885
|
+
at: value.at,
|
|
886
|
+
touchedAt: typeof value.touchedAt === "number" ? value.touchedAt : 0
|
|
887
|
+
};
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function readText(path) {
|
|
893
|
+
try {
|
|
894
|
+
return readFileSync2(path, "utf8");
|
|
895
|
+
} catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function readRegistry(dataDir) {
|
|
900
|
+
return parseInboxRegistry(readText(inboxRegistryFileOf(dataDir)));
|
|
901
|
+
}
|
|
902
|
+
function errorText(error) {
|
|
903
|
+
return error instanceof Error ? error.message : String(error);
|
|
904
|
+
}
|
|
905
|
+
async function mcpCall(port, method, params, signal) {
|
|
906
|
+
const response = await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
907
|
+
method: "POST",
|
|
908
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
909
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
910
|
+
signal
|
|
911
|
+
});
|
|
912
|
+
return mcpAnswerOfResponse(response.status, await response.text());
|
|
913
|
+
}
|
|
914
|
+
async function discoverInboxTools(dataDir, options = {}) {
|
|
915
|
+
if (!existsSync3(inboxRegistryFileOf(dataDir)))
|
|
916
|
+
return null;
|
|
917
|
+
const registry = readRegistry(dataDir);
|
|
918
|
+
if (registry) {
|
|
919
|
+
const answer = await mcpCall(registry.port, "tools/list", {}, AbortSignal.timeout(options.timeoutMs ?? INBOX_DISCOVER_TIMEOUT_MS)).catch(() => null);
|
|
920
|
+
const tools2 = answer && "result" in answer ? parseInboxToolList(answer.result) : [];
|
|
921
|
+
if (tools2.length > 0) {
|
|
922
|
+
try {
|
|
923
|
+
const file = rememberedToolsFileOf(dataDir);
|
|
924
|
+
writeFileSync2(`${file}.tmp`, JSON.stringify({ tools: tools2 }), { mode: 384 });
|
|
925
|
+
chmodSync(`${file}.tmp`, 384);
|
|
926
|
+
renameSync2(`${file}.tmp`, file);
|
|
927
|
+
} catch {}
|
|
928
|
+
return tools2;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
let remembered = null;
|
|
932
|
+
try {
|
|
933
|
+
remembered = JSON.parse(readText(rememberedToolsFileOf(dataDir)) ?? "null");
|
|
934
|
+
} catch {
|
|
935
|
+
remembered = null;
|
|
936
|
+
}
|
|
937
|
+
const tools = parseInboxToolList(remembered);
|
|
938
|
+
return tools.length > 0 ? tools : null;
|
|
939
|
+
}
|
|
940
|
+
function inboxProjectDir(cwd, given) {
|
|
941
|
+
try {
|
|
942
|
+
const real = realpathSync2(isAbsolute3(given) ? given : resolve3(cwd, given));
|
|
943
|
+
return statSync2(real).isDirectory() ? real : null;
|
|
944
|
+
} catch {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
class InboxAgentConnection {
|
|
950
|
+
options;
|
|
951
|
+
tools;
|
|
952
|
+
saidInstall = false;
|
|
953
|
+
constructor(options) {
|
|
954
|
+
this.options = options;
|
|
955
|
+
this.tools = options.tools;
|
|
956
|
+
}
|
|
957
|
+
get dataDir() {
|
|
958
|
+
return this.options.dataDir;
|
|
959
|
+
}
|
|
960
|
+
get host() {
|
|
961
|
+
return this.options.host;
|
|
962
|
+
}
|
|
963
|
+
async callTool(input, at) {
|
|
964
|
+
const call = inboxToolCall(input, this.tools, {
|
|
965
|
+
project_path: at.cwd,
|
|
966
|
+
agent_session: at.sessionId,
|
|
967
|
+
agent_host: this.options.host,
|
|
968
|
+
agent_name: this.options.agentName
|
|
969
|
+
});
|
|
970
|
+
if ("error" in call)
|
|
971
|
+
return { text: call.error, isError: true };
|
|
972
|
+
if (call.projectPath !== undefined) {
|
|
973
|
+
const resolved = inboxProjectDir(at.cwd, call.projectPath);
|
|
974
|
+
if (!resolved)
|
|
975
|
+
return { text: `Invalid plannotator_inbox call: project_path ${call.projectPath} is not an existing folder.`, isError: true };
|
|
976
|
+
call.arguments.project_path = resolved;
|
|
977
|
+
}
|
|
978
|
+
const running = await this.ensureRunning();
|
|
979
|
+
if ("error" in running)
|
|
980
|
+
return { text: running.error, isError: true };
|
|
981
|
+
let answer;
|
|
982
|
+
try {
|
|
983
|
+
answer = await mcpCall(running.port, "tools/call", inboxToolCallParams(call, { wakes: at.wakes === true }));
|
|
984
|
+
} catch (error) {
|
|
985
|
+
return { text: `The Plannotator Inbox did not answer (${errorText(error)}).`, isError: true };
|
|
986
|
+
}
|
|
987
|
+
if (!answer)
|
|
988
|
+
return { text: "The Plannotator Inbox gave an answer this session could not read.", isError: true };
|
|
989
|
+
if (answer.error) {
|
|
990
|
+
const message = answer.error.message ?? "unknown error";
|
|
991
|
+
if (/not found|unknown tool/i.test(message))
|
|
992
|
+
return { text: `This Plannotator Inbox has no ${call.name}; update Plannotator. (${message})`, isError: true };
|
|
993
|
+
return { text: `The Plannotator Inbox refused the call: ${message}`, isError: true };
|
|
994
|
+
}
|
|
995
|
+
return inboxToolResultText(answer.result);
|
|
996
|
+
}
|
|
997
|
+
async sessionThreads(at, toolName = INBOX_TOOL_NAME, timeoutMs = INBOX_SESSION_THREADS_TIMEOUT_MS) {
|
|
998
|
+
const registry = readRegistry(this.dataDir);
|
|
999
|
+
if (!registry || !at.sessionId || !at.cwd)
|
|
1000
|
+
return null;
|
|
1001
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
1002
|
+
try {
|
|
1003
|
+
const health = await fetch(`http://127.0.0.1:${registry.port}/api/inbox/health`, { signal });
|
|
1004
|
+
if (health.status !== 200 || !inboxHealthPeeks(await health.json(), registry.serverSession))
|
|
1005
|
+
return null;
|
|
1006
|
+
const answer = await mcpCall(registry.port, "tools/call", inboxSessionThreadsParams(at.cwd, at.sessionId), signal);
|
|
1007
|
+
if (!answer || !("result" in answer))
|
|
1008
|
+
return null;
|
|
1009
|
+
const result = answer.result;
|
|
1010
|
+
return result?.isError === true ? null : inboxSessionThreadsText(result, toolName);
|
|
1011
|
+
} catch {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
async isRunning(registry) {
|
|
1016
|
+
try {
|
|
1017
|
+
const response = await fetch(`http://127.0.0.1:${registry.port}/api/inbox/health`, { signal: AbortSignal.timeout(5000) });
|
|
1018
|
+
if (response.status !== 200)
|
|
1019
|
+
return false;
|
|
1020
|
+
return (await response.json()).serverSession === registry.serverSession;
|
|
1021
|
+
} catch {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
async ensureRunning() {
|
|
1026
|
+
const known = readRegistry(this.dataDir);
|
|
1027
|
+
if (known && await this.isRunning(known))
|
|
1028
|
+
return known;
|
|
1029
|
+
const started = await this.startInbox();
|
|
1030
|
+
if (started.missing) {
|
|
1031
|
+
const text = this.saidInstall ? INBOX_STILL_MISSING_TEXT : INBOX_INSTALL_TEXT;
|
|
1032
|
+
this.saidInstall = true;
|
|
1033
|
+
return { error: text };
|
|
1034
|
+
}
|
|
1035
|
+
if (started.exitCode !== 0) {
|
|
1036
|
+
const why = started.output.trim() || `exit ${started.exitCode}`;
|
|
1037
|
+
if (/unknown command/i.test(why))
|
|
1038
|
+
return { error: "The plannotator on PATH has no Inbox (an older version); update Plannotator." };
|
|
1039
|
+
return { error: `The Plannotator Inbox did not start: ${why}` };
|
|
1040
|
+
}
|
|
1041
|
+
return readRegistry(this.dataDir) ?? { error: "The Plannotator Inbox started but wrote no registry." };
|
|
1042
|
+
}
|
|
1043
|
+
async startInbox() {
|
|
1044
|
+
for (const command of plannotatorCommands(this.options.command)) {
|
|
1045
|
+
const run = await runOnce(command, ["inbox", "--background"], { ...process.env, PLANNOTATOR_DATA_DIR: this.dataDir });
|
|
1046
|
+
if (!run.missing)
|
|
1047
|
+
return run;
|
|
1048
|
+
}
|
|
1049
|
+
return { missing: true, exitCode: null, output: "" };
|
|
1050
|
+
}
|
|
1051
|
+
async bridge(path, session, body, signal) {
|
|
1052
|
+
const registry = readRegistry(this.dataDir);
|
|
1053
|
+
if (!registry)
|
|
1054
|
+
return null;
|
|
1055
|
+
try {
|
|
1056
|
+
const response = await fetch(`http://127.0.0.1:${registry.port}${path}`, {
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${registry.token}` },
|
|
1059
|
+
body: JSON.stringify({ session, host: this.options.host, ...body }),
|
|
1060
|
+
signal
|
|
1061
|
+
});
|
|
1062
|
+
return { status: response.status, text: await response.text() };
|
|
1063
|
+
} catch {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
function runOnce(command, args, env) {
|
|
1069
|
+
return new Promise((resolve4) => {
|
|
1070
|
+
let output = "";
|
|
1071
|
+
let stdout = "";
|
|
1072
|
+
let stderr = "";
|
|
1073
|
+
let settled = false;
|
|
1074
|
+
const done = (value) => {
|
|
1075
|
+
if (settled)
|
|
1076
|
+
return;
|
|
1077
|
+
settled = true;
|
|
1078
|
+
clearTimeout(timer);
|
|
1079
|
+
resolve4(value);
|
|
1080
|
+
};
|
|
1081
|
+
let child;
|
|
1082
|
+
try {
|
|
1083
|
+
child = spawn(command, args, { env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
resolve4({ missing: error.code === "ENOENT", exitCode: null, output: errorText(error) });
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const timer = setTimeout(() => {
|
|
1089
|
+
child.kill();
|
|
1090
|
+
done({ missing: false, exitCode: null, output: `${output}
|
|
1091
|
+
(no answer within ${START_TIMEOUT_MS / 1000} s)` });
|
|
1092
|
+
}, START_TIMEOUT_MS);
|
|
1093
|
+
child.stdout?.on("data", (chunk) => {
|
|
1094
|
+
output += String(chunk);
|
|
1095
|
+
stdout += String(chunk);
|
|
1096
|
+
});
|
|
1097
|
+
child.stderr?.on("data", (chunk) => {
|
|
1098
|
+
output += String(chunk);
|
|
1099
|
+
stderr += String(chunk);
|
|
1100
|
+
});
|
|
1101
|
+
child.on("error", (error) => done({ missing: error.code === "ENOENT", exitCode: null, output: error.message }));
|
|
1102
|
+
child.on("close", (code) => done({ missing: false, exitCode: code, output, stdout, stderr }));
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
function plannotatorCommands(command) {
|
|
1106
|
+
if (command)
|
|
1107
|
+
return [command];
|
|
1108
|
+
const bin = process.env.PLANNOTATOR_BIN?.trim();
|
|
1109
|
+
return bin ? [bin] : ["plannotator", join3(homedir2(), ".local", "bin", "plannotator")];
|
|
1110
|
+
}
|
|
1111
|
+
class InboxDeliverLater extends Error {
|
|
1112
|
+
deliverLater = true;
|
|
1113
|
+
}
|
|
1114
|
+
function isInboxDeliverLater(error) {
|
|
1115
|
+
return error instanceof InboxDeliverLater || typeof error === "object" && error !== null && error.deliverLater === true;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
class InboxWake {
|
|
1119
|
+
connection;
|
|
1120
|
+
sessionId;
|
|
1121
|
+
target;
|
|
1122
|
+
tickMs;
|
|
1123
|
+
timer = null;
|
|
1124
|
+
disposed = false;
|
|
1125
|
+
ticking = false;
|
|
1126
|
+
polling = false;
|
|
1127
|
+
delivering = false;
|
|
1128
|
+
retryAt = 0;
|
|
1129
|
+
backoffMs = 0;
|
|
1130
|
+
idleTicks = 0;
|
|
1131
|
+
abort = new AbortController;
|
|
1132
|
+
pending = new Map;
|
|
1133
|
+
noSteer = new Set;
|
|
1134
|
+
settled = new Set;
|
|
1135
|
+
unacked = new Set;
|
|
1136
|
+
instanceId = randomUUID();
|
|
1137
|
+
leaseFile;
|
|
1138
|
+
leader = false;
|
|
1139
|
+
leaseCheckedAt = Number.NEGATIVE_INFINITY;
|
|
1140
|
+
touchedAt = 0;
|
|
1141
|
+
startedAt = Date.now();
|
|
1142
|
+
lastLabel = "";
|
|
1143
|
+
busy = null;
|
|
1144
|
+
idleSince = null;
|
|
1145
|
+
stateCheckedAt = Number.NEGATIVE_INFINITY;
|
|
1146
|
+
lastSteerable = null;
|
|
1147
|
+
constructor(connection, sessionId, target, tickMs = INBOX_TICK_MS) {
|
|
1148
|
+
this.connection = connection;
|
|
1149
|
+
this.sessionId = sessionId;
|
|
1150
|
+
this.target = target;
|
|
1151
|
+
this.tickMs = tickMs;
|
|
1152
|
+
this.leaseFile = inboxLeaseFileOf(connection.dataDir, connection.host, sessionId);
|
|
1153
|
+
}
|
|
1154
|
+
get isDisposed() {
|
|
1155
|
+
return this.disposed;
|
|
1156
|
+
}
|
|
1157
|
+
start() {
|
|
1158
|
+
if (this.timer || this.disposed)
|
|
1159
|
+
return;
|
|
1160
|
+
this.touch();
|
|
1161
|
+
this.timer = setInterval(() => {
|
|
1162
|
+
if (this.ticking)
|
|
1163
|
+
return;
|
|
1164
|
+
this.ticking = true;
|
|
1165
|
+
this.tick().catch(() => {
|
|
1166
|
+
return;
|
|
1167
|
+
}).finally(() => {
|
|
1168
|
+
this.ticking = false;
|
|
1169
|
+
});
|
|
1170
|
+
}, this.tickMs);
|
|
1171
|
+
this.timer.unref?.();
|
|
1172
|
+
}
|
|
1173
|
+
dispose() {
|
|
1174
|
+
if (this.disposed)
|
|
1175
|
+
return;
|
|
1176
|
+
this.disposed = true;
|
|
1177
|
+
if (this.timer)
|
|
1178
|
+
clearInterval(this.timer);
|
|
1179
|
+
this.timer = null;
|
|
1180
|
+
this.abort.abort();
|
|
1181
|
+
if (this.leader)
|
|
1182
|
+
writeLease(this.leaseFile, { owner: null, at: 0, touchedAt: 0 });
|
|
1183
|
+
this.leader = false;
|
|
1184
|
+
}
|
|
1185
|
+
touch() {
|
|
1186
|
+
this.touchedAt = Date.now();
|
|
1187
|
+
if (!this.disposed)
|
|
1188
|
+
this.holdLease(this.touchedAt);
|
|
1189
|
+
}
|
|
1190
|
+
holdLease(now) {
|
|
1191
|
+
this.leaseCheckedAt = now;
|
|
1192
|
+
const lease = parseLease(readText(this.leaseFile));
|
|
1193
|
+
const foreign = !!lease?.owner && lease.owner !== this.instanceId && Math.abs(now - lease.at) < INBOX_LEASE_STALE_MS;
|
|
1194
|
+
if (foreign && lease && lease.touchedAt >= this.touchedAt) {
|
|
1195
|
+
this.leader = false;
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
writeLease(this.leaseFile, { owner: this.instanceId, at: now, touchedAt: this.touchedAt });
|
|
1199
|
+
this.leader = true;
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
onForeignPrompt() {
|
|
1203
|
+
this.idleTicks = 0;
|
|
1204
|
+
}
|
|
1205
|
+
async tick() {
|
|
1206
|
+
if (this.disposed)
|
|
1207
|
+
return;
|
|
1208
|
+
let busy = null;
|
|
1209
|
+
const asked = Date.now();
|
|
1210
|
+
if (this.pending.size > 0 || asked - this.stateCheckedAt >= this.tickMs * INBOX_STATE_EVERY_TICKS) {
|
|
1211
|
+
this.stateCheckedAt = asked;
|
|
1212
|
+
busy = await this.target.isBusy();
|
|
1213
|
+
this.noteBusy(busy, asked);
|
|
1214
|
+
}
|
|
1215
|
+
this.idleTicks = this.pending.size === 0 || busy !== false ? 0 : this.idleTicks + 1;
|
|
1216
|
+
for (const id of [...this.unacked])
|
|
1217
|
+
await this.acknowledge(id);
|
|
1218
|
+
const now = Date.now();
|
|
1219
|
+
if (now - this.leaseCheckedAt >= INBOX_LEASE_EVERY_MS)
|
|
1220
|
+
this.holdLease(now);
|
|
1221
|
+
if (!this.leader)
|
|
1222
|
+
return;
|
|
1223
|
+
if (!this.polling && now >= this.retryAt)
|
|
1224
|
+
this.poll();
|
|
1225
|
+
const urgent = [...this.pending.values()].some((command) => command.now === true && !this.noSteer.has(command.id)) && (this.steerable || busy === false);
|
|
1226
|
+
if (!this.delivering && this.pending.size > 0 && (this.idleTicks >= 2 || urgent)) {
|
|
1227
|
+
this.delivering = true;
|
|
1228
|
+
this.deliverNext(this.idleTicks < 2 && urgent).catch(() => {
|
|
1229
|
+
return;
|
|
1230
|
+
}).finally(() => {
|
|
1231
|
+
this.delivering = false;
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
get steerable() {
|
|
1236
|
+
return typeof this.target.steer === "function" && (this.target.canSteer?.() ?? true);
|
|
1237
|
+
}
|
|
1238
|
+
noteBusy(busy, now) {
|
|
1239
|
+
const steerable = this.steerable;
|
|
1240
|
+
if (busy === this.busy && steerable === this.lastSteerable)
|
|
1241
|
+
return;
|
|
1242
|
+
const known = this.busy !== null;
|
|
1243
|
+
if (busy !== this.busy && !busy)
|
|
1244
|
+
this.idleSince = now;
|
|
1245
|
+
this.busy = busy;
|
|
1246
|
+
this.lastSteerable = steerable;
|
|
1247
|
+
if (known && this.leader && !this.disposed) {
|
|
1248
|
+
this.connection.bridge(INBOX_BRIDGE_EVENT_PATH, this.sessionId, { type: "state", busy, idle_since: this.idleSince, [INBOX_SEND_NOW_POLL_FIELD]: steerable }).catch(() => null);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async pendingNow(waitMs) {
|
|
1252
|
+
const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(waitMs + 1e4)]);
|
|
1253
|
+
let projectPath = null;
|
|
1254
|
+
try {
|
|
1255
|
+
projectPath = await this.target.projectPath?.();
|
|
1256
|
+
} catch {
|
|
1257
|
+
projectPath = null;
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
const label = await this.target.label?.();
|
|
1261
|
+
if (typeof label === "string")
|
|
1262
|
+
this.lastLabel = label;
|
|
1263
|
+
} catch {}
|
|
1264
|
+
let plugin = null;
|
|
1265
|
+
try {
|
|
1266
|
+
plugin = this.target.plugin?.() ?? null;
|
|
1267
|
+
if (!plugin) {
|
|
1268
|
+
const version = this.target.pluginVersion?.() ?? null;
|
|
1269
|
+
plugin = version ? { version } : null;
|
|
1270
|
+
}
|
|
1271
|
+
} catch {
|
|
1272
|
+
plugin = null;
|
|
1273
|
+
}
|
|
1274
|
+
const response = await this.connection.bridge(INBOX_BRIDGE_POLL_PATH, this.sessionId, {
|
|
1275
|
+
waitMs,
|
|
1276
|
+
...typeof projectPath === "string" && projectPath ? { project_path: projectPath } : {},
|
|
1277
|
+
...this.target.label ? { label: this.lastLabel } : {},
|
|
1278
|
+
started_at: this.startedAt,
|
|
1279
|
+
...this.busy === null ? {} : { busy: this.busy, idle_since: this.idleSince },
|
|
1280
|
+
...this.steerable ? { [INBOX_SEND_NOW_POLL_FIELD]: true } : {},
|
|
1281
|
+
[INBOX_STEER_POLLS_POLL_FIELD]: true,
|
|
1282
|
+
[INBOX_TAGS_POLL_FIELD]: true,
|
|
1283
|
+
...plugin?.version ? { plugin } : {}
|
|
1284
|
+
}, signal);
|
|
1285
|
+
return response && response.status === 200 ? parseInboxBridgeCommands(response.text) : null;
|
|
1286
|
+
}
|
|
1287
|
+
async poll() {
|
|
1288
|
+
this.polling = true;
|
|
1289
|
+
try {
|
|
1290
|
+
const commands = await this.pendingNow(INBOX_POLL_WAIT_MS);
|
|
1291
|
+
if (this.disposed)
|
|
1292
|
+
return;
|
|
1293
|
+
if (commands === null) {
|
|
1294
|
+
this.backoffMs = this.backoffMs ? Math.min(this.backoffMs * 2, INBOX_RETRY_MS.max) : INBOX_RETRY_MS.first;
|
|
1295
|
+
this.retryAt = Date.now() + this.backoffMs;
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
this.backoffMs = 0;
|
|
1299
|
+
this.retryAt = 0;
|
|
1300
|
+
const handedOut = new Set(commands.map((command) => command.id));
|
|
1301
|
+
for (const id of this.settled)
|
|
1302
|
+
if (!handedOut.has(id))
|
|
1303
|
+
this.settled.delete(id);
|
|
1304
|
+
for (const command of commands) {
|
|
1305
|
+
if (!this.settled.has(command.id))
|
|
1306
|
+
this.pending.set(command.id, command);
|
|
1307
|
+
}
|
|
1308
|
+
} finally {
|
|
1309
|
+
this.polling = false;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
settle(id) {
|
|
1313
|
+
this.pending.delete(id);
|
|
1314
|
+
this.settled.add(id);
|
|
1315
|
+
}
|
|
1316
|
+
async deliverNext(urgent = false) {
|
|
1317
|
+
const waiting = this.pending.values().next().value;
|
|
1318
|
+
if (!waiting)
|
|
1319
|
+
return;
|
|
1320
|
+
const fresh = await this.pendingNow(0);
|
|
1321
|
+
if (fresh === null || this.disposed)
|
|
1322
|
+
return;
|
|
1323
|
+
const command = fresh.find((candidate) => candidate.id === waiting.id);
|
|
1324
|
+
if (!command) {
|
|
1325
|
+
this.pending.delete(waiting.id);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
const busy = await this.target.isBusy();
|
|
1329
|
+
const steering = busy && urgent && this.steerable;
|
|
1330
|
+
if (busy && !steering) {
|
|
1331
|
+
this.idleTicks = 0;
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (this.disposed || !this.holdLease(Date.now()))
|
|
1335
|
+
return;
|
|
1336
|
+
if (!command.tagged && existsSync3(join3(this.connection.dataDir, "inbox", "claims", command.id))) {
|
|
1337
|
+
this.settle(command.id);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const claim = claimDirOf(this.connection.dataDir, command.id, this.sessionId);
|
|
1341
|
+
try {
|
|
1342
|
+
mkdirSync2(join3(claim, ".."), { recursive: true, mode: 448 });
|
|
1343
|
+
mkdirSync2(claim, { mode: 448 });
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
if (error.code === "EEXIST")
|
|
1346
|
+
this.settle(command.id);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
writeQuietly(join3(claim, "by"), this.instanceId);
|
|
1350
|
+
writeQuietly(join3(claim, "pid"), String(process.pid));
|
|
1351
|
+
if (steering) {
|
|
1352
|
+
try {
|
|
1353
|
+
await this.target.steer(inboxWakeText(command), command);
|
|
1354
|
+
} catch {
|
|
1355
|
+
try {
|
|
1356
|
+
rmSync(claim, { recursive: true, force: true });
|
|
1357
|
+
} catch {}
|
|
1358
|
+
this.noSteer.add(command.id);
|
|
1359
|
+
this.connection.bridge(INBOX_BRIDGE_EVENT_PATH, this.sessionId, { type: "send_now_failed", id: command.id }).catch(() => null);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
writeQuietly(join3(claim, "delivered"), new Date().toISOString());
|
|
1363
|
+
this.settle(command.id);
|
|
1364
|
+
this.unacked.add(command.id);
|
|
1365
|
+
await this.acknowledge(command.id);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
await this.target.deliver(inboxWakeText(command), command);
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
if (isInboxDeliverLater(error)) {
|
|
1372
|
+
try {
|
|
1373
|
+
rmSync(claim, { recursive: true, force: true });
|
|
1374
|
+
} catch {}
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
this.settle(command.id);
|
|
1378
|
+
writeQuietly(join3(claim, "reported"), "1");
|
|
1379
|
+
this.target.notify(`A reply in the Plannotator Inbox (${command.subject ?? "a thread"}) could not be delivered to this session (${errorText(error)}). Read it in the Inbox: ${command.url}`);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
writeQuietly(join3(claim, "delivered"), new Date().toISOString());
|
|
1383
|
+
this.settle(command.id);
|
|
1384
|
+
this.unacked.add(command.id);
|
|
1385
|
+
await this.acknowledge(command.id);
|
|
1386
|
+
}
|
|
1387
|
+
async acknowledge(id) {
|
|
1388
|
+
const response = await this.connection.bridge(INBOX_BRIDGE_EVENT_PATH, this.sessionId, { type: "delivered", id });
|
|
1389
|
+
if (!response)
|
|
1390
|
+
return;
|
|
1391
|
+
if (response.status === 200 || response.status >= 400 && response.status < 500 && response.status !== 401)
|
|
1392
|
+
this.unacked.delete(id);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function writeLease(file, lease) {
|
|
1396
|
+
try {
|
|
1397
|
+
mkdirSync2(join3(file, ".."), { recursive: true, mode: 448 });
|
|
1398
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
1399
|
+
writeFileSync2(tmp, JSON.stringify(lease), { mode: 384 });
|
|
1400
|
+
renameSync2(tmp, file);
|
|
1401
|
+
} catch {}
|
|
1402
|
+
}
|
|
1403
|
+
function writeQuietly(path, text) {
|
|
1404
|
+
try {
|
|
1405
|
+
writeFileSync2(path, text, { mode: 384 });
|
|
1406
|
+
} catch {}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// inbox.ts
|
|
1410
|
+
var CLINE_INBOX_HOST = "cline";
|
|
1411
|
+
var CLINE_INBOX_AGENT_NAME = "Cline";
|
|
1412
|
+
var CLINE_DELIVERY_WAIT_MS = 15000;
|
|
1413
|
+
var CLINE_DISCOVER_TIMEOUT_MS = 750;
|
|
1414
|
+
var CLINE_TOOL_TIMEOUT_MS = 120000;
|
|
1415
|
+
function say(ctx, message) {
|
|
1416
|
+
try {
|
|
1417
|
+
(ctx.logger?.warn ?? ctx.logger?.log)?.(`plannotator: ${message}`);
|
|
1418
|
+
} catch {}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
class ClineInboxSession {
|
|
1422
|
+
connection;
|
|
1423
|
+
sessionId;
|
|
1424
|
+
ctx;
|
|
1425
|
+
deps;
|
|
1426
|
+
wake;
|
|
1427
|
+
runs = new Set;
|
|
1428
|
+
waiting = [];
|
|
1429
|
+
disposed = false;
|
|
1430
|
+
constructor(connection, sessionId, ctx, deps) {
|
|
1431
|
+
this.connection = connection;
|
|
1432
|
+
this.sessionId = sessionId;
|
|
1433
|
+
this.ctx = ctx;
|
|
1434
|
+
this.deps = deps;
|
|
1435
|
+
this.wake = new InboxWake(connection, sessionId, this.target(), deps.tickMs);
|
|
1436
|
+
}
|
|
1437
|
+
target() {
|
|
1438
|
+
return {
|
|
1439
|
+
isBusy: () => this.runs.size > 0 || this.waiting.length > 0,
|
|
1440
|
+
deliver: (text, _command) => this.put(text),
|
|
1441
|
+
notify: (message) => say(this.ctx, message),
|
|
1442
|
+
projectPath: () => this.ctx.cwd ?? this.ctx.workspaceInfo?.rootPath ?? null,
|
|
1443
|
+
pluginVersion: () => this.deps.version ?? null
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
put(text) {
|
|
1447
|
+
const emit = this.ctx.emitEvent;
|
|
1448
|
+
if (this.disposed || typeof emit !== "function")
|
|
1449
|
+
return Promise.reject(new Error("this Cline session cannot take a prompt from a plugin"));
|
|
1450
|
+
const waitMs = this.deps.deliveryWaitMs ?? CLINE_DELIVERY_WAIT_MS;
|
|
1451
|
+
return new Promise((resolve4, reject) => {
|
|
1452
|
+
let timer;
|
|
1453
|
+
const settle = (error) => {
|
|
1454
|
+
clearTimeout(timer);
|
|
1455
|
+
const at = this.waiting.indexOf(entry);
|
|
1456
|
+
if (at < 0)
|
|
1457
|
+
return;
|
|
1458
|
+
this.waiting.splice(at, 1);
|
|
1459
|
+
if (error)
|
|
1460
|
+
reject(error);
|
|
1461
|
+
else
|
|
1462
|
+
resolve4();
|
|
1463
|
+
};
|
|
1464
|
+
const entry = {
|
|
1465
|
+
done: () => settle(),
|
|
1466
|
+
later: (why) => settle(new InboxDeliverLater(why)),
|
|
1467
|
+
rearm: () => {
|
|
1468
|
+
clearTimeout(timer);
|
|
1469
|
+
timer = setTimeout(() => entry.later("no Cline run took it"), waitMs);
|
|
1470
|
+
timer.unref?.();
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
this.waiting.push(entry);
|
|
1474
|
+
entry.rearm();
|
|
1475
|
+
try {
|
|
1476
|
+
emit("queue_message", { prompt: text });
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
settle(error instanceof Error ? error : new Error(String(error)));
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
start() {
|
|
1483
|
+
this.wake.start();
|
|
1484
|
+
}
|
|
1485
|
+
beforeRun(context) {
|
|
1486
|
+
const fresh = this.runs.size === 0;
|
|
1487
|
+
this.runs.add(context?.snapshot?.agentId ?? "run");
|
|
1488
|
+
if (!fresh)
|
|
1489
|
+
return;
|
|
1490
|
+
const ours = this.waiting[0];
|
|
1491
|
+
if (ours) {
|
|
1492
|
+
ours.done();
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
this.wake.onForeignPrompt();
|
|
1496
|
+
this.wake.touch();
|
|
1497
|
+
}
|
|
1498
|
+
afterRun(context) {
|
|
1499
|
+
const key = context?.snapshot?.agentId ?? "run";
|
|
1500
|
+
if (this.runs.delete(key) || key !== "run" && this.runs.delete("run"))
|
|
1501
|
+
return;
|
|
1502
|
+
for (const entry of this.waiting)
|
|
1503
|
+
entry.rearm();
|
|
1504
|
+
}
|
|
1505
|
+
touch() {
|
|
1506
|
+
this.wake.touch();
|
|
1507
|
+
}
|
|
1508
|
+
dispose() {
|
|
1509
|
+
if (this.disposed)
|
|
1510
|
+
return;
|
|
1511
|
+
this.disposed = true;
|
|
1512
|
+
this.wake.dispose();
|
|
1513
|
+
for (const entry of [...this.waiting])
|
|
1514
|
+
entry.later("the Cline session closed before a run took it");
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
async function setupClineInbox(api, ctx, deps = {}) {
|
|
1518
|
+
const sessionId = ctx.session?.sessionId;
|
|
1519
|
+
if (typeof sessionId !== "string" || !sessionId)
|
|
1520
|
+
return null;
|
|
1521
|
+
const env = deps.env ?? process.env;
|
|
1522
|
+
if (!resolveInboxTool(loadConfig(), env, CLINE_INBOX_HOST))
|
|
1523
|
+
return null;
|
|
1524
|
+
const dataDir = deps.dataDir ?? getPlannotatorDataDir();
|
|
1525
|
+
if (!existsSync4(inboxRegistryFileOf(dataDir)))
|
|
1526
|
+
return null;
|
|
1527
|
+
const tools = await (deps.discover ?? ((dir) => discoverInboxTools(dir, { timeoutMs: CLINE_DISCOVER_TIMEOUT_MS })))(dataDir).catch(() => null);
|
|
1528
|
+
if (!tools)
|
|
1529
|
+
return null;
|
|
1530
|
+
const connection = new InboxAgentConnection({ dataDir, host: CLINE_INBOX_HOST, agentName: CLINE_INBOX_AGENT_NAME, tools });
|
|
1531
|
+
const session = new ClineInboxSession(connection, sessionId, ctx, deps);
|
|
1532
|
+
const spec = inboxAgentTool(connection.tools);
|
|
1533
|
+
if (spec && typeof api.registerTool === "function") {
|
|
1534
|
+
try {
|
|
1535
|
+
api.registerTool({
|
|
1536
|
+
name: spec.name,
|
|
1537
|
+
description: spec.description,
|
|
1538
|
+
inputSchema: spec.inputSchema,
|
|
1539
|
+
timeoutMs: CLINE_TOOL_TIMEOUT_MS,
|
|
1540
|
+
retryable: false,
|
|
1541
|
+
maxRetries: 0,
|
|
1542
|
+
async execute(input) {
|
|
1543
|
+
session.touch();
|
|
1544
|
+
const cwd = ctx.cwd ?? ctx.workspaceInfo?.rootPath ?? process.cwd();
|
|
1545
|
+
const result = await connection.callTool(input, { sessionId, cwd, wakes: true });
|
|
1546
|
+
if (result.isError)
|
|
1547
|
+
throw new Error(result.text);
|
|
1548
|
+
return result.text;
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
say(ctx, `could not register plannotator_inbox (${error instanceof Error ? error.message : String(error)}); replies still wake this session`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
ctx.onDispose?.(() => session.dispose());
|
|
1556
|
+
session.start();
|
|
1557
|
+
return session;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// index.ts
|
|
1561
|
+
function readPluginVersion() {
|
|
1562
|
+
let here;
|
|
1563
|
+
try {
|
|
1564
|
+
here = dirname(fileURLToPath(import.meta.url));
|
|
1565
|
+
} catch {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
for (const candidate of [join4(here, "package.json"), join4(here, "..", "package.json")]) {
|
|
1569
|
+
try {
|
|
1570
|
+
const pkg = JSON.parse(readFileSync3(candidate, "utf8"));
|
|
1571
|
+
if (pkg.name === "@plannotator/cline" && typeof pkg.version === "string" && pkg.version)
|
|
1572
|
+
return pkg.version;
|
|
1573
|
+
} catch {}
|
|
1574
|
+
}
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
var session = null;
|
|
1578
|
+
var plugin = {
|
|
1579
|
+
name: "plannotator",
|
|
1580
|
+
manifest: { capabilities: ["tools", "hooks"] },
|
|
1581
|
+
async setup(api, ctx) {
|
|
1582
|
+
try {
|
|
1583
|
+
session?.dispose();
|
|
1584
|
+
session = await setupClineInbox(api, ctx, { version: readPluginVersion() });
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
session = null;
|
|
1587
|
+
try {
|
|
1588
|
+
ctx.logger?.warn?.(`plannotator: the Inbox connection did not start (${error instanceof Error ? error.message : String(error)})`);
|
|
1589
|
+
} catch {}
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1592
|
+
hooks: {
|
|
1593
|
+
beforeRun(context) {
|
|
1594
|
+
try {
|
|
1595
|
+
session?.beforeRun(context);
|
|
1596
|
+
} catch {}
|
|
1597
|
+
return;
|
|
1598
|
+
},
|
|
1599
|
+
afterRun(context) {
|
|
1600
|
+
try {
|
|
1601
|
+
session?.afterRun(context);
|
|
1602
|
+
} catch {}
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
var cline_plugin_default = plugin;
|
|
1607
|
+
export {
|
|
1608
|
+
plugin,
|
|
1609
|
+
cline_plugin_default as default
|
|
1610
|
+
};
|